╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │747 else => {}, │ │748 }; │ │749 + │ │750 + // on an edit page, register the issue's content as the inputs' │ │751 + // page-constant initial values, which the web overlay renders │ │752 + // into them. │ │753 + if (self.data.view == .edit) { │ │754 + if (self.data.selectedIssue()) |entry| { │ │755 + const fields = form.children.values(); │ │756 + try self.session.input_values.put(inputs_arena, fields[title_field_index].widget.text_input.getFocus().id, entry.issue.title); │ │757 + try self.session.input_values.put(inputs_arena, fields[tags_field_index].widget.text_input.getFocus().id, entry.issue.tags); │ │758 + try self.session.input_values.put(inputs_arena, fields[description_field_index].widget.text_input.getFocus().id, entry.issue.description);│ │759 + } │ │760 + } │ │761 │ │762 try self.box.build(allocator, constraint, root_focus); │ │763 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │808 try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } });│ │809 } │ │810 │ │811 + // the edit button links to the issue's edit page. │ │812 + { │ │813 + var button = try wgt.TextBox(ui.Widget).init(allocator, "edit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │814 + errdefer button.deinit(allocator); │ │815 + button.getFocus().focusable = true; │ │816 + const route = ui.RoutablePage.repoIssuesEditRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │817 + button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │818 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "edit".len + 2, .height = null } });│ │819 + } │ │820 + │ │821 row.getFocus().child_id = row.children.keys()[button_in_row_index]; │ │822 } │ │823 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │888 self.tagsViewInput(key, root_focus); │ │889 return; │ │890 } │ │840 - if (self.newViewActive()) { │ │841 - try self.newViewInput(allocator, key, root_focus);│ │891 + if (self.formViewActive()) { │ │892 + try self.formInput(allocator, key, root_focus); │ │893 return; │ │894 } │ │895 const i = self.selectedSplitIndex() orelse return; │ ╰───────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │936 } │ │937 │ │938 fn detailInput(self: *View, allocator: std.mem.Allocator, index: usize, key: Key, root_focus: *Focus) !void {│ │888 - if (self.statusButtonFocused(index)) { │ │889 - try self.statusButtonInput(allocator, index, key, root_focus); │ │939 + if (self.toolRowFocused(index)) { │ │940 + try self.toolRowInput(allocator, index, key, root_focus); │ │941 } else if (self.titleFocused(index)) { │ │942 try self.titleInput(index, key, root_focus); │ │943 } else if (self.tagsFocused(index)) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │947 } │ │948 } │ │949 │ │899 - // the open/close button: enter or a click flips the issue's status; │ │900 - // arrows cross to the neighboring widgets. │ │901 - fn statusButtonInput(self: *View, allocator: std.mem.Allocator, index: usize, key: Key, root_focus: *Focus) !void { │ │950 + // the tool row: enter or a click on the open/close button flips the │ │951 + // issue's status; the edit button is an a: link the host follows. │ │952 + // arrows cross between the buttons and to the neighboring widgets. │ │953 + fn toolRowInput(self: *View, allocator: std.mem.Allocator, index: usize, key: Key, root_focus: *Focus) !void { │ │954 + const row = self.toolRow(index); │ │955 + const on_edit = if (row.getFocus().child_id) |cid| row.children.getIndex(cid) == edit_in_row_index else false; │ │956 switch (key) { │ │903 - .arrow_left => try self.focusList(index, root_focus), │ │957 + .arrow_left => if (on_edit) root_focus.setFocus(row.children.keys()[button_in_row_index]) else try self.focusList(index, root_focus),│ │958 + .arrow_right => if (!on_edit) root_focus.setFocus(row.children.keys()[edit_in_row_index]), │ │959 .arrow_up => self.focusHeader(root_focus), │ │960 .arrow_down => self.focusTitle(index, root_focus), │ │906 - .enter => try self.toggleIssueStatus(allocator, index), │ │961 + .enter => if (!on_edit) try self.toggleIssueStatus(allocator, index), │ │962 .mouse => |mouse| switch (mouse.action) { │ │963 .scroll => |dir| { │ │964 const sc = self.detailScroll(index); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │977 const sc = self.detailScroll(index); │ │978 switch (key) { │ │979 .arrow_left => try self.focusList(index, root_focus), │ │925 - .arrow_up => self.focusStatusButton(index, root_focus), │ │980 + .arrow_up => self.focusToolRow(index, root_focus), │ │981 .arrow_down => if (self.tagFlow(index) != null) try self.focusTags(index, root_focus) else try self.focusDescription(index, root_focus),│ │982 .mouse => |mouse| switch (mouse.action) { │ │983 .scroll => |dir| { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯