diff --git a/admin/app/components/solidus_admin/users/edit/api_access/component.html.erb b/admin/app/components/solidus_admin/users/edit/api_access/component.html.erb
new file mode 100644
index 00000000000..96d4e00ed9a
--- /dev/null
+++ b/admin/app/components/solidus_admin/users/edit/api_access/component.html.erb
@@ -0,0 +1,49 @@
+<%= render component('ui/panel').new(title: t('.api_access')) do %>
+
+ <% if @user.spree_api_key.present? %>
+
+
<%= t('.key') %>
+ <% if @user == helpers.current_solidus_admin_user %>
+ <%= @user.spree_api_key %>
+ <% else %>
+ (<%= t('spree.hidden') %>)
+ <% end %>
+
+
+
+ <%= 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 %>
+
+
+ <% else %>
+ <%= t('.no_key') %>
+
+
+ <%= 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 %>
+
+
+ <% end %>
+
+<% end %>
diff --git a/admin/app/components/solidus_admin/users/edit/api_access/component.js b/admin/app/components/solidus_admin/users/edit/api_access/component.js
new file mode 100644
index 00000000000..910294c5462
--- /dev/null
+++ b/admin/app/components/solidus_admin/users/edit/api_access/component.js
@@ -0,0 +1,9 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ confirm(event) {
+ if (!confirm(event.params.message)) {
+ event.preventDefault()
+ }
+ }
+}
diff --git a/admin/app/components/solidus_admin/users/edit/api_access/component.rb b/admin/app/components/solidus_admin/users/edit/api_access/component.rb
new file mode 100644
index 00000000000..be6df3729b8
--- /dev/null
+++ b/admin/app/components/solidus_admin/users/edit/api_access/component.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class SolidusAdmin::Users::Edit::ApiAccess::Component < SolidusAdmin::BaseComponent
+ def initialize(user:)
+ @user = user
+ end
+end
diff --git a/admin/app/components/solidus_admin/users/edit/api_access/component.yml b/admin/app/components/solidus_admin/users/edit/api_access/component.yml
new file mode 100644
index 00000000000..e865ba47c30
--- /dev/null
+++ b/admin/app/components/solidus_admin/users/edit/api_access/component.yml
@@ -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.
diff --git a/admin/app/components/solidus_admin/users/edit/component.html.erb b/admin/app/components/solidus_admin/users/edit/component.html.erb
new file mode 100644
index 00000000000..795241410ff
--- /dev/null
+++ b/admin/app/components/solidus_admin/users/edit/component.html.erb
@@ -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| %>
+
+ <%= render component("ui/forms/field").text_field(f, :email, autocomplete: "none") %>
+
+
+ <%= render component("ui/forms/field").text_field(f, :password, autocomplete: "none") %>
+
+
+ <%= render component("ui/forms/field").text_field(f, :password_confirmation) %>
+
+
+ <%= render component("ui/checkbox_row").new(options: role_options, row_title: "Roles", form: f, method: "spree_role_ids", layout: :subsection) %>
+
+
+ <%= 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) %>
+
+ <% 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 %>
diff --git a/admin/app/components/solidus_admin/users/edit/component.rb b/admin/app/components/solidus_admin/users/edit/component.rb
new file mode 100644
index 00000000000..78f1b8a82eb
--- /dev/null
+++ b/admin/app/components/solidus_admin/users/edit/component.rb
@@ -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
diff --git a/admin/app/components/solidus_admin/users/edit/component.yml b/admin/app/components/solidus_admin/users/edit/component.yml
new file mode 100644
index 00000000000..01b3cf230ac
--- /dev/null
+++ b/admin/app/components/solidus_admin/users/edit/component.yml
@@ -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
diff --git a/admin/app/components/solidus_admin/users/index/component.rb b/admin/app/components/solidus_admin/users/index/component.rb
index 81fb38444ff..0b911061d90 100644
--- a/admin/app/components/solidus_admin/users/index/component.rb
+++ b/admin/app/components/solidus_admin/users/index/component.rb
@@ -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
@@ -104,7 +104,8 @@ def last_login(user)
t(
'.last_login.login_time_ago',
- # @note The second `.try` is only here for the specs to work.
+ # @note The second `.try` is here for the specs and for setups that use a
+ # custom User class which may not have this attribute.
last_login_time: time_ago_in_words(user.try(:last_sign_in_at))
).capitalize
end
diff --git a/admin/app/controllers/solidus_admin/users_controller.rb b/admin/app/controllers/solidus_admin/users_controller.rb
index 7f674b4e09e..5a679a4d2e1 100644
--- a/admin/app/controllers/solidus_admin/users_controller.rb
+++ b/admin/app/controllers/solidus_admin/users_controller.rb
@@ -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])
@@ -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
diff --git a/admin/config/routes.rb b/admin/config/routes.rb
index 61f5a729799..afbb4be37c5 100644
--- a/admin/config/routes.rb
+++ b/admin/config/routes.rb
@@ -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
diff --git a/admin/spec/features/users_spec.rb b/admin/spec/features/users_spec.rb
index f0e88296031..36cd9eba915 100644
--- a/admin/spec/features/users_spec.rb
+++ b/admin/spec/features/users_spec.rb
@@ -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")
@@ -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
diff --git a/admin/spec/requests/solidus_admin/users_spec.rb b/admin/spec/requests/solidus_admin/users_spec.rb
new file mode 100644
index 00000000000..59df8ac3c6c
--- /dev/null
+++ b/admin/spec/requests/solidus_admin/users_spec.rb
@@ -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
diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml
index d77fd1e12dd..5968fab8ad5 100644
--- a/core/config/locales/en.yml
+++ b/core/config/locales/en.yml
@@ -1855,6 +1855,7 @@ en:
order_approved: Order approved
order_canceled: Order canceled
order_completed: Order completed
+ order_count: Order Count
order_details: Order Details
order_email_resent: Order Email Resent
order_information: Order Information