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
Sign up to event #104
Conversation
There was a problem hiding this 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.
src/components/EventList.tsx
Outdated
<Button | ||
data-test="sign-up-button" | ||
marginTop="size-50" | ||
onPress={ () => putEventResponse(e.id, org?.id) } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.
src/fetching/putEventResponse.ts
Outdated
|
||
import { ZetkinMembership } from '../types/zetkin'; | ||
|
||
export default function putEventResponse(event : number, org : number | undefined) : Promise<ZetkinMembership> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my reply above.
src/fetching/putEventResponse.ts
Outdated
return eventData; | ||
} | ||
}; | ||
return eventSignUp(); |
There was a problem hiding this comment.
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.
src/fetching/putUndoEventResponse.ts
Outdated
if (eventRes.redirected === false) { | ||
return null; | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/fetching/putUndoEventResponse.ts
Outdated
|
||
import { ZetkinMembership } from '../types/zetkin'; | ||
|
||
export default function putUndoEventResponse(event : number, org : number | undefined) : Promise<ZetkinMembership> { |
There was a problem hiding this comment.
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).
src/fetching/putUndoEventResponse.ts
Outdated
} | ||
|
||
const eventData = await eventRes.json(); | ||
return eventData; |
There was a problem hiding this comment.
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.
There was a problem hiding this 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?
src/fetching/deleteEventResponse.ts
Outdated
|
||
import { ZetkinMembership } from '../types/zetkin'; | ||
|
||
export default async function deleteEventResponse(event : number, org : number | undefined) : Promise<void> { |
There was a problem hiding this comment.
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
.
src/components/EventList.tsx
Outdated
@@ -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> |
There was a problem hiding this comment.
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.
src/components/EventList.tsx
Outdated
<Button | ||
data-test="sign-up-button" | ||
marginTop="size-50" | ||
onPress={ () => putEventResponse(e.id, org!.id) } |
There was a problem hiding this comment.
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.
src/fetching/putEventResponse.ts
Outdated
|
||
import { ZetkinEventResponse, ZetkinMembership } from '../types/zetkin'; | ||
|
||
export default async function putEventResponse(event : number, org : number | undefined) : Promise<ZetkinEventResponse> { |
There was a problem hiding this comment.
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()
).
Great! Sorry, I forgot to remove 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! |
…rop where EventList is run
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. |
There was a problem hiding this 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.
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. |
…button and mutation
There was a problem hiding this 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.
src/components/EventList.spec.tsx
Outdated
}); | ||
}); | ||
|
||
it('contains data for each event', () => { | ||
const spyOnSubmit = cy.spy(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
src/components/EventList.spec.tsx
Outdated
//Checks for buttons on all events | ||
cy.findByText('misc.eventList.signup'); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
src/pages/o/[orgId]/events.tsx
Outdated
const responseQuery = useQuery('eventResponses', getEventResponses); | ||
const eventResponses = responseQuery.data; | ||
|
||
const onEventResponse = useOnEventResponse(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/types/zetkin.ts
Outdated
id: number; | ||
} | ||
|
||
export interface ZetkinEventSignup{ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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".
src/components/EventList.spec.tsx
Outdated
}); | ||
|
||
it('contains a button for more info on each event', () => { | ||
const spyOnSubmit = cy.spy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the spy here?
There was a problem hiding this comment.
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.
src/fetching/deleteEventResponse.ts
Outdated
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); |
There was a problem hiding this comment.
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.
…/app.zetkin.org into issue-93/sign-up-to-event Pull changes from PR merge
f896046
to
8970016
Compare
…eventRepsonses queryClient
There was a problem hiding this 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. 😊
src/fetching/deleteEventResponse.ts
Outdated
function defaultFetch(path : string, init? : RequestInit) { | ||
const url = apiUrl(path); | ||
return fetch(url, init); | ||
} |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree!
src/fetching/putEventResponse.ts
Outdated
orgId: number; | ||
} | ||
|
||
export default function putEventResponse(fetch = defaultFetch) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/utils/next.ts
Outdated
@@ -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(); |
There was a problem hiding this comment.
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()
.
src/utils/next.ts
Outdated
@@ -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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
src/utils/next.ts
Outdated
@@ -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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/utils/next.ts
Outdated
@@ -26,7 +26,7 @@ export type ScaffoldedProps = RegularProps & { | |||
|
|||
export type ScaffoldedContext = GetServerSidePropsContext & { | |||
apiFetch: (path : string, init? : RequestInit) => Promise<Response>; | |||
user: ZetkinZResult | null; | |||
user: unknown; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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'.
src/fetching/putEventResponse.ts
Outdated
interface MutationVariables { | ||
eventId: number; | ||
orgId: number; | ||
} | ||
|
||
export default function putEventResponse(fetch = defaultFetch) { | ||
export default function putEventResponse() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing! This feels massive :) |
Adds possibility for a user to sign up to an event.
Fixes #93