Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sign up to event #104

Merged
merged 25 commits into from Apr 21, 2021
Merged

Sign up to event #104

merged 25 commits into from Apr 21, 2021

Conversation

kristofferlarberg
Copy link
Contributor

@kristofferlarberg kristofferlarberg commented Mar 29, 2021

Adds possibility for a user to sign up to an event.

Fixes #93

Skärmavbild 2021-04-16 kl  12 24 19

Skärmavbild 2021-04-16 kl  12 24 34

Copy link
Member

@richardolsson richardolsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this! Definitely a new level of understanding of how things go together.

I have a number of comments though, find them below.

<Button
data-test="sign-up-button"
marginTop="size-50"
onPress={ () => putEventResponse(e.id, org?.id) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! This is exactly what an event handler should look like.

But why can org be undefined here? That should never be the case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! In EventListProps we've previously defined that org can be undefined. I have a vague memory of us leaving it like this since events and hence EventList will never run if we in events don't get orgState?.status === 'success' in getServerSideProps?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If org will never be undefined, it should not be typed as potentially being undefined.

If we know that org will never be undefined we should use non-null assertion or conditionals to indicate that to typescript. Otherwise we're just sweeping the problem in front of us.

This line is essentially saying that if org is undefined, pass undefined to putEventResponse(). What happens if we do that? The function might not work as expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this sounds reasonable. Does the same go for events (non-null assertion or conditionals)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the question. It looks like e is already guaranteed to have a value here, so no changes should be required there.


import { ZetkinMembership } from '../types/zetkin';

export default function putEventResponse(event : number, org : number | undefined) : Promise<ZetkinMembership> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if org is undefined here? Will the code work? I don't think so, and hence undefined should not be an acceptable value for org.

Luckily enough, it's very unlikely that it will ever be undefined, because this function should only be used in cases where an event, and hence the org it belongs to, is known.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my reply above.

return eventData;
}
};
return eventSignUp();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why an inner function that is immediately executed and returned? Doing this is effectively the same as just running the code inside the function, without an inner function.

Comment on lines 18 to 20
if (eventRes.redirected === false) {
return null;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I had a return value on this function I got a parse-related error message from Next when clicking Undo sign-up twice, since this value is erased after the first click. After removing the return value per you comment below this is no longer an issue.


import { ZetkinMembership } from '../types/zetkin';

export default function putUndoEventResponse(event : number, org : number | undefined) : Promise<ZetkinMembership> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the return type? Looking at the body of this function, the promise it returns doesn't seem to resolve to a ZetkinMembership.

This same issue exists for the putEventResponse() function (but I can't comment twice on the same line).

}

const eventData = await eventRes.json();
return eventData;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why return anything from this function? The only place where this is used (in EventList) doesn't do anything with the return value. That is an indication that there is no need to return anything.

Copy link
Member

@richardolsson richardolsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great progress! Some more comments below. Next up we need to make sure that we can use react-query for this and keep track of the state locally. Have you had any luck in your research?


import { ZetkinMembership } from '../types/zetkin';

export default async function deleteEventResponse(event : number, org : number | undefined) : Promise<void> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still allowing org to be undefined.

@@ -36,7 +40,7 @@ const EventList = ({ events, org } : EventListProps) : JSX.Element => {
<View data-test="event-title">
{ e.title ? e.title : e.activity.title }
</View>
<View data-test="org-title">{ org?.title }</View>
<View data-test="org-title">{ org!.title }</View>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've moved the problem up one level. This line means that we claim to know that org is never undefined. If we know as much, then why do we accept undefined in EventListProps?

Either this line is wrong, which I don't think it is, or the props types are wrong.

<Button
data-test="sign-up-button"
marginTop="size-50"
onPress={ () => putEventResponse(e.id, org!.id) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here as above, i.e. we shouldn't use non-null assertion unless it's inevitable to type something as undefined or null (or any/unknown), even though we know it never can be.


import { ZetkinEventResponse, ZetkinMembership } from '../types/zetkin';

export default async function putEventResponse(event : number, org : number | undefined) : Promise<ZetkinEventResponse> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here with undefined. Also, if org is just an ID, it should be named orgId (same for deleteEventResponse()).

@kristofferlarberg
Copy link
Contributor Author

kristofferlarberg commented Mar 30, 2021

Great!

Sorry, I forgot to remove undefined from the prop types. Will do that now!

Regarding react-query that sounds like a good plan. I only have a basic understanding of how mutations work so far, but let’s try!

@richardolsson
Copy link
Member

This looks good now! Please move on to preload responses and show only the correct button (either "sign up" or "undo") depending on current state, and update that state when the user interacts.

Copy link
Member

@richardolsson richardolsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good!

As a next step, the code for making API calls should be moved out of EventList, and by "out of", I mean upwards in the tree. I see now that you've moved it down into a separate component ResponseButton.

The problem with this is that it means that EventList (and it's children) now have side effects. They're no longer pure, which makes it much more difficult to test.

The EventList component should not make any network requests itself. It should just take the data that is given to it as props and render UI that reflects that data. If the user clicks a button in the list, it should bubble that up to the surroundings.

I would recommend moving the mutation logic to the container that contains EventList, and adding an event callback function property to EventList, e.g. onEventResponse with three parameters: orgId, eventId and response (boolean).

This will render the <ResponseButton> component useless, so you can remove that again.

You will likely find that these changes mean the same logic needs to be in several places, i.e. every place where EventList is used. If it's a lot of logic, it should be moved into a separate function, e.g. a useEventResponse() hook that returns the onEventResponse function.

@kristofferlarberg
Copy link
Contributor Author

Perfect, this sounds good!

I've been struggling with keeping the requests as far up in the component tree as possible but only made it this far. Your proposal is v helpful! Will start applying this.

Copy link
Member

@richardolsson richardolsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structure looks great! There are only some minor naming issues, and you are unnecessarily creating a spy even when you're not spying on the function call.

Now all that remains is a custom hook (or any other means) for reusing the mutation logic.

});
});

it('contains data for each event', () => {
const spyOnSubmit = cy.spy();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming is off, implies that there is an onSubmit-callback. Is it a rest product from another test?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, there should be no need for a spy in this test (or the subsequent ones) where the spy is not actually used for spying.

src/components/EventList.spec.tsx Show resolved Hide resolved
Copy link
Member

@richardolsson richardolsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. This is very close to something I would be very happy merging! I have a few comments below, please let me know how you would like to proceed.

Comment on lines 77 to 78
//Checks for buttons on all events
cy.findByText('misc.eventList.signup');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now it's unnecessary since line 81 is already doing the same thing. Will remove this!

Comment on lines 57 to 60
const responseQuery = useQuery('eventResponses', getEventResponses);
const eventResponses = responseQuery.data;

const onEventResponse = useOnEventResponse();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this again, I think this should all be included in the hook, like I mentioned yesterday. Maybe the hook should even be called useEventResponses() (plural) and be used like so:

const { eventResponses, onEventResponse } = useEventResponses();

Do you feel like you could easily make this work, or should we postpone it to when I can help out? We could merge without it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree! It became very clear when I applied useOnEventResponse() to [eventId] page yesterday. I believe I can manage with this direction. I'll give it a few hours and if it feels too hard I'll let you know and we merge.

id: number;
}

export interface ZetkinEventSignup{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a space missing before {. I'm surprised the linter didn't catch this. Maybe you could add it to the configuration? See space-before-blocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already use space-before-blocks and seems like this is currently not working on interface typescript-eslint/typescript-eslint#1606

"data": [
{
"action_id": 22,
"response_date": "1111 11 11, 11:11",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike the use of this very weird date. Why the year 1111, and not something more alike to what would occur in live data? It's also not using the correct ISO format. The API delivers dates like 2021-01-21T18:30:00+00:00, and the dummy data should mimic that.

@@ -22,6 +22,20 @@ describe('/o/[orgId]/events', () => {
cy.visit('/o/1/events');
cy.get('[data-test="no-events-placeholder"]').should('be.visible');
});

it('contains conditional functionality for sign-up button', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better name would be something like "shows sign up button if not signed up, and undo button when signed up".

});

it('contains a button for more info on each event', () => {
const spyOnSubmit = cy.spy();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the spy here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like I missed this when removing spies, will remove.

Comment on lines 6 to 9
const mUrl = apiUrl('/users/me/memberships');
const mRes = await fetch(mUrl);
const mData = await mRes.json();
const orgMembership = mData.data.find((m : ZetkinMembership) => m.organization.id === orgId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solution is a hack. Maybe leave it like this for now, but add a TODO comment or an issue so we remember to get back to it. It should not be necessary to retrieve the memberships every time. They should be cached.

Copy link
Member

@richardolsson richardolsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sooo close now! See comments below. 😊

Comment on lines 5 to 8
function defaultFetch(path : string, init? : RequestInit) {
const url = apiUrl(path);
return fetch(url, init);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reoccurs in many locations, and should probably moved to a shared module and reused. Perhaps in src/fetching/index.ts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree!

orgId: number;
}

export default function putEventResponse(fetch = defaultFetch) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is fetch injection necessary for this function? Is it ever used on the server?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same question stands for the delete variant as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, it's only used from the client. I'll change this.

@@ -74,6 +75,13 @@ export const scaffold = (wrapped : ScaffoldedGetServerSideProps, options? : Scaf
ctx.z.setTokenData(reqWithSession.session.tokenData);
}

try {
ctx.user = await ctx.z.resource('users', 'me').get();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extract the actual user data object and store that as ctx.user, not the entire response object returned from get().

@@ -74,6 +75,13 @@ export const scaffold = (wrapped : ScaffoldedGetServerSideProps, options? : Scaf
ctx.z.setTokenData(reqWithSession.session.tokenData);
}

try {
ctx.user = await ctx.z.resource('users', 'me').get();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be possible to reuse this user object further down in the file, to avoid having to retrieve the user again.

Copy link
Member

@richardolsson richardolsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making progress! I feel like we've lost track of the general direction of things and why things were done a certain way to begin with. Take a step back, look at the code as a whole, and if we've removed things that are no longer relevant, any complications introduce to achieve those things should also be removed/simplified again.

I found a few such cases right away below.

@@ -104,8 +105,7 @@ export const scaffold = (wrapped : ScaffoldedGetServerSideProps, options? : Scaf
};

try {
const user = await ctx.z.resource('users', 'me').get();
augmentProps(user.data.data as ZetkinUser);
augmentProps(ctx.user as ZetkinUser);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right. The try used to be there to verify that the user could be retrieved. If it couldn't, the catch would run augmentProps(null). Now, you are not doing anything here that should raise an exception, so the try is no longer relevant.

The typecast should also not be necessary, as ctx.user should be typed correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, sorry I missed this, looking at it now it's obvious.

@@ -26,7 +26,7 @@ export type ScaffoldedProps = RegularProps & {

export type ScaffoldedContext = GetServerSidePropsContext & {
apiFetch: (path : string, init? : RequestInit) => Promise<Response>;
user: ZetkinZResult | null;
user: unknown;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be typed as ZetkinUser.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I tried that, but get the following:

Type 'unknown' is not assignable to type 'ZetkinUser'.

interface MutationVariables {
eventId: number;
orgId: number;
}

export default function putEventResponse(fetch = defaultFetch) {
export default function putEventResponse() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why return a function in this function? It should not be necessary to wrap the actual operations of this function in an inner function which is then returned. Just run the code in the body of putEventResponse and pass around putEventResponse instead of passing around the return value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a side-effect of the refactoring for server side rendering. But since neither the put or delete functions are run server side I'll revert these back to their previous state (same as you describe).

Copy link
Member

@richardolsson richardolsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! We made it. 🎉

Celebrate

@richardolsson richardolsson merged commit da13605 into main Apr 21, 2021
@richardolsson richardolsson deleted the issue-93/sign-up-to-event branch April 21, 2021 10:04
@kristofferlarberg
Copy link
Contributor Author

Amazing! This feels massive :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sign up to event
2 participants