╭──────────╮ │← previous│ ╰──────────╯ ╭ removed ────────────╮ │src/ui/UserLogout.zig│ ╰─────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │73 new_repo: ui.NewRepo.View, │ │74 new_user: ui.NewUser.View, │ │75 user_login: ui.UserLogin.View, │ │76 - user_logout: ui.UserLogout.View, │ │76 + user_settings: ui.UserSettings.View, │ │77 footer: Footer, │ │78 │ │79 pub fn deinit(self: *Widget, allocator: std.mem.Allocator) void {│ ╰─────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │115 // whether moving up should return to the page header │ │116 pub fn atTop(self: *Widget, root_focus: *Focus) bool { │ │117 return switch (self.*) { │ │118 - inline .home_about, .home_users, .user_repos, .flow_box_scroll, .new_repo, .new_user, .user_login, .user_logout, .quit, .repo_files, .repo_commits, .diff_view, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_settings => |*view| view.atTop(), │ │118 + inline .home_about, .home_users, .user_repos, .flow_box_scroll, .new_repo, .new_user, .user_login, .user_settings, .quit, .repo_files, .repo_commits, .diff_view, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_settings => |*view| view.atTop(),│ │119 inline .repo_patch_detail => |*view| view.atTop(root_focus), │ │120 else => false, │ │121 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1471 } │ │1472 }; │ │1473 │ │1474 +// keep `rect`, in a centered widget's space, in view within its scroll │ │1475 +pub fn scrollToCenteredRect(scroll: *wgt.Scroll(Widget), rect: layout.IRect) void { │ │1476 + const center = &scroll.child.center; │ │1477 + const center_grid = center.getGrid() orelse return; │ │1478 + const child_grid = center.child.getGrid() orelse return; │ │1479 + const offset_y: isize = @intCast((center_grid.size.height -| child_grid.size.height) / 2);│ │1480 + scroll.scrollToRect(.{ .x = rect.x, .y = rect.y + offset_y, .size = rect.size }); │ │1481 +} │ │1482 + │ │1483 pub const Center = struct { │ │1484 focus: *Focus, │ │1485 grid: ?Grid, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ────────────────╮ │src/ui/UserSettings.zig│ ╰───────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │156 switch (host) { │ │157 .server => |server| { │ │158 // "repo/new" and "user/new" come before "new", which would claim them │ │159 - const PostRoute = enum { login, logout, @"repo/new", @"user/new", repo, new, edit, remove, open, close, resolve, publish, merge, squash, attach, undo, clear }; │ │159 + const PostRoute = enum { login, logout, @"repo/new", @"user/new", user, repo, new, edit, remove, open, close, resolve, publish, merge, squash, attach, undo, clear };│ │160 inline for (@typeInfo(PostRoute).@"enum".field_names) |name| { │ │161 const suffix = "/" ++ name; │ │162 if (std.mem.endsWith(u8, path, suffix)) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │166 .logout => handleLogout(request, base, server.session_store), │ │167 .@"repo/new" => handleRepoNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store),│ │168 .@"user/new" => handleUserNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store),│ │169 + .user => handleUserSettings(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store), │ │170 .repo => handleRepoSettings(io, request, allocator, path, base, host), │ │171 .new => handleNew(io, request, allocator, base, host), │ │172 .edit => handleEdit(io, request, allocator, base, host), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │773 }); │ │774 } │ │775 │ │776 +// change the logged-in user's settings, then return to the page that posted │ │777 +// the form, which a failure also goes back to. │ │778 +fn handleUserSettings( │ │779 + io: std.Io, │ │780 + request: *std.http.Server.Request, │ │781 + allocator: std.mem.Allocator, │ │782 + form_location: []const u8, │ │783 + admin_repo_path: []const u8, │ │784 + users_dir: []const u8, │ │785 + session_store: SessionStore, │ │786 +) !void { │ │787 + const user_id = requestUserId(request, session_store) orelse return respondLoginRequired(request); │ │788 + │ │789 + const body = try readFormBody(request, allocator); │ │790 + defer allocator.free(body); │ │791 + const email = (try parseFormField(allocator, body, "email")) orelse try allocator.dupe(u8, ""); │ │792 + defer allocator.free(email); │ │793 + const current_password = (try parseFormField(allocator, body, "current_password")) orelse try allocator.dupe(u8, ""); │ │794 + defer allocator.free(current_password); │ │795 + const new_password = (try parseFormField(allocator, body, "new_password")) orelse try allocator.dupe(u8, ""); │ │796 + defer allocator.free(new_password); │ │797 + const new_password_again = (try parseFormField(allocator, body, "new_password_again")) orelse try allocator.dupe(u8, ""); │ │798 + defer allocator.free(new_password_again); │ │799 + │ │800 + { │ │801 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }); │ │802 + defer admin_repo.deinit(io, allocator); │ │803 + evt.updateUser(io, allocator, users_dir, &admin_repo, &user_id, email, current_password, new_password, new_password_again) catch |err| switch (err) { │ │804 + error.NotFound => return respondLoginRequired(request), │ │805 + else => { │ │806 + const failure = ui.Session.FormFeedback.UserFailure.fromError(err) orelse return err; │ │807 + return respondFormFailure(request, allocator, session_store, form_location, .{ .user_settings = .{ .failure = failure, .fields = .{ .email = email } } });│ │808 + }, │ │809 + }; │ │810 + } │ │811 + │ │812 + try request.respond("", .{ │ │813 + .status = .see_other, │ │814 + .extra_headers = &.{.{ .name = "location", .value = form_location }}, │ │815 + }); │ │816 +} │ │817 + │ │818 // create a thread in the repo the form's page names and redirect to it. │ │819 fn handleThreadNew( │ │820 io: std.Io, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯