╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │326 .patch_role = repo.event.patch_role, │ │327 .hash_kind = opened.self_repo_opts.hash, │ │328 .view = if (route == .repo_roles) .roles else .settings, │ │329 - .roles = try Roles.init(io, gpa, arena, session.haxy_moment orelse return error.NoMoment, session.users_dir orelse return error.NoMoment, repo_identity.identity, &owner_id, &repo_id, roles_start), │ │329 + .roles = try Roles.init(io, gpa, arena, session.haxy_moment orelse return error.NoMoment, session.users_dir orelse return error.NoMoment, repo_identity.identity, &owner_id, &repo_id, roles_route.start, if (roles_route.confirm) |action| .{ .action = action, .name = roles_route.user.slice() } else null),│ │330 }; │ │331 } │ │332 // tabs switch in-page, so every tab's data is read here │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Roles.zig│ ╰─────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │21 start: usize, │ │22 next_start: ?usize = null, │ │23 rows: []const Row, │ │24 +// the role change the route asks to confirm, which stands in for the list│ │25 +confirm: ?Confirm = null, │ │26 │ │27 const Self = @This(); │ │28 + │ │29 +pub const Confirm = struct { action: Action, name: []const u8 }; │ │30 │ │31 pub const Row = struct { │ │32 name: []const u8, │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │47 owner_id: *const [evt.event_id_size]u8, │ │48 repo_id: *const [evt.event_id_size]u8, │ │49 start: usize, │ │50 + confirm: ?Confirm, │ │51 ) !Self { │ │52 const aa = arena.allocator(); │ │53 const grants = try evt.readRepoGrants(io, allocator, arena, users_dir, owner_id, repo_id);│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │70 } │ │71 try rows.append(aa, .{ .name = user.event.name, .role = grant.role }); │ │72 } │ │68 - return .{ .identity = try aa.dupe(u8, identity), .start = start, .next_start = next_start, .rows = rows.items }; │ │73 + return .{ │ │74 + .identity = try aa.dupe(u8, identity), │ │75 + .start = start, │ │76 + .next_start = next_start, │ │77 + .rows = rows.items, │ │78 + .confirm = if (confirm) |value| .{ .action = value.action, .name = try aa.dupe(u8, value.name) } else null, │ │79 + }; │ │80 +} │ │81 + │ │82 +// the confirmation of taking `name`'s role away │ │83 +fn confirmRoute(identity: []const u8, start: usize, name: []const u8, action: Action) !ui.RoutablePage { │ │84 + var route = ui.RoutablePage.repoRolesRoute(identity, start) orelse return error.RouteTooLong; │ │85 + route.repo_roles.user = ui.RoutablePage.Array(evt.User.name_max_len).from(name) orelse return error.RouteTooLong;│ │86 + route.repo_roles.confirm = action; │ │87 + return route; │ │88 } │ │89 │ │90 // act on `name`'s role in the repo `identity` names as `actor`, one of its │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │148 const add_index = 0; │ │149 │ │150 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │151 + const aa = session.page_arena.allocator(); │ │152 + if (data.confirm) |confirm| { │ │153 + const url = try (try confirmRoute(data.identity, data.start, confirm.name, confirm.action)).toUrl(session.page_arena); │ │154 + var center = try ui.widget.initConfirmationForm(allocator, try aa.print("form:{s}", .{url}), &.{.{ │ │155 + .label = if (confirm.action == .demote) "demote to collaborator" else "remove user", │ │156 + .bottom_label = try aa.print(" {s} ", .{confirm.name}), │ │157 + }}); │ │158 + errdefer center.deinit(allocator); │ │159 + return .{ │ │160 + .scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .center = center }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }),│ │161 + .data = data, │ │162 + .session = session, │ │163 + }; │ │164 + } │ │165 + │ │166 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert }); │ │167 errdefer box.deinit(allocator); │ │134 - const aa = session.page_arena.allocator(); │ │168 const route = ui.RoutablePage.repoRolesRoute(data.identity, data.start) orelse return error.RouteTooLong; │ │169 const url = try route.toUrl(session.page_arena); │ │170 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │202 name.options.bottom_label.text = if (row.role == .owner) " owner " else " collaborator "; │ │203 try row_box.children.put(allocator, name.getFocus().id, .{ .widget = .{ .text_box = name }, .rect = null, .min_size = null }); │ │204 } │ │172 - try addButton(allocator, &row_box, "remove", try aa.print("submit:{s}/revoke", .{row_url})); │ │173 - if (row.role == .owner) │ │174 - try addButton(allocator, &row_box, "demote to collaborator", try aa.print("submit:{s}/demote", .{row_url})) │ │175 - else │ │176 - try addButton(allocator, &row_box, "promote to owner", try aa.print("submit:{s}/promote", .{row_url})); │ │205 + // taking a role away asks first │ │206 + if (row.role == .owner) { │ │207 + const demote_route = try confirmRoute(data.identity, data.start, row.name, .demote); │ │208 + try addButton(allocator, &row_box, "demote to collaborator", try aa.print("a:{s}", .{try demote_route.toUrl(session.page_arena)})); │ │209 + } else try addButton(allocator, &row_box, "promote to owner", try aa.print("submit:{s}/promote", .{row_url})); │ │210 + { │ │211 + const revoke_route = try confirmRoute(data.identity, data.start, row.name, .revoke); │ │212 + var remove = try wgt.TextBox.init(allocator, "✕", .{ .border = .single, .round_corners = true, .wrap_kind = .none }); │ │213 + errdefer remove.deinit(allocator); │ │214 + remove.getFocus().mode = .all; │ │215 + remove.getFocus().kind = .{ .custom = try aa.print("a:{s}", .{try revoke_route.toUrl(session.page_arena)}) }; │ │216 + try row_box.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } });│ │217 + } │ │218 row_box.getFocus().child_id = row_box.children.keys()[0]; │ │219 try box.children.put(allocator, row_box.getFocus().id, .{ .widget = .{ .box = row_box }, .rect = null, .min_size = null }); │ │220 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │264 │ │265 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │266 self.clearGrid(); │ │267 + if (self.data.confirm == null) try self.labelAddInput(root_focus); │ │268 + // the window's height as a minimum centers rows that fit │ │269 + try self.scroll.build(allocator, .{ │ │270 + .min_size = .{ .width = constraint.min_size.width, .height = constraint.max_size.height orelse constraint.min_size.height },│ │271 + .max_size = constraint.max_size, │ │272 + }, root_focus); │ │273 + } │ │274 + │ │275 + // label the add input for this frame and register it for the web form │ │276 + // handling │ │277 + fn labelAddInput(self: *View, root_focus: *Focus) !void { │ │278 const name = self.nameInput(); │ │279 const failure = if (self.session.formFeedback(.repo_roles)) |saved| saved.failure else null; │ │280 name.options.top_label.text = if (failure) |value| switch (value) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │282 .already_added => " add a collaborator (already added) ", │ │283 } else " add a collaborator "; │ │284 name.options.bottom_label.text = if (root_focus.grandchild_id == name.getFocus().id) " press enter " else ""; │ │233 - // the web form handling finds the input by focus id │ │285 try self.session.text_inputs.put(self.session.arena.allocator(), name.getFocus().id, name); │ │235 - // the window's height as a minimum centers rows that fit │ │236 - try self.scroll.build(allocator, .{ │ │237 - .min_size = .{ .width = constraint.min_size.width, .height = constraint.max_size.height orelse constraint.min_size.height },│ │238 - .max_size = constraint.max_size, │ │239 - }, root_focus); │ │286 } │ │287 │ │288 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │289 + if (self.data.confirm) |confirm| { │ │290 + const button_id = self.contentBox().getFocus().child_id orelse return; │ │291 + if (inp.activated(root_focus, button_id, key)) try self.act(allocator, confirm.action, confirm.name);│ │292 + return; │ │293 + } │ │294 // the add row keeps every key but the vertical ones, which move between rows │ │295 if (self.addActive() and inp.vertDirection(key) == .none) { │ │296 if (key == .enter) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │315 if (current < offset or current - offset >= self.data.rows.len) return; │ │316 const row = self.data.rows[current - offset]; │ │317 const row_box = &rows.children.values()[current].widget.box; │ │318 + // remove and demote are links to their confirmations, which the host follows │ │319 + if (row.role == .owner) return; │ │320 const button_id = row_box.getFocus().child_id orelse return; │ │268 - if (!inp.activated(root_focus, button_id, key)) return; │ │269 - const action: Action = switch (row_box.children.getIndex(button_id) orelse return) { │ │270 - 1 => .revoke, │ │271 - 2 => if (row.role == .owner) .demote else .promote, │ │272 - else => return, │ │273 - }; │ │274 - try self.act(allocator, action, row.name); │ │321 + if (row_box.children.getIndex(button_id) != 1 or !inp.activated(root_focus, button_id, key)) return;│ │322 + try self.act(allocator, .promote, row.name); │ │323 }, │ │324 } │ │325 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯