╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1019 sc.clampToContent(); │ │1020 } │ │1021 │ │ 967 - // up/down (and tab/shift+tab) move between the form's fields; up from the │ │1022 + // up/down (and tab/shift+tab) move between a form's fields; up from the │ │1023 // title crosses into the header tabs. the multiline description keeps │ │1024 // enter and any up/down that has a row to move to. │ │ 970 - fn newViewInput(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void {│ │ 971 - const form = self.newForm(); │ │1025 + fn formInput(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │1026 + const form = self.issueForm(); │ │1027 const cid = form.getFocus().child_id orelse return; │ │1028 const cur = form.children.getIndex(cid) orelse return; │ │1029 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1048 .arrow_down, .tab => if (cur < submit_field_index) { │ │1049 root_focus.setFocus(form.children.keys()[cur + 1]); │ │1050 }, │ │ 996 - .enter => if (cur == submit_field_index) try self.submitNewIssue(allocator), │ │1051 + .enter => if (cur == submit_field_index) try self.submitForm(allocator), │ │1052 .mouse => |mouse| if (cur == submit_field_index and inp.leftClickOn(root_focus, cid, mouse)) {│ │ 998 - try self.submitNewIssue(allocator); │ │1053 + try self.submitForm(allocator); │ │1054 }, │ │1055 else => try form.children.values()[cur].widget.input(allocator, key, root_focus), │ │1056 + } │ │1057 + } │ │1058 + │ │1059 + // the terminal submit paths: the new form commits a new-issue event, the │ │1060 + // edit form re-emits the selected issue's event with the form's content. │ │1061 + // the web posts the forms to their routes instead. │ │1062 + fn submitForm(self: *View, allocator: std.mem.Allocator) !void { │ │1063 + if (self.data.view == .edit) { │ │1064 + try self.submitEditedIssue(allocator); │ │1065 + } else { │ │1066 + try self.submitNewIssue(allocator); │ │1067 } │ │1068 } │ │1069 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1075 const io = self.session.io orelse return; │ │1076 const src = self.data.repo_source orelse return; │ │1077 │ │1012 - const form = self.newForm(); │ │1078 + const form = self.issueForm(); │ │1079 const title_input = &form.children.values()[title_field_index].widget.text_input; │ │1080 const tags_input = &form.children.values()[tags_field_index].widget.text_input; │ │1081 const description_input = &form.children.values()[description_field_index].widget.text_input;│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1125 description_input.clear(allocator); │ │1126 │ │1127 const route = ui.RoutablePage.repoIssuesRoute(self.data.identity, .open, "", &event_id_hex) orelse return; │ │1128 + try self.session.navigate(route); │ │1129 + } │ │1130 + │ │1131 + // re-emit the selected issue's event with the form's content (status │ │1132 + // preserved), then reload the issue's page. this is the terminal path; │ │1133 + // the web posts the form to the edit route. │ │1134 + fn submitEditedIssue(self: *View, allocator: std.mem.Allocator) !void { │ │1135 + if (comptime wasm) return; │ │1136 + const io = self.session.io orelse return; │ │1137 + const src = self.data.repo_source orelse return; │ │1138 + const entry = self.data.selectedIssue() orelse return; │ │1139 + │ │1140 + const form = self.issueForm(); │ │1141 + const title_input = &form.children.values()[title_field_index].widget.text_input; │ │1142 + const tags_input = &form.children.values()[tags_field_index].widget.text_input; │ │1143 + const description_input = &form.children.values()[description_field_index].widget.text_input; │ │1144 + │ │1145 + const title = try title_input.text(allocator); │ │1146 + defer allocator.free(title); │ │1147 + const tags = try tags_input.text(allocator); │ │1148 + defer allocator.free(tags); │ │1149 + const description = try description_input.text(allocator); │ │1150 + defer allocator.free(description); │ │1151 + │ │1152 + // a title is required, and a too-long tag would fail consumption │ │1153 + // after the event is already committed │ │1154 + if (title.len == 0) return; │ │1155 + var tag_iter = evt.Issue.tagIterator(tags); │ │1156 + while (tag_iter.next()) |tag| { │ │1157 + if (tag.len > evt.Issue.tag_max_len) return; │ │1158 + } │ │1159 + │ │1160 + var updated = entry.issue; │ │1161 + updated.title = title; │ │1162 + updated.tags = tags; │ │1163 + updated.description = description; │ │1164 + │ │1165 + const event = evt.EventWithId{ │ │1166 + .id = entry.id[0 .. evt.event_id_size * 2].*, │ │1167 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │1168 + .event = .{ .issue = updated }, │ │1169 + }; │ │1170 + │ │1171 + switch (src.repo_kind) { │ │1172 + inline else => |repo_kind| { │ │1173 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, .{ .path = src.path }); │ │1174 + defer any_repo.deinit(io, allocator); │ │1175 + switch (any_repo) { │ │1176 + inline else => |*repo| try evt.commitAndConsume(repo_kind, repo.self_repo_opts, io, allocator, repo, evt.events_ref, &.{event}),│ │1177 + } │ │1178 + }, │ │1179 + } │ │1180 + │ │1181 + const route = ui.RoutablePage.repoIssuesRoute(self.data.identity, updated.status, "", entry.id) orelse return; │ │1182 try self.session.navigate(route); │ │1183 } │ │1184 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────╮ │1245 } │ │1246 │ │1247 const button_in_row_index: usize = 1;│ │1248 + const edit_in_row_index: usize = 2; │ │1249 const title_child_index: usize = 0; │ │1250 const tags_child_index: usize = 1; │ │1251 │ ╰───────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1271 return &row.children.values()[button_in_row_index].widget.text_box;│ │1272 } │ │1273 │ │1153 - fn statusButtonFocused(self: *View, index: usize) bool { │ │1274 + fn toolRowFocused(self: *View, index: usize) bool { │ │1275 const outer = self.detailOuter(index); │ │1276 const cid = outer.getFocus().child_id orelse return false; │ │1277 return outer.children.getIndex(cid) == tool_row_index; │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │1317 if (self.description_id[index]) |id| root_focus.setFocus(id); │ │1318 } │ │1319 │ │1199 - fn focusStatusButton(self: *View, index: usize, root_focus: *Focus) void { │ │1200 - if (self.statusButton(index)) |button| root_focus.setFocus(button.getFocus().id);│ │1320 + // return to the tool row's last-focused button. │ │1321 + fn focusToolRow(self: *View, index: usize, root_focus: *Focus) void { │ │1322 + const cid = self.toolRow(index).getFocus().child_id orelse return; │ │1323 + root_focus.setFocus(cid); │ │1324 } │ │1325 │ │1326 // enter the detail pane. an empty pane (no issues) can't be entered. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1436 }); │ │1437 } │ │1438 │ │1316 - // new-issue tab │ │1439 + // new-issue tab; an edit url shows the edit tab in its place │ │1440 { │ │1318 - const new_route = ui.RoutablePage.repoIssuesNewRoute(data.identity) orelse return error.RouteTooLong; │ │1319 - const new_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try new_route.toUrl(session.page_arena)}); │ │1320 - var text_box = try wgt.TextBox(ui.Widget).init(allocator, "new", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │1441 + const route = if (data.view == .edit) │ │1442 + ui.RoutablePage.repoIssuesEditRoute(data.identity, data.selected_id) orelse return error.RouteTooLong │ │1443 + else │ │1444 + ui.RoutablePage.repoIssuesNewRoute(data.identity) orelse return error.RouteTooLong; │ │1445 + const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); │ │1446 + const label: []const u8 = if (data.view == .edit) "edit" else "new"; │ │1447 + var text_box = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │1448 errdefer text_box.deinit(allocator); │ │1449 text_box.getFocus().focusable = true; │ │1323 - text_box.getFocus().kind = .{ .custom = new_link }; │ │1450 + text_box.getFocus().kind = .{ .custom = link }; │ │1451 try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │1452 try box.children.put(allocator, text_box.getFocus().id, .{ │ │1453 .widget = .{ .text_box = text_box }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │1454 .rect = null, │ │1328 - .min_size = .{ .width = "new".len + 2, .height = null },│ │1455 + .min_size = .{ .width = label.len + 2, .height = null },│ │1456 }); │ │1457 } │ │1458 │ ╰──────────────────────────────────────────────────────────────────────────────╯