diff --git a/lib/graphql/schema/user.ex b/lib/graphql/schema/user.ex index 6d428f2e4..863206dfe 100644 --- a/lib/graphql/schema/user.ex +++ b/lib/graphql/schema/user.ex @@ -332,6 +332,11 @@ defmodule Mobilizon.GraphQL.Schema.UserType do description: "Filter users by current signed-in IP address" ) + arg(:pending_user, :boolean, + default_value: false, + description: "Filter users by pending or not" + ) + arg(:page, :integer, default_value: 1, description: "The page in the paginated users list") arg(:limit, :integer, default_value: 10, description: "The limit of users per page") diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex index c512c094e..0a2b5e1c0 100644 --- a/lib/mobilizon/users/users.ex +++ b/lib/mobilizon/users/users.ex @@ -290,6 +290,7 @@ defmodule Mobilizon.Users do @spec list_users(Keyword.t()) :: Page.t(User.t()) def list_users(options) do User + |> filter_by_pending(Keyword.get(options, :pending_user)) |> filter_by_email(Keyword.get(options, :email)) |> filter_by_ip(Keyword.get(options, :current_sign_in_ip)) |> sort(Keyword.get(options, :sort), Keyword.get(options, :direction)) @@ -534,6 +535,11 @@ defmodule Mobilizon.Users do where(User, [u], u.id == ^user_id) end + @spec filter_by_pending(Ecto.Queryable.t(), Boolean.t() | nil) :: Ecto.Query.t() + defp filter_by_pending(query, nil), do: query + defp filter_by_pending(query, true), do: where(query, [q], q.role == :pending) + defp filter_by_pending(query, false), do: where(query, [q], q.role != :pending) + @spec filter_by_email(Ecto.Queryable.t(), String.t() | nil) :: Ecto.Query.t() defp filter_by_email(query, nil), do: query defp filter_by_email(query, ""), do: query diff --git a/src/graphql/user.ts b/src/graphql/user.ts index bcdb2e46d..13628f416 100644 --- a/src/graphql/user.ts +++ b/src/graphql/user.ts @@ -271,6 +271,7 @@ export const LIST_USERS = gql` query ListUsers( $email: String $currentSignInIp: String + $pendingUser: Boolean $page: Int $limit: Int $sort: SortableUserField @@ -279,6 +280,7 @@ export const LIST_USERS = gql` users( email: $email currentSignInIp: $currentSignInIp + pendingUser: $pendingUser page: $page limit: $limit sort: $sort diff --git a/src/i18n/en_US.json b/src/i18n/en_US.json index 0e2763819..c4b33cb5b 100644 --- a/src/i18n/en_US.json +++ b/src/i18n/en_US.json @@ -854,7 +854,7 @@ "Open user menu": "Open user menu", "Opened reports": "Opened reports", "Open": "Open", - "Options":"Options", + "Options": "Options", "Or": "Or", "Ordered list": "Ordered list", "Organized": "Organized", @@ -1374,6 +1374,7 @@ "User suspended and report resolved": "User suspended and report resolved", "Username": "Username", "Users": "Users", + "Users pending for moderation": "Users pending for moderation", "Validating account": "Validating account", "Validating email": "Validating email", "Video Conference": "Video Conference", diff --git a/src/views/Admin/UsersView.vue b/src/views/Admin/UsersView.vue index 0dab455fe..81b01458f 100644 --- a/src/views/Admin/UsersView.vue +++ b/src/views/Admin/UsersView.vue @@ -10,8 +10,13 @@ ]" />
-
+ +

+ {{ + $t("Users pending for moderation") + }} +

@@ -19,7 +24,7 @@

- {{ + {{ $t("Filter") }}

@@ -112,17 +117,29 @@ import { useI18n } from "vue-i18n"; import { useHead } from "@/utils/head"; import { integerTransformer, useRouteQuery } from "vue-use-route-query"; import { formatDateTimeString } from "@/filters/datetime"; +import { useRegistrationConfig } from "@/composition/apollo/config"; + +const { registrationsModeration } = useRegistrationConfig(); const USERS_PER_PAGE = 10; const emailFilter = useRouteQuery("emailFilter", ""); const ipFilter = useRouteQuery("ipFilter", ""); +const pendingFilter = useRouteQuery("pendingFilter", "true"); const page = useRouteQuery("page", 1, integerTransformer); const languagesCodes = computed((): string[] => { return (users.value?.elements ?? []).map((user: IUser) => user.locale); }); +const pendingFilterBuilder = computed(() => { + if (registrationsModeration.value) { + return pendingFilter.value == "true"; + } else { + return false; + } +}); + const { result: usersResult, fetchMore, @@ -130,6 +147,7 @@ const { } = useQuery<{ users: Paginate }>(LIST_USERS, () => ({ email: emailFilter.value, currentSignInIp: ipFilter.value, + pendingUser: pendingFilterBuilder.value, page: page.value, limit: USERS_PER_PAGE, })); @@ -156,6 +174,7 @@ useHead({ const emailFilterFieldValue = ref(emailFilter.value); const ipFilterFieldValue = ref(ipFilter.value); +const pendingFieldValue = ref(pendingFilter.value == "true"); const getLanguageNameForCode = (code: string): string => { return ( @@ -171,20 +190,31 @@ const onPageChange = async (newPage: number): Promise => { variables: { email: emailFilter.value, currentSignInIp: ipFilter.value, + pendingUser: pendingFilterBuilder.value, page: page.value, limit: USERS_PER_PAGE, }, }); }; -const activateFilters = (): void => { +const filterUsers = async (): void => { emailFilter.value = emailFilterFieldValue.value; ipFilter.value = ipFilterFieldValue.value; + if (registrationsModeration.value) { + if (pendingFieldValue.value) { + pendingFilter.value = "true"; + } else { + pendingFilter.value = "false"; + } + } else { + pendingFilter.value = ""; + } }; const resetFilters = (): void => { emailFilterFieldValue.value = ""; ipFilterFieldValue.value = ""; + pendingFieldValue.value = true; activateFilters(); }; diff --git a/test/graphql/resolvers/user_test.exs b/test/graphql/resolvers/user_test.exs index a15809b08..ae27ac6f9 100644 --- a/test/graphql/resolvers/user_test.exs +++ b/test/graphql/resolvers/user_test.exs @@ -38,6 +38,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do query ListUsers( $email: String $currentSignInIp: String + $pendingUser: Boolean $page: Int $limit: Int $sort: SortableUserField @@ -46,6 +47,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do users( email: $email currentSignInIp: $currentSignInIp + pendingUser: $pendingUser page: $page limit: $limit sort: $sort @@ -246,6 +248,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do user = insert(:user, email: "riri@example.com", role: :moderator) insert(:user, email: "fifi@example.com") insert(:user, email: "loulou@example.com") + insert(:user, email: "picsous@example.com", role: :pending) res = conn @@ -259,6 +262,25 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do assert res["data"]["users"]["total"] == 3 assert res["data"]["users"]["elements"] |> length == 3 + assert res["data"]["users"]["elements"] + |> Enum.map(& &1["email"]) == [ + "loulou@example.com", + "fifi@example.com", + "riri@example.com" + ] + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @list_users_query, + variables: %{pendingUser: false} + ) + + assert res["errors"] == nil + assert res["data"]["users"]["total"] == 3 + assert res["data"]["users"]["elements"] |> length == 3 + assert res["data"]["users"]["elements"] |> Enum.map(& &1["email"]) == [ "loulou@example.com", @@ -297,6 +319,22 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do assert res["data"]["users"]["elements"] |> Enum.map(& &1["email"]) == [ "riri@example.com" ] + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @list_users_query, + variables: %{pendingUser: true} + ) + + assert res["errors"] == nil + assert res["data"]["users"]["total"] == 1 + assert res["data"]["users"]["elements"] |> length == 1 + + assert res["data"]["users"]["elements"] |> Enum.map(& &1["email"]) == [ + "picsous@example.com" + ] end test "list_users/3 allows filtering the list of users by email", %{conn: conn} do diff --git a/tests/unit/specs/components/admin/__snapshots__/usersView.spec.ts.snap b/tests/unit/specs/components/admin/__snapshots__/usersView.spec.ts.snap index 0f2234d81..367b96cfd 100644 --- a/tests/unit/specs/components/admin/__snapshots__/usersView.spec.ts.snap +++ b/tests/unit/specs/components/admin/__snapshots__/usersView.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`UsersView > Show simple list 1`] = ` +exports[`UsersView > Show list with moderation 1`] = ` "
@@ -9,6 +9,7 @@ exports[`UsersView > Show simple list 1`] = `
+

@@ -31,7 +32,121 @@ exports[`UsersView > Show simple list 1`] = `
-

+
+
+ +
+ +
+
Email Last seen on Language
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Email Last seen on Language
6truc@mobilizon.testEnglish
1admin@mobilizon.testEnglish
+ + + +
+
+
+
+ +
+
+
+
+
" +`; + +exports[`UsersView > Show simple list 1`] = ` +"
+ +
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+ +
+
+
+
+
+ + +
+
+
+ +
+

diff --git a/tests/unit/specs/components/admin/usersView.spec.ts b/tests/unit/specs/components/admin/usersView.spec.ts index e66dcfb11..2152f591e 100644 --- a/tests/unit/specs/components/admin/usersView.spec.ts +++ b/tests/unit/specs/components/admin/usersView.spec.ts @@ -16,6 +16,7 @@ import { createRouter, createWebHistory, Router } from "vue-router"; import { routes } from "@/router"; import { Oruga } from "@oruga-ui/oruga-next"; import { htmlRemoveId } from "../../common"; +import { CONFIG } from "@/graphql/config"; let router: Router; @@ -103,15 +104,23 @@ const listUsersMock = { config.global.plugins.push(Oruga); -const generateWrapper = (currentUsersMock = listUsersMock) => { +const generateWrapper = (currentModeration = false) => { mockClient = createMockClient({ cache, resolvers: buildCurrentUserResolver(cache), }); requestHandlers = { + config: vi.fn().mockResolvedValue({ + data: { + config: { + registrationsModeration: currentModeration, + }, + }, + }), languagecode: vi.fn().mockResolvedValue(languageCodeMock), - list_users: vi.fn().mockResolvedValue(currentUsersMock), + list_users: vi.fn().mockResolvedValue(listUsersMock), }; + mockClient.setRequestHandler(CONFIG, requestHandlers.config); mockClient.setRequestHandler(LANGUAGES_CODES, requestHandlers.languagecode); mockClient.setRequestHandler(LIST_USERS, requestHandlers.list_users); @@ -145,6 +154,44 @@ describe("UsersView", () => { expect(wrapper.exists()).toBe(true); expect(requestHandlers.languagecode).toHaveBeenCalled(); expect(requestHandlers.list_users).toHaveBeenCalled(); + expect(requestHandlers.list_users).toHaveBeenCalledWith({ + currentSignInIp: "", + email: "", + limit: 10, + page: 1, + pendingUser: false, + }); expect(htmlRemoveId(wrapper.html())).toMatchSnapshot(); }); + + it("Show list with moderation", async () => { + const wrapper = generateWrapper(true); + await wrapper.vm.$nextTick(); + await flushPromises(); + expect(wrapper.exists()).toBe(true); + expect(requestHandlers.languagecode).toHaveBeenCalledTimes(0); + expect(requestHandlers.list_users).toHaveBeenCalledTimes(1); + expect(requestHandlers.list_users).toHaveBeenCalledWith({ + currentSignInIp: "", + email: "", + limit: 10, + page: 1, + pendingUser: true, + }); + expect(htmlRemoveId(wrapper.html())).toMatchSnapshot(); + + wrapper.vm.pendingFieldValue = false; + //wrapper.find('input[type="checkbox"]').trigger("change"); + wrapper.find('input[type="text"]').setValue("@email.tld"); + wrapper.find('button[type="button"]').trigger("click"); + await flushPromises(); + expect(requestHandlers.list_users).toHaveBeenCalledTimes(3); + expect(requestHandlers.list_users).toHaveBeenNthCalledWith(3, { + currentSignInIp: "", + email: "@email.tld", + limit: 10, + page: 1, + pendingUser: false, + }); + }); });