From ababdd7752bf07d8559e0c27dc6e23ddeecb1ac2 Mon Sep 17 00:00:00 2001 From: Guy McAuliffe <38873380+gmcauliffe@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:45:10 -0800 Subject: [PATCH] FW-1818, FW-3934 list view for join requests and admin approve and ignore (#187) * remove padding from DashboardTable * Add reusable infiniteScroll hook * Use infiniteScroll hook in useJoinRequests * Increase max char for join message * FW-1818 Dashboard JoinRequests List * Add JoinRequest list to Dashboard homepage * Rename DashboardJoinRequests to List * Approve/Ignore mvp with button and select * Styling and copy adjustments * Add auth check to Join Requests list * Adjustments for smaller screens and styling tweaks * Sonar fix --- src/common/dataHooks/useInfiniteScroll.js | 60 +++++++ src/common/dataHooks/useJoinRequests.js | 26 ++- .../DashboardHomePresentation.js | 80 +++++++-- .../DashboardJoinCardContainer.js | 25 +++ .../DashboardJoinCardData.js | 21 +++ .../DashboardJoinCardPresentation.js | 153 ++++++++++++++++++ src/components/DashboardJoinCard/index.js | 9 ++ .../DashboardJoinListContainer.js | 23 +++ .../DashboardJoinListData.js | 23 +++ .../DashboardJoinListPresentation.js | 79 +++++++++ src/components/DashboardJoinList/index.js | 9 ++ .../DashboardLandingPresentation.js | 2 +- .../DashboardTablePresentation.js | 4 +- src/components/Join/JoinForm.js | 2 +- 14 files changed, 487 insertions(+), 29 deletions(-) create mode 100644 src/common/dataHooks/useInfiniteScroll.js create mode 100644 src/components/DashboardJoinCard/DashboardJoinCardContainer.js create mode 100644 src/components/DashboardJoinCard/DashboardJoinCardData.js create mode 100644 src/components/DashboardJoinCard/DashboardJoinCardPresentation.js create mode 100644 src/components/DashboardJoinCard/index.js create mode 100644 src/components/DashboardJoinList/DashboardJoinListContainer.js create mode 100644 src/components/DashboardJoinList/DashboardJoinListData.js create mode 100644 src/components/DashboardJoinList/DashboardJoinListPresentation.js create mode 100644 src/components/DashboardJoinList/index.js diff --git a/src/common/dataHooks/useInfiniteScroll.js b/src/common/dataHooks/useInfiniteScroll.js new file mode 100644 index 00000000..cd13d84a --- /dev/null +++ b/src/common/dataHooks/useInfiniteScroll.js @@ -0,0 +1,60 @@ +import { useRef } from 'react' +import { useInfiniteQuery } from '@tanstack/react-query' + +// FPCC +import useIntersectionObserver from 'common/hooks/useIntersectionObserver' +import { useSiteStore } from 'context/SiteContext' + +/** + * Calls API and provides results and infinite scroll info. + */ +function useInfiniteScroll({ queryKey, queryFn, resultAdaptor }) { + const { site } = useSiteStore() + + const pagesDataAdaptor = (pages) => + pages.map((page, index) => singlePageDataAdaptor(page, index)) + + const singlePageDataAdaptor = (page, index) => { + const formattedResult = page?.results?.map((result) => + resultAdaptor ? resultAdaptor(result) : result, + ) + return { + ...page, + pageNumber: index + 1, + results: formattedResult, + } + } + + // Fetch search results + const response = useInfiniteQuery({ + queryKey, + queryFn, + enabled: !!site?.sitename, + getNextPageParam: (currentPage) => currentPage.next, + select: (responseData) => ({ + pages: pagesDataAdaptor(responseData.pages), + pageParams: responseData.pageParams, + }), + }) + + const infiniteScroll = { + fetchNextPage: response?.fetchNextPage, + hasNextPage: response?.hasNextPage, + isFetchingNextPage: response?.isFetchingNextPage, + } + + const loadRef = useRef(null) + useIntersectionObserver({ + target: loadRef, + onIntersect: response?.fetchNextPage, + enabled: response?.hasNextPage, + }) + + return { + ...response, + infiniteScroll, + loadRef, + } +} + +export default useInfiniteScroll diff --git a/src/common/dataHooks/useJoinRequests.js b/src/common/dataHooks/useJoinRequests.js index 3c135fa0..c02e6537 100644 --- a/src/common/dataHooks/useJoinRequests.js +++ b/src/common/dataHooks/useJoinRequests.js @@ -1,32 +1,26 @@ -import { - useInfiniteQuery, - useMutation, - useQueryClient, -} from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useParams } from 'react-router-dom' // FPCC import api from 'services/api' import { JOIN_REQUESTS, MEMBER } from 'common/constants' import useMutationWithNotification from 'common/dataHooks/useMutationWithNotification' +import useInfiniteScroll from 'common/dataHooks/useInfiniteScroll' +import { useSiteStore } from 'context/SiteContext' export function useJoinRequests() { - const { sitename } = useParams() + const { site } = useSiteStore() - const allJoinRequestsResponse = useInfiniteQuery( - [JOIN_REQUESTS, sitename], - ({ pageParam = 1 }) => + const response = useInfiniteScroll({ + queryKey: [JOIN_REQUESTS, site?.sitename], + queryFn: ({ pageParam = 1 }) => api.joinRequests.getJoinRequests({ - sitename, + sitename: site?.sitename, pageParam, }), - { - // The query will not execute until the sitename exists - enabled: !!sitename, - }, - ) + }) - return allJoinRequestsResponse + return response } export function useJoinRequestCreate(options = {}) { diff --git a/src/components/DashboardHome/DashboardHomePresentation.js b/src/components/DashboardHome/DashboardHomePresentation.js index a62b3ef7..62ead3f3 100644 --- a/src/components/DashboardHome/DashboardHomePresentation.js +++ b/src/components/DashboardHome/DashboardHomePresentation.js @@ -1,21 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' +import { Link } from 'react-router-dom' // FPCC -import DashboardTiles from 'components/DashboardTiles' +import getIcon from 'common/utils/getIcon' +import RequireAuth from 'common/RequireAuth' +import { LANGUAGE_ADMIN, MEMBER } from 'common/constants/roles' import DashboardLocator from 'components/DashboardLocator' +import DashboardJoinList from 'components/DashboardJoinList' function DashboardHomePresentation({ site, tiles, currentUser }) { return (
-
-

Dashboard Landing Page

- -
+

Dashboard Landing Page

+
+
-

- Dashboard Overview -

@@ -41,7 +41,69 @@ function DashboardHomePresentation({ site, tiles, currentUser }) {
- +
+

+ Quick Links +

+ +
+ +
+

+ Requests to join {site?.title} +

+ +
+
) diff --git a/src/components/DashboardJoinCard/DashboardJoinCardContainer.js b/src/components/DashboardJoinCard/DashboardJoinCardContainer.js new file mode 100644 index 00000000..64ee6f76 --- /dev/null +++ b/src/components/DashboardJoinCard/DashboardJoinCardContainer.js @@ -0,0 +1,25 @@ +import React from 'react' +import PropTypes from 'prop-types' + +// FPCC +import DashboardJoinCardData from 'components/DashboardJoinCard/DashboardJoinCardData' +import DashboardJoinCardPresentation from 'components/DashboardJoinCard/DashboardJoinCardPresentation' + +function DashboardJoinCardContainer({ joinRequest }) { + const { handleIgnore, handleApprove } = DashboardJoinCardData({ joinRequest }) + return ( + + ) +} + +// PROPTYPES +const { object } = PropTypes +DashboardJoinCardContainer.propTypes = { + joinRequest: object, +} + +export default DashboardJoinCardContainer diff --git a/src/components/DashboardJoinCard/DashboardJoinCardData.js b/src/components/DashboardJoinCard/DashboardJoinCardData.js new file mode 100644 index 00000000..c8d96708 --- /dev/null +++ b/src/components/DashboardJoinCard/DashboardJoinCardData.js @@ -0,0 +1,21 @@ +// FPCC +import { + useJoinRequestApprove, + useJoinRequestIgnore, +} from 'common/dataHooks/useJoinRequests' + +function DashboardJoinCardData({ joinRequest }) { + const { onSubmit: ignoreRequest } = useJoinRequestIgnore() + const { onSubmit: approveRequest } = useJoinRequestApprove() + + const handleApprove = (roleToAssign) => { + approveRequest({ id: joinRequest.id, role: roleToAssign }) + } + + return { + handleIgnore: () => ignoreRequest(joinRequest?.id), + handleApprove, + } +} + +export default DashboardJoinCardData diff --git a/src/components/DashboardJoinCard/DashboardJoinCardPresentation.js b/src/components/DashboardJoinCard/DashboardJoinCardPresentation.js new file mode 100644 index 00000000..d0f6bcbd --- /dev/null +++ b/src/components/DashboardJoinCard/DashboardJoinCardPresentation.js @@ -0,0 +1,153 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' + +// FPCC +import { localDateMDYT } from 'common/utils/stringHelpers' +import { + MEMBER, + ASSISTANT, + EDITOR, + LANGUAGE_ADMIN, +} from 'common/constants/roles' +import getIcon from 'common/utils/getIcon' + +function DashboardJoinCardPresentation({ + joinRequest, + handleIgnore, + handleApprove, +}) { + const [isExpanded, setIsExpanded] = useState(false) + const [selectedRole, setSelectedRole] = useState(MEMBER) + + const nameOfUser = + joinRequest?.user?.firstName || joinRequest?.user?.lastName + ? `${joinRequest?.user?.firstName} ${joinRequest?.user?.lastName}` + : null + + return ( +
  • +
    +
    +
    +

    + {nameOfUser || joinRequest?.user?.email} +

    +
    +
    +

    {joinRequest?.user?.email}

    + + + +

    + Requested on{' '} + +

    +
    + + +
    +
    +
    +
    + + + + + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    + {joinRequest?.reasons?.length === 1 + ? 'Reason for request' + : 'Reasons for request'} +
    +
    + {joinRequest?.reasons?.map((reason) => reason?.reason).join(', ')} +
    +
    +
    +
    + Message from the user +
    +
    + {joinRequest?.reasonNote} +
    +
    + +
    +
    +
  • + ) +} +// PROPTYPES +const { func, object } = PropTypes +DashboardJoinCardPresentation.propTypes = { + joinRequest: object, + handleIgnore: func, + handleApprove: func, +} + +export default DashboardJoinCardPresentation diff --git a/src/components/DashboardJoinCard/index.js b/src/components/DashboardJoinCard/index.js new file mode 100644 index 00000000..0052be7a --- /dev/null +++ b/src/components/DashboardJoinCard/index.js @@ -0,0 +1,9 @@ +import DashboardJoinCardContainer from 'components/DashboardJoinCard/DashboardJoinCardContainer' +import DashboardJoinCardData from 'components/DashboardJoinCard/DashboardJoinCardData' +import DashboardJoinCardPresentation from 'components/DashboardJoinCard/DashboardJoinCardPresentation' + +export default { + Container: DashboardJoinCardContainer, + Data: DashboardJoinCardData, + Presentation: DashboardJoinCardPresentation, +} diff --git a/src/components/DashboardJoinList/DashboardJoinListContainer.js b/src/components/DashboardJoinList/DashboardJoinListContainer.js new file mode 100644 index 00000000..876a18d4 --- /dev/null +++ b/src/components/DashboardJoinList/DashboardJoinListContainer.js @@ -0,0 +1,23 @@ +import React from 'react' + +// FPCC +import DashboardJoinListData from 'components/DashboardJoinList/DashboardJoinListData' +import DashboardJoinListPresentation from 'components/DashboardJoinList/DashboardJoinListPresentation' + +function DashboardJoinListContainer() { + const { joinRequests, infiniteScroll, loadRef, isLoading, site } = + DashboardJoinListData() + return ( +
    + +
    + ) +} + +export default DashboardJoinListContainer diff --git a/src/components/DashboardJoinList/DashboardJoinListData.js b/src/components/DashboardJoinList/DashboardJoinListData.js new file mode 100644 index 00000000..e4d4c9a9 --- /dev/null +++ b/src/components/DashboardJoinList/DashboardJoinListData.js @@ -0,0 +1,23 @@ +import { useParams } from 'react-router-dom' + +// FPCC +import { useSiteStore } from 'context/SiteContext' +import { useJoinRequests } from 'common/dataHooks/useJoinRequests' + +function DashboardJoinListData() { + const { site } = useSiteStore() + const { sitename } = useParams() + + const { data, isError, infiniteScroll, loadRef, isInitialLoading } = + useJoinRequests() + return { + isLoading: isInitialLoading || isError, + infiniteScroll, + loadRef, + site, + sitename, + joinRequests: data || {}, + } +} + +export default DashboardJoinListData diff --git a/src/components/DashboardJoinList/DashboardJoinListPresentation.js b/src/components/DashboardJoinList/DashboardJoinListPresentation.js new file mode 100644 index 00000000..3028e50f --- /dev/null +++ b/src/components/DashboardJoinList/DashboardJoinListPresentation.js @@ -0,0 +1,79 @@ +import React, { Fragment } from 'react' +import PropTypes from 'prop-types' + +// FPCC +import Loading from 'components/Loading' +import DashboardJoinCard from 'components/DashboardJoinCard' + +function DashboardJoinListPresentation({ + isLoading, + infiniteScroll, + joinRequests, + loadRef, +}) { + const { isFetchingNextPage, fetchNextPage, hasNextPage } = infiniteScroll + + const getLoadLabel = () => { + if (infiniteScroll?.isFetchingNextPage) { + return 'Loading more...' + } + if (infiniteScroll?.hasNextPage) { + return 'Load more' + } + return '' + } + return ( + +
    +
    + {joinRequests?.pages?.[0]?.count > 0 ? ( +
    +
    + {joinRequests?.pages.map((page) => ( + + {page.results.map((request) => ( + + ))} + + ))} + +
    +
    + ) : ( +
    +
    + There are currently no pending join requests for your site. +
    +
    + )} +
    +
    +
    + + ) +} +// PROPTYPES +const { bool, object } = PropTypes +DashboardJoinListPresentation.propTypes = { + joinRequests: object, + isLoading: bool, + loadRef: object, + infiniteScroll: object, +} + +export default DashboardJoinListPresentation diff --git a/src/components/DashboardJoinList/index.js b/src/components/DashboardJoinList/index.js new file mode 100644 index 00000000..6862567c --- /dev/null +++ b/src/components/DashboardJoinList/index.js @@ -0,0 +1,9 @@ +import DashboardJoinListContainer from 'components/DashboardJoinList/DashboardJoinListContainer' +import DashboardJoinListData from 'components/DashboardJoinList/DashboardJoinListData' +import DashboardJoinListPresentation from 'components/DashboardJoinList/DashboardJoinListPresentation' + +export default { + Container: DashboardJoinListContainer, + Data: DashboardJoinListData, + Presentation: DashboardJoinListPresentation, +} diff --git a/src/components/DashboardLanding/DashboardLandingPresentation.js b/src/components/DashboardLanding/DashboardLandingPresentation.js index dbae8445..3c92bb68 100644 --- a/src/components/DashboardLanding/DashboardLandingPresentation.js +++ b/src/components/DashboardLanding/DashboardLandingPresentation.js @@ -49,7 +49,7 @@ function DashboardLandingPresentation({
    )} - {children} +
    {children}
    diff --git a/src/components/DashboardTable/DashboardTablePresentation.js b/src/components/DashboardTable/DashboardTablePresentation.js index d7c2f90f..ebced098 100644 --- a/src/components/DashboardTable/DashboardTablePresentation.js +++ b/src/components/DashboardTable/DashboardTablePresentation.js @@ -13,8 +13,8 @@ function DashboardTablePresentation({ return (
    {title && ( diff --git a/src/components/Join/JoinForm.js b/src/components/Join/JoinForm.js index 2132c8d7..8cefd817 100644 --- a/src/components/Join/JoinForm.js +++ b/src/components/Join/JoinForm.js @@ -49,7 +49,7 @@ function JoinForm({ site, submitHandler }) { .min(1, 'At least one reason must be selected') .required(), message: definitions - .paragraph() + .paragraph({ charCount: 2500 }) .required('Please include a message to the Language Administrator'), sitename: yup.string().required(), })