Skip to content

Commit

Permalink
Merge pull request #9106 from neinteractiveliterature/sign-up-now-if-…
Browse files Browse the repository at this point in the history
…free-slots

Handle signing up for a thing that's currently in your queue if you have free slots
  • Loading branch information
nbudin authored Jun 23, 2024
2 parents 9f38e0a + 7b5bcba commit 661364e
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 64 deletions.
2 changes: 1 addition & 1 deletion app/graphql/graphql_operations_generated.json

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions app/javascript/EventsApp/EventPage/EventPageRunCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,23 @@ export type EventPageRunCardProps = {
event: EventPageQueryData['convention']['event'];
run: EventPageQueryData['convention']['event']['runs'][0];
myProfile: EventPageQueryData['convention']['my_profile'];
mySignups: EventPageQueryData['convention']['my_signups'];
mySignupRequests: EventPageQueryData['convention']['my_signup_requests'];
signupRounds: EventPageQueryData['convention']['signup_rounds'];
currentAbility: EventPageQueryData['currentAbility'];
addToQueue: boolean;
};

function EventPageRunCard({ event, run, myProfile, currentAbility, addToQueue }: EventPageRunCardProps): JSX.Element {
function EventPageRunCard({
event,
run,
myProfile,
mySignups,
mySignupRequests,
currentAbility,
signupRounds,
addToQueue,
}: EventPageRunCardProps): JSX.Element {
const { t } = useTranslation();
const { signupMode } = useContext(AppRootContext);
const myPendingRankedChoices = useMemo(
Expand All @@ -80,10 +92,13 @@ function EventPageRunCard({ event, run, myProfile, currentAbility, addToQueue }:
event,
SignupCountData.fromRun(run),
addToQueue,
mySignups,
mySignupRequests,
myPendingRankedChoices,
signupRounds,
myProfile ?? undefined,
),
[event, run, myProfile, addToQueue, myPendingRankedChoices],
[event, run, myProfile, mySignups, mySignupRequests, signupRounds, addToQueue, myPendingRankedChoices],
);
const confirm = useConfirm();
const createModeratedSignupModal = useModal<{ signupOption: SignupOption }>();
Expand Down
12 changes: 2 additions & 10 deletions app/javascript/EventsApp/EventPage/RunCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useRef, useEffect, useCallback, useMemo } from 'react';
import { useContext, useRef, useEffect, useCallback } from 'react';
import classNames from 'classnames';
import { useLocation } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
Expand All @@ -17,7 +17,7 @@ import AuthenticationModalContext from '../../Authentication/AuthenticationModal
import { EventPageQueryData } from './queries.generated';
import { PartitionedSignupOptions, SignupOption } from './buildSignupOptions';
import { useFormatRunTimespan } from '../runTimeFormatting';
import { Signup, SignupRankedChoiceState, SignupState } from '../../graphqlTypes.generated';
import { Signup, SignupState } from '../../graphqlTypes.generated';
import EventTicketPurchaseModal from './EventTicketPurchaseModal';

function describeSignupState(
Expand Down Expand Up @@ -87,11 +87,6 @@ function RunCard({
}, [location.hash, run.id]);
const eventTicketPurchaseModal = useModal<{ run: NonNullable<typeof run>; signup: NonNullable<typeof mySignup> }>();

const myPendingRankedChoices = useMemo(
() => run.my_signup_ranked_choices.filter((choice) => choice.state === SignupRankedChoiceState.Pending),
[run.my_signup_ranked_choices],
);

const performSignup = useCallback(
async (signupOption: SignupOption) => {
const signup = await createSignup(signupOption);
Expand Down Expand Up @@ -166,13 +161,11 @@ function RunCard({
signupOptions={signupOptions.mainPreference}
disabled={mutationInProgress}
onClick={signupButtonClicked}
myPendingRankedChoices={myPendingRankedChoices}
/>
<SignupButtons
signupOptions={signupOptions.mainNoPreference}
disabled={mutationInProgress}
onClick={signupButtonClicked}
myPendingRankedChoices={myPendingRankedChoices}
/>
{mutationInProgress && <LoadingIndicator iconSet="bootstrap-icons" />}
<ErrorDisplay graphQLError={signupError as ApolloError} />
Expand All @@ -192,7 +185,6 @@ function RunCard({
signupOptions={signupOptions.auxiliary}
disabled={mutationInProgress}
onClick={signupButtonClicked}
myPendingRankedChoices={myPendingRankedChoices}
/>
</li>
</ul>
Expand Down
3 changes: 3 additions & 0 deletions app/javascript/EventsApp/EventPage/RunsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export default LoadQueryWithVariablesWrapper(
run={run}
key={run.id}
myProfile={myProfile}
mySignups={data.convention.my_signups}
mySignupRequests={data.convention.my_signup_requests}
signupRounds={data.convention.signup_rounds}
currentAbility={currentAbility}
addToQueue={addToQueue}
/>
Expand Down
35 changes: 19 additions & 16 deletions app/javascript/EventsApp/EventPage/SignupButtonDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,15 @@ import { useTranslation } from 'react-i18next';
import { ButtonWithTooltip } from '@neinteractiveliterature/litform';

import { SignupOption } from './buildSignupOptions';
import { SignupRankedChoice } from '../../graphqlTypes.generated';
import RankedChoicePriorityIndicator from '../MySignupQueue/RankedChoicePriorityIndicator';

export type SignupButtonDisplayProps = {
signupOption: SignupOption;
onClick?: (signupOption: SignupOption) => void;
disabled?: boolean;
rankedChoices: Pick<SignupRankedChoice, 'priority'>[];
};

function SignupButtonDisplay({
signupOption,
onClick,
disabled,
rankedChoices,
}: SignupButtonDisplayProps): JSX.Element {
function SignupButtonDisplay({ signupOption, onClick, disabled }: SignupButtonDisplayProps): JSX.Element {
const { t } = useTranslation();

let actionLabel;
Expand All @@ -33,16 +26,18 @@ function SignupButtonDisplay({
actionLabel = t('signups.signupButton.addToQueue', 'Add to my queue');
break;
case 'IN_QUEUE':
actionLabel = (
<>
{rankedChoices.map((choice) => (
<RankedChoicePriorityIndicator fontSize={12} priority={choice.priority ?? 0} key={choice.priority} />
))}{' '}
{t('signups.signupButton.inMyQueue', 'In my queue')}
</>
);
actionLabel = t('signups.signupButton.inMyQueue', 'In my queue');
}

const rankedChoicePriorityIndicators =
signupOption.pendingRankedChoices.length > 0 ? (
<>
{signupOption.pendingRankedChoices.map((choice) => (
<RankedChoicePriorityIndicator fontSize={12} priority={choice.priority ?? 0} key={choice.priority} />
))}{' '}
</>
) : undefined;

return (
<ButtonWithTooltip
buttonProps={{
Expand All @@ -60,11 +55,19 @@ function SignupButtonDisplay({
<>
<strong>{signupOption.label}</strong>
<br />
{rankedChoicePriorityIndicators}
{actionLabel}
</>
) : (
<>
<strong>{actionLabel}</strong>
{rankedChoicePriorityIndicators && (
<>
<br />
{rankedChoicePriorityIndicators}
<em>{t('signups.signupButton.inMyQueue', 'In my queue')}</em>
</>
)}
</>
)}
</ButtonWithTooltip>
Expand Down
21 changes: 2 additions & 19 deletions app/javascript/EventsApp/EventPage/SignupButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,21 @@
import { useMemo } from 'react';
import { SignupRankedChoice } from '../../graphqlTypes.generated';
import SignupButtonDisplay from './SignupButtonDisplay';
import { SignupOption } from './buildSignupOptions';
import sortBy from 'lodash/sortBy';

export type SignupButtonsProps = {
signupOptions: SignupOption[];
onClick?: (signupOption: SignupOption) => void;
disabled?: boolean;
myPendingRankedChoices: Pick<SignupRankedChoice, 'requested_bucket_key' | 'priority'>[];
};

function SignupButtons({ signupOptions, onClick, disabled, myPendingRankedChoices }: SignupButtonsProps): JSX.Element {
const rankedChoicesSorted = useMemo(
() => sortBy(myPendingRankedChoices, (choice) => choice.priority),
[myPendingRankedChoices],
);

function SignupButtons({ signupOptions, onClick, disabled }: SignupButtonsProps): JSX.Element {
if (signupOptions.length === 0) {
return <></>;
}

return (
<div className="d-flex flex-wrap justify-content-center">
{signupOptions.map((signupOption) => (
<SignupButtonDisplay
key={signupOption.key}
signupOption={signupOption}
onClick={onClick}
disabled={disabled}
rankedChoices={rankedChoicesSorted.filter(
(choice) => choice.requested_bucket_key === signupOption.bucket?.key,
)}
/>
<SignupButtonDisplay key={signupOption.key} signupOption={signupOption} onClick={onClick} disabled={disabled} />
))}
</div>
);
Expand Down
70 changes: 62 additions & 8 deletions app/javascript/EventsApp/EventPage/buildSignupOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import { notEmpty } from '@neinteractiveliterature/litform';
import { EventPageQueryData, RunCardRegistrationPolicyFieldsFragment } from './queries.generated';
import sortBuckets from './sortBuckets';
import SignupCountData from '../SignupCountData';
import { SignupState, SignupRankedChoice } from '../../graphqlTypes.generated';
import {
SignupState,
SignupRankedChoice,
SignupRound,
Signup,
SignupRequest,
SignupRequestState,
} from '../../graphqlTypes.generated';
import { parseSignupRounds } from '../../SignupRoundUtils';
import { DateTime } from 'luxon';

type SignupOptionBucket = RunCardRegistrationPolicyFieldsFragment['buckets'][0];

Expand All @@ -19,6 +28,7 @@ export type SignupOption = {
counted: boolean;
teamMember: boolean;
action: SignupOptionAction;
pendingRankedChoices: Pick<SignupRankedChoice, 'priority' | 'requested_bucket_key'>[];
};

function isMainOption(option: SignupOption, noPreferenceOptions: SignupOption[], notCountedOptions: SignupOption[]) {
Expand Down Expand Up @@ -49,6 +59,7 @@ function buildBucketSignupOption(
index: number,
hideLabel: boolean,
action: SignupOptionAction,
pendingRankedChoices: SignupOption['pendingRankedChoices'],
): SignupOption {
return {
key: bucket.key,
Expand All @@ -60,6 +71,7 @@ function buildBucketSignupOption(
teamMember: false,
counted: !bucket.not_counted,
action,
pendingRankedChoices,
};
}

Expand All @@ -68,6 +80,7 @@ function buildNoPreferenceOptions(
signupCounts: SignupCountData,
addToQueue: boolean,
inQueue: boolean,
pendingRankedChoices: SignupOption['pendingRankedChoices'],
): SignupOption[] {
if ((event.registration_policy || {}).prevent_no_preference_signups) {
return [];
Expand Down Expand Up @@ -109,6 +122,7 @@ function buildNoPreferenceOptions(
teamMember: false,
counted: true, // no preference signups only go to counted buckets,
action,
pendingRankedChoices,
},
];
}
Expand All @@ -122,7 +136,10 @@ function allSignupOptions(
},
signupCounts: SignupCountData,
addToQueue: boolean,
myPendingRankedChoices: Pick<SignupRankedChoice, 'requested_bucket_key'>[],
mySignups: Pick<Signup, 'state' | 'counted'>[],
mySignupRequests: Pick<SignupRequest, 'state'>[],
myPendingRankedChoices: Pick<SignupRankedChoice, 'requested_bucket_key' | 'priority'>[],
signupRounds: Pick<SignupRound, 'start' | 'maximum_event_signups'>[],
userConProfile?: { id: string },
): SignupOption[] {
if (isTeamMember(event, userConProfile)) {
Expand All @@ -137,12 +154,27 @@ function allSignupOptions(
teamMember: true,
counted: false,
action: 'SIGN_UP_NOW',
pendingRankedChoices: [],
},
];
}

const buckets = sortBuckets((event.registration_policy || {}).buckets || []);
const nonAnythingBuckets = buckets.filter((bucket) => !bucket.anything);
const parsedRounds = parseSignupRounds(signupRounds);
const now = DateTime.local();
const currentRound = parsedRounds.find((round) => round.timespan.includesTime(now));
const maximumEventSignups = currentRound?.maximum_event_signups ?? 'not_now';
const signupCount =
mySignups.filter(
(signup) =>
(signup.state === SignupState.Confirmed || signup.state === SignupState.TicketPurchaseHold) && signup.counted,
).length + mySignupRequests.filter((signupRequest) => signupRequest.state === SignupRequestState.Pending).length;
const hasAvailableSignups =
typeof maximumEventSignups === 'number' ? signupCount < maximumEventSignups : maximumEventSignups === 'unlimited';
const noPreferencePendingRankedChoices = myPendingRankedChoices.filter(
(request) => request.requested_bucket_key == null,
);

return [
...buckets
Expand All @@ -154,9 +186,12 @@ function allSignupOptions(
}

let action: SignupOptionAction = 'SIGN_UP_NOW';
if (myPendingRankedChoices.find((request) => request.requested_bucket_key === bucket.key) != null) {
const pendingRankedChoices = myPendingRankedChoices.filter(
(request) => request.requested_bucket_key === bucket.key,
);
if (!hasAvailableSignups && pendingRankedChoices.length > 0) {
action = 'IN_QUEUE';
} else if (addToQueue) {
} else if (!hasAvailableSignups && addToQueue) {
action = 'ADD_TO_QUEUE';
} else if (
bucket.slots_limited &&
Expand All @@ -166,14 +201,21 @@ function allSignupOptions(
action = 'WAITLIST';
}

return buildBucketSignupOption(bucket, index, !bucket.not_counted && nonAnythingBuckets.length === 1, action);
return buildBucketSignupOption(
bucket,
index,
!bucket.not_counted && nonAnythingBuckets.length === 1,
action,
pendingRankedChoices,
);
})
.filter(notEmpty),
...buildNoPreferenceOptions(
event,
signupCounts,
addToQueue,
myPendingRankedChoices.find((request) => request.requested_bucket_key == null) != null,
noPreferencePendingRankedChoices.length > 0,
noPreferencePendingRankedChoices,
),
];
}
Expand All @@ -188,10 +230,22 @@ export default function buildSignupOptions(
event: Parameters<typeof allSignupOptions>[0],
signupCounts: SignupCountData,
addToQueue: boolean,
myPendingRankedChoices: Pick<SignupRankedChoice, 'requested_bucket_key'>[],
mySignups: Pick<Signup, 'state' | 'counted'>[],
mySignupRequests: Pick<SignupRequest, 'state'>[],
myPendingRankedChoices: Pick<SignupRankedChoice, 'requested_bucket_key' | 'priority'>[],
signupRounds: Pick<SignupRound, 'start' | 'maximum_event_signups'>[],
userConProfile?: { id: string },
): PartitionedSignupOptions {
const allOptions = allSignupOptions(event, signupCounts, addToQueue, myPendingRankedChoices, userConProfile);
const allOptions = allSignupOptions(
event,
signupCounts,
addToQueue,
mySignups,
mySignupRequests,
myPendingRankedChoices,
signupRounds,
userConProfile,
);
const noPreferenceOptions = allOptions.filter((option) => option.noPreference);
const notCountedOptions = allOptions.filter((option) => !option.counted);

Expand Down
Loading

0 comments on commit 661364e

Please sign in to comment.