From 451c9bddf0008f4c1eb89305d8a05e7ab2e25715 Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Thu, 25 Mar 2021 23:10:34 +0100 Subject: [PATCH 01/23] Add sign-up functionality for event --- src/components/EventList.tsx | 16 +++++++++++++++- src/fetching/putEventResponse.ts | 22 ++++++++++++++++++++++ src/fetching/putUndoEventResponse.ts | 27 +++++++++++++++++++++++++++ src/locale/misc/eventList/en.yml | 3 ++- src/locale/misc/eventList/sv.yml | 3 ++- src/locale/pages/orgEvent/en.yml | 2 +- src/locale/pages/orgEvent/sv.yml | 2 +- src/types/zetkin.ts | 9 +++++++++ 8 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 src/fetching/putEventResponse.ts create mode 100644 src/fetching/putUndoEventResponse.ts create mode 100644 src/types/zetkin.ts diff --git a/src/components/EventList.tsx b/src/components/EventList.tsx index cce5f0b33..9eb5047a8 100644 --- a/src/components/EventList.tsx +++ b/src/components/EventList.tsx @@ -11,6 +11,8 @@ import { FormattedMessage as Msg, } from 'react-intl'; +import putEventResponse from '../fetching/putEventResponse'; +import putUndoEventResponse from '../fetching/putUndoEventResponse'; import { ZetkinEvent } from '../interfaces/ZetkinEvent'; import { ZetkinOrganization } from '../interfaces/ZetkinOrganization'; @@ -20,6 +22,7 @@ interface EventListProps { } const EventList = ({ events, org } : EventListProps) : JSX.Element => { + if (!events || events.length === 0) { return ( @@ -59,9 +62,20 @@ const EventList = ({ events, org } : EventListProps) : JSX.Element => { /> { e.location.title } - + - + - + - + + ); + } + return ( + + ); +}; + +export default ResponseButton; \ No newline at end of file diff --git a/src/fetching/deleteEventResponse.ts b/src/fetching/deleteEventResponse.ts index 3a90180d6..f016469ca 100644 --- a/src/fetching/deleteEventResponse.ts +++ b/src/fetching/deleteEventResponse.ts @@ -1,15 +1,15 @@ import apiUrl from '../utils/apiUrl'; -import { ZetkinMembership } from '../types/zetkin'; +import { ZetkinEventSignup, ZetkinMembership } from '../types/zetkin'; -export default async function deleteEventResponse(event : number, orgId : number) : Promise { +export default async function deleteEventResponse({ eventId, orgId } : ZetkinEventSignup) : Promise { 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); if (orgMembership) { - const url = apiUrl(`/orgs/${orgId}/actions/${event}/responses/${orgMembership.profile.id}`); + const url = apiUrl(`/orgs/${orgId}/actions/${eventId}/responses/${orgMembership.profile.id}`); await fetch(url, { method: 'DELETE', }); diff --git a/src/fetching/getEventResponses.ts b/src/fetching/getEventResponses.ts new file mode 100644 index 000000000..dc612759c --- /dev/null +++ b/src/fetching/getEventResponses.ts @@ -0,0 +1,10 @@ +import apiUrl from '../utils/apiUrl'; + +import { ZetkinEventResponse } from '../types/zetkin'; + +export default async function getEventResponses() : Promise { + const rUrl = apiUrl('/users/me/action_responses'); + const rRes = await fetch(rUrl); + const rData = await rRes.json(); + return rData.data; +} \ No newline at end of file diff --git a/src/fetching/putEventResponse.ts b/src/fetching/putEventResponse.ts index 8fa0e0c08..f332f0b92 100644 --- a/src/fetching/putEventResponse.ts +++ b/src/fetching/putEventResponse.ts @@ -1,15 +1,15 @@ import apiUrl from '../utils/apiUrl'; -import { ZetkinEventResponse, ZetkinMembership } from '../types/zetkin'; +import { ZetkinEventResponse, ZetkinEventSignup, ZetkinMembership } from '../types/zetkin'; -export default async function putEventResponse(event : number, orgId : number) : Promise { +export default async function putEventResponse({ eventId, orgId } : ZetkinEventSignup) : Promise { 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); if (orgMembership) { - const eventUrl = apiUrl(`/orgs/${orgId}/actions/${event}/responses/${orgMembership.profile.id}`); + const eventUrl = apiUrl(`/orgs/${orgId}/actions/${eventId}/responses/${orgMembership.profile.id}`); const eventRes = await fetch(eventUrl, { method: 'PUT', }); diff --git a/src/pages/o/[orgId]/campaigns/[campId].tsx b/src/pages/o/[orgId]/campaigns/[campId].tsx index 4b354a299..ee263e3bb 100644 --- a/src/pages/o/[orgId]/campaigns/[campId].tsx +++ b/src/pages/o/[orgId]/campaigns/[campId].tsx @@ -7,6 +7,7 @@ import { QueryClient, useQuery } from 'react-query'; import DefaultOrgLayout from '../../../../components/layout/DefaultOrgLayout'; import getCampaign from '../../../../fetching/getCampaign'; import getCampaignEvents from '../../../../fetching/getCampaignEvents'; +import getEventResponses from '../../../../fetching/getEventResponses'; import getOrg from '../../../../fetching/getOrg'; import { PageWithLayout } from '../../../../types'; import { scaffold } from '../../../../utils/next'; @@ -50,6 +51,7 @@ const OrgCampaignPage : PageWithLayout = (props) => { const campaignQuery = useQuery(['campaign', campId], getCampaign(orgId, campId)); const orgQuery = useQuery(['org', orgId], getOrg(orgId)); const campaignEventsQuery = useQuery(['campaignEvents', campId], getCampaignEvents(orgId, campId)); + const responseQuery = useQuery('eventResponses', getEventResponses); return ( @@ -60,6 +62,7 @@ const OrgCampaignPage : PageWithLayout = (props) => { { campaignQuery.data?.info_text } = (props) => { const { orgId } = props; const eventsQuery = useQuery('events', getEvents(orgId)); const orgQuery = useQuery(['org', orgId], getOrg(orgId)); + const responseQuery = useQuery('eventResponses', getEventResponses); return ( Date: Mon, 12 Apr 2021 20:33:05 +0200 Subject: [PATCH 06/23] Refactor functionality and general structure for conditional sign-up button and mutation --- cypress/integration/org_events_page.spec.ts | 10 ++ .../reocurring_functionality.spec.ts | 1 + src/components/EventList.spec.tsx | 71 +++++++++-- src/components/EventList.tsx | 120 +++++++++--------- src/components/ResponseButton.tsx | 51 -------- src/pages/o/[orgId]/campaigns/[campId].tsx | 30 ++++- src/pages/o/[orgId]/events.tsx | 30 ++++- src/types/misc.ts | 3 + 8 files changed, 190 insertions(+), 126 deletions(-) delete mode 100644 src/components/ResponseButton.tsx create mode 100644 src/types/misc.ts diff --git a/cypress/integration/org_events_page.spec.ts b/cypress/integration/org_events_page.spec.ts index 8e36dfe85..598ffe581 100644 --- a/cypress/integration/org_events_page.spec.ts +++ b/cypress/integration/org_events_page.spec.ts @@ -22,6 +22,16 @@ describe('/o/[orgId]/events', () => { cy.visit('/o/1/events'); cy.get('[data-test="no-events-placeholder"]').should('be.visible'); }); + + //TODO: Figure out how to make this work. Requires login? + xit('contains conditional sign-up/undo sign-up button functionality for event sign-up', () => { + cy.visit('/o/1/events'); + cy.get('[data-test="sign-up-button"]') + .eq(5) + .contains('Sign-up') + .click() + .contains('Undo sign-up'); + }); }); // Hack to flag for typescript as module diff --git a/cypress/integration/reocurring_functionality.spec.ts b/cypress/integration/reocurring_functionality.spec.ts index 22ed57747..2375c31eb 100644 --- a/cypress/integration/reocurring_functionality.spec.ts +++ b/cypress/integration/reocurring_functionality.spec.ts @@ -1,4 +1,5 @@ describe('Reocurring functionality', () => { + it('contains a clickable org logo which leads to org page', () => { cy.visit('/o/1/events'); cy.get('[data-test="org-avatar"]') diff --git a/src/components/EventList.spec.tsx b/src/components/EventList.spec.tsx index ef37c16c0..5922e186d 100644 --- a/src/components/EventList.spec.tsx +++ b/src/components/EventList.spec.tsx @@ -7,7 +7,7 @@ import { ZetkinOrganization } from '../interfaces/ZetkinOrganization'; describe('EventList', () => { let dummyOrg : ZetkinOrganization; let dummyEvents : ZetkinEvent[]; - let eventResponses : ZetkinEventResponse[]; + let dummyEventResponses : ZetkinEventResponse[]; beforeEach(()=> { cy.fixture('dummyOrg.json') @@ -19,14 +19,21 @@ describe('EventList', () => { dummyEvents = data.data; }); cy.fixture('dummyEventResponses.json') - .then((data : {data: ZetkinEvent[]}) => { - dummyEvents = data.data; + .then((data : {data: ZetkinEventResponse[]}) => { + dummyEventResponses = data.data; }); }); it('contains data for each event', () => { + const spyOnSubmit = cy.spy(); + mountWithProviders( - , + , ); cy.get('[data-test="event"]').each((item) => { @@ -42,8 +49,15 @@ describe('EventList', () => { it('contains an activity title instead of missing event title', () => { dummyEvents[0].title = undefined; + const spyOnSubmit = cy.spy(); + mountWithProviders( - , + , ); cy.get('[data-test="event"]') @@ -52,33 +66,70 @@ describe('EventList', () => { }); it('contains a sign-up button for each event', () => { + const spyOnSubmit = cy.spy(); + mountWithProviders( - , + , ); - cy.contains('misc.eventList.signup'); + //Checks for buttons on all events + cy.findByText('misc.eventList.signup'); + + //Tests button on a single event + cy.findByText('misc.eventList.signup') + .eq(0) + .click() + .then(() => { + expect(spyOnSubmit).to.be.calledOnce; + }); }); it('contains a button for more info on each event', () => { + const spyOnSubmit = cy.spy(); + mountWithProviders( - , + , ); cy.contains('misc.eventList.moreInfo'); }); it('shows a placeholder when the list is empty', () => { + const spyOnSubmit = cy.spy(); + dummyEvents = []; mountWithProviders( - , + , ); cy.contains('misc.eventList.placeholder'); }); it('shows a placeholder when the list is undefined', () => { + const spyOnSubmit = cy.spy(); + mountWithProviders( - , + , ); cy.contains('misc.eventList.placeholder'); diff --git a/src/components/EventList.tsx b/src/components/EventList.tsx index fd3498dac..b2950734e 100644 --- a/src/components/EventList.tsx +++ b/src/components/EventList.tsx @@ -10,11 +10,8 @@ import { FormattedTime, FormattedMessage as Msg, } from 'react-intl'; -import { useMutation, useQueryClient } from 'react-query'; -import deleteEventResponse from '../fetching/deleteEventResponse'; -import putEventResponse from '../fetching/putEventResponse'; -import ResponseButton from './ResponseButton'; +import { OnEventResponse } from '../types/misc'; import { ZetkinEvent } from '../interfaces/ZetkinEvent'; import { ZetkinEventResponse } from '../types/zetkin'; import { ZetkinOrganization } from '../interfaces/ZetkinOrganization'; @@ -23,22 +20,10 @@ interface EventListProps { events: ZetkinEvent[] | undefined; org: ZetkinOrganization; eventResponses: ZetkinEventResponse[] | undefined; + onEventResponse: OnEventResponse; } -const EventList = ({ events, org, eventResponses } : EventListProps) : JSX.Element => { - const queryClient = useQueryClient(); - - const mutationAdd = useMutation(putEventResponse, { - onSettled: () => { - queryClient.invalidateQueries('eventResponses'); - }, - }); - - const mutationRemove = useMutation(deleteEventResponse, { - onSettled: () => { - queryClient.invalidateQueries('eventResponses'); - }, - }); +const EventList = ({ eventResponses, events, onEventResponse, org } : EventListProps) : JSX.Element => { if (!events || events.length === 0) { return ( @@ -51,50 +36,63 @@ const EventList = ({ events, org, eventResponses } : EventListProps) : JSX.Eleme return ( <> - { events?.map((e) => ( - - - { e.title ? e.title : e.activity.title } - - { org.title } - { e.campaign.title } - - - , - - - - , - - { e.location.title } - - - - + ) : ( + - - - - )) } + ) } + + + + + + + ); + }) } ); diff --git a/src/components/ResponseButton.tsx b/src/components/ResponseButton.tsx deleted file mode 100644 index 752f49686..000000000 --- a/src/components/ResponseButton.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Button } from '@adobe/react-spectrum'; -import { FormattedMessage as Msg } from 'react-intl'; -import { UseMutationResult } from 'react-query'; - -import { ZetkinEventResponse, ZetkinEventSignup } from '../types/zetkin'; - -interface ResponseButtonProps { - eventId: number; - orgId: number; - eventResponses: ZetkinEventResponse[] | undefined; - mutationAdd: UseMutationResult; - mutationRemove: UseMutationResult; -} - -const ResponseButton = ( - { eventId, orgId, eventResponses, mutationAdd, mutationRemove } - : ResponseButtonProps) : JSX.Element => { - - const addResponse = (eventId : number, orgId : number) => { - mutationAdd.mutate({ eventId, orgId }); - }; - - const removeResponse = (eventId : number, orgId : number) => { - mutationRemove.mutate({ eventId, orgId }); - }; - - const active = eventResponses?.find(response => response.action_id === eventId); - - if (active) { - return ( - - ); - } - return ( - - ); -}; - -export default ResponseButton; \ No newline at end of file diff --git a/src/pages/o/[orgId]/campaigns/[campId].tsx b/src/pages/o/[orgId]/campaigns/[campId].tsx index ee263e3bb..2fc6a73db 100644 --- a/src/pages/o/[orgId]/campaigns/[campId].tsx +++ b/src/pages/o/[orgId]/campaigns/[campId].tsx @@ -2,14 +2,17 @@ import { dehydrate } from 'react-query/hydration'; import EventList from '../../../../components/EventList'; import { GetServerSideProps } from 'next'; import { Flex, Heading, Text } from '@adobe/react-spectrum'; -import { QueryClient, useQuery } from 'react-query'; +import { QueryClient, useMutation, useQuery, useQueryClient } from 'react-query'; import DefaultOrgLayout from '../../../../components/layout/DefaultOrgLayout'; +import deleteEventResponse from '../../../../fetching/deleteEventResponse'; import getCampaign from '../../../../fetching/getCampaign'; import getCampaignEvents from '../../../../fetching/getCampaignEvents'; import getEventResponses from '../../../../fetching/getEventResponses'; import getOrg from '../../../../fetching/getOrg'; +import { OnEventResponse } from '../../../../types/misc'; import { PageWithLayout } from '../../../../types'; +import putEventResponse from '../../../../fetching/putEventResponse'; import { scaffold } from '../../../../utils/next'; export const getServerSideProps : GetServerSideProps = scaffold(async (context) => { @@ -52,6 +55,28 @@ const OrgCampaignPage : PageWithLayout = (props) => { const orgQuery = useQuery(['org', orgId], getOrg(orgId)); const campaignEventsQuery = useQuery(['campaignEvents', campId], getCampaignEvents(orgId, campId)); const responseQuery = useQuery('eventResponses', getEventResponses); + const eventResponses = responseQuery.data; + + const queryClient = useQueryClient(); + + const mutationAdd = useMutation(putEventResponse, { + onSettled: () => { + queryClient.invalidateQueries('eventResponses'); + }, + }); + + const mutationRemove = useMutation(deleteEventResponse, { + onSettled: () => { + queryClient.invalidateQueries('eventResponses'); + }, + }); + + const onEventResponse : OnEventResponse = (eventId, orgId, response) => { + if (response) { + return mutationRemove.mutate({ eventId, orgId }); + } + return mutationAdd.mutate({ eventId, orgId }); + }; return ( @@ -62,8 +87,9 @@ const OrgCampaignPage : PageWithLayout = (props) => { { campaignQuery.data?.info_text } onEventResponse(eventId, orgId, response) } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion org={ orgQuery.data! } /> diff --git a/src/pages/o/[orgId]/events.tsx b/src/pages/o/[orgId]/events.tsx index 9d4bcfbd9..3cf78b0bf 100644 --- a/src/pages/o/[orgId]/events.tsx +++ b/src/pages/o/[orgId]/events.tsx @@ -1,14 +1,17 @@ import { dehydrate } from 'react-query/hydration'; import { Flex } from '@adobe/react-spectrum'; import { GetServerSideProps } from 'next'; -import { QueryClient, useQuery } from 'react-query'; +import { QueryClient, useMutation, useQuery, useQueryClient } from 'react-query'; +import deleteEventResponse from '../../../fetching/deleteEventResponse'; import EventList from '../../../components/EventList'; import getEventResponses from '../../../fetching/getEventResponses'; import getEvents from '../../../fetching/getEvents'; import getOrg from '../../../fetching/getOrg'; import MainOrgLayout from '../../../components/layout/MainOrgLayout'; +import { OnEventResponse } from '../../../types/misc'; import { PageWithLayout } from '../../../types'; +import putEventResponse from '../../../fetching/putEventResponse'; import { scaffold } from '../../../utils/next'; const scaffoldOptions = { @@ -54,12 +57,35 @@ const OrgEventsPage : PageWithLayout = (props) => { const eventsQuery = useQuery('events', getEvents(orgId)); const orgQuery = useQuery(['org', orgId], getOrg(orgId)); const responseQuery = useQuery('eventResponses', getEventResponses); + const eventResponses = responseQuery.data; + + const queryClient = useQueryClient(); + + const mutationAdd = useMutation(putEventResponse, { + onSettled: () => { + queryClient.invalidateQueries('eventResponses'); + }, + }); + + const mutationRemove = useMutation(deleteEventResponse, { + onSettled: () => { + queryClient.invalidateQueries('eventResponses'); + }, + }); + + const onEventResponse : OnEventResponse = (eventId, orgId, response) => { + if (response) { + return mutationRemove.mutate({ eventId, orgId }); + } + return mutationAdd.mutate({ eventId, orgId }); + }; return ( onEventResponse(eventId, orgId, response) } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion org={ orgQuery.data! } /> diff --git a/src/types/misc.ts b/src/types/misc.ts new file mode 100644 index 000000000..ed0db2ee1 --- /dev/null +++ b/src/types/misc.ts @@ -0,0 +1,3 @@ +export interface OnEventResponse { + (eventId: number, orgId: number, response: boolean) : void; +} \ No newline at end of file From c85776cac411afe1278f32686eed80f0da52547c Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Wed, 14 Apr 2021 12:25:07 +0200 Subject: [PATCH 07/23] Add login procedure in button test --- cypress/integration/org_events_page.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cypress/integration/org_events_page.spec.ts b/cypress/integration/org_events_page.spec.ts index 598ffe581..a25121796 100644 --- a/cypress/integration/org_events_page.spec.ts +++ b/cypress/integration/org_events_page.spec.ts @@ -23,8 +23,12 @@ describe('/o/[orgId]/events', () => { cy.get('[data-test="no-events-placeholder"]').should('be.visible'); }); - //TODO: Figure out how to make this work. Requires login? - xit('contains conditional sign-up/undo sign-up button functionality for event sign-up', () => { + it('contains conditional functionality for sign-up button', () => { + cy.visit('/login'); + cy.get('input[aria-label="E-mail address"]').type('testadmin@example.com'); + cy.get('input[aria-label="Password"]').type('password'); + cy.get('input[aria-label="Log in"]') + .click(); cy.visit('/o/1/events'); cy.get('[data-test="sign-up-button"]') .eq(5) From c83f3fe55fcd1c08ba9895a60ba9b8f290829c30 Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Wed, 14 Apr 2021 12:33:21 +0200 Subject: [PATCH 08/23] Remove unnecessary spies --- src/components/EventList.spec.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/components/EventList.spec.tsx b/src/components/EventList.spec.tsx index 5922e186d..de52759af 100644 --- a/src/components/EventList.spec.tsx +++ b/src/components/EventList.spec.tsx @@ -25,13 +25,11 @@ describe('EventList', () => { }); it('contains data for each event', () => { - const spyOnSubmit = cy.spy(); - mountWithProviders( null } org={ dummyOrg } />, ); @@ -49,13 +47,12 @@ describe('EventList', () => { it('contains an activity title instead of missing event title', () => { dummyEvents[0].title = undefined; - const spyOnSubmit = cy.spy(); mountWithProviders( null } org={ dummyOrg } />, ); @@ -105,14 +102,13 @@ describe('EventList', () => { }); it('shows a placeholder when the list is empty', () => { - const spyOnSubmit = cy.spy(); - dummyEvents = []; + mountWithProviders( null } org={ dummyOrg } />, ); @@ -121,13 +117,11 @@ describe('EventList', () => { }); it('shows a placeholder when the list is undefined', () => { - const spyOnSubmit = cy.spy(); - mountWithProviders( null } org={ dummyOrg } />, ); From db23b2c9abebc32d6986a0b3864ce29499e58db5 Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Thu, 15 Apr 2021 14:54:31 +0200 Subject: [PATCH 09/23] Refactor useMutation functionality to reusable hook --- src/components/EventList.tsx | 3 +- src/hooks/index.ts | 32 +++++++++++++++++++++- src/pages/o/[orgId]/campaigns/[campId].tsx | 27 ++---------------- src/pages/o/[orgId]/events.tsx | 29 +++----------------- src/types/misc.ts | 3 -- 5 files changed, 39 insertions(+), 55 deletions(-) delete mode 100644 src/types/misc.ts diff --git a/src/components/EventList.tsx b/src/components/EventList.tsx index b2950734e..b14be59cb 100644 --- a/src/components/EventList.tsx +++ b/src/components/EventList.tsx @@ -11,7 +11,6 @@ import { FormattedMessage as Msg, } from 'react-intl'; -import { OnEventResponse } from '../types/misc'; import { ZetkinEvent } from '../interfaces/ZetkinEvent'; import { ZetkinEventResponse } from '../types/zetkin'; import { ZetkinOrganization } from '../interfaces/ZetkinOrganization'; @@ -20,7 +19,7 @@ interface EventListProps { events: ZetkinEvent[] | undefined; org: ZetkinOrganization; eventResponses: ZetkinEventResponse[] | undefined; - onEventResponse: OnEventResponse; + onEventResponse: (eventId: number, orgId: number, response: boolean) => void; } const EventList = ({ eventResponses, events, onEventResponse, org } : EventListProps) : JSX.Element => { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 4bff73f92..c8d8cf27b 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,8 +1,38 @@ import React from 'react'; +import { useMutation, useQueryClient } from 'react-query'; + +import deleteEventResponse from '../fetching/deleteEventResponse'; +import putEventResponse from '../fetching/putEventResponse'; import { ZetkinUser } from '../interfaces/ZetkinUser'; export const UserContext = React.createContext(null); export const useUser = () : ZetkinUser | null => { return React.useContext(UserContext); -}; \ No newline at end of file +}; + +type OnEventResponse = (eventId : number, orgId : number, response : boolean) => void; + +export const useOnEventResponse = () : OnEventResponse => { + const queryClient = useQueryClient(); + + const removeFunc = useMutation(deleteEventResponse, { + onSettled: () => { + queryClient.invalidateQueries('eventResponses'); + }, + }); + + const addFunc = useMutation(putEventResponse, { + onSettled: () => { + queryClient.invalidateQueries('eventResponses'); + }, + }); + + return (eventId : number, orgId : number, response : boolean) => { + if (response) { + removeFunc.mutate({ eventId, orgId }); + return; + } + addFunc.mutate({ eventId, orgId }); + }; +}; diff --git a/src/pages/o/[orgId]/campaigns/[campId].tsx b/src/pages/o/[orgId]/campaigns/[campId].tsx index 2fc6a73db..4ad32fe27 100644 --- a/src/pages/o/[orgId]/campaigns/[campId].tsx +++ b/src/pages/o/[orgId]/campaigns/[campId].tsx @@ -2,18 +2,16 @@ import { dehydrate } from 'react-query/hydration'; import EventList from '../../../../components/EventList'; import { GetServerSideProps } from 'next'; import { Flex, Heading, Text } from '@adobe/react-spectrum'; -import { QueryClient, useMutation, useQuery, useQueryClient } from 'react-query'; +import { QueryClient, useQuery } from 'react-query'; import DefaultOrgLayout from '../../../../components/layout/DefaultOrgLayout'; -import deleteEventResponse from '../../../../fetching/deleteEventResponse'; import getCampaign from '../../../../fetching/getCampaign'; import getCampaignEvents from '../../../../fetching/getCampaignEvents'; import getEventResponses from '../../../../fetching/getEventResponses'; import getOrg from '../../../../fetching/getOrg'; -import { OnEventResponse } from '../../../../types/misc'; import { PageWithLayout } from '../../../../types'; -import putEventResponse from '../../../../fetching/putEventResponse'; import { scaffold } from '../../../../utils/next'; +import { useOnEventResponse } from '../../../../hooks'; export const getServerSideProps : GetServerSideProps = scaffold(async (context) => { const queryClient = new QueryClient(); @@ -57,26 +55,7 @@ const OrgCampaignPage : PageWithLayout = (props) => { const responseQuery = useQuery('eventResponses', getEventResponses); const eventResponses = responseQuery.data; - const queryClient = useQueryClient(); - - const mutationAdd = useMutation(putEventResponse, { - onSettled: () => { - queryClient.invalidateQueries('eventResponses'); - }, - }); - - const mutationRemove = useMutation(deleteEventResponse, { - onSettled: () => { - queryClient.invalidateQueries('eventResponses'); - }, - }); - - const onEventResponse : OnEventResponse = (eventId, orgId, response) => { - if (response) { - return mutationRemove.mutate({ eventId, orgId }); - } - return mutationAdd.mutate({ eventId, orgId }); - }; + const onEventResponse = useOnEventResponse(); return ( diff --git a/src/pages/o/[orgId]/events.tsx b/src/pages/o/[orgId]/events.tsx index 3cf78b0bf..0a0e814a2 100644 --- a/src/pages/o/[orgId]/events.tsx +++ b/src/pages/o/[orgId]/events.tsx @@ -1,18 +1,16 @@ import { dehydrate } from 'react-query/hydration'; import { Flex } from '@adobe/react-spectrum'; import { GetServerSideProps } from 'next'; -import { QueryClient, useMutation, useQuery, useQueryClient } from 'react-query'; +import { QueryClient, useQuery } from 'react-query'; -import deleteEventResponse from '../../../fetching/deleteEventResponse'; import EventList from '../../../components/EventList'; import getEventResponses from '../../../fetching/getEventResponses'; import getEvents from '../../../fetching/getEvents'; import getOrg from '../../../fetching/getOrg'; import MainOrgLayout from '../../../components/layout/MainOrgLayout'; -import { OnEventResponse } from '../../../types/misc'; import { PageWithLayout } from '../../../types'; -import putEventResponse from '../../../fetching/putEventResponse'; import { scaffold } from '../../../utils/next'; +import { useOnEventResponse } from '../../../hooks'; const scaffoldOptions = { localeScope: [ @@ -59,33 +57,14 @@ const OrgEventsPage : PageWithLayout = (props) => { const responseQuery = useQuery('eventResponses', getEventResponses); const eventResponses = responseQuery.data; - const queryClient = useQueryClient(); - - const mutationAdd = useMutation(putEventResponse, { - onSettled: () => { - queryClient.invalidateQueries('eventResponses'); - }, - }); - - const mutationRemove = useMutation(deleteEventResponse, { - onSettled: () => { - queryClient.invalidateQueries('eventResponses'); - }, - }); - - const onEventResponse : OnEventResponse = (eventId, orgId, response) => { - if (response) { - return mutationRemove.mutate({ eventId, orgId }); - } - return mutationAdd.mutate({ eventId, orgId }); - }; + const onEventResponse = useOnEventResponse(); return ( onEventResponse(eventId, orgId, response) } + onEventResponse={ onEventResponse } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion org={ orgQuery.data! } /> diff --git a/src/types/misc.ts b/src/types/misc.ts deleted file mode 100644 index ed0db2ee1..000000000 --- a/src/types/misc.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface OnEventResponse { - (eventId: number, orgId: number, response: boolean) : void; -} \ No newline at end of file From f75a42ede441e911aaf7e33dc4ab1efabe5867f8 Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Fri, 16 Apr 2021 10:59:52 +0200 Subject: [PATCH 10/23] Make small changes in integration and component tests for events page and EventList --- cypress/fixtures/dummyEventResponses.json | 2 +- cypress/fixtures/dummyEvents.json | 4 ++-- cypress/integration/org_events_page.spec.ts | 3 ++- src/components/EventList.spec.tsx | 8 +------- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/cypress/fixtures/dummyEventResponses.json b/cypress/fixtures/dummyEventResponses.json index 199f4a776..c38fe9133 100644 --- a/cypress/fixtures/dummyEventResponses.json +++ b/cypress/fixtures/dummyEventResponses.json @@ -2,7 +2,7 @@ "data": [ { "action_id": 22, - "response_date": "1111 11 11, 11:11", + "response_date": "2021-01-21T18:30:00+00:00", "person": { "name": "Dummy User", "id": 2 diff --git a/cypress/fixtures/dummyEvents.json b/cypress/fixtures/dummyEvents.json index 7a6009ac0..9d1d06a31 100644 --- a/cypress/fixtures/dummyEvents.json +++ b/cypress/fixtures/dummyEvents.json @@ -8,13 +8,13 @@ "id": 941, "title": "Dummy campaign" }, - "end_time": "1111 11 11, 11:11", + "end_time": "2021-01-21T19:30:00+00:00", "id": 16831, "info_text": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", "location": { "title": "Dummy location" }, - "start_time": "0000 00 00, 00:00", + "start_time": "2021-01-21T18:30:00+00:00", "title": "Dummy event" } ] diff --git a/cypress/integration/org_events_page.spec.ts b/cypress/integration/org_events_page.spec.ts index a25121796..1e90dcc5b 100644 --- a/cypress/integration/org_events_page.spec.ts +++ b/cypress/integration/org_events_page.spec.ts @@ -23,13 +23,14 @@ describe('/o/[orgId]/events', () => { cy.get('[data-test="no-events-placeholder"]').should('be.visible'); }); - it('contains conditional functionality for sign-up button', () => { + it.only('shows sign up button if not signed up, and undo button when signed up', () => { cy.visit('/login'); cy.get('input[aria-label="E-mail address"]').type('testadmin@example.com'); cy.get('input[aria-label="Password"]').type('password'); cy.get('input[aria-label="Log in"]') .click(); cy.visit('/o/1/events'); + cy.waitUntilReactRendered(); cy.get('[data-test="sign-up-button"]') .eq(5) .contains('Sign-up') diff --git a/src/components/EventList.spec.tsx b/src/components/EventList.spec.tsx index de52759af..8c6994685 100644 --- a/src/components/EventList.spec.tsx +++ b/src/components/EventList.spec.tsx @@ -74,10 +74,6 @@ describe('EventList', () => { />, ); - //Checks for buttons on all events - cy.findByText('misc.eventList.signup'); - - //Tests button on a single event cy.findByText('misc.eventList.signup') .eq(0) .click() @@ -87,13 +83,11 @@ describe('EventList', () => { }); it('contains a button for more info on each event', () => { - const spyOnSubmit = cy.spy(); - mountWithProviders( null } org={ dummyOrg } />, ); From cdd458a9d5142c5e5e4dddd84bc53e62fa17d48c Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Fri, 16 Apr 2021 11:49:30 +0200 Subject: [PATCH 11/23] Include eventResponses in useOnEventResponse --- src/hooks/index.ts | 27 ++++++++++++++++------ src/pages/o/[orgId]/campaigns/[campId].tsx | 9 +++----- src/pages/o/[orgId]/events.tsx | 7 ++---- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index c8d8cf27b..c8f6b9f77 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,8 +1,10 @@ import React from 'react'; -import { useMutation, useQueryClient } from 'react-query'; +import { useMutation, useQuery, useQueryClient } from 'react-query'; import deleteEventResponse from '../fetching/deleteEventResponse'; +import getEventResponses from '../fetching/getEventResponses'; import putEventResponse from '../fetching/putEventResponse'; +import { ZetkinEventResponse } from '../types/zetkin'; import { ZetkinUser } from '../interfaces/ZetkinUser'; export const UserContext = React.createContext(null); @@ -13,7 +15,15 @@ export const useUser = () : ZetkinUser | null => { type OnEventResponse = (eventId : number, orgId : number, response : boolean) => void; -export const useOnEventResponse = () : OnEventResponse => { +type EventResponses = { + eventResponses: ZetkinEventResponse[] | undefined; + onEventResponse: OnEventResponse; +} + +export const useEventResponses = () : EventResponses => { + const responseQuery = useQuery('eventResponses', getEventResponses); + const eventResponses = responseQuery.data; + const queryClient = useQueryClient(); const removeFunc = useMutation(deleteEventResponse, { @@ -28,11 +38,14 @@ export const useOnEventResponse = () : OnEventResponse => { }, }); - return (eventId : number, orgId : number, response : boolean) => { + function onEventResponse (eventId : number, orgId : number, response : boolean) { if (response) { removeFunc.mutate({ eventId, orgId }); - return; } - addFunc.mutate({ eventId, orgId }); - }; -}; + else { + addFunc.mutate({ eventId, orgId }); + } + } + + return { eventResponses, onEventResponse }; +}; \ No newline at end of file diff --git a/src/pages/o/[orgId]/campaigns/[campId].tsx b/src/pages/o/[orgId]/campaigns/[campId].tsx index 4ad32fe27..c667b5cfd 100644 --- a/src/pages/o/[orgId]/campaigns/[campId].tsx +++ b/src/pages/o/[orgId]/campaigns/[campId].tsx @@ -7,11 +7,10 @@ import { QueryClient, useQuery } from 'react-query'; import DefaultOrgLayout from '../../../../components/layout/DefaultOrgLayout'; import getCampaign from '../../../../fetching/getCampaign'; import getCampaignEvents from '../../../../fetching/getCampaignEvents'; -import getEventResponses from '../../../../fetching/getEventResponses'; import getOrg from '../../../../fetching/getOrg'; import { PageWithLayout } from '../../../../types'; import { scaffold } from '../../../../utils/next'; -import { useOnEventResponse } from '../../../../hooks'; +import { useEventResponses } from '../../../../hooks'; export const getServerSideProps : GetServerSideProps = scaffold(async (context) => { const queryClient = new QueryClient(); @@ -52,10 +51,8 @@ const OrgCampaignPage : PageWithLayout = (props) => { const campaignQuery = useQuery(['campaign', campId], getCampaign(orgId, campId)); const orgQuery = useQuery(['org', orgId], getOrg(orgId)); const campaignEventsQuery = useQuery(['campaignEvents', campId], getCampaignEvents(orgId, campId)); - const responseQuery = useQuery('eventResponses', getEventResponses); - const eventResponses = responseQuery.data; - const onEventResponse = useOnEventResponse(); + const { eventResponses, onEventResponse } = useEventResponses(); return ( @@ -68,7 +65,7 @@ const OrgCampaignPage : PageWithLayout = (props) => { onEventResponse(eventId, orgId, response) } + onEventResponse={ onEventResponse } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion org={ orgQuery.data! } /> diff --git a/src/pages/o/[orgId]/events.tsx b/src/pages/o/[orgId]/events.tsx index 0a0e814a2..1efe375cc 100644 --- a/src/pages/o/[orgId]/events.tsx +++ b/src/pages/o/[orgId]/events.tsx @@ -4,13 +4,12 @@ import { GetServerSideProps } from 'next'; import { QueryClient, useQuery } from 'react-query'; import EventList from '../../../components/EventList'; -import getEventResponses from '../../../fetching/getEventResponses'; import getEvents from '../../../fetching/getEvents'; import getOrg from '../../../fetching/getOrg'; import MainOrgLayout from '../../../components/layout/MainOrgLayout'; import { PageWithLayout } from '../../../types'; import { scaffold } from '../../../utils/next'; -import { useOnEventResponse } from '../../../hooks'; +import { useEventResponses } from '../../../hooks'; const scaffoldOptions = { localeScope: [ @@ -54,10 +53,8 @@ const OrgEventsPage : PageWithLayout = (props) => { const { orgId } = props; const eventsQuery = useQuery('events', getEvents(orgId)); const orgQuery = useQuery(['org', orgId], getOrg(orgId)); - const responseQuery = useQuery('eventResponses', getEventResponses); - const eventResponses = responseQuery.data; - const onEventResponse = useOnEventResponse(); + const { eventResponses, onEventResponse } = useEventResponses(); return ( From 3dea6164499126a2657b13fe52f8d34e139ce9ad Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Fri, 16 Apr 2021 11:52:40 +0200 Subject: [PATCH 12/23] Add TODO --- src/fetching/deleteEventResponse.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fetching/deleteEventResponse.ts b/src/fetching/deleteEventResponse.ts index f016469ca..2e473a467 100644 --- a/src/fetching/deleteEventResponse.ts +++ b/src/fetching/deleteEventResponse.ts @@ -6,6 +6,7 @@ export default async function deleteEventResponse({ eventId, orgId } : ZetkinEve const mUrl = apiUrl('/users/me/memberships'); const mRes = await fetch(mUrl); const mData = await mRes.json(); + //TODO: Memberships should be cached. const orgMembership = mData.data.find((m : ZetkinMembership) => m.organization.id === orgId); if (orgMembership) { From 7d82701b166d15aa234d71b8f3174fe32e379c35 Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Fri, 16 Apr 2021 12:20:41 +0200 Subject: [PATCH 13/23] Apply useEventResponses on event page --- cypress/integration/org_event_page.spec.ts | 21 +++++++++++++++------ src/locale/pages/orgEvent/en.yml | 3 ++- src/locale/pages/orgEvent/sv.yml | 3 ++- src/pages/o/[orgId]/events/[eventId].tsx | 22 +++++++++++++++++++--- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/cypress/integration/org_event_page.spec.ts b/cypress/integration/org_event_page.spec.ts index ad80a36ad..8447ac8aa 100644 --- a/cypress/integration/org_event_page.spec.ts +++ b/cypress/integration/org_event_page.spec.ts @@ -1,28 +1,37 @@ describe('/o/[orgId]/events/[eventId]', () => { it('contains non-interactive event content', () => { - cy.visit('/o/1/events/22'); + cy.visit('/o/1/events/25'); cy.get('[data-test="event-title"]').should('be.visible'); cy.get('[data-test="duration"]').should('be.visible'); cy.get('[data-test="location"]').should('be.visible'); }); it('contains clickable org name that leads to org page', () => { - cy.visit('/o/1/events/22'); + cy.visit('/o/1/events/25'); cy.waitUntilReactRendered(); cy.findByText('My Organization').click(); cy.url().should('match', /\/o\/1$/); }); it('contains clickable campaign name that leads to campaign page', () => { - cy.visit('/o/1/events/22'); + cy.visit('/o/1/events/25'); cy.waitUntilReactRendered(); cy.findByText('Second campaign').click(); cy.url().should('match', /\/o\/1\/campaigns\/2$/); }); - it('contains a sign-up button', () => { - cy.visit('/o/1/events/22'); - cy.findByText('Sign-up').should('be.visible'); + it.only('contains a conditional sign-up button', () => { + cy.visit('/login'); + cy.get('input[aria-label="E-mail address"]').type('testadmin@example.com'); + cy.get('input[aria-label="Password"]').type('password'); + cy.get('input[aria-label="Log in"]') + .click(); + cy.visit('/o/1/events/25'); + cy.waitUntilReactRendered(); + cy.findByText('Sign-up') + .click(); + cy.waitUntilReactRendered(); + cy.findByText('Undo sign-up'); }); }); diff --git a/src/locale/pages/orgEvent/en.yml b/src/locale/pages/orgEvent/en.yml index a27ea725b..d6014be83 100644 --- a/src/locale/pages/orgEvent/en.yml +++ b/src/locale/pages/orgEvent/en.yml @@ -1,2 +1,3 @@ actions: - signUp: Sign-up + signup: Sign-up + undoSignup: Undo sign-up diff --git a/src/locale/pages/orgEvent/sv.yml b/src/locale/pages/orgEvent/sv.yml index 6bbb16582..b3e65f08a 100644 --- a/src/locale/pages/orgEvent/sv.yml +++ b/src/locale/pages/orgEvent/sv.yml @@ -1,2 +1,3 @@ actions: - signUp: Anmälan \ No newline at end of file + signup: Anmälan + undoSignup: Ångra anmälan \ No newline at end of file diff --git a/src/pages/o/[orgId]/events/[eventId].tsx b/src/pages/o/[orgId]/events/[eventId].tsx index 16f694aaa..ba6ee0c2b 100644 --- a/src/pages/o/[orgId]/events/[eventId].tsx +++ b/src/pages/o/[orgId]/events/[eventId].tsx @@ -27,6 +27,7 @@ import getEvent from '../../../../fetching/getEvent'; import getOrg from '../../../../fetching/getOrg'; import { PageWithLayout } from '../../../../types'; import { scaffold } from '../../../../utils/next'; +import { useEventResponses } from '../../../../hooks'; import { ZetkinEvent } from '../../../../interfaces/ZetkinEvent'; import { ZetkinOrganization } from '../../../../interfaces/ZetkinOrganization'; @@ -73,6 +74,7 @@ const OrgEventPage : PageWithLayout = (props) => { const { orgId, eventId } = props; const eventQuery = useQuery(['event', eventId], getEvent(orgId, eventId)); const orgQuery = useQuery(['org', orgId], getOrg(orgId)); + const { eventResponses, onEventResponse } = useEventResponses(); if (!eventQuery.data) { return null; @@ -81,6 +83,8 @@ const OrgEventPage : PageWithLayout = (props) => { const event = eventQuery.data as ZetkinEvent; const org = orgQuery.data as ZetkinOrganization; + const response = eventResponses?.find(response => response.action_id === event.id); + return ( <>
@@ -151,9 +155,21 @@ const OrgEventPage : PageWithLayout = (props) => { marginTop="size-200" position="absolute" right="size-200"> - + { response ? ( + + ) : ( + + ) } ); From 748c496b13792fa8a9abf00af7bfabec2405f4d5 Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Fri, 16 Apr 2021 12:21:56 +0200 Subject: [PATCH 14/23] Add space before block --- src/types/zetkin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/zetkin.ts b/src/types/zetkin.ts index e37603d09..642aa6468 100644 --- a/src/types/zetkin.ts +++ b/src/types/zetkin.ts @@ -18,7 +18,7 @@ export interface ZetkinEventResponse { id: number; } -export interface ZetkinEventSignup{ +export interface ZetkinEventSignup { eventId: number; orgId: number; } \ No newline at end of file From 6e5a17dfe4cadc62db429ed443c52b05b144e96a Mon Sep 17 00:00:00 2001 From: kristofferlarberg Date: Fri, 16 Apr 2021 15:11:57 +0200 Subject: [PATCH 15/23] Make integration-tests for events/event pages less flaky --- cypress/integration/org_events_page.spec.ts | 7 +++++-- src/components/EventList.tsx | 4 ++-- src/pages/o/[orgId]/events/[eventId].tsx | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cypress/integration/org_events_page.spec.ts b/cypress/integration/org_events_page.spec.ts index 1e90dcc5b..4af7e21a3 100644 --- a/cypress/integration/org_events_page.spec.ts +++ b/cypress/integration/org_events_page.spec.ts @@ -31,10 +31,13 @@ describe('/o/[orgId]/events', () => { .click(); cy.visit('/o/1/events'); cy.waitUntilReactRendered(); - cy.get('[data-test="sign-up-button"]') + cy.get('[data-test="event-response-button"]') .eq(5) .contains('Sign-up') - .click() + .click(); + cy.waitUntilReactRendered(); + cy.get('[data-test="event-response-button"]') + .eq(5) .contains('Undo sign-up'); }); }); diff --git a/src/components/EventList.tsx b/src/components/EventList.tsx index b14be59cb..3267b6d6e 100644 --- a/src/components/EventList.tsx +++ b/src/components/EventList.tsx @@ -67,7 +67,7 @@ const EventList = ({ eventResponses, events, onEventResponse, org } : EventListP { e.location.title } { response ? ( ) : ( ) : (