diff --git a/app/controllers/admin/gdpr_delete_requests_controller.rb b/app/controllers/admin/gdpr_delete_requests_controller.rb
index 744209272..4e20cbf3c 100644
--- a/app/controllers/admin/gdpr_delete_requests_controller.rb
+++ b/app/controllers/admin/gdpr_delete_requests_controller.rb
@@ -3,7 +3,7 @@ module Admin
layout "admin"
def index
- @gdpr_delete_requests = ::GDPRDeleteRequest.order(created_at: :desc).page(params[:page]).per(50)
+ @gdpr_delete_requests = Admin::GDPRDeleteRequestsQuery.call(search: params[:search]).page(params[:page]).per(50)
end
def destroy
diff --git a/app/queries/admin/gdpr_delete_requests_query.rb b/app/queries/admin/gdpr_delete_requests_query.rb
new file mode 100644
index 000000000..a3ab2335a
--- /dev/null
+++ b/app/queries/admin/gdpr_delete_requests_query.rb
@@ -0,0 +1,16 @@
+module Admin
+ module GDPRDeleteRequestsQuery
+ QUERY_CLAUSE = "#{GDPRDeleteRequest.table_name}.email ILIKE :search OR " \
+ "#{GDPRDeleteRequest.table_name}.username ILIKE :search".freeze
+
+ def self.call(relation: ::GDPRDeleteRequest.all, search: {})
+ relation = search_relation(relation, search) if search.presence
+
+ relation.order(created_at: :desc)
+ end
+
+ def self.search_relation(relation, search)
+ relation.where(QUERY_CLAUSE, search: "%#{search}%")
+ end
+ end
+end
diff --git a/app/views/admin/gdpr_delete_requests/index.html.erb b/app/views/admin/gdpr_delete_requests/index.html.erb
index ece6e6b8d..003d3a1d2 100644
--- a/app/views/admin/gdpr_delete_requests/index.html.erb
+++ b/app/views/admin/gdpr_delete_requests/index.html.erb
@@ -2,10 +2,15 @@
Members (GDPR Delete Requests)
+ <%= form_with url: admin_gdpr_delete_requests_path, method: :get, local: true, class: "grow-1 mb-3 m:mb-0" do |f| %>
+
+
+ <%= paginate @gdpr_delete_requests, theme: "admin", scope: @gdpr_delete_requests, label: "Paginate GDPR delete requests", context: "top" %>
+
+ <%= render "admin/users/controls/search_field", f: f, placeholder: "Search members...", aria_label: "Search members by email or username", context: "large" %>
+
+ <% end %>
-
- <%= paginate @gdpr_delete_requests, theme: "admin", scope: @gdpr_delete_requests, label: "Paginate GDPR delete requests", context: "top" %>
-
These accounts have been deleted from the community. Ensure that any data held in external databases or mailing lists is deleted before marking as "Deleted."
diff --git a/cypress/integration/seededFlows/adminFlows/users/gdprDeleteRequests.spec.js b/cypress/integration/seededFlows/adminFlows/users/gdprDeleteRequests.spec.js
index 54c1d37d6..811731f9d 100644
--- a/cypress/integration/seededFlows/adminFlows/users/gdprDeleteRequests.spec.js
+++ b/cypress/integration/seededFlows/adminFlows/users/gdprDeleteRequests.spec.js
@@ -62,4 +62,35 @@ describe('GDPR Delete Requests', () => {
}).should('not.exist');
cy.get('@confirmButton').should('exist').should('have.focus');
});
+
+ it('Searches for a user', () => {
+ // The user should be visible on the page
+ cy.findByRole('table')
+ .findByText('gdpr-delete-user@forem.local')
+ .should('exist');
+
+ // Search for a term that should match the entry
+ searchForMember('delete');
+
+ cy.url().should('contain', 'search=delete');
+ cy.findByRole('table')
+ .findByText('gdpr-delete-user@forem.local')
+ .should('exist');
+
+ // Search for a term that shouldn't match the entry
+ searchForMember('something');
+ cy.url().should('contain', 'search=something');
+ cy.findByRole('table')
+ .findByText('gdpr-delete-user@forem.local')
+ .should('not.exist');
+ });
+
+ const searchForMember = (searchTerm) => {
+ cy.findByRole('textbox', {
+ name: 'Search members by email or username',
+ })
+ .clear()
+ .type(searchTerm);
+ cy.findByRole('button', { name: 'Search' }).click();
+ };
});
diff --git a/spec/queries/admin/gdpr_delete_requests_query_spec.rb b/spec/queries/admin/gdpr_delete_requests_query_spec.rb
new file mode 100644
index 000000000..1be4867b3
--- /dev/null
+++ b/spec/queries/admin/gdpr_delete_requests_query_spec.rb
@@ -0,0 +1,52 @@
+require "rails_helper"
+
+RSpec.describe Admin::GDPRDeleteRequestsQuery, type: :query do
+ subject { described_class.call(search: search) }
+
+ let!(:gdpr_delete_request_1) { create(:gdpr_delete_request, username: "delete_1", email: "delete_1@test.com") }
+ let!(:gdpr_delete_request_2) { create(:gdpr_delete_request, username: "delete_2", email: "delete_2@test.com") }
+ let!(:gdpr_delete_request_3) { create(:gdpr_delete_request, username: "delete_3", email: "delete_3@test.com") }
+ let!(:gdpr_delete_request_11) { create(:gdpr_delete_request, username: "delete_11", email: "delete_11@test.com") }
+
+ describe ".call" do
+ context "when no arguments are given" do
+ it "returns all users" do
+ # rubocop:disable Layout/LineLength
+ expect(described_class.call).to match_array([gdpr_delete_request_1, gdpr_delete_request_2, gdpr_delete_request_3, gdpr_delete_request_11])
+ # rubocop:enable Layout/LineLength
+ end
+ end
+
+ context "when searching for a user by username" do
+ let(:search) { "delete_2" }
+
+ it { is_expected.to match_array([gdpr_delete_request_2]) }
+ end
+
+ context "when searching for a user by email" do
+ let(:search) { "delete_1@test.com" }
+
+ it { is_expected.to match_array([gdpr_delete_request_1]) }
+ end
+
+ context "when searching for ambiguous terms that matches multiple users" do
+ let(:search) { "delete_1" }
+
+ it { is_expected.to match_array([gdpr_delete_request_1, gdpr_delete_request_11]) }
+ end
+
+ context "when searching for ambiguous terms that matches both emails and usernames" do
+ let(:search) { "delete" }
+
+ # rubocop:disable Layout/LineLength
+ it { is_expected.to match_array([gdpr_delete_request_1, gdpr_delete_request_2, gdpr_delete_request_3, gdpr_delete_request_11]) }
+ # rubocop:enable Layout/LineLength
+ end
+
+ context "when passed a non-existent email or username" do
+ let(:search) { "non_existent_email" }
+
+ it { is_expected.to match_array([]) }
+ end
+ end
+end