Skip to content

Commit

Permalink
Add search for members on paginated member page (cobudget#369)
Browse files Browse the repository at this point in the history
  • Loading branch information
shsunmoonlee committed Feb 11, 2022
1 parent 8a85e26 commit d507619
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 1,076 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -14,6 +14,8 @@ Next.js, Urql, Apollo, Prisma, PostgreSQL
- Install dependencies in `/ui`
- `yarn`
- In /ui, copy `.env.local.default` to `.env.local`
- In /ui, copy and paste DATABASE_URL string from `.env.development` to `.env.local`
- put MAGIC_LINK_SECRET="fake" in `.env.local`

### Running the project

Expand All @@ -25,6 +27,8 @@ In `/ui`:
docker-compose up
```

Then, run `yarn migrate` in another terminal.

### Start development process

In `/ui`
Expand All @@ -33,6 +37,8 @@ In `/ui`
yarn dev
```

You can login with the link shown in the terminal.

## License

Released under AGPL-3.0-or-later, with some additional terms. All of which are included in the file [LICENSE](LICENSE) in the git repository.
3 changes: 1 addition & 2 deletions ui/.env.local.default
@@ -1,6 +1,5 @@
COOKIE_SECRET="A_LONG_RANDOM_VALUE_THAT_IS_NOT_THIS_STRING"
DATABASE_URL=""
POSTMARK_FROM_EMAIL="fake"
POSTMARK_API_TOKEN="fake"
MAGIC_LINK_SECRET=""
MAGIC_LINK_SECRET="fake"
FROM_EMAIL=""
32 changes: 32 additions & 0 deletions ui/components/EventMembers/SearchBar.tsx
@@ -0,0 +1,32 @@
import React from "react";
import { SearchIcon, CloseIcon } from "../Icons";

export default ({ placeholder, collection, clearInput, value, onChange }) => {
return (
<div
className={`bg-white shadow-sm rounded-md border-transparent focus-within:border-${collection.color} border-3 px-1 relative pr-10 flex items-center overflow-hidden`}
>
<input
className="appearance-none block px-3 py-2 w-full placeholder-gray-400 text-gray-600 focus:text-gray-800 focus:outline-none"
type="text"
placeholder={placeholder}
value={value}
onChange={onChange}
autoFocus
/>
<button
className={
`h-full absolute inset-y-0 right-0 flex items-center p-3 focus:outline-none transition-colors` +
" " +
(value !== "" ? `bg-${collection.color} text-white` : "text-gray-400")
}
>
{value === "" ? (
<SearchIcon className="h-5 w-5" />
) : (
<CloseIcon className="h-5 w-5" onClick={clearInput} />
)}
</button>
</div>
);
};
40 changes: 33 additions & 7 deletions ui/components/EventMembers/index.tsx
@@ -1,25 +1,27 @@
import { useState } from "react";
import { useState, useMemo, useEffect } from "react";
import { useQuery, useMutation, gql } from "urql";
import { debounce } from "lodash";

import Button from "../Button";
import InviteMembersModal from "../InviteMembersModal";
import LoadMore from "../LoadMore";

import MembersTable from "./MembersTable";
import RequestsToJoinTable from "./RequestToJoinTable";
import SearchBar from "components/EventMembers/SearchBar";

export const COLLECTION_MEMBERS_QUERY = gql`
query Members($collectionId: ID!, $offset: Int, $limit: Int) {
query Members($collectionId: ID!, $usernameStartsWith: String!, $offset: Int, $limit: Int) {
approvedMembersPage: membersPage(
collectionId: $collectionId
isApproved: true
usernameStartsWith: $usernameStartsWith
offset: $offset
limit: $limit
) {
moreExist
approvedMembers: members(
collectionId: $collectionId
isApproved: true
isApproved: false
offset: $offset
limit: $limit
) {
Expand Down Expand Up @@ -95,6 +97,7 @@ const DELETE_MEMBER = gql`
`;

const EventMembers = ({ collection, currentUser }) => {
const [searchString, setSearchString] = useState("");
const [
{
data: {
Expand All @@ -114,11 +117,28 @@ const EventMembers = ({ collection, currentUser }) => {
fetching: loading,
error,
},
searchApprovedMembers
] = useQuery({
query: COLLECTION_MEMBERS_QUERY,
variables: { collectionId: collection.id, offset: 0, limit: 1000 },
variables: { collectionId: collection.id, usernameStartsWith: searchString, offset: 0, limit: 1000 },
pause: true,
});

const debouncedSearchMembers = useMemo(() => {
return debounce(searchApprovedMembers, 300, { leading: true });
}, [searchApprovedMembers]);

const items = useMemo(() => {
if (loading || !approvedMembers) {
return [];
}
return approvedMembers
}, [approvedMembers, loading]);

useEffect(() => {
debouncedSearchMembers();
}, [debouncedSearchMembers]);

const [, updateMember] = useMutation(UPDATE_MEMBER);

const [, deleteMember] = useMutation(DELETE_MEMBER);
Expand All @@ -145,7 +165,13 @@ const EventMembers = ({ collection, currentUser }) => {
/>

<div className="flex justify-between mb-3 items-center">
<h2 className="text-xl font-semibold">All collection members</h2>
<SearchBar
collection={collection}
value={searchString}
placeholder="Search members"
onChange={(e) => setSearchString(e.target.value)}
clearInput={() => setSearchString("")}
/>
{isAdmin && (
<div className="flex items-center space-x-2">
<Button onClick={() => setInviteModalOpen(true)}>
Expand All @@ -162,7 +188,7 @@ const EventMembers = ({ collection, currentUser }) => {
</div>

<MembersTable
approvedMembers={approvedMembers}
approvedMembers={items}
updateMember={updateMember}
deleteMember={deleteMember}
collection={collection}
Expand Down
2 changes: 1 addition & 1 deletion ui/package.json
Expand Up @@ -58,7 +58,7 @@
"keycloak-admin": "^1.14.22",
"lodash": "^4.17.21",
"micro": "^9.3.4",
"next": "12.0.7",
"next": "12.0.11-canary.9",
"next-connect": "^0.10.2",
"next-cookies": "^2.0.3",
"next-fonts": "^0.19.0",
Expand Down
15 changes: 12 additions & 3 deletions ui/server/graphql/resolvers/index.ts
Expand Up @@ -360,30 +360,39 @@ const resolvers = {
),
members: combineResolvers(
isCollMemberOrOrgAdmin,
async (parent, { collectionId, isApproved }, { user }) => {
async (parent, { collectionId, isApproved, usernameStartsWith }) => {
return await prisma.collectionMember.findMany({
where: {
collectionId,
...(typeof isApproved === "boolean" && { isApproved }),
...(usernameStartsWith && {
user: { username: { startsWith: usernameStartsWith } },
}),
},
...(usernameStartsWith && { include: { user: true } }),
});
}
),
membersPage: combineResolvers(
isCollMemberOrOrgAdmin,
async (
parent,
{ collectionId, isApproved, offset = 0, limit = 10 },
{ collectionId, isApproved, usernameStartsWith, offset = 0, limit = 10 },
{ user }
) => {

const collectionMembersWithExtra = await prisma.collectionMember.findMany(
{
where: {
collectionId,
...(typeof isApproved === "boolean" && { isApproved }),
...(usernameStartsWith && {
user: { username: { startsWith: usernameStartsWith } },
}),
},
take: limit + 1,
skip: offset,
...(usernameStartsWith && { include: { user: true } }),
}
);

Expand Down Expand Up @@ -2196,7 +2205,7 @@ const resolvers = {
if (!user) return null;
if (parent.id !== user.id) return null;
return parent.name;
},
}
},
Organization: {
info: (org) => {
Expand Down
8 changes: 7 additions & 1 deletion ui/server/graphql/schema/index.js
Expand Up @@ -25,10 +25,15 @@ const schema = gql`
membersPage(
collectionId: ID!
isApproved: Boolean
usernameStartsWith: String
offset: Int
limit: Int
): MembersPage
members(collectionId: ID!, isApproved: Boolean): [CollectionMember]
members(
collectionId: ID!
isApproved: Boolean
usernameStartsWith: String
): [CollectionMember]
categories(orgId: ID!): [Category!]
contributionsPage(
collectionId: ID!
Expand Down Expand Up @@ -356,6 +361,7 @@ const schema = gql`
members(
collectionId: ID!
isApproved: Boolean
usernameStartsWith: String
offset: Int
limit: Int
): [CollectionMember]
Expand Down

0 comments on commit d507619

Please sign in to comment.