From 7771b27b5560b658d35236ceb97c7ac0bdb959ea Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 13 Dec 2021 17:02:10 +0100 Subject: [PATCH] Allow to filter user memberships and group memberships (contacts=) on backend side Closes #981 #969 Signed-off-by: Thomas Citharel --- js/src/components/Event/OrganizerPicker.vue | 7 ++ .../Event/OrganizerPickerWrapper.vue | 86 ++++++++++++------- js/src/graphql/actor.ts | 8 +- js/src/graphql/member.ts | 12 ++- js/src/views/Group/GroupMembers.vue | 6 +- lib/graphql/resolvers/member.ex | 6 +- lib/graphql/resolvers/user.ex | 3 +- lib/graphql/schema/actors/group.ex | 1 + lib/graphql/schema/user.ex | 2 + lib/mobilizon/actors/actors.ex | 42 +++++++-- lib/web/views/activity_pub/actor_view.ex | 2 +- 11 files changed, 122 insertions(+), 53 deletions(-) diff --git a/js/src/components/Event/OrganizerPicker.vue b/js/src/components/Event/OrganizerPicker.vue index eaca31a8..d9c2b1d1 100644 --- a/js/src/components/Event/OrganizerPicker.vue +++ b/js/src/components/Event/OrganizerPicker.vue @@ -51,6 +51,13 @@ import { MemberRole } from "@/types/enums"; groupMemberships: { query: LOGGED_USER_MEMBERSHIPS, update: (data) => data.loggedUser.memberships, + variables() { + return { + page: 1, + limit: 10, + membershipName: this.actorFilter, + }; + }, }, identities: IDENTITIES, currentActor: CURRENT_ACTOR_CLIENT, diff --git a/js/src/components/Event/OrganizerPickerWrapper.vue b/js/src/components/Event/OrganizerPickerWrapper.vue index de352277..8a07705a 100644 --- a/js/src/components/Event/OrganizerPickerWrapper.vue +++ b/js/src/components/Event/OrganizerPickerWrapper.vue @@ -65,42 +65,60 @@ />
-
+

{{ $t("Add a contact") }}

-

- -

-
-
- +

+ +

+
+
+ +
+ -
- -
-
-

{{ actor.name }}

-

+

+
+

{{ actor.name }}

+

+ {{ `@${usernameWithDomain(actor)}` }} +

+
+
{{ `@${usernameWithDomain(actor)}` }} -

+
-
- {{ `@${usernameWithDomain(actor)}` }} -
-
- -

+ +

+
+
+ + {{ $t("No group member found") }} + +

{{ $t("Your profile will be shown as contact.") }}

@@ -122,6 +140,7 @@ import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { IMember } from "@/types/actor/member.model"; import { IActor, IGroup, IPerson, usernameWithDomain } from "../../types/actor"; import OrganizerPicker from "./OrganizerPicker.vue"; +import EmptyContent from "../Utils/EmptyContent.vue"; import { CURRENT_ACTOR_CLIENT, IDENTITIES, @@ -139,16 +158,17 @@ const MEMBER_ROLES = [ ]; @Component({ - components: { OrganizerPicker }, + components: { OrganizerPicker, EmptyContent }, apollo: { members: { query: GROUP_MEMBERS, variables() { return { - name: usernameWithDomain(this.selectedActor), + groupName: usernameWithDomain(this.selectedActor), page: this.membersPage, limit: 10, roles: MEMBER_ROLES.join(","), + name: this.contactFilter, }; }, update: (data) => data.group.members, @@ -161,9 +181,11 @@ const MEMBER_ROLES = [ currentActor: CURRENT_ACTOR_CLIENT, userMemberships: { query: LOGGED_USER_MEMBERSHIPS, - variables: { - page: 1, - limit: 100, + variables() { + return { + page: 1, + limit: 10, + }; }, update: (data) => data.loggedUser.memberships, }, diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index 9bbbc3b7..962611c1 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -239,10 +239,14 @@ export const LOGGED_USER_DRAFTS = gql` `; export const LOGGED_USER_MEMBERSHIPS = gql` - query LoggedUserMemberships($page: Int, $limit: Int) { + query LoggedUserMemberships( + $membershipName: String + $page: Int + $limit: Int + ) { loggedUser { id - memberships(page: $page, limit: $limit) { + memberships(name: $membershipName, page: $page, limit: $limit) { total elements { id diff --git a/js/src/graphql/member.ts b/js/src/graphql/member.ts index 672e236b..4dc32f92 100644 --- a/js/src/graphql/member.ts +++ b/js/src/graphql/member.ts @@ -44,10 +44,16 @@ export const REJECT_INVITATION = gql` `; export const GROUP_MEMBERS = gql` - query ($name: String!, $roles: String, $page: Int, $limit: Int) { - group(preferredUsername: $name) { + query ( + $groupName: String! + $name: String + $roles: String + $page: Int + $limit: Int + ) { + group(preferredUsername: $groupName) { ...ActorFragment - members(page: $page, limit: $limit, roles: $roles) { + members(name: $name, page: $page, limit: $limit, roles: $roles) { elements { id role diff --git a/js/src/views/Group/GroupMembers.vue b/js/src/views/Group/GroupMembers.vue index b799bef1..3904299b 100644 --- a/js/src/views/Group/GroupMembers.vue +++ b/js/src/views/Group/GroupMembers.vue @@ -275,7 +275,7 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue"; query: GROUP_MEMBERS, variables() { return { - name: this.$route.params.preferredUsername, + groupName: this.$route.params.preferredUsername, page: this.page, limit: this.MEMBERS_PER_PAGE, roles: this.roles, @@ -325,7 +325,7 @@ export default class GroupMembers extends mixins(GroupMixin) { this.inviteError = ""; const { roles, MEMBERS_PER_PAGE, group, page } = this; const variables = { - name: usernameWithDomain(group), + groupName: usernameWithDomain(group), page, limit: MEMBERS_PER_PAGE, roles, @@ -393,7 +393,7 @@ export default class GroupMembers extends mixins(GroupMixin) { async removeMember(oldMember: IMember): Promise { const { roles, MEMBERS_PER_PAGE, group, page } = this; const variables = { - name: usernameWithDomain(group), + groupName: usernameWithDomain(group), page, limit: MEMBERS_PER_PAGE, roles, diff --git a/lib/graphql/resolvers/member.ex b/lib/graphql/resolvers/member.ex index 7c1f7cdb..9f479ff9 100644 --- a/lib/graphql/resolvers/member.ex +++ b/lib/graphql/resolvers/member.ex @@ -21,7 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do {:ok, Page.t(Member.t())} def find_members_for_group( %Actor{id: group_id} = group, - %{page: page, limit: limit, roles: roles}, + %{page: page, limit: limit, roles: roles} = args, %{ context: %{current_user: %User{role: user_role}, current_actor: %Actor{id: actor_id}} } = _resolution @@ -39,7 +39,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do |> Enum.map(&String.to_existing_atom/1) end - %Page{} = page = Actors.list_members_for_group(group, roles, page, limit) + %Page{} = + page = Actors.list_members_for_group(group, Map.get(args, :name), roles, page, limit) + {:ok, page} else # Actor is not member of group, fallback to public diff --git a/lib/graphql/resolvers/user.ex b/lib/graphql/resolvers/user.ex index 7e85a3e6..b9bda039 100644 --- a/lib/graphql/resolvers/user.ex +++ b/lib/graphql/resolvers/user.ex @@ -376,13 +376,14 @@ defmodule Mobilizon.GraphQL.Resolvers.User do """ def user_memberships( %User{id: user_id}, - %{page: page, limit: limit} = _args, + %{page: page, limit: limit} = args, %{context: %{current_user: %User{id: logged_user_id}}} ) do with true <- user_id == logged_user_id, memberships <- Actors.list_memberships_for_user( user_id, + Map.get(args, :name), page, limit ) do diff --git a/lib/graphql/schema/actors/group.ex b/lib/graphql/schema/actors/group.ex index 492382fa..6efeb0b2 100644 --- a/lib/graphql/schema/actors/group.ex +++ b/lib/graphql/schema/actors/group.ex @@ -103,6 +103,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do ) field :members, :paginated_member_list do + arg(:name, :string, description: "A name to filter members by") arg(:page, :integer, default_value: 1, description: "The page in the paginated member list") arg(:limit, :integer, default_value: 10, description: "The limit of members per page") arg(:roles, :string, default_value: "", description: "Filter members by their role") diff --git a/lib/graphql/schema/user.ex b/lib/graphql/schema/user.ex index cb567e28..ff27f9a8 100644 --- a/lib/graphql/schema/user.ex +++ b/lib/graphql/schema/user.ex @@ -85,6 +85,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do field(:memberships, :paginated_member_list, description: "The list of memberships for this user" ) do + arg(:name, :string, description: "A name to filter members by") + arg(:page, :integer, default_value: 1, description: "The page in the paginated memberships list" diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 35ca6ee1..a1a0caab 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -794,12 +794,14 @@ defmodule Mobilizon.Actors do """ @spec list_memberships_for_user( integer, + String.t() | nil, integer | nil, integer | nil - ) :: Page.t() - def list_memberships_for_user(user_id, page, limit) do + ) :: Page.t(Member.t()) + def list_memberships_for_user(user_id, name, page, limit) do user_id |> list_members_for_user_query() + |> filter_members_by_group_name(name) |> Page.build_page(page, limit) end @@ -827,12 +829,15 @@ defmodule Mobilizon.Actors do Page.t(Member.t()) def list_members_for_group( %Actor{id: group_id, type: :Group}, + name \\ nil, roles \\ [], page \\ nil, limit \\ nil ) do group_id |> members_for_group_query() + |> join_members_actor() + |> filter_members_by_actor_name(name) |> filter_member_role(roles) |> Page.build_page(page, limit) end @@ -1380,13 +1385,10 @@ defmodule Mobilizon.Actors do @spec list_members_for_user_query(integer()) :: Ecto.Query.t() defp list_members_for_user_query(user_id) do - from( - m in Member, - join: a in Actor, - on: m.actor_id == a.id, - where: a.user_id == ^user_id and m.role != ^:not_approved, - preload: [:parent, :actor, :invited_by] - ) + Member + |> join_members_actor() + |> where([m, a], a.user_id == ^user_id and m.role != ^:not_approved) + |> preload([:parent, :actor, :invited_by]) end @spec members_for_actor_query(integer | String.t()) :: Ecto.Query.t() @@ -1446,6 +1448,28 @@ defmodule Mobilizon.Actors do from(m in query, where: m.role == ^role) end + @spec filter_members_by_actor_name(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t() + defp filter_members_by_actor_name(query, nil), do: query + defp filter_members_by_actor_name(query, ""), do: query + + defp filter_members_by_actor_name(query, name) when is_binary(name) do + where(query, [_q, a], like(a.name, ^"%#{name}%") or like(a.preferred_username, ^"%#{name}%")) + end + + defp filter_members_by_group_name(query, nil), do: query + defp filter_members_by_group_name(query, ""), do: query + + defp filter_members_by_group_name(query, name) when is_binary(name) do + query + |> join(:inner, [q], a in Actor, on: q.parent_id == a.id) + |> where([_q, ..., a], like(a.name, ^"%#{name}%") or like(a.preferred_username, ^"%#{name}%")) + end + + @spec join_members_actor(Ecto.Query.t()) :: Ecto.Query.t() + defp join_members_actor(query) do + join(query, :inner, [q], a in Actor, on: q.actor_id == a.id) + end + @spec administrator_members_for_group_query(integer | String.t()) :: Ecto.Query.t() defp administrator_members_for_group_query(group_id) do from( diff --git a/lib/web/views/activity_pub/actor_view.ex b/lib/web/views/activity_pub/actor_view.ex index 085f1d0b..7f3143a5 100644 --- a/lib/web/views/activity_pub/actor_view.ex +++ b/lib/web/views/activity_pub/actor_view.ex @@ -100,7 +100,7 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do end defp fetch_collection(:members, actor, page) do - Actors.list_members_for_group(actor, @selected_member_roles, page) + Actors.list_members_for_group(actor, nil, @selected_member_roles, page) end defp fetch_collection(:resources, actor, page) do