Skip to content

Commit

Permalink
Add new users admin edit page
Browse files Browse the repository at this point in the history
This migrates the `users/:id/edit` page to the new admin. It still
relies on the old backend admin controller for the `#update` action as
well as the other top level user tabs such as address, order history,
and so on.

Co-authored-by: benjamin wil <benjamin@super.gd>
  • Loading branch information
MadelineCollier and benjaminwil committed Sep 18, 2024
1 parent 0f77e8b commit 9276214
Show file tree
Hide file tree
Showing 13 changed files with 348 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<%= render component('ui/panel').new(title: t('.api_access')) do %>
<section>
<% if @user.spree_api_key.present? %>
<div id="current-api-key">
<h2 class="py-1.5 font-semibold"><%= t('.key') %></h2>
<% if @user == helpers.current_solidus_admin_user %>
<%= @user.spree_api_key %>
<% else %>
<i>(<%= t('spree.hidden') %>)</i>
<% end %>
</div>

<div class="py-1.5 text-center">
<%= form_with url: spree.admin_user_api_key_path(@user), method: :delete, local: true, html: { class: 'clear_api_key inline-flex' } do %>
<%= render component("ui/button").new(
text: t('.clear_key'),
scheme: :secondary,
type: :submit,
"data-action": "click->#{stimulus_id}#confirm",
"data-#{stimulus_id}-message-param": t(".confirm_clear_key"),
) %>
<% end %>
<%= form_with url: spree.admin_user_api_key_path(@user), method: :post, local: true, html: { class: 'regen_api_key inline-flex' } do %>
<%= render component("ui/button").new(
text: t('.regenerate_key'),
scheme: :secondary,
type: :submit,
"data-action": "click->#{stimulus_id}#confirm",
"data-#{stimulus_id}-message-param": t(".confirm_regenerate_key"),
) %>
<% end %>
</div>

<% else %>
<div class="no-objects-found"><%= t('.no_key') %></div>
<div class="filter-actions actions">
<div class="py-1.5 text-center">
<%= form_with url: spree.admin_user_api_key_path(@user), method: :post, local: true, html: { class: 'generate_api_key inline-flex' } do %>
<%= render component("ui/button").new(
text: t('.generate_key'),
type: :submit,
) %>
<% end %>
</div>
</div>
<% end %>
</section>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
confirm(event) {
if (!confirm(event.params.message)) {
event.preventDefault()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class SolidusAdmin::Users::Edit::ApiAccess::Component < SolidusAdmin::BaseComponent
def initialize(user:)
@user = user
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
en:
api_access: API Access
no_key: No key
key: "Key"
generate_key: Generate API key
clear_key: Clear key
regenerate_key: Regenerate key
hidden: Hidden
confirm_clear_key: Are you sure you want to clear this user's API key? It will invalidate the existing key.
confirm_regenerate_key: Are you sure you want to regenerate this user's API key? It will invalidate the existing key.
62 changes: 62 additions & 0 deletions admin/app/components/solidus_admin/users/edit/component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<%= page do %>
<%= page_header do %>
<%= page_header_back(solidus_admin.users_path) %>
<%= page_header_title(t(".title", email: @user.email)) %>
<% # @todo: I am not sure how we want to handle Cancan stuff in the new admin. %>
<% # if can?(:admin, Spree::Order) && can?(:create, Spree::Order) %>
<%= page_header_actions do %>
<%= render component("ui/button").new(tag: :a, text: t(".create_order_for_user"), href: spree.new_admin_order_path(user_id: @user.id)) %>
<% end %>
<% # end %>
<% end %>
<%= page_header do %>
<% tabs.each do |tab| %>
<%= render(component("ui/button").new(tag: :a, scheme: :ghost, text: tab[:text], 'aria-current': tab[:current], href: tab[:href])) %>
<% end %>
<% end %>
<%= page_with_sidebar do %>
<%= page_with_sidebar_main do %>
<%= render component('ui/panel').new(title: Spree.user_class.model_name.human) do %>
<%= form_for @user, url: solidus_admin.user_path(@user), html: { id: form_id } do |f| %>
<div class="py-1.5">
<%= render component("ui/forms/field").text_field(f, :email) %>
</div>
<div class="py-1.5">
<%= render component("ui/forms/field").text_field(f, :password) %>
</div>
<div class="py-1.5">
<%= render component("ui/forms/field").text_field(f, :password_confirmation) %>
</div>
<div class="py-1.5">
<%= render component("ui/checkbox_row").new(options: role_options, row_title: "Roles", form: f, method: "spree_role_ids", layout: :subsection) %>
</div>
<div class="py-1.5 text-center">
<%= render component("ui/button").new(tag: :button, text: t(".update"), form: form_id) %>
<%= render component("ui/button").new(tag: :a, text: t(".cancel"), href: solidus_admin.user_path(@user), scheme: :secondary) %>
</div>
<% end %>
<% end %>
<%= render component("users/edit/api_access").new(user: @user) %>
<% end %>
<%= page_with_sidebar_aside do %>
<%= render component("ui/panel").new(title: t("spree.lifetime_stats")) do %>
<%= render component("ui/details_list").new(
items: [
{ label: t("spree.total_sales"), value: @user.display_lifetime_value.to_html },
{ label: t("spree.order_count"), value: @user.order_count.to_i },
{ label: t("spree.average_order_value"), value: @user.display_average_order_value.to_html },
{ label: t("spree.member_since"), value: @user.created_at.to_date },
{ label: t(".last_active"), value: last_login(@user) },
]
) %>
<% end %>
<% end %>
<% end %>
<% end %>
63 changes: 63 additions & 0 deletions admin/app/components/solidus_admin/users/edit/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

class SolidusAdmin::Users::Edit::Component < SolidusAdmin::BaseComponent
include SolidusAdmin::Layout::PageHelpers

def initialize(user:)
@user = user
end

def form_id
@form_id ||= "#{stimulus_id}--form-#{@user.id}"
end

def tabs
[
{
text: t('.account'),
href: solidus_admin.users_path,
current: action_name == "show",
},
{
text: t('.addresses'),
href: spree.addresses_admin_user_path(@user),
# @todo: update this "current" logic once folded into new admin
current: action_name != "show",
},
{
text: t('.order_history'),
href: spree.orders_admin_user_path(@user),
# @todo: update this "current" logic once folded into new admin
current: action_name != "show",
},
{
text: t('.items'),
href: spree.items_admin_user_path(@user),
# @todo: update this "current" logic once folded into new admin
current: action_name != "show",
},
{
text: t('.store_credit'),
href: spree.admin_user_store_credits_path(@user),
# @todo: update this "current" logic once folded into new admin
current: action_name != "show",
},
]
end

def last_login(user)
return t('.last_login.never') if user.try(:last_sign_in_at).blank?

t(
'.last_login.login_time_ago',
# @note The second `.try` is only here for the specs to work.
last_login_time: time_ago_in_words(user.try(:last_sign_in_at))
).capitalize
end

def role_options
Spree::Role.all.map do |role|
{ label: role.name, id: role.id }
end
end
end
16 changes: 16 additions & 0 deletions admin/app/components/solidus_admin/users/edit/component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
en:
title: "Users / %{email}"
account: Account
addresses: Addresses
order_history: Order History
items: Items
store_credit: Store Credit
last_active: Last Active
last_login:
login_time_ago: "%{last_login_time} ago"
never: Never
invitation_sent: Invitation sent
create_order_for_user: Create order for this user
update: Update
cancel: Cancel
back: Back
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def search_url
end

def row_url(user)
spree.admin_user_path(user)
solidus_admin.edit_user_path(user)
end

def page_actions
Expand Down
12 changes: 12 additions & 0 deletions admin/app/controllers/solidus_admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def index
end
end

def edit
set_user

respond_to do |format|
format.html { render component('users/edit').new(user: @user) }
end
end

def destroy
@users = Spree.user_class.where(id: params[:id])

Expand All @@ -34,6 +42,10 @@ def destroy

private

def set_user
@user = Spree.user_class.find(params[:id])
end

def user_params
params.require(:user).permit(:user_id, permitted_user_attributes)
end
Expand Down
2 changes: 1 addition & 1 deletion admin/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
end
end

admin_resources :users, only: [:index, :destroy]
admin_resources :users, only: [:index, :edit, :destroy]
admin_resources :promotions, only: [:index, :destroy]
admin_resources :properties, only: [:index, :destroy]
admin_resources :option_types, only: [:index, :destroy], sortable: true
Expand Down
59 changes: 58 additions & 1 deletion admin/spec/features/users_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
require "spec_helper"

describe "Users", :js, type: :feature do
before { sign_in create(:admin_user, email: "admin@example.com") }
let(:admin) { create(:admin_user, email: "admin@example.com") }

before do
sign_in admin
end

it "lists users and allows deleting them" do
create(:user, email: "customer@example.com")
Expand Down Expand Up @@ -52,4 +56,57 @@
expect(page).not_to have_content("Never")
end
end

context "when editing an existing user" do
before do
# This is needed for the actions which are still powered by the backend
# and not the new admin. (#update, etc.)
stub_authorization!(admin)

create(:user, email: "customer@example.com")
visit "/admin/users"
find_row("customer@example.com").click
end

it "shows the edit page" do
expect(page).to have_content("Users / customer@example.com")
expect(page).to have_content("Lifetime Stats")
expect(page).to have_content("Roles")
expect(find("label", text: /admin/i).find("input[type=checkbox]").checked?).to eq(false)
end

it "allows editing of the existing user" do
# API key interactions
expect(page).to have_content("No key")
click_on "Generate API key"
expect(page).to have_content("Key generated")
expect(page).to have_content("(hidden)")

click_on "Regenerate key"
expect(page).to have_content("Key generated")
expect(page).to have_content("(hidden)")

click_on "Clear key"
expect(page).to have_content("Key cleared")
expect(page).to have_content("No key")

# Update user
within("form.edit_user") do
fill_in "Email", with: "dogtown@example.com"
find("label", text: /admin/i).find("input[type=checkbox]").check
click_on "Update"
end

expect(page).to have_content("Users / dogtown@example.com")
expect(find("label", text: /admin/i).find("input[type=checkbox]").checked?).to eq(true)

# Cancel out of editing
within("form.edit_user") do
fill_in "Email", with: "newemail@example.com"
click_on "Cancel"
end

expect(page).not_to have_content("newemail@example.com")
end
end
end
59 changes: 59 additions & 0 deletions admin/spec/requests/solidus_admin/users_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe "SolidusAdmin::UsersController", type: :request do
let(:admin_user) { create(:admin_user) }
let(:user) { create(:user) }

before do
allow_any_instance_of(SolidusAdmin::BaseController).to receive(:spree_current_user).and_return(admin_user)
end

describe "GET /index" do
it "renders the index template with a 200 OK status" do
get solidus_admin.users_path
expect(response).to have_http_status(:ok)
end
end

describe "GET /edit" do
it "renders the edit template with a 200 OK status" do
get solidus_admin.edit_user_path(user)
expect(response).to have_http_status(:ok)
end
end

describe "DELETE /destroy" do
it "deletes the user and redirects to the index page with a 303 See Other status" do
# Ensure the user exists prior to deletion
user

expect {
delete solidus_admin.user_path(user)
}.to change(Spree.user_class, :count).by(-1)

expect(response).to redirect_to(solidus_admin.users_path)
expect(response).to have_http_status(:see_other)
end

it "displays a success flash message after deletion" do
delete solidus_admin.user_path(user)
follow_redirect!
expect(response.body).to include("Users were successfully removed.")
end
end

describe "search functionality" do
before do
create(:user, email: "test@example.com")
create(:user, email: "another@example.com")
end

it "filters users based on search parameters" do
get solidus_admin.users_path, params: { q: { email_cont: "test" } }
expect(response.body).to include("test@example.com")
expect(response.body).not_to include("another@example.com")
end
end
end
Loading

0 comments on commit 9276214

Please sign in to comment.