╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │101 │ │102 pub const RefKind = enum { branch, tag }; │ │103 │ │104 - pub const IssuesView = enum { open, closed, tags, new }; │ │104 + pub const IssuesView = enum { open, closed, tags, new, edit };│ │105 │ │106 pub const RefOrOid = enum { │ │107 branch, │ ╰───────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │323 return .{ .repo_issues = .{ │ │324 .name = Array(repo_route_max_len).from(identity) orelse return null, │ │325 .view = .new, │ │326 + } }; │ │327 + } │ │328 + │ │329 + // build a `.repo_issues` route showing the edit form for the issue with │ │330 + // hex event id `selected`. │ │331 + pub fn repoIssuesEditRoute(identity: []const u8, selected: []const u8) ?RoutablePage {│ │332 + if (selected.len == 0) return null; │ │333 + return .{ .repo_issues = .{ │ │334 + .name = Array(repo_route_max_len).from(identity) orelse return null, │ │335 + .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │336 + .view = .edit, │ │337 } }; │ │338 } │ │339 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │409 }, │ │410 .repo_issues => |i| blk: { │ │411 const prefix = try repoUrlPrefix(arena, i.name.slice()); │ │412 + if (i.view == .edit) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/edit", .{ prefix, i.selected.slice() });│ │413 // a selected issue's url carries its id instead of a view │ │414 // name, so it survives status changes │ │415 const word = if (i.selected.len != 0) i.selected.slice() else @tagName(i.view); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │517 return repoRefsRoute(pair, kind, ref.value); │ │518 } │ │519 if (std.mem.eql(u8, tab, "issues")) { │ │520 + // "<id>/edit" is the edit form for that issue, with no params. │ │521 + const after_tab = segments.rest(); │ │522 + if (std.mem.endsWith(u8, after_tab, "/edit")) { │ │523 + const id = after_tab[0 .. after_tab.len - "/edit".len]; │ │524 + if (id.len == 0 or std.mem.indexOfScalar(u8, id, '/') != null) return null;│ │525 + return repoIssuesEditRoute(pair, id); │ │526 + } │ │527 // the word is a view name or an issue event id (whose status │ │528 // picks the view); a filter with neither is never emitted. │ │529 const word = params.scanTail(&segments) catch return null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │735 // focus id -> the live TextInput, refreshed each frame by the views that own │ │736 // inputs. web/wasm form handling looks widgets up here by focus id. │ │737 text_inputs: std.AutoHashMapUnmanaged(usize, *wgt.TextInput(Widget)) = .empty, │ │738 + // focus id -> the page-constant text the web overlay renders into an │ │739 + // input initially, refreshed each frame alongside `text_inputs`. never │ │740 + // the input's live content: the overlay html must not change as the │ │741 + // user types, or the diff in _setOverlay would rebuild the element. │ │742 + input_values: std.AutoHashMapUnmanaged(usize, []const u8) = .empty, │ │743 nav_back: bool = false, // set by input (escape) to request the native TUI pop a page; see Nav│ │744 refresh_requested: bool = false, // set by input (ctrl+r) │ │745 // a requested forward navigation. set this (via navigate) to move to a new │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │1172 // input-owning views build their TextInputs in init — so reset the│ │1173 // focus-id -> *TextInput map here │ │1174 session.text_inputs.clearRetainingCapacity(); │ │1175 + session.input_values.clearRetainingCapacity(); │ │1176 │ │1177 try root.build(allocator, .{ │ │1178 .min_size = .{ .width = null, .height = 40 }, │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │70 }; │ │71 } │ │72 │ │73 +// the issue `selected_id` names, in whichever window holds it. │ │74 +pub fn selectedIssue(self: *const Self) ?*const IssueWithId { │ │75 + if (self.selected_id.len == 0) return null; │ │76 + for ([_]*const Window{ &self.open, &self.closed }) |win| { │ │77 + for (win.issues) |*entry| { │ │78 + if (std.mem.eql(u8, entry.id, self.selected_id)) return entry; │ │79 + } │ │80 + } │ │81 + return null; │ │82 +} │ │83 + │ │84 // an empty listing, for the wasm / no-repo paths. │ │85 pub fn emptyResult(aa: std.mem.Allocator, identity: []const u8, tag: []const u8, selected_id: []const u8, view: ui.RoutablePage.IssuesView) !Self {│ │86 return .{ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │181 if (!try set.contains(order_key)) return error.NotFound; │ │182 │ │183 switch (issue_event.status) { │ │173 - .open => { │ │174 - open_root = order_key; │ │175 - resolved_view = .open; │ │176 - }, │ │177 - .closed => { │ │178 - closed_root = order_key; │ │179 - resolved_view = .closed; │ │180 - }, │ │184 + .open => open_root = order_key, │ │185 + .closed => closed_root = order_key, │ │186 } │ │187 + // an edit url keeps its view; otherwise the issue's status picks it. │ │188 + if (empty.view != .edit) resolved_view = switch (issue_event.status) { │ │189 + .open => .open, │ │190 + .closed => .closed, │ │191 + }; │ │192 } │ │193 │ │194 const open_window = try loadWindow(repo_opts.hash, arena, event_id_to_issue, open_set, open_root);│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │329 const open_view_index: usize = 0; │ │330 const closed_view_index: usize = 1; │ │331 const tags_view_index: usize = 2; │ │322 - const new_view_index: usize = 3; │ │332 + // the new-issue form, or the edit form when the page was loaded at an│ │333 + // edit url. │ │334 + const form_view_index: usize = 3; │ │335 const split_count: usize = 2; │ │336 // indices within a split (the horizontal box inside the stack). │ │337 const list_index: usize = 0; │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────╮ │349 .open => open_view_index, │ │350 .closed => closed_view_index, │ │351 .tags => tags_view_index, │ │340 - .new => new_view_index, │ │352 + .new, .edit => form_view_index,│ │353 }; │ │354 } │ │355 │ ╰────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯