╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │278 .open => open_root = order_key, │ │279 .closed => closed_root = order_key, │ │280 } │ │281 - // an edit url keeps its view; otherwise the issue's status picks it. │ │282 - if (view != .edit) resolved_view = switch (issue_event.event.status) { │ │281 + // form urls keep their view; otherwise the issue's status picks it. │ │282 + if (view != .edit and view != .new_comment) resolved_view = switch (issue_event.event.status) {│ │283 .open => .open, │ │284 .closed => .closed, │ │285 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │576 const open_view_index: usize = 0; │ │577 const closed_view_index: usize = 1; │ │578 const tags_view_index: usize = 2; │ │579 - // the new-issue form, or the edit or resolve form when the page was │ │579 + // the new-issue form, or the edit, comment, or resolve form when the page was│ │580 // loaded at one of their urls. │ │581 const form_view_index: usize = 3; │ │582 // the conflicts split; the tab and stack child exist only when the repo │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │591 const detail_index: usize = 1; │ │592 const list_max_width: usize = 40; │ │593 const detail_min_width: usize = 40; │ │594 - // indices within the new-issue form. │ │594 + // indices within the issue form. │ │595 const title_field_index: usize = 0; │ │596 const tags_field_index: usize = 1; │ │597 const description_field_index: usize = 2;│ ╰──────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────╮ │598 const submit_field_index: usize = 3; │ │599 + const comment_author_field_index: usize = 0; │ │600 + const comment_body_field_index: usize = 1; │ │601 + const comment_submit_field_index: usize = 2; │ │602 │ │603 fn viewIndex(view: ui.RoutablePage.IssuesView) usize {│ │604 return switch (view) { │ ╰───────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │605 .open => open_view_index, │ │606 .closed => closed_view_index, │ │607 .tags => tags_view_index, │ │605 - .new, .edit, .resolve => form_view_index, │ │608 + .new, .edit, .new_comment, .resolve => form_view_index, │ │609 .conflicts => conflicts_view_index, │ │610 // init resolves a description url to its issue's status list│ │611 .description => unreachable, │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │660 try stack.children.put(allocator, tf.getFocus().id, .{ .tag_flow = tf }); │ │661 } │ │662 │ │660 - // the new-issue form, or — on an edit or resolve url — that form in │ │663 + // the new-issue form, or — on an edit, comment, or resolve url — that form in│ │664 // its place, prefilled with the selected issue's content. a │ │665 // logged-out session can't create events, so the unauthorized view │ │666 // stands in. │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │667 if (session.data.is_local or session.data.user_id != null) { │ │665 - if (data.view == .resolve) { │ │668 + if (data.view == .new_comment) { │ │669 + const route = ui.RoutablePage.repoCommentNewRoute(data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong; │ │670 + const page_url = try route.toUrl(session.page_arena); │ │671 + const action = try std.fmt.allocPrint(session.page_arena.allocator(), "form:{s}/comment", .{page_url[0 .. page_url.len - "/new".len]}); │ │672 + const parent_route = if (data.comment_page) |page| │ │673 + ui.RoutablePage.repoCommentsRoute(data.identity, data.selected_id, page.selected.id, 0) │ │674 + else │ │675 + ui.RoutablePage.repoIssueCommentsRoute(data.identity, data.selected_id, 0); │ │676 + var form = try initCommentForm(allocator, session, action, if (data.comment_page) |page| page.selected.author else if (data.selectedIssue()) |entry| entry.author else .unknown, parent_route orelse return error.RouteTooLong);│ │677 + errdefer form.deinit(allocator); │ │678 + try stack.children.put(allocator, form.getFocus().id, .{ .box = form }); │ │679 + } else if (data.view == .resolve) { │ │680 // on the web the page grows to the form's height and the │ │681 // browser scrolls it; the terminal scrolls the form itself │ │682 var form_widget: ui.Widget = blk: { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │853 return box; │ │854 } │ │855 │ │856 + fn initCommentForm(allocator: std.mem.Allocator, session: *ui.Session, action: []const u8, author: ui.Author, parent_route: ui.RoutablePage) !wgt.Box(ui.Widget) {│ │857 + var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │858 + errdefer box.deinit(allocator); │ │859 + box.getFocus().kind = .{ .custom = action }; │ │860 + │ │861 + { │ │862 + var parent = try ui.authorBox(allocator, session.page_arena, author); │ │863 + errdefer parent.deinit(allocator); │ │864 + parent.options.label = " replying to "; │ │865 + parent.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), "a:{s}", .{try parent_route.toUrl(session.page_arena)}) }; │ │866 + try box.children.put(allocator, parent.getFocus().id, .{ .widget = .{ .text_box = parent }, .rect = null, .min_size = null }); │ │867 + } │ │868 + │ │869 + { │ │870 + var body = try wgt.TextInput(ui.Widget).init(allocator, .{ │ │871 + .label = " comment ", │ │872 + .name = "body", │ │873 + .visible_width = null, │ │874 + .rounded_corners = true, │ │875 + .render_content = session.is_terminal, │ │876 + .multiline = true, │ │877 + .visible_height = 5, │ │878 + }); │ │879 + errdefer body.deinit(allocator); │ │880 + body.getFocus().focusable = true; │ │881 + try box.children.put(allocator, body.getFocus().id, .{ .widget = .{ .text_input = body }, .rect = null, .min_size = null }); │ │882 + } │ │883 + │ │884 + try addSubmitButtonLabeled(allocator, &box, "submit reply"); │ │885 + box.getFocus().child_id = box.children.keys()[comment_author_field_index]; │ │886 + return box; │ │887 + } │ │888 + │ │889 // a form's submit button, then a spacer absorbing the leftover │ │890 // min-height the box hands its last child, so the button keeps its │ │891 // natural height │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │892 fn addSubmitButton(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget)) !void { │ │893 + return addSubmitButtonLabeled(allocator, box, "submit"); │ │894 + } │ │895 + │ │896 + fn addSubmitButtonLabeled(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8) !void { │ │897 { │ │847 - var submit = try ui.SubmitButton.init(allocator); │ │898 + var submit = try ui.SubmitButton.initLabeled(allocator, label); │ │899 errdefer submit.deinit(allocator); │ │900 try box.children.put(allocator, submit.getFocus().id, .{ .widget = .{ .submit_button = submit }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │901 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1236 self.session.data.current_page = (if (index == form_view_index) │ │1237 (switch (self.data.view) { │ │1238 .edit => ui.RoutablePage.repoIssuesEditRoute(self.data.identity, self.data.selected_id), │ │1239 + .new_comment => ui.RoutablePage.repoCommentNewRoute(self.data.identity, self.data.selected_id, self.data.comment_id), │ │1240 .resolve => ui.RoutablePage.repoIssuesResolveRoute(self.data.identity, self.data.selected_id, self.data.theirs_picks),│ │1241 else => ui.RoutablePage.repoIssuesNewRoute(self.data.identity), │ │1242 }) │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯