From c8959e7d472036fc72374f367411a4e44ca96198 Mon Sep 17 00:00:00 2001 From: Neon Date: Mon, 13 Jul 2026 21:38:28 -0400 Subject: [PATCH 01/12] update: admin inspector + admin block helper + hammer + flippered Signed-off-by: Neon --- app/assets/stylesheets/application.scss | 2 ++ .../stylesheets/components/_inspector.scss | 18 +++++++++++++++ .../components/_username_link.scss | 10 +++++++++ app/assets/stylesheets/utils.scss | 10 +++++++-- app/components/posts/card_component.html.erb | 8 +++---- app/helpers/application_helper.rb | 13 +++++++---- app/views/comments/_comment.html.erb | 2 +- app/views/daily_rolls/leaderboard.html.erb | 4 ++-- app/views/leaderboard/_row.html.erb | 3 +-- app/views/notifications/inbox/_actor.html.erb | 2 +- app/views/projects/_ship_card.html.erb | 6 ++--- .../projects/_ship_decision_card.html.erb | 6 ++--- app/views/projects/show.html.erb | 6 +++-- app/views/shared/_inspector.html.erb | 22 +++++++++---------- app/views/shared/_username_link.html.erb | 9 ++++++++ app/views/votes/_project_card.html.erb | 3 ++- config/initializers/flipper.rb | 1 + 17 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 app/assets/stylesheets/components/_inspector.scss create mode 100644 app/assets/stylesheets/components/_username_link.scss create mode 100644 app/views/shared/_username_link.html.erb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d43ed8bec..3542f535d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -29,6 +29,8 @@ @use "components/tooltip"; @use "components/like_button"; @use "components/welcome_tour"; +@use "components/inspector"; +@use "components/username_link"; @use "components/markdown_content"; @use "components/guide_content"; @use "components/guide_code"; diff --git a/app/assets/stylesheets/components/_inspector.scss b/app/assets/stylesheets/components/_inspector.scss new file mode 100644 index 000000000..42ab4281e --- /dev/null +++ b/app/assets/stylesheets/components/_inspector.scss @@ -0,0 +1,18 @@ +.inspector { + display: inline-block; + overflow-x: auto; + max-width: 1000px; + + &__summary { + cursor: pointer; + font-family: var(--font-family-text); + font-weight: 600; + } + + &__body { + margin: 0; + padding: 8px; + font-size: 0.75rem; + color: var(--color-space-text); + } +} diff --git a/app/assets/stylesheets/components/_username_link.scss b/app/assets/stylesheets/components/_username_link.scss new file mode 100644 index 000000000..5c8e7ba82 --- /dev/null +++ b/app/assets/stylesheets/components/_username_link.scss @@ -0,0 +1,10 @@ +.username-link__admin { + margin-left: var(--space-xxxs); + font-size: 0.85em; + text-decoration: none; + opacity: 0.85; + + &:hover { + opacity: 1; + } +} diff --git a/app/assets/stylesheets/utils.scss b/app/assets/stylesheets/utils.scss index 0026723ff..b1e6ae146 100644 --- a/app/assets/stylesheets/utils.scss +++ b/app/assets/stylesheets/utils.scss @@ -1,5 +1,5 @@ :root { - --color-admin: #ff9100; + --color-admin: var(--color-brand-orange); --color-dev: #52d831; } @@ -24,7 +24,6 @@ } .admin.tools-do { - background-color: color-mix(in srgb, var(--color-admin) 20%, transparent); border-color: var(--color-admin); } @@ -32,3 +31,10 @@ background-color: color-mix(in srgb, var(--color-dev) 20%, transparent); border-color: var(--color-dev); } + +.tools-do--inline { + display: inline-flex; + align-items: center; + padding: 1px 4px; + border-radius: 4px; +} diff --git a/app/components/posts/card_component.html.erb b/app/components/posts/card_component.html.erb index 305427767..04d46f93b 100644 --- a/app/components/posts/card_component.html.erb +++ b/app/components/posts/card_component.html.erb @@ -17,9 +17,8 @@
<%= inline_svg_tag "icons/repost.svg", class: "feed-post-card__reposted-by-icon" %> Reposted by - <%= link_to "@#{post.user.display_name.presence || "stardancer"}", - profile_path(post.user.display_name), - class: "feed-post-card__reposted-by-link" %><%= render StreakBadgeComponent.new(user: post.user) %><%= " ⚡" if post.user.admin? %> + <%= render "shared/username_link", user: post.user, link_class: "feed-post-card__reposted-by-link", + text: "@#{post.user.display_name.presence || "stardancer"}" %>
<% end %> @@ -49,8 +48,7 @@
- <%= link_to "@#{comment.user.display_name}", profile_path(comment.user.display_name), class: "devlog-comment__author" %><%= render StreakBadgeComponent.new(user: comment.user) %><%= " ⚡" if comment.user.admin? %> + <%= render "shared/username_link", user: comment.user, link_class: "devlog-comment__author" %> diff --git a/app/views/daily_rolls/leaderboard.html.erb b/app/views/daily_rolls/leaderboard.html.erb index 1171c8889..f9badab66 100644 --- a/app/views/daily_rolls/leaderboard.html.erb +++ b/app/views/daily_rolls/leaderboard.html.erb @@ -54,7 +54,7 @@
"> <%= index.zero? ? "★" : "##{index + 1}" %> <%= image_tag roll.user.avatar, alt: "", class: "rng-board__pedestal-avatar" %> - <%= link_to roll.user.display_name, profile_path(roll.user.display_name), class: "rng-board__pedestal-name" %> + <%= render "shared/username_link", user: roll.user, link_class: "rng-board__pedestal-name", text: roll.user.display_name, badge: false %> <% if current_user&.id == roll.user_id %>you<% end %> <%= formatted_roll_value(roll.total) %>
@@ -99,7 +99,7 @@ <% if @record %>

All-time best: - <%= link_to @record.user.display_name, profile_path(@record.user.display_name), class: "rng-board__alltime-name" %> + <%= render "shared/username_link", user: @record.user, link_class: "rng-board__alltime-name", text: @record.user.display_name, badge: false %> <%= formatted_roll_value(@record.total) %> on <%= link_to rng_date_label(@record.rolled_on, today: @today), rng_date_path(@record.rolled_on, today: @today), class: "rng-board__alltime-date" %>

diff --git a/app/views/leaderboard/_row.html.erb b/app/views/leaderboard/_row.html.erb index cc888e93c..8af41b95c 100644 --- a/app/views/leaderboard/_row.html.erb +++ b/app/views/leaderboard/_row.html.erb @@ -10,8 +10,7 @@

- <%= link_to user.display_name, profile_path(user.display_name), - class: "leaderboard-row__name-link" %><%= render StreakBadgeComponent.new(user: user) %><%= " ⚡" if user.admin? %> + <%= render "shared/username_link", user: user, link_class: "leaderboard-row__name-link", text: user.display_name %> <% if is_current_user %> YOU <% end %> diff --git a/app/views/notifications/inbox/_actor.html.erb b/app/views/notifications/inbox/_actor.html.erb index 9e3edb516..004cb24c3 100644 --- a/app/views/notifications/inbox/_actor.html.erb +++ b/app/views/notifications/inbox/_actor.html.erb @@ -1,7 +1,7 @@ <%# The notification's actor as a profile link, or a fallback label when the actor is missing (e.g. a deleted account). Pass `fallback:` to override. %> <% if notification.actor %> - <%= link_to "@#{notification.actor.display_name}", user_path(notification.actor), class: "notifications-item__actor" %><%= render StreakBadgeComponent.new(user: notification.actor) %><%= " ⚡" if notification.actor.admin? %> + <%= render "shared/username_link", user: notification.actor, link_class: "notifications-item__actor" %> <% else %> <%= local_assigns.fetch(:fallback, "Someone") %> <% end %> diff --git a/app/views/projects/_ship_card.html.erb b/app/views/projects/_ship_card.html.erb index a5a649da6..51405bfcd 100644 --- a/app/views/projects/_ship_card.html.erb +++ b/app/views/projects/_ship_card.html.erb @@ -89,10 +89,8 @@

By <% members.each_with_index do |m, i| %> - <%= " and " if i > 0 && i == members.length - 1 %><%= link_to "@#{m.display_name}", profile_path(m.display_name), class: "project-show__author", data: { turbo_frame: "_top", turbo_prefetch: "true" } %><%= render StreakBadgeComponent.new(user: m) %><%= " ⚡" if m.admin? %><%= "," if i < members.length - 2 && members.length > 2 %> + <%= " and " if i > 0 && i == members.length - 1 %><%= render "shared/username_link", user: m, link_class: "project-show__author", + data: { turbo_frame: "_top", turbo_prefetch: "true" } %><%= "," if i < members.length - 2 && members.length > 2 %> <% end %>

@@ -407,7 +408,8 @@

By <% members.each_with_index do |m, i| %> - <%= " and " if i > 0 && i == members.length - 1 %><%= link_to "@#{m.display_name}", profile_path(m.display_name), class: "project-show__author", data: { turbo_frame: "_top", turbo_prefetch: "true" } %><%= render StreakBadgeComponent.new(user: m) %><%= " ⚡" if m.admin? %><%= "," if i < members.length - 2 && members.length > 2 %> + <%= " and " if i > 0 && i == members.length - 1 %><%= render "shared/username_link", user: m, link_class: "project-show__author", + data: { turbo_frame: "_top", turbo_prefetch: "true" } %><%= "," if i < members.length - 2 && members.length > 2 %> <% end %>

diff --git a/app/views/shared/_inspector.html.erb b/app/views/shared/_inspector.html.erb index 82fcf6994..6f00a46e6 100644 --- a/app/views/shared/_inspector.html.erb +++ b/app/views/shared/_inspector.html.erb @@ -1,12 +1,12 @@ -<% if current_user&.admin? %> -
- - <% if record.is_a?(ActiveRecord::Base) %> - <%= record.model_name.human %> record #<%= record.id %> - <% else %> - inspect <%= defined?(thing) ? thing : 'object' %> - <% end %> - -
<%== awesome_inspect(record) %>
-
+<%= admin_tool do %> +
+ + <% if record.is_a?(ActiveRecord::Base) %> + <%= record.model_name.human %> record #<%= record.id %> + <% else %> + inspect <%= defined?(thing) ? thing : 'object' %> + <% end %> + +
<%== awesome_inspect(record) %>
+
<% end %> diff --git a/app/views/shared/_username_link.html.erb b/app/views/shared/_username_link.html.erb new file mode 100644 index 000000000..ffa404385 --- /dev/null +++ b/app/views/shared/_username_link.html.erb @@ -0,0 +1,9 @@ +<%# locals: user:, link_class:, text: nil, data: nil, badge: true + Shared "@handle" profile link + streak badge + admin lightning marker, + used everywhere a username shows up in a byline (comments, feed cards, + leaderboards, notifications). Pass badge: false to omit the streak + badge / admin marker (e.g. for reviewer/system bylines). %> +<%= link_to local_assigns[:text] || "@#{user.display_name}", + profile_path(user.display_name), + class: link_class, + data: local_assigns[:data] %><% if local_assigns.fetch(:badge, true) %><%= render StreakBadgeComponent.new(user: user) %><%= " ⚡" if user.admin? %><% end %><%= admin_tool(compact: true) do %><%= link_to "🔨", admin_user_path(user), class: "username-link__admin", "aria-label": "View #{user.display_name} in admin" %><% end %> diff --git a/app/views/votes/_project_card.html.erb b/app/views/votes/_project_card.html.erb index 4d3af836e..47f2f5315 100644 --- a/app/views/votes/_project_card.html.erb +++ b/app/views/votes/_project_card.html.erb @@ -37,7 +37,8 @@

By <% members.each_with_index do |m, i| %> - <%= " and " if i > 0 && i == members.length - 1 %><%= link_to "@#{m.display_name}", profile_path(m.display_name), class: "project-show__author", data: { turbo_frame: "_top", turbo_prefetch: "true" } %><%= render StreakBadgeComponent.new(user: m) %><%= " ⚡" if m.admin? %><%= "," if i < members.length - 2 && members.length > 2 %> + <%= " and " if i > 0 && i == members.length - 1 %><%= render "shared/username_link", user: m, link_class: "project-show__author", + data: { turbo_frame: "_top", turbo_prefetch: "true" } %><%= "," if i < members.length - 2 && members.length > 2 %> <% end %>

diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index 52257754a..825baf52d 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -45,6 +45,7 @@ hardware_to_outpost disable_internal_sw_dash_reviews sharable_purchase + shigimi_eyes ].each { |flag| Flipper.add(flag) } end rescue StandardError => e From 3506af6da9080823ded7444330ae87cfcbd518fa Mon Sep 17 00:00:00 2001 From: Neon Date: Tue, 14 Jul 2026 21:34:01 -0400 Subject: [PATCH 02/12] update: hammer on navbar for logged in user + pinned cu on /admin/users Signed-off-by: Neon --- app/assets/stylesheets/components/_sidebar.scss | 13 +++++++++++++ app/assets/stylesheets/pages/shop/_suggestions.scss | 5 +++-- app/components/sidebar_component.html.erb | 9 ++++++--- app/controllers/admin/users_controller.rb | 5 ++++- app/helpers/application_helper.rb | 3 ++- app/views/admin/users/index.html.erb | 5 ++++- app/views/shop/suggestions/_card.html.erb | 7 +++---- 7 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/components/_sidebar.scss b/app/assets/stylesheets/components/_sidebar.scss index 52ff0ad30..a03bdaeb8 100644 --- a/app/assets/stylesheets/components/_sidebar.scss +++ b/app/assets/stylesheets/components/_sidebar.scss @@ -325,10 +325,23 @@ button.sidebar__nav-link { overflow: hidden; } +.sidebar__user-handle-row { + align-items: center; + display: flex; + gap: var(--space-xxs); + min-width: 0; + + .tools-do--inline { + flex-shrink: 0; + } +} + .sidebar__user-meta-handle { + flex-shrink: 1; font-weight: 700; font-size: var(--font-size-m); color: inherit; + min-width: 0; text-decoration: none; overflow: hidden; text-overflow: ellipsis; diff --git a/app/assets/stylesheets/pages/shop/_suggestions.scss b/app/assets/stylesheets/pages/shop/_suggestions.scss index 63556038d..eb2a05fe3 100644 --- a/app/assets/stylesheets/pages/shop/_suggestions.scss +++ b/app/assets/stylesheets/pages/shop/_suggestions.scss @@ -342,9 +342,10 @@ } } - &__admin-strip { + // Border/color come from the shared .admin.tools-do box (admin_tool helper); + // this only adds the card-specific layout and corner radius. + &__admin-strip.admin.tools-do { background: rgba(255, 176, 122, 0.05); - border: 2px dashed rgba(255, 176, 122, 0.4); border-radius: 0 0 12px 12px; display: flex; flex-direction: column; diff --git a/app/components/sidebar_component.html.erb b/app/components/sidebar_component.html.erb index 059cd7aae..2cded50c1 100644 --- a/app/components/sidebar_component.html.erb +++ b/app/components/sidebar_component.html.erb @@ -68,9 +68,12 @@ <%= helpers.stardust_icon(extra_class: "sidebar__user-balance-icon") %> <%= number_with_delimiter(user.cached_balance) %>
- <%= link_to profile_path(user.display_name), class: "sidebar__user-meta-handle", data: { turbo: true } do %> - @<%= user.display_name %> - <% end %> +
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d7bcbde89..24c4ee8db 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -46,8 +46,13 @@ def onboarding_count_up(n) # compact: true renders an inline with no dashed box, for markers # that sit inside a line of text (e.g. a username byline) rather than # wrapping a standalone block of debug content. + # + # Uses real_user rather than current_user while impersonating, so the + # admin retains their own debug tooling instead of losing it to whatever + # role the impersonated account happens to have. def admin_tool(compact: false, extra_class: nil, &block) - return unless current_user&.admin? && Flipper.enabled?(:shigimi_eyes, current_user) + acting_user = impersonating? ? real_user : current_user + return unless acting_user&.admin? && Flipper.enabled?(:shigimi_eyes, acting_user) classes = [ "admin", "tools-do" ] classes << "tools-do--inline" if compact diff --git a/app/views/shared/_username_link.html.erb b/app/views/shared/_username_link.html.erb index ffa404385..0eabc95c0 100644 --- a/app/views/shared/_username_link.html.erb +++ b/app/views/shared/_username_link.html.erb @@ -6,4 +6,4 @@ <%= link_to local_assigns[:text] || "@#{user.display_name}", profile_path(user.display_name), class: link_class, - data: local_assigns[:data] %><% if local_assigns.fetch(:badge, true) %><%= render StreakBadgeComponent.new(user: user) %><%= " ⚡" if user.admin? %><% end %><%= admin_tool(compact: true) do %><%= link_to "🔨", admin_user_path(user), class: "username-link__admin", "aria-label": "View #{user.display_name} in admin" %><% end %> + data: local_assigns[:data] %><% if local_assigns.fetch(:badge, true) %><%= render StreakBadgeComponent.new(user: user) %><%= " ⚡" if user.admin? %><% end %><%= admin_tool(compact: true) do %><%= link_to "🔨", admin_user_path(user), class: "username-link__admin", "aria-label": "View #{user.display_name} in admin" %><% end %><% if admin_policy(user).impersonate? %><%= admin_tool(compact: true) do %><%= link_to "👁️", admin_user_impersonation_path(user), class: "username-link__admin", data: { turbo_method: :post, turbo_confirm: "Are you sure you want to impersonate #{user.display_name}? You will see the site as they do." }, "aria-label": "Impersonate #{user.display_name}" %><% end %><% end %> From 4b01f4ef64b51d0c299fa98e1b9cf28c5300856c Mon Sep 17 00:00:00 2001 From: Neon Date: Wed, 15 Jul 2026 21:58:25 -0400 Subject: [PATCH 04/12] fix(comments): correct delete button authorization check Signed-off-by: Neon --- app/views/comments/_comment.html.erb | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 065b5b371..386c390bb 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -10,12 +10,22 @@ <% if policy(comment).destroy? %> - <%= button_to devlog_comment_path(comment.commentable, comment), - method: :delete, - class: "devlog-comment__delete", - form: { onsubmit: "return confirm('Delete this comment?')" }, - aria: { label: "Delete comment" } do %> - × + <% delete_button = capture do %> + <%= button_to devlog_comment_path(comment.commentable, comment), + method: :delete, + class: "devlog-comment__delete", + form: { onsubmit: "return confirm('Delete this comment?')" }, + aria: { label: "Delete comment" } do %> + × + <% end %> + <% end %> + + <% if comment.user == current_user %> + <%= delete_button %> + <% else %> + <%= admin_tool do %> + <%= delete_button %> + <% end %> <% end %> <% end %> From a689820261bf19031847e0c0e428271230d9d7cc Mon Sep 17 00:00:00 2001 From: Neon Date: Wed, 15 Jul 2026 21:58:41 -0400 Subject: [PATCH 05/12] fix(username-link): break out of turbo frames on admin links Signed-off-by: Neon --- app/views/shared/_username_link.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_username_link.html.erb b/app/views/shared/_username_link.html.erb index 0eabc95c0..2937e928b 100644 --- a/app/views/shared/_username_link.html.erb +++ b/app/views/shared/_username_link.html.erb @@ -6,4 +6,4 @@ <%= link_to local_assigns[:text] || "@#{user.display_name}", profile_path(user.display_name), class: link_class, - data: local_assigns[:data] %><% if local_assigns.fetch(:badge, true) %><%= render StreakBadgeComponent.new(user: user) %><%= " ⚡" if user.admin? %><% end %><%= admin_tool(compact: true) do %><%= link_to "🔨", admin_user_path(user), class: "username-link__admin", "aria-label": "View #{user.display_name} in admin" %><% end %><% if admin_policy(user).impersonate? %><%= admin_tool(compact: true) do %><%= link_to "👁️", admin_user_impersonation_path(user), class: "username-link__admin", data: { turbo_method: :post, turbo_confirm: "Are you sure you want to impersonate #{user.display_name}? You will see the site as they do." }, "aria-label": "Impersonate #{user.display_name}" %><% end %><% end %> + data: local_assigns[:data] %><% if local_assigns.fetch(:badge, true) %><%= render StreakBadgeComponent.new(user: user) %><%= " ⚡" if user.admin? %><% end %><%= admin_tool(compact: true) do %><%= link_to "🔨", admin_user_path(user), class: "username-link__admin", data: { turbo_frame: "_top" }, "aria-label": "View #{user.display_name} in admin" %><% end %><% if admin_policy(user).impersonate? %><%= admin_tool(compact: true) do %><%= link_to "👁️", admin_user_impersonation_path(user), class: "username-link__admin", data: { turbo_method: :post, turbo_frame: "_top", turbo_confirm: "Are you sure you want to impersonate #{user.display_name}? You will see the site as they do." }, "aria-label": "Impersonate #{user.display_name}" %><% end %><% end %> From a2d5844a74ae6cdc0a70c314d539de9d5b746b1e Mon Sep 17 00:00:00 2001 From: Neon Date: Wed, 15 Jul 2026 21:58:52 -0400 Subject: [PATCH 06/12] style(projects): mark admin-only super star controls with admin_tool Signed-off-by: Neon --- app/views/projects/_fire_controls.html.erb | 34 +++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/app/views/projects/_fire_controls.html.erb b/app/views/projects/_fire_controls.html.erb index f575a77f2..ed987a5f4 100644 --- a/app/views/projects/_fire_controls.html.erb +++ b/app/views/projects/_fire_controls.html.erb @@ -1,21 +1,25 @@ <% if project.fire? %> ⭐ Super Star Project <% if current_user&.admin? %> - <%= button_to "Remove Super Star", project_magic_path(project), - method: :delete, - form_class: "project-show__pill-form", - class: "project-show__pill project-show__pill--outline" %> + <%= admin_tool do %> + <%= button_to "Remove Super Star", project_magic_path(project), + method: :delete, + form_class: "project-show__pill-form", + class: "project-show__pill project-show__pill--outline" %> + <% end %> <% end %> <% elsif project.fire_nomination_pending? && current_user&.can_nominate_super_star? %> ⭐ Nominated by <%= project.nominated_fire_by&.display_name %> <% if current_user.admin? %> - <%= button_to "Approve Super Star", project_magic_path(project), - form_class: "project-show__pill-form", - class: "project-show__pill project-show__pill--save" %> - <%= button_to "Reject", project_fire_nomination_path(project), - method: :delete, - form_class: "project-show__pill-form", - class: "project-show__pill project-show__pill--outline" %> + <%= admin_tool do %> + <%= button_to "Approve Super Star", project_magic_path(project), + form_class: "project-show__pill-form", + class: "project-show__pill project-show__pill--save" %> + <%= button_to "Reject", project_fire_nomination_path(project), + method: :delete, + form_class: "project-show__pill-form", + class: "project-show__pill project-show__pill--outline" %> + <% end %> <% else %> <%= button_to "Withdraw nomination", project_fire_nomination_path(project), method: :delete, @@ -23,9 +27,11 @@ class: "project-show__pill project-show__pill--outline" %> <% end %> <% elsif current_user&.admin? %> - <%= button_to "Mark as Super Star", project_magic_path(project), - form_class: "project-show__pill-form", - class: "project-show__pill project-show__pill--save" %> + <%= admin_tool do %> + <%= button_to "Mark as Super Star", project_magic_path(project), + form_class: "project-show__pill-form", + class: "project-show__pill project-show__pill--save" %> + <% end %> <% elsif current_user&.can_nominate_super_star? && !project.users.include?(current_user) %> <%= button_to "Nominate for Super Star", project_fire_nomination_path(project), form_class: "project-show__pill-form", From 39a5989616ed4afa48a8b6ffd31ec4074465a637 Mon Sep 17 00:00:00 2001 From: Neon Date: Wed, 15 Jul 2026 21:59:11 -0400 Subject: [PATCH 07/12] feat(users): allow admins to edit other users' profiles Signed-off-by: Neon --- app/assets/stylesheets/pages/user/_show.scss | 22 +++++++++++ app/controllers/users_controller.rb | 38 ++++++++++--------- app/policies/user_policy.rb | 2 +- app/views/users/show.html.erb | 24 +++++++++--- .../users_controller_update_test.rb | 15 ++++++++ 5 files changed, 77 insertions(+), 24 deletions(-) diff --git a/app/assets/stylesheets/pages/user/_show.scss b/app/assets/stylesheets/pages/user/_show.scss index f3552444e..8318619b4 100644 --- a/app/assets/stylesheets/pages/user/_show.scss +++ b/app/assets/stylesheets/pages/user/_show.scss @@ -599,6 +599,28 @@ turbo-frame#profile_tabs { align-items: center; } +// Wraps the admin-editing-someone-else's-profile notice. The admin_tool box +// itself (border + padding) must stay hidden until edit mode is entered — +// hiding only the text inside it still leaves an empty dashed square visible +// (mirrors .profile__banner-placeholder and friends, which reveal on the +// .profile--editing class). +.profile__admin-edit-tool { + display: none; + + .profile--editing & { + display: inline-block; + } +} + +.profile__admin-edit-notice { + font-size: 0.85rem; + color: var(--color-brand-orange); + + strong { + font-weight: 700; + } +} + // Profile action buttons (Edit / Follow / Save / Cancel). Sits inside the // set-3 profile card, so this uses the light-bg small-action variant per // branding §4.0 / §4.1. Base is the secondary action (transparent + diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 784412a41..cd4532465 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -21,24 +21,28 @@ def show end def update - if @user.update(user_params) - respond_to do |format| - format.turbo_stream do - flash.now[:notice] = "Profile updated." - render turbo_stream: turbo_stream.update("flash-region", partial: "shared/flash") + whodunnit = impersonating? ? real_user&.id : current_user&.id + + PaperTrail.request(whodunnit: whodunnit) do + if @user.update(user_params) + respond_to do |format| + format.turbo_stream do + flash.now[:notice] = "Profile updated." + render turbo_stream: turbo_stream.update("flash-region", partial: "shared/flash") + end + format.html { redirect_to profile_path(@user.display_name), notice: "Profile updated." } end - format.html { redirect_to profile_path(@user.display_name), notice: "Profile updated." } - end - else - respond_to do |format| - format.turbo_stream do - flash.now[:alert] = @user.errors.full_messages.to_sentence - render turbo_stream: turbo_stream.update("flash-region", partial: "shared/flash"), status: :unprocessable_entity - end - format.html do - flash.now[:alert] = @user.errors.full_messages.to_sentence - load_profile("feed") - render :show, status: :unprocessable_entity + else + respond_to do |format| + format.turbo_stream do + flash.now[:alert] = @user.errors.full_messages.to_sentence + render turbo_stream: turbo_stream.update("flash-region", partial: "shared/flash"), status: :unprocessable_entity + end + format.html do + flash.now[:alert] = @user.errors.full_messages.to_sentence + load_profile("feed") + render :show, status: :unprocessable_entity + end end end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index e7ee6ec0f..63dcde7f7 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -4,7 +4,7 @@ def show? end def update? - user.present? && user.id == record.id + user.present? && (user.id == record.id || user.admin?) end def follow? diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index a90e91f06..cc460427f 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -11,6 +11,8 @@ <% end %> <% own_profile = current_user&.id == @user.id %> +<% admin_editing = !own_profile && current_user&.admin? %> +<% can_edit_profile = own_profile || admin_editing %> <% banner_url = @user.banner.attached? ? url_for(@user.banner) : image_path("profile/default-banner.png") %>
@@ -28,7 +30,7 @@
<%= image_tag banner_url, alt: "", class: "profile__banner-image", data: { "profile-edit-target": "banner" } %> - <% if own_profile && !@user.banner.attached? %> + <% if can_edit_profile && !@user.banner.attached? %>