╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │569 .open => open_view_index, │ │570 .closed => closed_view_index, │ │571 .tags => tags_view_index, │ │372 - .new, .edit => form_view_index, │ │572 + .new, .edit, .resolve => form_view_index, │ │573 + .conflicts => conflicts_view_index, │ │574 // init resolves a description url to its issue's status list│ │575 .description => unreachable, │ │576 }; │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │577 } │ │578 │ │579 + // the status a status split lists; the conflicts split has none (its│ │580 + // issues carry their own). │ │581 fn splitStatus(index: usize) evt.Issue.Status { │ │582 return if (index == open_view_index) .open else .closed; │ │583 } │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │624 try stack.children.put(allocator, tf.getFocus().id, .{ .tag_flow = tf }); │ │625 } │ │626 │ │424 - // the new-issue form, or — on an edit url — the edit form in its │ │425 - // place, prefilled with the selected issue's content. a logged-out │ │426 - // session can't create events, so the unauthorized view stands in. │ │627 + // the new-issue form, or — on an edit or resolve url — that form in │ │628 + // its place, prefilled with the selected issue's content. a │ │629 + // logged-out session can't create events, so the unauthorized view │ │630 + // stands in. │ │631 if (session.data.is_local or session.data.user_id != null) { │ │428 - const aa = session.page_arena.allocator(); │ │429 - const action = if (data.view == .edit) │ │430 - (if (data.identity.len == 0) │ │431 - try std.fmt.allocPrint(aa, "form:/issues/{s}/edit", .{data.selected_id}) │ │632 + if (data.view == .resolve) { │ │633 + // on the web the page grows to the form's height and the │ │634 + // browser scrolls it; the terminal scrolls the form itself │ │635 + var form_widget: ui.Widget = blk: { │ │636 + var form = try initResolveForm(allocator, session, data); │ │637 + errdefer form.deinit(allocator); │ │638 + break :blk if (session.is_terminal) │ │639 + .{ .scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .box = form }, .{ .direction = .vert }) }│ │640 + else │ │641 + .{ .box = form }; │ │642 + }; │ │643 + errdefer form_widget.deinit(allocator); │ │644 + try stack.children.put(allocator, form_widget.getFocus().id, form_widget); │ │645 + } else { │ │646 + const aa = session.page_arena.allocator(); │ │647 + const action = if (data.view == .edit) │ │648 + (if (data.identity.len == 0) │ │649 + try std.fmt.allocPrint(aa, "form:/issues/{s}/edit", .{data.selected_id}) │ │650 + else │ │651 + try std.fmt.allocPrint(aa, "form:/repo/{s}/issues/{s}/edit", .{ data.identity, data.selected_id }))│ │652 + else if (data.identity.len == 0) │ │653 + "form:/issue" │ │654 else │ │433 - try std.fmt.allocPrint(aa, "form:/repo/{s}/issues/{s}/edit", .{ data.identity, data.selected_id })) │ │434 - else if (data.identity.len == 0) │ │435 - "form:/issue" │ │436 - else │ │437 - try std.fmt.allocPrint(aa, "form:/repo/{s}/issue", .{data.identity}); │ │438 - const issue = if (data.view == .edit) │ │439 - (if (data.selectedIssue()) |entry| &entry.issue else null) │ │440 - else │ │441 - null; │ │442 - var form = try initIssueForm(allocator, session, action, issue); │ │443 - errdefer form.deinit(allocator); │ │444 - try stack.children.put(allocator, form.getFocus().id, .{ .box = form }); │ │655 + try std.fmt.allocPrint(aa, "form:/repo/{s}/issue", .{data.identity}); │ │656 + const issue = if (data.view == .edit) │ │657 + (if (data.selectedIssue()) |entry| &entry.issue else null) │ │658 + else │ │659 + null; │ │660 + var form = try initIssueForm(allocator, session, action, issue); │ │661 + errdefer form.deinit(allocator); │ │662 + try stack.children.put(allocator, form.getFocus().id, .{ .box = form }); │ │663 + } │ │664 } else { │ │665 var message = try ui.Unauthorized.View.init(allocator); │ │666 errdefer message.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │667 try stack.children.put(allocator, message.getFocus().id, .{ .unauthorized = message });│ │668 } │ │669 │ │670 + // the conflicts split; its tab exists only when the repo has │ │671 + // conflicts, so skip the child too to keep the stack 1:1 with the │ │672 + // tabs. │ │673 + if (data.conflicts.count > 0) { │ │674 + var split = try initSplit(allocator, session, data, null); │ │675 + errdefer split.deinit(allocator); │ │676 + try stack.children.put(allocator, split.getFocus().id, .{ .box = split }); │ │677 + } │ │678 + │ │679 // the stack starts on the page's view. │ │680 stack.getFocus().child_id = stack.children.keys()[viewIndex(data.view)]; │ │681 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────╮ │686 .box = outer, │ │687 .data = data, │ │688 .session = session, │ │461 - .detailed_index = .{ null, null },│ │462 - .description_id = .{ null, null },│ │463 - .author_id = .{ null, null }, │ │689 + .detailed_index = @splat(null), │ │690 + .description_id = @splat(null), │ │691 + .author_id = @splat(null), │ │692 }; │ │693 } │ │694 │ ╰───────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │467 - // the master-detail split showing `status`'s window. │ │468 - fn initSplit(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self, status: evt.Issue.Status) !wgt.Box(ui.Widget) { │ │695 + // the master-detail split showing `status`'s window, or the conflicts │ │696 + // window when null. │ │697 + fn initSplit(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self, status_maybe: ?evt.Issue.Status) !wgt.Box(ui.Widget) {│ │698 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │699 errdefer box.deinit(allocator); │ │700 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │472 - const win = data.window(status); │ │701 + const win = if (status_maybe) |status| data.window(status) else &data.conflicts;│ │702 │ │703 // the issue list (one focusable row per title), plus a "next" link that │ │704 // reloads the page rooted at the following issue. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │707 var list_box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert, .stretch = true }); │ │708 errdefer list_box.deinit(allocator); │ │709 if (win.prev_id) |prev| │ │481 - try addRow(allocator, &list_box, "← previous", try issuesLink(session.page_arena, data.identity, status, data.tag, prev)); │ │482 - for (win.issues) |entry| │ │483 - try addRow(allocator, &list_box, entry.issue.event.title, try issueRowLink(session.page_arena, data.identity, entry.id)); │ │710 + try addRow(allocator, &list_box, "← previous", "", try windowLink(session.page_arena, data, status_maybe, prev)); │ │711 + for (win.issues) |entry| { │ │712 + // conflicted issues carry a marker in the status lists │ │713 + const label: []const u8 = if (entry.conflicted and status_maybe != null) "[conflict]" else ""; │ │714 + try addRow(allocator, &list_box, entry.issue.event.title, label, try issueRowLink(session.page_arena, data.identity, entry.id));│ │715 + } │ │716 if (win.next_id) |next| │ │485 - try addRow(allocator, &list_box, "next →", try issuesLink(session.page_arena, data.identity, status, data.tag, next)); │ │717 + try addRow(allocator, &list_box, "next →", "", try windowLink(session.page_arena, data, status_maybe, next)); │ │718 // select the window's first issue (past a leading "previous" │ │719 // row) so its description shows on load. │ │720 if (win.issues.len > 0) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │794 if (issue) |i| try description.setContent(allocator, i.event.description); │ │795 try box.children.put(allocator, description.getFocus().id, .{ .widget = .{ .text_input = description }, .rect = null, .min_size = null }); │ │796 } │ │797 + │ │798 + try addSubmitButton(allocator, &box); │ │799 │ │800 + box.getFocus().child_id = box.children.keys()[title_field_index]; │ │801 + return box; │ │802 + } │ │803 + │ │804 + // a form's submit button, then a spacer absorbing the leftover │ │805 + // min-height the box hands its last child, so the button keeps its │ │806 + // natural height │ │807 + fn addSubmitButton(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget)) !void { │ │808 { │ │809 var button = try wgt.TextBox(ui.Widget).init(allocator, "submit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │810 errdefer button.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │814 button.getFocus().kind = .{ .custom = "submit" }; │ │815 try box.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │816 } │ │575 - │ │576 - // absorbs the leftover min-height the box hands its last child, so │ │577 - // the button keeps its natural height │ │817 { │ │818 var spacer = try ui.Spacer.init(allocator); │ │819 errdefer spacer.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯