╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────╮ │32 timestamp: []const u8, │ │33 link: []const u8, │ │34 form: []const u8, │ │35 - button: ?DetailButton = null, │ │35 + buttons: []const DetailButton = &.{},│ │36 }; │ │37 │ │38 identity: []const u8, │ ╰─────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │73 .index = remaining, │ │74 .action = if (detail) |value| value.action else if (record) |value| try aa.dupe(u8, value.action) else "unknown",│ │75 .description = description.written(), │ │76 - .button = if (detail) |value| value.button else null, │ │76 + .buttons = if (detail) |value| value.buttons else &.{}, │ │77 .timestamp = if (record) |value| try formatTimestamp(aa, value.timestamp) else "timestamp unavailable", │ │78 .link = try std.fmt.allocPrint(aa, "ai:{s}", .{url}), │ │79 .form = try std.fmt.allocPrint(aa, "form:{s}/undo", .{url}), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │85 const Detail = struct { │ │86 action: []const u8, │ │87 description: []const u8, │ │88 - button: ?DetailButton = null, │ │88 + buttons: []const DetailButton = &.{}, │ │89 }; │ │90 │ │91 // every haxy event transaction shares one action, and the events it wrote are│ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 99 moment: rp.Repo(.xit, opts).DB.HashMap(.read_only), │ │100 history_index: u64, │ │101 ) !?Detail { │ │102 + const aa = arena.allocator(); │ │103 if (std.mem.eql(u8, record.action, evt.merge_undo_action)) return .{ .action = "merge events", .description = "merged the events ref from a remote" }; │ │104 if (std.mem.eql(u8, record.action, pch.merge_undo_action)) return .{ .action = "merge patch", .description = "merged a patch into its target branch" };│ │105 if (std.mem.eql(u8, record.action, fork.undo_action)) return .{ .action = "fork", .description = "created a fork to draft a patch in" }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │105 - if (std.mem.eql(u8, record.action, push.undo_action)) return .{ │ │106 - .action = "push", │ │107 - .description = "received a push", │ │108 - .button = try pushUser(arena, haxy_moment, record.payload), │ │109 - }; │ │106 + if (std.mem.eql(u8, record.action, push.undo_action)) { │ │107 + // a push consumes the events it carried in its own transaction │ │108 + var buttons: std.ArrayList(DetailButton) = .empty; │ │109 + if (try pushUser(arena, haxy_moment, record.payload)) |button| try buttons.append(aa, button); │ │110 + const count = try momentEventCount(opts, moment) orelse 0; │ │111 + if (count > 0) try buttons.append(aa, try eventsButton(arena, identity, history_index, count)); │ │112 + return .{ .action = "push", .description = "received a push", .buttons = buttons.items }; │ │113 + } │ │114 if (!std.mem.eql(u8, record.action, evt.undo_action)) return null; │ │115 │ │116 const count = try momentEventCount(opts, moment) orelse return .{ .action = "events", .description = "updated the event database" };│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │117 // a transaction that consumed only merge commits indexed no events of its own │ │118 if (count == 0) return .{ .action = "events", .description = "merged the event history" }; │ │119 + const buttons = try aa.dupe(DetailButton, &.{try eventsButton(arena, identity, history_index, count)}); │ │120 + return .{ │ │121 + .action = try std.fmt.allocPrint(aa, "events ({d})", .{count}), │ │122 + .description = "updated the event database", │ │123 + .buttons = buttons, │ │124 + }; │ │125 +} │ │126 │ │116 - // the page reads the transaction's own moment, so the url names the │ │117 - // transaction rather than the state it produced │ │127 +// the page reads the transaction's own moment, so the url names the │ │128 +// transaction rather than the state it produced │ │129 +fn eventsButton(arena: *std.heap.ArenaAllocator, identity: []const u8, history_index: u64, count: u64) !DetailButton { │ │130 const aa = arena.allocator(); │ │131 const route = ui.RoutablePage.repoEventsRoute(identity, .active, null, "", history_index) orelse return error.RouteTooLong;│ │132 return .{ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │121 - .action = try std.fmt.allocPrint(aa, "events ({d})", .{count}), │ │122 - .description = "updated the event database", │ │123 - .button = .{ │ │124 - .label = " events ", │ │125 - .text = try std.fmt.allocPrint(aa, "view events ({d})", .{count}), │ │126 - .link = try std.fmt.allocPrint(aa, "a:{s}", .{try route.toUrl(arena)}),│ │127 - }, │ │133 + .label = " events ", │ │134 + .text = try std.fmt.allocPrint(aa, "view events ({d})", .{count}), │ │135 + .link = try std.fmt.allocPrint(aa, "a:{s}", .{try route.toUrl(arena)}), │ │136 }; │ │137 } │ │138 │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │241 data: *const Self, │ │242 session: *ui.Session, │ │243 detailed_index: ?usize = null, │ │236 - failure_id: ?usize = null, │ │244 shown_failure: ?[]const u8 = null,│ │245 │ │246 const list_max_width = 20; │ ╰───────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │247 const detail_min_width = 40; │ │248 const header_index = 0; │ │249 const content_index = 1; │ │250 + // the undo button and the timestamp; everything below them is rebuilt │ │251 + const fixed_rows = 2; │ │252 │ │253 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │254 var outer = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert });│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │296 var details = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │297 errdefer details.deinit(allocator); │ │298 if (data.items.len > 0) { │ │290 - try addText(allocator, &details, "", null, .single); │ │299 try addText(allocator, &details, "", null, .single); │ │292 - details.children.values()[1].widget.text_box.options.label = " timestamp "; │ │293 - try addText(allocator, &details, "", null, .single); │ │294 - details.children.values()[2].widget.text_box.options.label = " description "; │ │300 + try addLabeled(allocator, &details, " timestamp ", "", null); │ │301 details.getFocus().child_id = details.children.keys()[0]; │ │302 } │ │303 break :blk try wgt.Scroll(ui.Widget).init(allocator, .{ .box = details }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true });│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯