diff --git a/CollAction/GraphQl/Queries/QueryGraph.cs b/CollAction/GraphQl/Queries/QueryGraph.cs index c66497f67..dac94a9b9 100644 --- a/CollAction/GraphQl/Queries/QueryGraph.cs +++ b/CollAction/GraphQl/Queries/QueryGraph.cs @@ -3,6 +3,7 @@ using CollAction.Helpers; using CollAction.Models; using CollAction.Services.Crowdactions; +using CollAction.Services.User; using GraphQL.Authorization; using GraphQL.EntityFramework; using GraphQL.Types; @@ -107,9 +108,20 @@ public QueryGraph(IEfGraphQLService entityFrameworkGraphQl }); AddQueryField( - nameof(ApplicationDbContext.Users), - c => c.DbContext.Users, - typeof(ApplicationUserGraph)).AuthorizeWith(AuthorizationConstants.GraphQlAdminPolicy); + name: nameof(ApplicationDbContext.Users), + arguments: new QueryArgument[] + { + new QueryArgument() { Name = "search" } + }, + resolve: c => + { + string? search = c.GetArgument("search"); + return c.GetUserContext() + .ServiceProvider + .GetRequiredService() + .SearchUsers(search); + }, + graphType: typeof(ApplicationUserGraph)).AuthorizeWith(AuthorizationConstants.GraphQlAdminPolicy); AddSingleField( name: "user", @@ -118,7 +130,16 @@ public QueryGraph(IEfGraphQLService entityFrameworkGraphQl FieldAsync, int>( "userCount", - resolve: c => c.GetUserContext().Context.Users.CountAsync()).AuthorizeWith(AuthorizationConstants.GraphQlAdminPolicy); + arguments: new QueryArguments(new QueryArgument() { Name = "search" }), + resolve: c => + { + string? search = c.GetArgument("search"); + return c.GetUserContext() + .ServiceProvider + .GetRequiredService() + .SearchUsers(search) + .CountAsync(); + }).AuthorizeWith(AuthorizationConstants.GraphQlAdminPolicy); AddSingleField( name: "currentUser", diff --git a/CollAction/Services/User/IUserService.cs b/CollAction/Services/User/IUserService.cs index 1f165a969..a2baabcbd 100644 --- a/CollAction/Services/User/IUserService.cs +++ b/CollAction/Services/User/IUserService.cs @@ -1,6 +1,8 @@ -using CollAction.Services.User.Models; +using CollAction.Models; +using CollAction.Services.User.Models; using Microsoft.AspNetCore.Identity; using Newtonsoft.Json.Linq; +using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; @@ -26,5 +28,7 @@ public interface IUserService Task FinishRegistration(NewUser newUser, string code); Task IngestUserEvent(ClaimsPrincipal trackedUser, JObject eventData, bool canTrack, CancellationToken token); + + IQueryable SearchUsers(string? searchString); } } \ No newline at end of file diff --git a/CollAction/Services/User/UserService.cs b/CollAction/Services/User/UserService.cs index 9ede6cf71..9d7ccb151 100644 --- a/CollAction/Services/User/UserService.cs +++ b/CollAction/Services/User/UserService.cs @@ -141,6 +141,23 @@ public async Task CreateUser(NewUser newUser) return (IdentityResult.Success, code); } + public IQueryable SearchUsers(string? searchString) + { + if (!string.IsNullOrWhiteSpace(searchString)) + { +#pragma warning disable CA1307 // Not needed, translated to sql + return context.Users + .Where(u => u.Email.Contains(searchString) || + u.FirstName!.Contains(searchString) || + u.LastName!.Contains(searchString)); +#pragma warning restore CA1307 // Not needed, translated to sql + } + else + { + return context.Users; + } + } + public async Task ResetPassword(string email, string code, string password) { ApplicationUser? user = await userManager.FindByEmailAsync(email).ConfigureAwait(false); diff --git a/Frontend/src/components/Admin/Users/AdminListUsers.tsx b/Frontend/src/components/Admin/Users/AdminListUsers.tsx index e4b198b37..aa6f10e88 100644 --- a/Frontend/src/components/Admin/Users/AdminListUsers.tsx +++ b/Frontend/src/components/Admin/Users/AdminListUsers.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Paper, TableContainer, Table, TableHead, TableCell, TableRow, TableBody, TablePagination, Dialog, DialogTitle, DialogContent, DialogActions } from "@material-ui/core"; +import { Paper, TableContainer, Table, TableHead, TableCell, TableRow, TableBody, TablePagination, Dialog, DialogTitle, DialogContent, DialogActions, TextField } from "@material-ui/core"; import { gql, useQuery, useMutation } from "@apollo/client"; import { IUser } from "../../../api/types"; import { Alert } from "../../Alert/Alert"; @@ -14,6 +14,7 @@ export default () => { const [toDelete, setToDelete] = useState(null); const [info, setInfo] = useState(null); const [error, setError] = useState(null); + const [search, setSearch] = useState(""); const {data, loading, error: loadingError} = useQuery( GET_USERS, { @@ -21,7 +22,8 @@ export default () => { variables: { skip: rowsPerPage * page, take: rowsPerPage, - orderBy: "lastName" + orderBy: "lastName", + search: search } } ); @@ -60,11 +62,27 @@ export default () => { ); const userCount = data?.userCount ?? 0; + const onSearchChange = (ev: React.ChangeEvent) => { + setSearch(ev.target.value); + setPage(0); + }; + + const onDeleteUserClick = (user: IUser) => { + setDeleteDialogOpen(true); + setToDelete(user); + }; + + const onChangeRowsPerPage = (ev: React.ChangeEvent) => { + setPage(0); + setRowsPerPage(parseInt((ev.target.value))); + }; + return <> { loading ? : null } + { data?.users && data.users.length === 0 && search.length > 0 && } setDeleteDialogOpen(false)}> Delete user { toDelete?.email }? @@ -75,6 +93,7 @@ export default () => { + @@ -97,11 +116,11 @@ export default () => { { u.isAdmin ? "Yes" : "No" } { Formatter.date(new Date(u.registrationDate)) } { Formatter.time(new Date(u.registrationDate)) } - + )) } - setPage(newPage)} onChangeRowsPerPage={(ev) => { setPage(0); setRowsPerPage(parseInt((ev.target.value))) } } /> + setPage(newPage)} onChangeRowsPerPage={onChangeRowsPerPage} />
@@ -110,8 +129,12 @@ export default () => { }; const GET_USERS = gql` - query GetUserData($skip: Int!, $take: Int!, $orderBy: String!) { - users(orderBy: [{ path: $orderBy, descending: false}], skip: $skip, take: $take) { + query GetUserData($skip: Int!, $take: Int!, $orderBy: String!, $search: String!) { + users( + orderBy: [{ path: $orderBy, descending: false}], + skip: $skip, + take: $take, + search: $search) { id email isSubscribedNewsletter @@ -122,7 +145,7 @@ const GET_USERS = gql` registrationDate representsNumberParticipants } - userCount + userCount(search: $search) } `;