From acba397075fd51888186797d406b056281de6289 Mon Sep 17 00:00:00 2001 From: Nat Budin Date: Thu, 10 Dec 2020 10:09:55 -0800 Subject: [PATCH 1/3] Rework the signup moderation queue based on react-table --- .../EventList/EventListPagination.tsx | 37 -- .../ScheduleGrid/queries.generated.ts | 2 +- .../SignupModerationQueue.tsx | 342 ++++++++++-------- app/javascript/SignupModeration/index.tsx | 21 +- .../SignupModeration/queries.generated.ts | 17 +- app/javascript/SignupModeration/queries.ts | 9 +- .../Tables/useReactRouterReactTable.tsx | 2 +- app/javascript/graphqlTypes.generated.ts | 2 + ...signup_requests_table_results_presenter.rb | 8 +- 9 files changed, 223 insertions(+), 217 deletions(-) delete mode 100644 app/javascript/EventsApp/EventList/EventListPagination.tsx diff --git a/app/javascript/EventsApp/EventList/EventListPagination.tsx b/app/javascript/EventsApp/EventList/EventListPagination.tsx deleted file mode 100644 index eab7785da5e..00000000000 --- a/app/javascript/EventsApp/EventList/EventListPagination.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from 'react'; -// @ts-expect-error -import Pagination from 'react-js-pagination'; -import classNames from 'classnames'; - -export type EventListPaginationProps = { - currentPage: number; - totalPages: number; - onPageChange: React.Dispatch; - extraClasses?: string[]; -}; - -function EventListPagination({ - currentPage, - totalPages, - onPageChange, - extraClasses, -}: EventListPaginationProps) { - return ( - - ); -} - -export default EventListPagination; diff --git a/app/javascript/EventsApp/ScheduleGrid/queries.generated.ts b/app/javascript/EventsApp/ScheduleGrid/queries.generated.ts index 84b71374332..2892e80ff7b 100644 --- a/app/javascript/EventsApp/ScheduleGrid/queries.generated.ts +++ b/app/javascript/EventsApp/ScheduleGrid/queries.generated.ts @@ -20,7 +20,7 @@ export type ScheduleGridEventFragmentFragment = ( )> } )>, runs: Array<( { __typename: 'Run' } - & Pick + & Types.MakeOptional, 'confirmed_signup_count' | 'not_counted_signup_count'> & RunBasicSignupDataFragment )> } ); diff --git a/app/javascript/SignupModeration/SignupModerationQueue.tsx b/app/javascript/SignupModeration/SignupModerationQueue.tsx index a677564b67d..67bb0807e99 100644 --- a/app/javascript/SignupModeration/SignupModerationQueue.tsx +++ b/app/javascript/SignupModeration/SignupModerationQueue.tsx @@ -1,13 +1,10 @@ -import { useContext, useState } from 'react'; -// @ts-expect-error -import Pagination from 'react-js-pagination'; +import { createContext, useContext, useMemo } from 'react'; import { assertNever } from 'assert-never'; -import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; +import { Column } from 'react-table'; import AppRootContext from '../AppRootContext'; import ErrorDisplay from '../ErrorDisplay'; -import LoadingIndicator from '../LoadingIndicator'; import { timespanFromRun } from '../TimespanUtils'; import { useConfirm } from '../ModalDialogs/Confirm'; import RunCapacityGraph from '../EventsApp/EventPage/RunCapacityGraph'; @@ -21,7 +18,20 @@ import { useAcceptSignupRequestMutation, useRejectSignupRequestMutation, } from './mutations.generated'; -import { useAppDateTimeFormat } from '../TimeUtils'; +import ReactTableWithTheWorks from '../Tables/ReactTableWithTheWorks'; +import useReactTableWithTheWorks from '../Tables/useReactTableWithTheWorks'; +import UserConProfileWithGravatarCell from '../Tables/UserConProfileWithGravatarCell'; +import TimestampCell from '../Tables/TimestampCell'; + +type SignupModerationContextValue = { + acceptClicked: (signupRequest: SignupModerationSignupRequestFieldsFragment) => void; + rejectClicked: (signupRequest: SignupModerationSignupRequestFieldsFragment) => void; +}; + +const SignupModerationContext = createContext({ + acceptClicked: () => {}, + rejectClicked: () => {}, +}); function signupRequestStateBadgeClass(state: SignupRequestState) { switch (state) { @@ -88,168 +98,184 @@ function SignupModerationRunDetails({ ); } -function SignupModerationQueue() { - const { timezoneName } = useContext(AppRootContext); - const format = useAppDateTimeFormat(); - const [currentPage, setCurrentPage] = useState(1); - const { data, loading, error } = useSignupModerationQueueQueryQuery({ - variables: { page: currentPage }, - }); - const [acceptSignupRequest] = useAcceptSignupRequestMutation(); - const [rejectSignupRequest] = useRejectSignupRequestMutation(); - const confirm = useConfirm(); +function SignupRequestCell({ value }: { value: SignupModerationSignupRequestFieldsFragment }) { + return ( + <> + {value.replace_signup && ( +

+ Withdraw from{' '} + +

+ )} + + {value.replace_signup ? 'And sign up for' : 'Sign up for'} + {' '} + +
+ + Requested bucket: {describeRequestedBucket(value)} + + + ); +} - const acceptClicked = (signupRequest: SignupModerationSignupRequestFieldsFragment) => - confirm({ - prompt: ( - <> -

- Please confirm you want to accept this signup request. This will attempt to sign{' '} - {signupRequest.user_con_profile.name} - {' up for '} - {signupRequest.target_run.event.title} - {' as '} - {describeRequestedBucket(signupRequest)}. If there is no space in the requested bucket, - the attendee will either be signed up in a flex bucket, if possible, or waitlisted. -

+function SignupRequestStateCell({ value }: { value: SignupRequestState }) { + return
{value}
; +} -
- Current space availability in this event run: - -
+function SignupRequestActionsCell({ + value, +}: { + value: SignupModerationSignupRequestFieldsFragment; +}) { + const { acceptClicked, rejectClicked } = useContext(SignupModerationContext); + + return ( + <> + {value.state === 'pending' && ( + <> + -

- This will automatically email both the attendee and the event team to let them know - about the signup. -

+ - ), - action: () => acceptSignupRequest({ variables: { id: signupRequest.id } }), - renderError: (acceptError) => , - }); + )} + {value.state === 'rejected' && ( + + )} + + ); +} - const rejectClicked = ( - signupRequest: NonNullable< - SignupModerationQueueQueryQuery['convention'] - >['signup_requests_paginated']['entries'][0], - ) => - confirm({ - prompt: ( -

- Please confirm you want to reject this signup request. This will not{' '} - automatically email anyone. After doing this, you may wish to email the attendee to let - them know. -

- ), - action: () => rejectSignupRequest({ variables: { id: signupRequest.id } }), - renderError: (acceptError) => , - }); +function getPossibleColumns(): Column< + SignupModerationQueueQueryQuery['convention']['signup_requests_paginated']['entries'][number] +>[] { + return [ + { + id: 'attendee', + Header: 'Attendee', + accessor: 'user_con_profile', + width: 130, + Cell: UserConProfileWithGravatarCell, + }, + { + id: 'request', + Header: 'Request', + Cell: SignupRequestCell, + accessor: (signupRequest) => signupRequest, + }, + { + id: 'state', + Header: 'Status', + Cell: SignupRequestStateCell, + width: 60, + accessor: 'state', + }, + { + id: 'created_at', + Header: 'Submitted at', + Cell: TimestampCell, + width: 60, + accessor: 'created_at', + }, + { + id: 'actions', + Header: 'Actions', + width: 100, + Cell: SignupRequestActionsCell, + accessor: (signupRequest) => signupRequest, + }, + ]; +} - if (error) { - return ; - } +function SignupModerationQueue() { + const [acceptSignupRequest] = useAcceptSignupRequestMutation(); + const [rejectSignupRequest] = useRejectSignupRequestMutation(); + const confirm = useConfirm(); + const { tableInstance, loading } = useReactTableWithTheWorks({ + useQuery: useSignupModerationQueueQueryQuery, + storageKeyPrefix: 'signupModerationQueue', + getData: (result) => result.data.convention.signup_requests_paginated.entries, + getPages: (result) => result.data.convention.signup_requests_paginated.total_pages, + getPossibleColumns, + }); - if (loading) { - return ; - } + const contextValue = useMemo( + () => ({ + acceptClicked: (signupRequest: SignupModerationSignupRequestFieldsFragment) => + confirm({ + prompt: ( + <> +

+ Please confirm you want to accept this signup request. This will attempt to sign{' '} + {signupRequest.user_con_profile.name} + {' up for '} + {signupRequest.target_run.event.title} + {' as '} + {describeRequestedBucket(signupRequest)}. If there is no space in the requested + bucket, the attendee will either be signed up in a flex bucket, if possible, or + waitlisted. +

- return ( - <> - - - - - - - - - - - {data!.convention!.signup_requests_paginated.entries.map((signupRequest) => ( - - - - - - - - ))} - -
AttendeeRequestStatusSubmitted at -
{signupRequest.user_con_profile.name} - {signupRequest.replace_signup && ( -

- Withdraw from{' '} - -

- )} - - {signupRequest.replace_signup ? 'And sign up for' : 'Sign up for'} - {' '} - -
- - Requested bucket: {describeRequestedBucket(signupRequest)} - -
-
- {signupRequest.state} -
-
- - {format( - DateTime.fromISO(signupRequest.created_at, { zone: timezoneName }), - 'shortWeekdayDateTime', - )} - - - {signupRequest.state === 'pending' && ( - <> - +
+ Current space availability in this event run: + +
- - - )} - {signupRequest.state === 'rejected' && ( - - )} -
+

+ This will automatically email both the attendee and the event team to let them know + about the signup. +

+ + ), + action: () => acceptSignupRequest({ variables: { id: signupRequest.id } }), + renderError: (acceptError) => , + }), + rejectClicked: ( + signupRequest: NonNullable< + SignupModerationQueueQueryQuery['convention'] + >['signup_requests_paginated']['entries'][0], + ) => + confirm({ + prompt: ( +

+ Please confirm you want to reject this signup request. This will not{' '} + automatically email anyone. After doing this, you may wish to email the attendee to + let them know. +

+ ), + action: () => rejectSignupRequest({ variables: { id: signupRequest.id } }), + renderError: (acceptError) => , + }), + }), + [confirm, rejectSignupRequest, acceptSignupRequest], + ); - - + return ( + + + ); } diff --git a/app/javascript/SignupModeration/index.tsx b/app/javascript/SignupModeration/index.tsx index 352036fef5a..475e8f21ec7 100644 --- a/app/javascript/SignupModeration/index.tsx +++ b/app/javascript/SignupModeration/index.tsx @@ -1,16 +1,19 @@ -import { useTabs, TabList, TabBody } from '../UIComponents/Tabs'; +import { TabList, TabBody, useTabsWithRouter } from '../UIComponents/Tabs'; import CreateSignup from './CreateSignup'; import SignupModerationQueue from './SignupModerationQueue'; function SignupModeration() { - const tabProps = useTabs([ - { - id: 'moderation-queue', - name: 'Moderation queue', - renderContent: () => , - }, - { id: 'create-signups', name: 'Create signups', renderContent: () => }, - ]); + const tabProps = useTabsWithRouter( + [ + { + id: 'moderation-queue', + name: 'Moderation queue', + renderContent: () => , + }, + { id: 'create-signups', name: 'Create signups', renderContent: () => }, + ], + '/signup_moderation', + ); return ( <> diff --git a/app/javascript/SignupModeration/queries.generated.ts b/app/javascript/SignupModeration/queries.generated.ts index b6ebd1d654f..6927eba4e3f 100644 --- a/app/javascript/SignupModeration/queries.generated.ts +++ b/app/javascript/SignupModeration/queries.generated.ts @@ -19,7 +19,7 @@ export type SignupModerationSignupRequestFieldsFragment = ( & Pick & { user_con_profile: ( { __typename: 'UserConProfile' } - & Pick + & Pick ), replace_signup?: Types.Maybe<( { __typename: 'Signup' } & Pick @@ -133,12 +133,13 @@ export type CreateSignupRunCardQueryQuery = ( export type SignupModerationQueueQueryQueryVariables = Types.Exact<{ page?: Types.Maybe; + perPage?: Types.Maybe; }>; export type SignupModerationQueueQueryQuery = ( { __typename: 'Query' } - & { convention?: Types.Maybe<( + & { convention: ( { __typename: 'Convention' } & Pick & { signup_requests_paginated: ( @@ -150,7 +151,7 @@ export type SignupModerationQueueQueryQuery = ( & SignupModerationSignupRequestFieldsFragment )> } ) } - )> } + ) } ); export const SignupModerationRunFieldsFragmentDoc = gql` @@ -175,6 +176,9 @@ export const SignupModerationSignupRequestFieldsFragmentDoc = gql` user_con_profile { id name + name_inverted + gravatar_enabled + gravatar_url } replace_signup { id @@ -343,13 +347,13 @@ export type CreateSignupRunCardQueryQueryHookResult = ReturnType; export type CreateSignupRunCardQueryQueryResult = Apollo.QueryResult; export const SignupModerationQueueQueryDocument = gql` - query SignupModerationQueueQuery($page: Int) { - convention { + query SignupModerationQueueQuery($page: Int, $perPage: Int) { + convention: assertConvention { id signup_requests_paginated( sort: [{field: "state", desc: false}, {field: "created_at", desc: false}] page: $page - per_page: 10 + per_page: $perPage ) { total_pages entries { @@ -374,6 +378,7 @@ export const SignupModerationQueueQueryDocument = gql` * const { data, loading, error } = useSignupModerationQueueQueryQuery({ * variables: { * page: // value for 'page' + * perPage: // value for 'perPage' * }, * }); */ diff --git a/app/javascript/SignupModeration/queries.ts b/app/javascript/SignupModeration/queries.ts index 821bdcfab67..67782f35e43 100644 --- a/app/javascript/SignupModeration/queries.ts +++ b/app/javascript/SignupModeration/queries.ts @@ -29,6 +29,9 @@ export const SignupModerationSignupRequestFields = gql` user_con_profile { id name + name_inverted + gravatar_enabled + gravatar_url } replace_signup { @@ -168,14 +171,14 @@ export const CreateSignupRunCardQuery = gql` `; export const SignupModerationQueueQuery = gql` - query SignupModerationQueueQuery($page: Int) { - convention { + query SignupModerationQueueQuery($page: Int, $perPage: Int) { + convention: assertConvention { id signup_requests_paginated( sort: [{ field: "state", desc: false }, { field: "created_at", desc: false }] page: $page - per_page: 10 + per_page: $perPage ) { total_pages diff --git a/app/javascript/Tables/useReactRouterReactTable.tsx b/app/javascript/Tables/useReactRouterReactTable.tsx index 4d43341a70d..b0a64610bbd 100644 --- a/app/javascript/Tables/useReactRouterReactTable.tsx +++ b/app/javascript/Tables/useReactRouterReactTable.tsx @@ -105,7 +105,7 @@ export default function useReactRouterReactTable({ const oldState = decodeSearchParams(history.location.search); const newSearch = encodeSearchParams({ ...oldState, ...newState }, history.location.search); if (newSearch !== history.location.search.replace(/^\?/, '')) { - history.replace(`${history.location.pathname}?${newSearch}`); + history.replace(`${history.location.pathname}?${newSearch}${history.location.hash}`); } }, [decodeSearchParams, encodeSearchParams, history], diff --git a/app/javascript/graphqlTypes.generated.ts b/app/javascript/graphqlTypes.generated.ts index 5faac0af44b..6c1cee58b4c 100644 --- a/app/javascript/graphqlTypes.generated.ts +++ b/app/javascript/graphqlTypes.generated.ts @@ -1,6 +1,8 @@ /* eslint-disable */ export type Maybe = T | null; export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; diff --git a/app/presenters/tables/signup_requests_table_results_presenter.rb b/app/presenters/tables/signup_requests_table_results_presenter.rb index 0d373c8c055..3659a1598ad 100644 --- a/app/presenters/tables/signup_requests_table_results_presenter.rb +++ b/app/presenters/tables/signup_requests_table_results_presenter.rb @@ -2,7 +2,11 @@ class Tables::SignupRequestsTableResultsPresenter < Tables::TableResultsPresente def self.for_convention(convention:, pundit_user:, filters: {}, sort: nil, visible_field_ids: nil) scope = SignupRequestPolicy::Scope.new(pundit_user, convention.signup_requests).resolve new( - base_scope: scope, + base_scope: scope.includes( + user_con_profile: [:team_members, :staff_positions], + target_run: { event: :convention }, + replace_signup: { run: { event: :convention }} + ), convention: convention, pundit_user: pundit_user, filters: filters, @@ -14,7 +18,7 @@ def self.for_convention(convention:, pundit_user:, filters: {}, sort: nil, visib field :state, 'State' do column_filter - def sql_order_for_sort_field(direction) + def sql_order(direction) Arel.sql(<<~SQL) ( CASE From 0a6c5ee971a24fb76784de9c00ddff19bdde8d6f Mon Sep 17 00:00:00 2001 From: Nat Budin Date: Thu, 10 Dec 2020 10:10:59 -0800 Subject: [PATCH 2/3] Remove react-js-pagination now that it's unused --- package.json | 1 - yarn.lock | 25 +------------------------ 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/package.json b/package.json index 8e4b1260b80..86a127b6566 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,6 @@ "react-google-recaptcha": "^2.1.0", "react-html-id": "^0.1.5", "react-i18next": "^11.7.3", - "react-js-pagination": "neinteractiveliterature/react-js-pagination#ellipsis", "react-onclickoutside": "^6.9.0", "react-popper": "^2.2.3", "react-router-dom": "^5.2.0", diff --git a/yarn.lock b/yarn.lock index 985550e935d..042584c0053 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10140,11 +10140,6 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" -paginator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/paginator/-/paginator-1.0.0.tgz#7565702af9ab9616dca61fc22c70eba2a4357265" - integrity sha1-dWVwKvmrlhbcph/CLHDroqQ1cmU= - pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -11294,7 +11289,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -"prop-types@15.x.x - 16.x.x", prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -11650,15 +11645,6 @@ react-is@^16.12.0, react-is@^16.6.0, react-is@^16.6.3, react-is@^16.7.0, react-i resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-js-pagination@neinteractiveliterature/react-js-pagination#ellipsis: - version "3.0.2" - resolved "https://codeload.github.com/neinteractiveliterature/react-js-pagination/tar.gz/b03a85b577d01e5e0490e1a763f5cc891d04c7d5" - dependencies: - classnames "^2.2.5" - paginator "^1.0.0" - prop-types "15.x.x - 16.x.x" - react "15.x.x - 16.x.x" - react-onclickoutside@^6.9.0: version "6.9.0" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz#a54bc317ae8cf6131a5d78acea55a11067f37a1f" @@ -11757,15 +11743,6 @@ react-waypoint@^9.0.3: prop-types "^15.0.0" react-is "^16.6.3" -"react@15.x.x - 16.x.x": - version "16.14.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - react@^17.0.0: version "17.0.1" resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" From aaef11c846c3c82af93f5cf7fc1701ec8fead516 Mon Sep 17 00:00:00 2001 From: Nat Budin Date: Thu, 10 Dec 2020 10:17:20 -0800 Subject: [PATCH 3/3] Fix linter warning --- .../tables/signup_requests_table_results_presenter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/presenters/tables/signup_requests_table_results_presenter.rb b/app/presenters/tables/signup_requests_table_results_presenter.rb index 3659a1598ad..5bcb348f07e 100644 --- a/app/presenters/tables/signup_requests_table_results_presenter.rb +++ b/app/presenters/tables/signup_requests_table_results_presenter.rb @@ -5,7 +5,7 @@ def self.for_convention(convention:, pundit_user:, filters: {}, sort: nil, visib base_scope: scope.includes( user_con_profile: [:team_members, :staff_positions], target_run: { event: :convention }, - replace_signup: { run: { event: :convention }} + replace_signup: { run: { event: :convention } } ), convention: convention, pundit_user: pundit_user,