╭ changed ────────────╮ │src/event/Comment.zig│ ╰─────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │26 // the indexes the thread reads │ │27 pub const thread_id_to_comment_id_set_key = "thread-id->comment-id-set";│ │28 pub const parent_id_to_comment_id_set_key = "parent-id->comment-id-set";│ │29 + │ │30 +pub fn fieldsValid(body: []const u8) bool { │ │31 + return body.len != 0; │ │32 +} │ │33 │ │34 pub fn consume( │ │35 comptime DB: type, │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │111 │ │112 pub const RefKind = enum { branch, tag }; │ │113 │ │114 - pub const IssuesView = enum { open, closed, tags, new, edit, description, conflicts, resolve }; │ │114 + pub const IssuesView = enum { open, closed, tags, new, edit, description, new_comment, conflicts, resolve };│ │115 │ │116 pub const RefOrOid = enum { │ │117 branch, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │366 return route; │ │367 } │ │368 │ │369 + // build the form route for replying to an issue or one of its comments. │ │370 + pub fn repoCommentNewRoute(identity: []const u8, thread: []const u8, parent_id: []const u8) ?RoutablePage {│ │371 + if (thread.len == 0) return null; │ │372 + var route = repoIssueCommentsRoute(identity, thread, 0) orelse return null; │ │373 + route.repo_issues.comment = Array(evt.event_id_size * 2).from(parent_id) orelse return null; │ │374 + route.repo_issues.view = .new_comment; │ │375 + return route; │ │376 + } │ │377 + │ │378 // build a `.repo_issues` route showing tags view, keeping the url-encoded │ │379 // `tag` filter ("" = unfiltered). │ │380 pub fn repoIssuesTagsRoute(identity: []const u8, tag: []const u8) ?RoutablePage { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │521 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/resolve", .{ prefix, i.selected.slice() }) │ │522 else │ │523 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/" ++ theirs_seg ++ "{s}/resolve", .{ prefix, i.selected.slice(), i.theirs.slice() });│ │524 + if (i.view == .new_comment) break :blk if (i.comment.len == 0) │ │525 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/new", .{ prefix, i.selected.slice() }) │ │526 + else │ │527 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/" ++ comment_seg ++ "{s}/new", .{ prefix, i.selected.slice(), i.comment.slice() }); │ │528 if (i.view == .conflicts) break :blk if (i.selected.len == 0) │ │529 try std.fmt.allocPrint(arena.allocator(), "{s}/issues/conflicts", .{prefix}) │ │530 else │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │603 // browser scroll the whole page, rather than being pinned to the viewport│ │604 pub fn fullHeight(self: RoutablePage) bool { │ │605 return switch (self) { │ │593 - .repo_issues => |i| i.view == .resolve, │ │606 + .repo_issues => |i| i.view == .resolve or i.view == .new_comment, │ │607 else => false, │ │608 }; │ │609 } │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │680 if (std.mem.startsWith(u8, tab, issue_seg)) { │ │681 const issue_id = tab[issue_seg.len..]; │ │682 if (issue_id.len == 0) return null; │ │670 - const word = params.scanTail(&segments) catch return null; │ │671 - if (word) |tail| { │ │683 + if (segments.peek()) |tail| { │ │684 if (std.mem.startsWith(u8, tail, comment_seg)) { │ │673 - if (!params.only(&.{.start})) return null; │ │685 + _ = segments.next(); │ │686 const comment_id = tail[comment_seg.len..]; │ │687 if (comment_id.len == 0) return null; │ │688 + const word = params.scanTail(&segments) catch return null; │ │689 + if (word) |last| { │ │690 + if (!std.mem.eql(u8, last, "new") or !params.only(&.{})) return null; │ │691 + return repoCommentNewRoute(pair, issue_id, comment_id); │ │692 + } │ │693 + if (!params.only(&.{.start})) return null; │ │694 return repoCommentsRoute(pair, issue_id, comment_id, params.start() orelse return null); │ │695 } │ │696 + } │ │697 + const word = params.scanTail(&segments) catch return null; │ │698 + if (word) |tail| { │ │699 + if (std.mem.eql(u8, tail, "new")) return if (params.only(&.{})) repoCommentNewRoute(pair, issue_id, "") else null; │ │700 if (std.mem.eql(u8, tail, "edit")) return if (params.only(&.{})) repoIssuesEditRoute(pair, issue_id) else null; │ │701 if (std.mem.eql(u8, tail, "description")) return if (params.only(&.{})) repoIssuesDescriptionRoute(pair, issue_id) else null;│ │702 if (std.mem.eql(u8, tail, "resolve")) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2186 box: wgt.Box(Widget), │ │2187 │ │2188 pub fn init(allocator: std.mem.Allocator) !SubmitButton { │ │2189 + return initLabeled(allocator, "submit"); │ │2190 + } │ │2191 + │ │2192 + pub fn initLabeled(allocator: std.mem.Allocator, label: []const u8) !SubmitButton { │ │2193 var box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .direction = .horiz });│ │2194 errdefer box.deinit(allocator); │ │2195 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2200 } │ │2201 │ │2202 { │ │2177 - const label = "submit"; │ │2203 var button = try wgt.TextBox(Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │2204 errdefer button.deinit(allocator); │ │2205 button.getFocus().focusable = true; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Comment.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 var author = try ui.authorBox(allocator, session.page_arena, entry.author); │ │171 errdefer author.deinit(allocator); │ │172 try bar.children.put(allocator, author.getFocus().id, .{ .widget = .{ .text_box = author }, .rect = null, .min_size = null, .shrink = true }); │ │173 + │ │174 + var reply = try linkBox(allocator, session, "new reply", ui.RoutablePage.repoCommentNewRoute(identity, &entry.comment.event.thread_id, entry.id) orelse return error.RouteTooLong); │ │175 + errdefer reply.deinit(allocator); │ │176 + try bar.children.put(allocator, reply.getFocus().id, .{ .widget = .{ .text_box = reply }, .rect = null, .min_size = .{ .width = "new reply".len + 2, .height = null } }); │ │177 │ │178 var permalink = try linkBox(allocator, session, "permalink", ui.RoutablePage.repoCommentsRoute(identity, &entry.comment.event.thread_id, entry.id, 0) orelse return error.RouteTooLong);│ │179 errdefer permalink.deinit(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯