╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │37 next: ?u64 = null, │ │38 failure: ?[]const u8 = null, │ │39 can_undo: bool = false, │ │40 +// the clear-history confirmation stands in for the list │ │41 +clear: bool = false, │ │42 │ │43 pub fn init(comptime opts: rp.RepoOpts(.xit), arena: *std.heap.ArenaAllocator, repo: *rp.Repo(.xit, opts), identity: []const u8, selected: ?u64) !Self {│ │44 const aa = arena.allocator(); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │142 } │ │143 } │ │144 │ │145 +// discard every earlier transaction by compacting the database. the patch │ │146 +// revisions events point at are unreachable from the refs, so they are named │ │147 +// as extra roots to survive the collection. │ │148 +pub fn clearHistory(io: std.Io, allocator: std.mem.Allocator, source: ui.RepoSource) !void { │ │149 + if (source.repo_kind != .xit) return error.NotFound; │ │150 + var any_repo = try rp.AnyRepo(.xit, .{}).open(io, allocator, source.localInitOpts()); │ │151 + defer any_repo.deinit(io, allocator); │ │152 + switch (any_repo) { │ │153 + inline else => |*repo| { │ │154 + const opts = repo.self_repo_opts; │ │155 + // a repo that has consumed no events has no revisions to protect │ │156 + const roots = if (evt.currentMoment(opts, repo)) |moment| │ │157 + try evt.PatchRev.gcRoots(rp.Repo(.xit, opts).DB, opts.hash, allocator, moment) │ │158 + else |_| │ │159 + &.{}; │ │160 + defer allocator.free(roots); │ │161 + _ = try repo.garbageCollect(io, allocator, .{ .extra_roots = roots }); │ │162 + }, │ │163 + } │ │164 +} │ │165 + │ │166 // keep action failures on the current tab without rebuilding a stale route. │ │167 pub fn handleRequest(allocator: std.mem.Allocator, session: *ui.Session, target: ui.RoutablePage.RepoUndoRoute) void {│ │168 perform(allocator, session, target) catch |err| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │175 // both terminal hosts resolve and authorize the request afresh before opening │ │176 // the repository; the web handler uses its matching http authorization path. │ │177 pub fn perform(allocator: std.mem.Allocator, session: *ui.Session, target: ui.RoutablePage.RepoUndoRoute) !void { │ │155 - const index = target.index orelse return error.InvalidHistoryIndex; │ │178 const identity = target.name.slice(); │ │157 - if (try session.authorize(identity, .owner) == null) return error.Forbidden; │ │179 + const source = try authorizedSource(session, identity) orelse return error.Forbidden; │ │180 const io = session.io orelse return error.NotFound; │ │159 - const source = session.local orelse blk: { │ │160 - const admin_repo = session.admin_repo orelse return error.NotFound; │ │161 - const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │162 - const pair = ui.RoutablePage.RepoIdentity.parse(identity) orelse return error.NotFound; │ │163 - const found = try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, session.page_arena, pair.owner, pair.name) orelse return error.NotFound; │ │164 - const hex = std.fmt.bytesToHex(found.event_id, .lower); │ │165 - break :blk ui.RepoSource{ .path = try std.fs.path.join(session.page_arena.allocator(), &.{ session.repos_dir orelse return error.NotFound, &hex }), .repo_kind = .xit };│ │166 - }; │ │167 - try execute(io, allocator, source, index); │ │181 + if (target.clear) │ │182 + try clearHistory(io, allocator, source) │ │183 + else │ │184 + try execute(io, allocator, source, target.index orelse return error.InvalidHistoryIndex); │ │185 try session.navigate(ui.RoutablePage.repoUndoRoute(identity, null) orelse return error.RouteTooLong); │ │186 } │ │187 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │188 +// the repo an owner may act on, or null when they may not │ │189 +fn authorizedSource(session: *ui.Session, identity: []const u8) !?ui.RepoSource { │ │190 + if (try session.authorize(identity, .owner) == null) return null; │ │191 + if (session.local) |local| return local; │ │192 + const admin_repo = session.admin_repo orelse return error.NotFound; │ │193 + const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │194 + const pair = ui.RoutablePage.RepoIdentity.parse(identity) orelse return error.NotFound; │ │195 + const found = try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, session.page_arena, pair.owner, pair.name) orelse return error.NotFound;│ │196 + const hex = std.fmt.bytesToHex(found.event_id, .lower); │ │197 + return .{ .path = try std.fs.path.join(session.page_arena.allocator(), &.{ session.repos_dir orelse return error.NotFound, &hex }), .repo_kind = .xit }; │ │198 +} │ │199 + │ │200 pub const View = struct { │ │201 box: wgt.Box(ui.Widget), │ │202 data: *const Self, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │207 │ │208 const list_max_width = 20; │ │209 const detail_min_width = 40; │ │210 + const header_index = 0; │ │211 + const content_index = 1; │ │212 │ │213 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │214 + var outer = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │215 + errdefer outer.deinit(allocator); │ │216 + │ │217 + // the sub header carries the tab's only action │ │218 + { │ │219 + var header = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │220 + errdefer header.deinit(allocator); │ │221 + const route = ui.RoutablePage.repoUndoClearRoute(data.identity) orelse return error.RouteTooLong; │ │222 + const link = try std.fmt.allocPrint(session.page_arena.allocator(), "a:{s}", .{try route.toUrl(session.page_arena)}); │ │223 + try addText(allocator, &header, "clear undo history", link, .single); │ │224 + header.getFocus().child_id = header.children.keys()[0]; │ │225 + try outer.children.put(allocator, header.getFocus().id, .{ .widget = .{ .box = header }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │226 + } │ │227 + │ │228 + if (data.clear) { │ │229 + var center = try initClearForm(allocator, data, session); │ │230 + errdefer center.deinit(allocator); │ │231 + try outer.children.put(allocator, center.getFocus().id, .{ .widget = .{ .center = center }, .rect = null, .min_size = null }); │ │232 + outer.getFocus().child_id = outer.children.keys()[content_index]; │ │233 + return .{ .box = outer, .data = data, .session = session }; │ │234 + } │ │235 + │ │236 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │237 errdefer box.deinit(allocator); │ │238 { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │270 try box.children.put(allocator, scroll.getFocus().id, .{ .widget = .{ .scroll = scroll }, .rect = null, .min_size = .{ .width = detail_min_width, .height = null } });│ │271 } │ │272 box.getFocus().child_id = box.children.keys()[0]; │ │220 - return .{ .box = box, .data = data, .session = session }; │ │273 + try outer.children.put(allocator, box.getFocus().id, .{ .widget = .{ .box = box }, .rect = null, .min_size = null }); │ │274 + outer.getFocus().child_id = outer.children.keys()[content_index]; │ │275 + return .{ .box = outer, .data = data, .session = session }; │ │276 + } │ │277 + │ │278 + // the same shape the thread views use to confirm a removal │ │279 + fn initClearForm(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !ui.widget.Center { │ │280 + var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .rounded_corners = true, .direction = .vert }); │ │281 + errdefer box.deinit(allocator); │ │282 + const route = ui.RoutablePage.repoUndoClearRoute(data.identity) orelse return error.RouteTooLong; │ │283 + box.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), "form:{s}", .{try route.toUrl(session.page_arena)}) }; │ │284 + │ │285 + var prompt = try wgt.Text.init(allocator, "are you sure?"); │ │286 + errdefer prompt.deinit(allocator); │ │287 + try box.children.put(allocator, prompt.getFocus().id, .{ .widget = .{ .text = prompt }, .rect = null, .min_size = null }); │ │288 + │ │289 + var button = try wgt.TextBox.init(allocator, "clear undo history", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │290 + errdefer button.deinit(allocator); │ │291 + button.getFocus().mode = .all; │ │292 + button.getFocus().kind = if (data.can_undo) .{ .custom = "submit" } else .text_box; │ │293 + try box.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = null }); │ │294 + box.getFocus().child_id = button.getFocus().id; │ │295 + │ │296 + return ui.widget.Center.init(allocator, .{ .box = box }); │ │297 } │ │298 │ │299 fn addText(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), text: []const u8, link: ?[]const u8, border: wgt.BorderStyle) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │304 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .text_box = row }, .rect = null, .min_size = null });│ │305 } │ │306 │ │307 + fn headerBox(self: *View) *wgt.Box(ui.Widget) { │ │308 + return &self.box.children.values()[header_index].widget.box; │ │309 + } │ │310 + fn headerActive(self: *View) bool { │ │311 + return self.box.getFocus().child_id == self.headerBox().getFocus().id; │ │312 + } │ │313 + fn contentBox(self: *View) *wgt.Box(ui.Widget) { │ │314 + return &self.box.children.values()[content_index].widget.box; │ │315 + } │ │316 fn listScroll(self: *View) *wgt.Scroll(ui.Widget) { │ │232 - return &self.box.children.values()[0].widget.scroll; │ │317 + return &self.contentBox().children.values()[0].widget.scroll; │ │318 } │ │319 fn detailScroll(self: *View) *wgt.Scroll(ui.Widget) { │ │235 - return &self.box.children.values()[1].widget.scroll; │ │320 + return &self.contentBox().children.values()[1].widget.scroll; │ │321 } │ │322 fn selected(self: *View) ?usize { │ │323 const rows = &self.listScroll().child.box; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │325 return if (index < self.data.items.len) index else null; │ │326 } │ │327 fn detailActive(self: *View) bool { │ │243 - return self.box.getFocus().child_id == self.detailScroll().getFocus().id; │ │328 + return self.contentBox().getFocus().child_id == self.detailScroll().getFocus().id;│ │329 } │ │330 │ │331 fn refreshDetail(self: *View, allocator: std.mem.Allocator) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │370 │ │371 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ │372 self.clearGrid(); │ │373 + if (self.data.clear) return try self.box.build(allocator, constraint, root_focus); │ │374 try self.refreshDetail(allocator); │ │375 const rows = &self.listScroll().child.box; │ │376 if (self.data.items.len == 0) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │383 } │ │384 for (rows.children.keys(), rows.children.values()) |id, *child| child.widget.text_box.options.border_style = if (rows.getFocus().child_id == id) .single else .hidden;│ │385 const both_fit = if (constraint.max_size.width) |width| width >= list_max_width + detail_min_width else true; │ │300 - self.box.children.values()[0].max_size = if (both_fit) .{ .width = list_max_width, .height = null } else null; │ │386 + self.contentBox().children.values()[0].max_size = if (both_fit) .{ .width = list_max_width, .height = null } else null; │ │387 const width = if (constraint.max_size.width) |value| if (both_fit) value - list_max_width else value else detail_min_width; │ │302 - self.box.children.values()[1].min_size = .{ .width = width, .height = null }; │ │388 + self.contentBox().children.values()[1].min_size = .{ .width = width, .height = null }; │ │389 for (self.detailScroll().child.box.children.values()) |*child| child.max_size = .{ .width = width -| 2, .height = null }; │ │390 try self.box.build(allocator, constraint, root_focus); │ │391 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯