╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │348 var text_box = try wgt.TextBox.init(allocator, empty_label, .{ .border = null, .wrap_kind = .none }); │ │349 errdefer text_box.deinit(allocator); │ │350 return Center.init(allocator, .{ .text_box = text_box }); │ │351 +} │ │352 + │ │353 +pub const ConfirmationButton = struct { │ │354 + label: []const u8, │ │355 + bottom_label: []const u8 = "", │ │356 + action: []const u8 = "submit", │ │357 +}; │ │358 + │ │359 +// "are you sure?" over the buttons, in a form posting to `action` │ │360 +pub fn initConfirmationForm(allocator: std.mem.Allocator, action: []const u8, buttons: []const ConfirmationButton) !Center { │ │361 + var box = try wgt.Box(Widget).init(allocator, .{ .border = null, .round_corners = true, .direction = .vert }); │ │362 + errdefer box.deinit(allocator); │ │363 + box.getFocus().kind = .{ .custom = action }; │ │364 + │ │365 + var prompt = try wgt.Text.init(allocator, "are you sure?"); │ │366 + errdefer prompt.deinit(allocator); │ │367 + try box.children.put(allocator, prompt.getFocus().id, .{ .widget = .{ .text = prompt }, .rect = null, .min_size = null }); │ │368 + │ │369 + for (buttons, 0..) |options, index| { │ │370 + var button = try wgt.TextBox.init(allocator, options.label, .{ .border = .single, .round_corners = true, .wrap_kind = .none, .bottom_label = .{ .text = options.bottom_label } });│ │371 + errdefer button.deinit(allocator); │ │372 + button.getFocus().mode = .all; │ │373 + button.getFocus().kind = .{ .custom = options.action }; │ │374 + try box.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = null }); │ │375 + if (index == 0) box.getFocus().child_id = button.getFocus().id; │ │376 + } │ │377 + │ │378 + return Center.init(allocator, .{ .box = box }); │ │379 } │ │380 │ │381 pub const FlowBox = struct { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1959 const action = try aa.print("form:{s}", .{page_url}); │ │1960 const event_name = if (data.comment_id.len == 0) thread_name else "comment"; │ │1961 var label_buf: ["remove ".len + @max(thread_name.len, "comment".len)]u8 = undefined;│ │1962 - var buttons: [2]ConfirmationButton = undefined; │ │1962 + var buttons: [2]widget.ConfirmationButton = undefined; │ │1963 buttons[0] = .{ .label = switch (confirmation_kind) { │ │1964 .remove => try std.fmt.bufPrint(&label_buf, "remove {s}", .{event_name}), │ │1965 .publish => "publish patch", │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1982 button_count += 1; │ │1983 } else unreachable; │ │1984 } │ │1985 - var center = try initConfirmationForm(allocator, action, buttons[0..button_count]); │ │1985 + var center = try widget.initConfirmationForm(allocator, action, buttons[0..button_count]);│ │1986 errdefer center.deinit(allocator); │ │1987 try stack.children.put(allocator, center.getFocus().id, .{ .center = center }); │ │1988 } else if (data.view == .new_comment or data.view == .edit_comment) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2241 try addSubmitButtonLabeled(allocator, &box, "submit"); │ │2242 return box; │ │2243 } │ │2244 - │ │2245 - const ConfirmationButton = struct { │ │2246 - label: []const u8, │ │2247 - bottom_label: []const u8 = "", │ │2248 - action: []const u8 = "submit", │ │2249 - }; │ │2250 - │ │2251 - fn initConfirmationForm(allocator: std.mem.Allocator, action: []const u8, buttons: []const ConfirmationButton) !Center { │ │2252 - var box = try wgt.Box(Widget).init(allocator, .{ .border = null, .round_corners = true, .direction = .vert }); │ │2253 - errdefer box.deinit(allocator); │ │2254 - box.getFocus().kind = .{ .custom = action }; │ │2244 │ │2256 - var prompt = try wgt.Text.init(allocator, "are you sure?"); │ │2257 - errdefer prompt.deinit(allocator); │ │2258 - try box.children.put(allocator, prompt.getFocus().id, .{ .widget = .{ .text = prompt }, .rect = null, .min_size = null }); │ │2259 - │ │2260 - for (buttons, 0..) |options, index| { │ │2261 - var button = try wgt.TextBox.init(allocator, options.label, .{ .border = .single, .round_corners = true, .wrap_kind = .none, .bottom_label = .{ .text = options.bottom_label } });│ │2262 - errdefer button.deinit(allocator); │ │2263 - button.getFocus().mode = .all; │ │2264 - button.getFocus().kind = .{ .custom = options.action }; │ │2265 - try box.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = null }); │ │2266 - if (index == 0) box.getFocus().child_id = button.getFocus().id; │ │2267 - } │ │2268 - │ │2269 - return Center.init(allocator, .{ .box = box }); │ │2270 - } │ │2271 - │ │2245 // a form's submit button, then a spacer absorbing the leftover │ │2246 // min-height the box hands its last child, so the button keeps its │ │2247 // natural height │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 .@"repo/new" => handleRepoNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store), │ │171 .@"user/new" => handleUserNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store), │ │172 .user => handleUserSettings(io, request, allocator, base, server.admin_repo_path, server.users_dir, server.session_store), │ │173 - .addkey => handleUserKey(io, request, allocator, base, server.admin_repo_path, server.users_dir, server.session_store, .add), │ │174 - .removekey => handleUserKey(io, request, allocator, base, server.admin_repo_path, server.users_dir, server.session_store, .remove),│ │173 + .addkey => handleUserKeyAdd(io, request, allocator, base, server.admin_repo_path, server.users_dir, server.session_store), │ │174 + .removekey => handleUserKeyRemove(io, request, allocator, base, server.admin_repo_path, server.users_dir, server.session_store), │ │175 .settings => handleRepoSettings(io, request, allocator, path, host), │ │176 .grant => handleRoles(io, request, allocator, base, host, .grant), │ │177 .revoke => handleRoles(io, request, allocator, base, host, .revoke), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │897 }); │ │898 } │ │899 │ │900 -// add or remove one of the logged-in user's ssh keys, then return to the ssh│ │901 -// keys tab that posted the form, which a failure also goes back to. │ │902 -fn handleUserKey( │ │900 +// add one of the logged-in user's ssh keys, then return to the ssh keys tab │ │901 +// that posted the form, which a failure also goes back to. │ │902 +fn handleUserKeyAdd( │ │903 io: std.Io, │ │904 request: *std.http.Server.Request, │ │905 allocator: std.mem.Allocator, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │907 admin_repo_path: []const u8, │ │908 users_dir: []const u8, │ │909 session_store: SessionStore, │ │910 - action: evt.KeyAction, │ │910 ) !void { │ │911 const user_id = requestUserId(request, session_store) orelse return respondLoginRequired(request);│ │912 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │918 { │ │919 var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path });│ │920 defer admin_repo.deinit(io, allocator); │ │922 - evt.changeUserKey(io, allocator, users_dir, &admin_repo, &user_id, action, key) catch |err| switch (err) {│ │921 + evt.addUserKey(io, allocator, users_dir, &admin_repo, &user_id, key) catch |err| switch (err) { │ │922 error.NotFound => return respondLoginRequired(request), │ │923 else => { │ │924 const failure = ui.Session.FormFeedback.KeyFailure.fromError(err) orelse return err; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │930 try request.respond("", .{ │ │931 .status = .see_other, │ │932 .extra_headers = &.{.{ .name = "location", .value = form_location }}, │ │933 + }); │ │934 +} │ │935 + │ │936 +// remove the logged-in user's ssh key the confirmation url names, then return │ │937 +// to the ssh keys tab │ │938 +fn handleUserKeyRemove( │ │939 + io: std.Io, │ │940 + request: *std.http.Server.Request, │ │941 + allocator: std.mem.Allocator, │ │942 + base: []const u8, │ │943 + admin_repo_path: []const u8, │ │944 + users_dir: []const u8, │ │945 + session_store: SessionStore, │ │946 +) !void { │ │947 + const user_id = requestUserId(request, session_store) orelse return respondLoginRequired(request); │ │948 + const route = ui.RoutablePage.fromUrl(base) orelse return respondKeyNotFound(request); │ │949 + const tab = route.userTab(); │ │950 + if (tab.view != .keys or tab.key.len != evt.User.fingerprint_len) return respondKeyNotFound(request); │ │951 + │ │952 + { │ │953 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }); │ │954 + defer admin_repo.deinit(io, allocator); │ │955 + evt.removeUserKey(io, allocator, users_dir, &admin_repo, &user_id, &tab.key.bytes) catch |err| switch (err) {│ │956 + error.NotFound => return respondLoginRequired(request), │ │957 + else => return err, │ │958 + }; │ │959 + } │ │960 + │ │961 + var arena = std.heap.ArenaAllocator.init(allocator); │ │962 + defer arena.deinit(); │ │963 + const location = try route.withUserTab(.{ .view = .keys }).toUrl(&arena); │ │964 + try request.respond("", .{ │ │965 + .status = .see_other, │ │966 + .extra_headers = &.{.{ .name = "location", .value = location }}, │ │967 }); │ │968 } │ │969 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │970 +fn respondKeyNotFound(request: *std.http.Server.Request) !void { │ │971 + try request.respond("ssh key not found", .{ │ │972 + .status = .not_found, │ │973 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }},│ │974 + }); │ │975 +} │ │976 + │ │977 // create a thread in the repo the form's page names and redirect to it. │ │978 fn handleThreadNew( │ │979 io: std.Io, │ ╰─────────────────────────────────────────────────────────────────────────────────────╯