From 9079255ea0014dfe3b333764b7782aff621a48b0 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:06:20 +0200 Subject: [PATCH 01/48] refactor LocationsModel in two diferent hooks --- src/features/events/hooks/useEventLocation.ts | 22 +++++++ .../events/hooks/useEventLocationMutations.ts | 65 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/features/events/hooks/useEventLocation.ts create mode 100644 src/features/events/hooks/useEventLocationMutations.ts diff --git a/src/features/events/hooks/useEventLocation.ts b/src/features/events/hooks/useEventLocation.ts new file mode 100644 index 0000000000..53ed282481 --- /dev/null +++ b/src/features/events/hooks/useEventLocation.ts @@ -0,0 +1,22 @@ +import { futureToObject, IFuture } from 'core/caching/futures'; +import { loadListIfNecessary } from 'core/caching/cacheUtils'; +import { ZetkinLocation } from 'utils/types/zetkin'; +import { locationsLoad, locationsLoaded } from '../store'; +import { useApiClient, useAppDispatch, useAppSelector } from 'core/hooks'; + +export default function useEventLocation( + orgId: number +): ZetkinLocation[] | null { + const apiClient = useApiClient(); + const locationsList = useAppSelector((state) => state.events.locationList); + const dispatch = useAppDispatch(); + + const locations = loadListIfNecessary(locationsList, dispatch, { + actionOnLoad: () => locationsLoad(), + actionOnSuccess: (data) => locationsLoaded(data), + loader: () => + apiClient.get(`/api/orgs/${orgId}/locations`), + }); + + return locations.data; +} diff --git a/src/features/events/hooks/useEventLocationMutations.ts b/src/features/events/hooks/useEventLocationMutations.ts new file mode 100644 index 0000000000..9376c89a3a --- /dev/null +++ b/src/features/events/hooks/useEventLocationMutations.ts @@ -0,0 +1,65 @@ +import { useApiClient, useAppDispatch, useAppSelector } from 'core/hooks'; +import { ZetkinLocation } from 'utils/types/zetkin'; +import { ZetkinLocationPatchBody } from '../repo/EventsRepo'; +import { locationAdded, locationUpdate, locationUpdated } from '../store'; + +type useEventLocationMutationsReturn = { + addLocation: (newLocation: Partial) => void; + setLocationDescription: (locationId: number, description: string) => void; + setLocationLatLng: (locationId: number, lat: number, lng: number) => void; + setLocationTitle: (locationId: number, title: string) => void; +}; + +export default function useEventLocationMutations( + orgId: number +): useEventLocationMutationsReturn { + const apiClient = useApiClient(); + const dispatch = useAppDispatch(); + + const addLocation = async (newLocation: Partial) => { + const location = await apiClient.post( + `/api/orgs/${orgId}/locations`, + { + info_text: newLocation.info_text, + lat: newLocation.lat, + lng: newLocation.lng, + title: newLocation.title, + } + ); + dispatch(locationAdded(location)); + }; + + const setLocationDescription = (locationId: number, description: string) => { + updateLocation(orgId, locationId, { + info_text: description, + }); + }; + + const setLocationLatLng = (locationId: number, lat: number, lng: number) => { + updateLocation(orgId, locationId, { lat, lng }); + }; + + const setLocationTitle = (locationId: number, title: string) => { + updateLocation(orgId, locationId, { title }); + }; + + const updateLocation = ( + orgId: number, + locationId: number, + data: ZetkinLocationPatchBody + ) => { + dispatch(locationUpdate([locationId, Object.keys(data)])); + apiClient + .patch(`/api/orgs/${orgId}/locations/${locationId}`, data) + .then((location) => { + dispatch(locationUpdated(location)); + }); + }; + + return { + addLocation, + setLocationDescription, + setLocationLatLng, + setLocationTitle, + }; +} From d4a9f722945780f9bc5b91afea6b1dd6a00a4346 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Thu, 12 Oct 2023 11:08:22 +0200 Subject: [PATCH 02/48] implement useEventLocations and useEventLocationMutations hooks to remove old LocationsModel --- .../events/components/EventDetailsForm.tsx | 7 ++-- .../components/EventOverviewCard/index.tsx | 16 +++++--- .../LocationModal/LocationDetailsCard.tsx | 12 +++--- .../events/components/LocationModal/index.tsx | 9 +++-- src/features/events/models/LocationsModel.ts | 38 ------------------- 5 files changed, 25 insertions(+), 57 deletions(-) delete mode 100644 src/features/events/models/LocationsModel.ts diff --git a/src/features/events/components/EventDetailsForm.tsx b/src/features/events/components/EventDetailsForm.tsx index a3b58904f1..4fa60a8435 100644 --- a/src/features/events/components/EventDetailsForm.tsx +++ b/src/features/events/components/EventDetailsForm.tsx @@ -7,10 +7,10 @@ import { Box, Button, Grid, GridSize, MenuItem } from '@mui/material'; import getActivities from 'utils/fetching/getActivities'; import getCampaigns from 'features/campaigns/fetching/getCampaigns'; -import getLocations from 'utils/fetching/getLocations'; import { Msg, useMessages } from 'core/i18n'; import messageIds from '../l10n/messageIds'; +import useEventLocation from '../hooks/useEventLocation'; interface EventDetailsFormProps { onSubmit: (data: Record) => void; @@ -27,12 +27,11 @@ const EventDetailsForm = ({ const { campId } = router.query; const campaignsQuery = useQuery(['campaigns', orgId], getCampaigns(orgId)); const activitiesQuery = useQuery(['actvities', orgId], getActivities(orgId)); - const locationsQuery = useQuery(['locations', orgId], getLocations(orgId)); const activities = activitiesQuery.data || []; - const locations = locationsQuery.data || []; const campaigns = campaignsQuery.data || []; const messages = useMessages(messageIds); + const locations = useEventLocation(parseInt(orgId)); const formattedNow = dayjs().format('YYYY-MM-DDTHH:mm:ss'); @@ -139,7 +138,7 @@ const EventDetailsForm = ({ name="location_id" select > - {locations.map((l) => ( + {locations?.map((l) => ( {l.title} diff --git a/src/features/events/components/EventOverviewCard/index.tsx b/src/features/events/components/EventOverviewCard/index.tsx index c3e70f61ee..2afd842fac 100644 --- a/src/features/events/components/EventOverviewCard/index.tsx +++ b/src/features/events/components/EventOverviewCard/index.tsx @@ -24,7 +24,6 @@ import EventDataModel from 'features/events/models/EventDataModel'; import { EventsModel } from 'features/events/models/EventsModel'; import { getWorkingUrl } from 'features/events/utils/getWorkingUrl'; import LocationModal from '../LocationModal'; -import LocationsModel from 'features/events/models/LocationsModel'; import messageIds from 'features/events/l10n/messageIds'; import theme from 'theme'; import useEditPreviewBlock from 'zui/hooks/useEditPreviewBlock'; @@ -38,21 +37,27 @@ import { removeOffset, } from 'utils/dateUtils'; import { ZetkinEvent, ZetkinLocation } from 'utils/types/zetkin'; +import useEventLocation from 'features/events/hooks/useEventLocation'; +import useEventLocationMutations from 'features/events/hooks/useEventLocationMutations'; type EventOverviewCardProps = { data: ZetkinEvent; dataModel: EventDataModel; eventsModel: EventsModel; - locationsModel: LocationsModel; }; const EventOverviewCard: FC = ({ data, dataModel, eventsModel, - locationsModel, }) => { - const locations = locationsModel.getLocations().data; + const locations = useEventLocation(orgId); + const { + addLocation, + setLocationDescription, + setLocationLatLng, + setLocationTitle, + } = useEventLocationMutations(orgId); const messages = useMessages(messageIds); const [editable, setEditable] = useState(false); const [link, setLink] = useState(data.url); @@ -476,11 +481,10 @@ const EventOverviewCard: FC = ({ events={events || []} locationId={locationId} locations={sortedLocation || []} - model={locationsModel} onCreateLocation={( newLocation: Partial ) => { - locationsModel.addLocation(newLocation); + addLocation(newLocation); }} onMapClose={() => { setLocationModalOpen(false); diff --git a/src/features/events/components/LocationModal/LocationDetailsCard.tsx b/src/features/events/components/LocationModal/LocationDetailsCard.tsx index 337c343139..0787b9fc57 100644 --- a/src/features/events/components/LocationModal/LocationDetailsCard.tsx +++ b/src/features/events/components/LocationModal/LocationDetailsCard.tsx @@ -10,10 +10,11 @@ import { import { Close, EventOutlined, OpenWith } from '@mui/icons-material'; import { FC, useCallback, useEffect, useState } from 'react'; -import LocationsModel from 'features/events/models/LocationsModel'; import messageIds from 'features/events/l10n/messageIds'; import RelatedEventCard from '../RelatedEvent'; +import useEventLocationMutations from 'features/events/hooks/useEventLocationMutations'; import { useMessages } from 'core/i18n'; +import { useNumericRouteParams } from 'core/hooks'; import { ZetkinEvent, ZetkinLocation } from 'utils/types/zetkin'; import ZUIPreviewableInput, { ZUIPreviewableMode, @@ -27,7 +28,6 @@ const useStyles = makeStyles((theme) => ({ })); interface LocationDetailsCardProps { - model: LocationsModel; onClose: () => void; onMove: () => void; onUseLocation: () => void; @@ -37,12 +37,12 @@ interface LocationDetailsCardProps { const LocationDetailsCard: FC = ({ location, - model, onClose, onMove, onUseLocation, relatedEvents, }) => { + const { orgId } = useNumericRouteParams(); const classes = useStyles(); const messages = useMessages(messageIds); const [title, setTitle] = useState(location.title); @@ -50,6 +50,8 @@ const LocationDetailsCard: FC = ({ const [fieldEditing, setFieldEditing] = useState< 'title' | 'description' | null >(null); + const { setLocationDescription, setLocationTitle } = + useEventLocationMutations(orgId); const handleDescriptionTextAreaRef = useCallback( (el: HTMLTextAreaElement | null) => { @@ -85,10 +87,10 @@ const LocationDetailsCard: FC = ({ onClickAway={() => { if (fieldEditing === 'title') { setFieldEditing(null); - model.setLocationTitle(location.id, title); + setLocationTitle(location.id, title); } else if (fieldEditing === 'description') { setFieldEditing(null); - model.setLocationDescription(location.id, description); + setLocationDescription(location.id, description); } }} > diff --git a/src/features/events/components/LocationModal/index.tsx b/src/features/events/components/LocationModal/index.tsx index e51ff4679f..4d2289766d 100644 --- a/src/features/events/components/LocationModal/index.tsx +++ b/src/features/events/components/LocationModal/index.tsx @@ -14,6 +14,8 @@ import messageIds from 'features/events/l10n/messageIds'; import MoveLocationCard from './MoveLocationCard'; import { useMessages } from 'core/i18n'; import { ZetkinEvent, ZetkinLocation } from 'utils/types/zetkin'; +import useEventLocationMutations from 'features/events/hooks/useEventLocationMutations'; +import { useNumericRouteParams } from 'core/hooks'; interface StyleProps { cardIsFullHeight: boolean; @@ -43,7 +45,6 @@ interface LocationModalProps { currentEventId: number; events: ZetkinEvent[]; locations: ZetkinLocation[]; - model: LocationsModel; onCreateLocation: (newLocation: Partial) => void; onMapClose: () => void; onSelectLocation: (location: ZetkinLocation) => void; @@ -56,16 +57,17 @@ const LocationModal: FC = ({ currentEventId, events, locations, - model, onCreateLocation, onMapClose, onSelectLocation, open, locationId = null, }) => { + const { orgId } = useNumericRouteParams(); const messages = useMessages(messageIds); const [searchString, setSearchString] = useState(''); const [selectedLocationId, setSelectedLocationId] = useState(locationId); + const { setLocationLatLng } = useEventLocationMutations(orgId); const [pendingLocation, setPendingLocation] = useState = ({ {selectedLocation && !inMoveState && ( { setSearchString(''); setSelectedLocationId(null); @@ -191,7 +192,7 @@ const LocationModal: FC = ({ }} onSaveLocation={() => { if (newLatLng) { - model.setLocationLatLng( + setLocationLatLng( selectedLocation.id, newLatLng.lat, newLatLng.lng diff --git a/src/features/events/models/LocationsModel.ts b/src/features/events/models/LocationsModel.ts deleted file mode 100644 index fa13f22648..0000000000 --- a/src/features/events/models/LocationsModel.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Environment from 'core/env/Environment'; -import EventsRepo from '../repo/EventsRepo'; -import { IFuture } from 'core/caching/futures'; -import { ModelBase } from 'core/models'; -import { ZetkinLocation } from 'utils/types/zetkin'; - -export default class LocationsModel extends ModelBase { - private _orgId: number; - private _repo: EventsRepo; - - addLocation(newLocation: Partial): void { - this._repo.addLocation(this._orgId, newLocation); - } - - constructor(env: Environment, orgId: number) { - super(); - this._orgId = orgId; - this._repo = new EventsRepo(env); - } - - getLocations(): IFuture { - return this._repo.getLocations(this._orgId); - } - - setLocationDescription(locationId: number, description: string) { - this._repo.updateLocation(this._orgId, locationId, { - info_text: description, - }); - } - - setLocationLatLng(locationId: number, lat: number, lng: number) { - this._repo.updateLocation(this._orgId, locationId, { lat, lng }); - } - - setLocationTitle(locationId: number, title: string) { - this._repo.updateLocation(this._orgId, locationId, { title }); - } -} From 1bb46beb40ec434ebe56ac404832d3d1a474bd9e Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Thu, 12 Oct 2023 11:13:02 +0200 Subject: [PATCH 03/48] fix lint errors --- .../events/components/EventOverviewCard/index.tsx | 13 +++++-------- .../events/components/LocationModal/index.tsx | 5 ++--- src/features/events/hooks/useEventLocation.ts | 1 - .../events/hooks/useEventLocationMutations.ts | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/features/events/components/EventOverviewCard/index.tsx b/src/features/events/components/EventOverviewCard/index.tsx index 2afd842fac..ec16cea882 100644 --- a/src/features/events/components/EventOverviewCard/index.tsx +++ b/src/features/events/components/EventOverviewCard/index.tsx @@ -27,6 +27,8 @@ import LocationModal from '../LocationModal'; import messageIds from 'features/events/l10n/messageIds'; import theme from 'theme'; import useEditPreviewBlock from 'zui/hooks/useEditPreviewBlock'; +import useEventLocation from 'features/events/hooks/useEventLocation'; +import useEventLocationMutations from 'features/events/hooks/useEventLocationMutations'; import { useMessages } from 'core/i18n'; import ZUIDate from 'zui/ZUIDate'; import ZUIPreviewableInput from 'zui/ZUIPreviewableInput'; @@ -37,27 +39,22 @@ import { removeOffset, } from 'utils/dateUtils'; import { ZetkinEvent, ZetkinLocation } from 'utils/types/zetkin'; -import useEventLocation from 'features/events/hooks/useEventLocation'; -import useEventLocationMutations from 'features/events/hooks/useEventLocationMutations'; type EventOverviewCardProps = { data: ZetkinEvent; dataModel: EventDataModel; eventsModel: EventsModel; + orgId: number; }; const EventOverviewCard: FC = ({ data, dataModel, eventsModel, + orgId, }) => { const locations = useEventLocation(orgId); - const { - addLocation, - setLocationDescription, - setLocationLatLng, - setLocationTitle, - } = useEventLocationMutations(orgId); + const { addLocation } = useEventLocationMutations(orgId); const messages = useMessages(messageIds); const [editable, setEditable] = useState(false); const [link, setLink] = useState(data.url); diff --git a/src/features/events/components/LocationModal/index.tsx b/src/features/events/components/LocationModal/index.tsx index 4d2289766d..8ac764b1b7 100644 --- a/src/features/events/components/LocationModal/index.tsx +++ b/src/features/events/components/LocationModal/index.tsx @@ -9,13 +9,12 @@ import 'leaflet/dist/leaflet.css'; import CreateLocationCard from './CreateLocationCard'; import LocationDetailsCard from './LocationDetailsCard'; import LocationSearch from './LocationSearch'; -import LocationsModel from 'features/events/models/LocationsModel'; import messageIds from 'features/events/l10n/messageIds'; import MoveLocationCard from './MoveLocationCard'; -import { useMessages } from 'core/i18n'; -import { ZetkinEvent, ZetkinLocation } from 'utils/types/zetkin'; import useEventLocationMutations from 'features/events/hooks/useEventLocationMutations'; +import { useMessages } from 'core/i18n'; import { useNumericRouteParams } from 'core/hooks'; +import { ZetkinEvent, ZetkinLocation } from 'utils/types/zetkin'; interface StyleProps { cardIsFullHeight: boolean; diff --git a/src/features/events/hooks/useEventLocation.ts b/src/features/events/hooks/useEventLocation.ts index 53ed282481..d980b35abf 100644 --- a/src/features/events/hooks/useEventLocation.ts +++ b/src/features/events/hooks/useEventLocation.ts @@ -1,4 +1,3 @@ -import { futureToObject, IFuture } from 'core/caching/futures'; import { loadListIfNecessary } from 'core/caching/cacheUtils'; import { ZetkinLocation } from 'utils/types/zetkin'; import { locationsLoad, locationsLoaded } from '../store'; diff --git a/src/features/events/hooks/useEventLocationMutations.ts b/src/features/events/hooks/useEventLocationMutations.ts index 9376c89a3a..66be029020 100644 --- a/src/features/events/hooks/useEventLocationMutations.ts +++ b/src/features/events/hooks/useEventLocationMutations.ts @@ -1,7 +1,7 @@ -import { useApiClient, useAppDispatch, useAppSelector } from 'core/hooks'; import { ZetkinLocation } from 'utils/types/zetkin'; import { ZetkinLocationPatchBody } from '../repo/EventsRepo'; import { locationAdded, locationUpdate, locationUpdated } from '../store'; +import { useApiClient, useAppDispatch } from 'core/hooks'; type useEventLocationMutationsReturn = { addLocation: (newLocation: Partial) => void; From 460080656807db3b66ced1e4c5388fe4a4830202 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Thu, 12 Oct 2023 16:47:39 +0200 Subject: [PATCH 04/48] refactor getAllEvents, getParallelEvents and getRelatedEvents in their on hooks and implement changes in components --- .../components/EventOverviewCard/index.tsx | 15 ++---- .../events/components/EventRelatedCard.tsx | 9 ++-- src/features/events/hooks/useAllEvents.ts | 17 ++++++ .../events/hooks/useParallelEvents.ts | 39 ++++++++++++++ src/features/events/hooks/useRelatedEvents.ts | 52 +++++++++++++++++++ 5 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 src/features/events/hooks/useAllEvents.ts create mode 100644 src/features/events/hooks/useParallelEvents.ts create mode 100644 src/features/events/hooks/useRelatedEvents.ts diff --git a/src/features/events/components/EventOverviewCard/index.tsx b/src/features/events/components/EventOverviewCard/index.tsx index ec16cea882..bae2d98042 100644 --- a/src/features/events/components/EventOverviewCard/index.tsx +++ b/src/features/events/components/EventOverviewCard/index.tsx @@ -21,7 +21,6 @@ import dayjs, { Dayjs } from 'dayjs'; import { FC, useMemo, useState } from 'react'; import EventDataModel from 'features/events/models/EventDataModel'; -import { EventsModel } from 'features/events/models/EventsModel'; import { getWorkingUrl } from 'features/events/utils/getWorkingUrl'; import LocationModal from '../LocationModal'; import messageIds from 'features/events/l10n/messageIds'; @@ -30,6 +29,7 @@ import useEditPreviewBlock from 'zui/hooks/useEditPreviewBlock'; import useEventLocation from 'features/events/hooks/useEventLocation'; import useEventLocationMutations from 'features/events/hooks/useEventLocationMutations'; import { useMessages } from 'core/i18n'; +import useParallelEvents from 'features/events/hooks/useParallelEvents'; import ZUIDate from 'zui/ZUIDate'; import ZUIPreviewableInput from 'zui/ZUIPreviewableInput'; import { @@ -43,16 +43,12 @@ import { ZetkinEvent, ZetkinLocation } from 'utils/types/zetkin'; type EventOverviewCardProps = { data: ZetkinEvent; dataModel: EventDataModel; - eventsModel: EventsModel; orgId: number; }; -const EventOverviewCard: FC = ({ - data, dataModel, - eventsModel, - orgId, -}) => { +const EventOverviewCard: FC = ({ data, orgId }) => { + const { updateEvent } = useEventMutations(orgId, data.id); const locations = useEventLocation(orgId); const { addLocation } = useEventLocationMutations(orgId); const messages = useMessages(messageIds); @@ -114,10 +110,7 @@ const EventOverviewCard: FC = ({ ? [...sortedLocation, 'NO_PHYSICAL_LOCATION', 'CREATE_NEW_LOCATION'] : ['NO_PHYSICAL_LOCATION', 'CREATE_NEW_LOCATION']; - const events = eventsModel.getParallelEvents( - data.start_time, - data.end_time - ).data; + const events = useParallelEvents(orgId, data.start_time, data.end_time).data; return ( diff --git a/src/features/events/components/EventRelatedCard.tsx b/src/features/events/components/EventRelatedCard.tsx index 0cc751565d..c3ac378a62 100644 --- a/src/features/events/components/EventRelatedCard.tsx +++ b/src/features/events/components/EventRelatedCard.tsx @@ -1,24 +1,25 @@ import { FC } from 'react'; import { Box, Divider } from '@mui/material'; -import { EventsModel } from '../models/EventsModel'; import messageIds from 'features/events/l10n/messageIds'; import RelatedEvent from './RelatedEvent'; import { useMessages } from 'core/i18n'; +import useRelatedEvents from '../hooks/useRelatedEvents'; import { ZetkinEvent } from 'utils/types/zetkin'; import ZUICard from 'zui/ZUICard'; import ZUIFuture from 'zui/ZUIFuture'; interface EventRelatedCardProps { data: ZetkinEvent; - model: EventsModel; + orgId: number; } -const EventRelatedCard: FC = ({ data, model }) => { +const EventRelatedCard: FC = ({ data, orgId }) => { const messages = useMessages(messageIds); + const relatedEvents = useRelatedEvents(data, orgId); return ( - + {(events) => { return ( <> diff --git a/src/features/events/hooks/useAllEvents.ts b/src/features/events/hooks/useAllEvents.ts new file mode 100644 index 0000000000..48d6d0c5dc --- /dev/null +++ b/src/features/events/hooks/useAllEvents.ts @@ -0,0 +1,17 @@ +import { IFuture } from 'core/caching/futures'; +import { loadListIfNecessary } from 'core/caching/cacheUtils'; +import { ZetkinEvent } from 'utils/types/zetkin'; +import { useApiClient, useAppDispatch, useAppSelector } from 'core/hooks'; +import { eventsLoad, eventsLoaded } from '../store'; + +export default function useAllEvents(orgId: number): IFuture { + const apiClient = useApiClient(); + const dispatch = useAppDispatch(); + const eventList = useAppSelector((state) => state.events.eventList); + + return loadListIfNecessary(eventList, dispatch, { + actionOnLoad: () => eventsLoad(), + actionOnSuccess: (events) => eventsLoaded(events), + loader: () => apiClient.get(`/api/orgs/${orgId}/actions`), + }); +} diff --git a/src/features/events/hooks/useParallelEvents.ts b/src/features/events/hooks/useParallelEvents.ts new file mode 100644 index 0000000000..efa9d1daf3 --- /dev/null +++ b/src/features/events/hooks/useParallelEvents.ts @@ -0,0 +1,39 @@ +import useAllEvents from './useAllEvents'; +import { ZetkinEvent } from 'utils/types/zetkin'; +import { dateIsAfter, dateIsBefore, isSameDate } from 'utils/dateUtils'; +import { + ErrorFuture, + IFuture, + LoadingFuture, + ResolvedFuture, +} from 'core/caching/futures'; + +export default function useParallelEvents( + orgId: number, + startString: string, + endString: string +): IFuture { + const allEvents = useAllEvents(orgId); + + if (allEvents.isLoading) { + return new LoadingFuture(); + } else if (allEvents.error) { + return new ErrorFuture(allEvents.error); + } + + const start = new Date(startString); + const end = new Date(endString); + + const filteredEvents = allEvents?.data?.filter((event) => { + const eventStart = new Date(event.start_time); + const eventEnd = new Date(event.end_time); + + return ( + isSameDate(start, eventStart) || + (dateIsBefore(start, eventStart) && dateIsAfter(start, eventEnd)) || + (dateIsAfter(start, eventStart) && dateIsBefore(end, eventStart)) + ); + }); + + return new ResolvedFuture(filteredEvents || []); +} diff --git a/src/features/events/hooks/useRelatedEvents.ts b/src/features/events/hooks/useRelatedEvents.ts new file mode 100644 index 0000000000..76f2213b58 --- /dev/null +++ b/src/features/events/hooks/useRelatedEvents.ts @@ -0,0 +1,52 @@ +import useAllEvents from './useAllEvents'; +import { ZetkinEvent } from 'utils/types/zetkin'; +import { + ErrorFuture, + IFuture, + LoadingFuture, + ResolvedFuture, +} from 'core/caching/futures'; + +export default function useRelatedEvents( + currentEvent: ZetkinEvent, + orgId: number +): IFuture { + const relatedEvents: ZetkinEvent[] = []; + const allEvents = useAllEvents(orgId); + + if (allEvents.isLoading) { + return new LoadingFuture(); + } else if (allEvents.error) { + return new ErrorFuture(allEvents.error); + } + + if (!allEvents.data) { + return new ResolvedFuture(relatedEvents); + } + for (const event of allEvents.data) { + if (event.id !== currentEvent.id) { + //check if it's same start date or same end date and same location and activity + if ( + currentEvent.start_time == event.end_time || + currentEvent.end_time == event.start_time + ) { + if ( + event.activity?.id == currentEvent.activity?.id && + event.location?.id == currentEvent.location?.id + ) { + relatedEvents.push(event); + } + } + + //check if event is exactly in parallel with same event type + if ( + currentEvent.start_time == event.start_time && + currentEvent.end_time == event.end_time && + event.activity?.id == currentEvent.activity?.id + ) { + relatedEvents.push(event); + } + } + } + return new ResolvedFuture(relatedEvents || []); +} From 580d38967ec50a18b26f9ed15b11f9b4eb133641 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Thu, 12 Oct 2023 16:53:31 +0200 Subject: [PATCH 05/48] refactor duplicateEvent in its own hook and implement changes --- .../events/components/EventActionButtons.tsx | 5 ++- .../components/EventPopper/SingleEvent.tsx | 5 ++- .../events/hooks/useDuplicateEvent.ts | 39 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/features/events/hooks/useDuplicateEvent.ts diff --git a/src/features/events/components/EventActionButtons.tsx b/src/features/events/components/EventActionButtons.tsx index b2a079697d..cf80f811a8 100644 --- a/src/features/events/components/EventActionButtons.tsx +++ b/src/features/events/components/EventActionButtons.tsx @@ -10,8 +10,8 @@ import React, { useContext } from 'react'; import EventDataModel from '../models/EventDataModel'; import messageIds from '../l10n/messageIds'; +import useDuplicateEvent from '../hooks/useDuplicateEvent'; import { useMessages } from 'core/i18n'; -import useModel from 'core/useModel'; import { ZetkinEvent } from 'utils/types/zetkin'; import { ZUIConfirmDialogContext } from 'zui/ZUIConfirmDialogProvider'; import ZUIDatePicker from 'zui/ZUIDatePicker'; @@ -30,6 +30,7 @@ const EventActionButtons: React.FunctionComponent = ({ const router = useRouter(); const model = useModel((env) => new EventDataModel(env, orgId, event.id)); + const { duplicateEvent } = useDuplicateEvent(orgId, event.id); const published = !!event.published && new Date(event.published) <= new Date(); @@ -51,7 +52,7 @@ const EventActionButtons: React.FunctionComponent = ({ }; const handleDuplicate = () => { - model.duplicateEvent(); + duplicateEvent(); }; const handleCancel = () => { diff --git a/src/features/events/components/EventPopper/SingleEvent.tsx b/src/features/events/components/EventPopper/SingleEvent.tsx index fb626f8401..57a965f6b7 100644 --- a/src/features/events/components/EventPopper/SingleEvent.tsx +++ b/src/features/events/components/EventPopper/SingleEvent.tsx @@ -23,6 +23,7 @@ import { removeOffset } from 'utils/dateUtils'; import StatusDot from './StatusDot'; import { useAppDispatch } from 'core/hooks'; import useModel from 'core/useModel'; +import useDuplicateEvent from 'features/events/hooks/useDuplicateEvent'; import { ZetkinEvent } from 'utils/types/zetkin'; import { ZUIConfirmDialogContext } from 'zui/ZUIConfirmDialogProvider'; import ZUIEllipsisMenu from 'zui/ZUIEllipsisMenu'; @@ -52,6 +53,7 @@ interface SingleEventProps { } const SingleEvent: FC = ({ event, onClickAway }) => { + const { orgId } = useNumericRouteParams(); const { showConfirmDialog } = useContext(ZUIConfirmDialogContext); const messages = useMessages(messageIds); const classes = useStyles(); @@ -59,6 +61,7 @@ const SingleEvent: FC = ({ event, onClickAway }) => { const model = useModel( (env) => new EventDataModel(env, event.organization.id, event.id) ); + const { duplicateEvent } = useDuplicateEvent(orgId, event.id); const dispatch = useAppDispatch(); const participants = model.getParticipants().data || []; const respondents = model.getRespondents().data || []; @@ -94,7 +97,7 @@ const SingleEvent: FC = ({ event, onClickAway }) => { { label: messages.eventPopper.duplicate(), onSelect: () => { - model.duplicateEvent(); + duplicateEvent(); onClickAway(); }, }, diff --git a/src/features/events/hooks/useDuplicateEvent.ts b/src/features/events/hooks/useDuplicateEvent.ts new file mode 100644 index 0000000000..da6c14c73e --- /dev/null +++ b/src/features/events/hooks/useDuplicateEvent.ts @@ -0,0 +1,39 @@ +import useEventData from './useEventData'; +import useEventMutations from './useEventMutations'; +import { ZetkinEventPostBody } from '../repo/EventsRepo'; + +type useDuplicateEventReturn = { + duplicateEvent: () => void; +}; + +export default function useDuplicateEvent( + orgId: number, + eventId: number +): useDuplicateEventReturn { + const { createEvent } = useEventMutations(orgId, eventId); + const event = useEventData(orgId, eventId); + + const duplicateEvent = () => { + createEvent(getDuplicatePostBody()); + }; + + const getDuplicatePostBody = (): ZetkinEventPostBody => { + const duplicateEventPostBody: ZetkinEventPostBody = { + activity_id: event.data?.activity?.id, + end_time: event.data?.end_time, + info_text: event.data?.info_text, + location_id: event.data?.location?.id, + num_participants_required: event.data?.num_participants_required, + organization_id: event.data?.organization.id, + start_time: event.data?.start_time, + title: event.data?.title, + }; + if (event.data?.campaign) { + duplicateEventPostBody.campaign_id = event.data?.campaign.id; + } + // TODO: should this include URL? + return duplicateEventPostBody; + }; + + return { duplicateEvent }; +} From 6ffc816b53d43162d6eefb82b5414767c90a474e Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:40:24 +0200 Subject: [PATCH 06/48] refactor getData for event to useEventData hook and implement changes --- .../events/components/EventContactCard.tsx | 10 +++--- .../components/EventParticipantsCard.tsx | 11 +++++-- .../events/components/EventWarningIcons.tsx | 15 ++++++--- .../components/ParticipantListSection.tsx | 12 +++++-- .../components/ParticipantSummaryCard.tsx | 11 +++++-- src/features/events/hooks/useEventData.ts | 29 ++++++++++++++++ src/features/events/layout/EventLayout.tsx | 12 ++++--- .../[campId]/events/[eventId]/index.tsx | 33 +++++++------------ .../events/[eventId]/participants.tsx | 21 ++++-------- 9 files changed, 94 insertions(+), 60 deletions(-) create mode 100644 src/features/events/hooks/useEventData.ts diff --git a/src/features/events/components/EventContactCard.tsx b/src/features/events/components/EventContactCard.tsx index 0f3150bf33..61be60d0c1 100644 --- a/src/features/events/components/EventContactCard.tsx +++ b/src/features/events/components/EventContactCard.tsx @@ -8,6 +8,7 @@ import { FC, useContext } from 'react'; import EventDataModel from 'features/events/models/EventDataModel'; import messageIds from 'features/events/l10n/messageIds'; +import useEventData from '../hooks/useEventData'; import { useMessages } from 'core/i18n'; import { ZetkinEvent } from 'utils/types/zetkin'; import ZUICard from 'zui/ZUICard'; @@ -89,19 +90,16 @@ const ContactSelect: FC = ({ model }) => { ); }; -const EventContactCard: FC = ({ - data, - model, - orgId, -}) => { +const EventContactCard: FC = ({ data, orgId }) => { const messages = useMessages(messageIds); + const eventFuture = useEventData(orgId, data.id); return ( - {model.getData().data?.contact?.id ? ( + {eventFuture.data?.contact?.id ? ( {messages.eventContactCard.header()} diff --git a/src/features/events/components/EventParticipantsCard.tsx b/src/features/events/components/EventParticipantsCard.tsx index 07f773fd44..d67017a60b 100644 --- a/src/features/events/components/EventParticipantsCard.tsx +++ b/src/features/events/components/EventParticipantsCard.tsx @@ -18,17 +18,22 @@ import getEventUrl from '../utils/getEventUrl'; import { getParticipantsStatusColor } from '../utils/eventUtils'; import messageIds from 'features/events/l10n/messageIds'; import theme from 'theme'; +import useEventData from '../hooks/useEventData'; import { useMessages } from 'core/i18n'; import ZUICard from 'zui/ZUICard'; import ZUINumberChip from 'zui/ZUINumberChip'; import ZUIPersonHoverCard from 'zui/ZUIPersonHoverCard'; type EventParticipantsCardProps = { - model: EventDataModel; + eventId: number; + orgId: number; }; -const EventParticipantsCard: FC = ({ model }) => { - const eventData = model.getData().data; +const EventParticipantsCard: FC = ({ + eventId, + orgId, +}) => { + const eventData = useEventData(orgId, eventId).data; const messages = useMessages(messageIds); const reqParticipants = eventData?.num_participants_required ?? 0; const availParticipants = eventData?.num_participants_available ?? 0; diff --git a/src/features/events/components/EventWarningIcons.tsx b/src/features/events/components/EventWarningIcons.tsx index a1b5fb9acc..6fff027875 100644 --- a/src/features/events/components/EventWarningIcons.tsx +++ b/src/features/events/components/EventWarningIcons.tsx @@ -8,17 +8,24 @@ import { import EventDataModel from '../models/EventDataModel'; import messageIds from 'features/campaigns/l10n/messageIds'; +import useEventData from '../hooks/useEventData'; import { useMessages } from 'core/i18n'; type EventWarningIconsProps = { compact?: boolean; model: EventDataModel; + eventId: number; + orgId: number; }; -const EventWarningIcons: FC = ({ compact, model }) => { - const data = model.getData().data; +const EventWarningIcons: FC = ({ + compact, + eventId, + orgId, +}) => { + const eventData = useEventData(orgId, eventId).data; - if (!data) { + if (!eventData) { return null; } @@ -26,7 +33,7 @@ const EventWarningIcons: FC = ({ compact, model }) => { return ( !!p.reminder_sent).length ?? 0 diff --git a/src/features/events/components/ParticipantListSection.tsx b/src/features/events/components/ParticipantListSection.tsx index bd23bf8cf9..b6abfcbc78 100644 --- a/src/features/events/components/ParticipantListSection.tsx +++ b/src/features/events/components/ParticipantListSection.tsx @@ -16,6 +16,7 @@ import EventDataModel from '../models/EventDataModel'; import filterParticipants from '../utils/filterParticipants'; import noPropagate from 'utils/noPropagate'; import { removeOffset } from 'utils/dateUtils'; +import useEventData from '../hooks/useEventData'; import { useMessages } from 'core/i18n'; import ZUINumberChip from '../../../zui/ZUINumberChip'; import ZUIPersonAvatar from 'zui/ZUIPersonAvatar'; @@ -96,6 +97,7 @@ interface ParticipantListSectionListProps { description: string; filterString: string; model: EventDataModel; + eventId: number; orgId: number; rows: ZetkinEventResponse[] | ZetkinEventParticipant[]; title: string; @@ -107,6 +109,7 @@ const ParticipantListSection: FC = ({ chipNumber, description, filterString, + eventId, orgId, model, rows, @@ -114,6 +117,7 @@ const ParticipantListSection: FC = ({ type, }) => { const messages = useMessages(messageIds); + const eventFuture = useEventData(orgId, eventId); const columns: GridColDef[] = [ { @@ -144,7 +148,7 @@ const ParticipantListSection: FC = ({ } else { return ( <> - {model.getData().data?.contact?.id === params.row.id ? ( + {eventFuture.data?.contact?.id === params.row.id ? ( {params.row.first_name + ' ' + params.row.last_name} = ({ /> ); } else if (type == 'booked') { - const event = model.getData().data; - if (event && new Date(removeOffset(event.start_time)) < new Date()) { + if ( + eventFuture.data && + new Date(removeOffset(eventFuture.data.start_time)) < new Date() + ) { const options: ButtonOption[] = [ { callback: () => { diff --git a/src/features/events/components/ParticipantSummaryCard.tsx b/src/features/events/components/ParticipantSummaryCard.tsx index 7f28c29216..919e736add 100644 --- a/src/features/events/components/ParticipantSummaryCard.tsx +++ b/src/features/events/components/ParticipantSummaryCard.tsx @@ -14,32 +14,37 @@ import { FC, useState } from 'react'; import EventDataModel from 'features/events/models/EventDataModel'; import messageIds from 'features/events/l10n/messageIds'; import { removeOffset } from 'utils/dateUtils'; +import useEventData from '../hooks/useEventData'; import ZUICard from 'zui/ZUICard'; import ZUINumberChip from 'zui/ZUINumberChip'; import { Msg, useMessages } from 'core/i18n'; type ParticipantSummaryCardProps = { model: EventDataModel; + eventId: number; onClickRecord: () => void; + orgId: number; }; const ParticipantSummaryCard: FC = ({ model, + eventId, onClickRecord, + orgId, }) => { - const eventData = model.getData().data; const respondents = model.getRespondents().data; + const eventData = useEventData(orgId, eventId).data; const messages = useMessages(messageIds); - const reqParticipants = eventData?.num_participants_required ?? 0; const availParticipants = model.getNumAvailParticipants(); const remindedParticipants = model.getNumRemindedParticipants(); const cancelledParticipants = model.getNumCancelledParticipants(); + const reqParticipants = eventData?.num_participants_required ?? 0; const signedParticipants = model.getNumSignedParticipants(); - const contactPerson = eventData?.contact; const confirmedParticipants = model.getNumConfirmedParticipants(); const noshowParticipants = model.getNumNoshowParticipants(); + const contactPerson = eventData?.contact; const hasRecordedAttendance = cancelledParticipants + confirmedParticipants + noshowParticipants > 0; diff --git a/src/features/events/hooks/useEventData.ts b/src/features/events/hooks/useEventData.ts new file mode 100644 index 0000000000..5b597cc1ed --- /dev/null +++ b/src/features/events/hooks/useEventData.ts @@ -0,0 +1,29 @@ +import shouldLoad from 'core/caching/shouldLoad'; +import { ZetkinEvent } from 'utils/types/zetkin'; +import { eventLoad, eventLoaded } from '../store'; +import { IFuture, PromiseFuture, RemoteItemFuture } from 'core/caching/futures'; +import { useApiClient, useAppDispatch, useAppSelector } from 'core/hooks'; + +export default function useEventData( + orgId: number, + id: number +): IFuture { + const apiClient = useApiClient(); + const dispatch = useAppDispatch(); + const eventsState = useAppSelector((state) => state.events); + + const item = eventsState.eventList.items.find((item) => item.id == id); + + if (!item || shouldLoad(item)) { + dispatch(eventLoad(id)); + const promise = apiClient + .get(`/api/orgs/${orgId}/actions/${id}`) + .then((event) => { + dispatch(eventLoaded(event)); + return event; + }); + return new PromiseFuture(promise); + } else { + return new RemoteItemFuture(item); + } +} diff --git a/src/features/events/layout/EventLayout.tsx b/src/features/events/layout/EventLayout.tsx index 8a56a86f76..c4bb66c3ca 100644 --- a/src/features/events/layout/EventLayout.tsx +++ b/src/features/events/layout/EventLayout.tsx @@ -12,6 +12,7 @@ import EventTypesModel from '../models/EventTypesModel'; import getEventUrl from '../utils/getEventUrl'; import messageIds from '../l10n/messageIds'; import { removeOffset } from 'utils/dateUtils'; +import useEventData from '../hooks/useEventData'; import useModel from 'core/useModel'; import ZUIEditTextinPlace from 'zui/ZUIEditTextInPlace'; import ZUIFuture from 'zui/ZUIFuture'; @@ -38,6 +39,7 @@ const EventLayout: React.FC = ({ const model = useModel( (env) => new EventDataModel(env, parseInt(orgId), parseInt(eventId)) + const eventFuture = useEventData(parseInt(orgId), parseInt(eventId)); ); const typesModel = useModel( @@ -47,13 +49,13 @@ const EventLayout: React.FC = ({ return ( + {(data) => { return ; }} } - baseHref={getEventUrl(model.getData().data)} + baseHref={getEventUrl(eventFuture.data)} defaultTab="/" subtitle={ @@ -62,7 +64,7 @@ const EventLayout: React.FC = ({ @@ -85,7 +87,7 @@ const EventLayout: React.FC = ({ }} - + {(data) => { const startDate = new Date(removeOffset(data.start_time)); const endDate = new Date(removeOffset(data.end_time)); @@ -125,7 +127,7 @@ const EventLayout: React.FC = ({ }, ]} title={ - + {(data) => { return ( = ({ orgId, eventId }) => { - const dataModel = useModel( - (env) => new EventDataModel(env, parseInt(orgId), parseInt(eventId)) - ); - const eventsModel = useModel((env) => new EventsModel(env, parseInt(orgId))); - const locationsModel = useModel( - (env) => new LocationsModel(env, parseInt(orgId)) - ); - const event = dataModel.getData().data; - if (!event) { + const eventFuture = useEventData(parseInt(orgId), parseInt(eventId)); + + if (!eventFuture.data) { return null; } return ( - + {(data) => { return ( - + - - + + ); diff --git a/src/pages/organize/[orgId]/projects/[campId]/events/[eventId]/participants.tsx b/src/pages/organize/[orgId]/projects/[campId]/events/[eventId]/participants.tsx index 45bef44684..ad0fd08768 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/events/[eventId]/participants.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/events/[eventId]/participants.tsx @@ -4,14 +4,13 @@ import { useRef, useState } from 'react'; import AddPersonButton from 'features/events/components/AddPersonButton'; import EventContactCard from 'features/events/components/EventContactCard'; -import EventDataModel from 'features/events/models/EventDataModel'; import EventLayout from 'features/events/layout/EventLayout'; import EventParticipantsFilter from 'features/events/components/EventParticipantsFilter'; import EventParticipantsList from 'features/events/components/EventParticipantsList'; import { PageWithLayout } from 'utils/types'; import ParticipantSummaryCard from 'features/events/components/ParticipantSummaryCard'; import { scaffold } from 'utils/next'; -import useModel from 'core/useModel'; +import useEventData from 'features/events/hooks/useEventData'; import ZUIFuture from 'zui/ZUIFuture'; export const getServerSideProps: GetServerSideProps = scaffold( @@ -45,32 +44,27 @@ const ParticipantsPage: PageWithLayout = ({ }) => { const [filterString, setFilterString] = useState(''); const listRef = useRef(); - const dataModel = useModel( - (env) => new EventDataModel(env, parseInt(orgId), parseInt(eventId)) - ); + const eventFuture = useEventData(parseInt(orgId), parseInt(eventId)); return ( - + {(data) => { return ( <> { if (listRef.current) { listRef.current.scrollIntoView({ behavior: 'smooth' }); } }} + orgId={parseInt(orgId)} /> - + = ({ } }} /> - + From bcdb53589cbaa843a08db0dd55bde012d0131602 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:44:10 +0200 Subject: [PATCH 07/48] refactor removeContact and setContact in its own hook --- .../events/components/EventContactCard.tsx | 32 ++++++++++----- .../components/ParticipantListSection.tsx | 6 +-- src/features/events/hooks/useEventContact.ts | 39 +++++++++++++++++++ 3 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 src/features/events/hooks/useEventContact.ts diff --git a/src/features/events/components/EventContactCard.tsx b/src/features/events/components/EventContactCard.tsx index 61be60d0c1..e660a8f7fb 100644 --- a/src/features/events/components/EventContactCard.tsx +++ b/src/features/events/components/EventContactCard.tsx @@ -6,8 +6,8 @@ import { } from '@mui/icons-material'; import { FC, useContext } from 'react'; -import EventDataModel from 'features/events/models/EventDataModel'; import messageIds from 'features/events/l10n/messageIds'; +import useEventContact from '../hooks/useEventContact'; import useEventData from '../hooks/useEventData'; import { useMessages } from 'core/i18n'; import { ZetkinEvent } from 'utils/types/zetkin'; @@ -19,23 +19,30 @@ import { MUIOnlyPersonSelect as ZUIPersonSelect } from 'zui/ZUIPersonSelect'; interface EventContactCardProps { data: ZetkinEvent; - model: EventDataModel; orgId: number; } interface ContactDetailsProps { contact: { id: number; name: string }; - model: EventDataModel; + + eventId: number; orgId: number; } interface ContactSelectProps { - model: EventDataModel; + orgId: number; + + eventId: number; } -const ContactDetails: FC = ({ contact, model, orgId }) => { +const ContactDetails: FC = ({ + contact, + eventId, + orgId, +}) => { const messages = useMessages(messageIds); const { showConfirmDialog } = useContext(ZUIConfirmDialogContext); + const { removeContact } = useEventContact(orgId, eventId); return ( <> @@ -52,7 +59,7 @@ const ContactDetails: FC = ({ contact, model, orgId }) => { onClick={() => { showConfirmDialog({ onSubmit: () => { - model.removeContact(); + removeContact(); }, warningText: messages.eventContactCard.warningText({ name: contact.name, @@ -70,10 +77,11 @@ const ContactDetails: FC = ({ contact, model, orgId }) => { ); }; -const ContactSelect: FC = ({ model }) => { +const ContactSelect: FC = ({ orgId, eventId }) => { const messages = useMessages(messageIds); + const { setContact } = useEventContact(orgId, eventId); const handleSelectedPerson = (personId: number) => { - model.setContact(personId); + setContact(personId); }; return ( @@ -116,9 +124,13 @@ const EventContactCard: FC = ({ data, orgId }) => { } > {data.contact ? ( - + ) : ( - + )} diff --git a/src/features/events/components/ParticipantListSection.tsx b/src/features/events/components/ParticipantListSection.tsx index b6abfcbc78..8c1840ca69 100644 --- a/src/features/events/components/ParticipantListSection.tsx +++ b/src/features/events/components/ParticipantListSection.tsx @@ -16,6 +16,7 @@ import EventDataModel from '../models/EventDataModel'; import filterParticipants from '../utils/filterParticipants'; import noPropagate from 'utils/noPropagate'; import { removeOffset } from 'utils/dateUtils'; +import useEventContact from '../hooks/useEventContact'; import useEventData from '../hooks/useEventData'; import { useMessages } from 'core/i18n'; import ZUINumberChip from '../../../zui/ZUINumberChip'; @@ -118,6 +119,7 @@ const ParticipantListSection: FC = ({ }) => { const messages = useMessages(messageIds); const eventFuture = useEventData(orgId, eventId); + const { setContact } = useEventContact(orgId, eventId); const columns: GridColDef[] = [ { @@ -167,9 +169,7 @@ const ParticipantListSection: FC = ({ title={messages.eventParticipantsList.participantTooltip()} > - model.setContact(params.row.id) - )} + onClick={noPropagate(() => setContact(params.row.id))} sx={{ display: 'none', marginLeft: '8px', diff --git a/src/features/events/hooks/useEventContact.ts b/src/features/events/hooks/useEventContact.ts new file mode 100644 index 0000000000..81ffd35077 --- /dev/null +++ b/src/features/events/hooks/useEventContact.ts @@ -0,0 +1,39 @@ +import useEventMutations from '../hooks/useEventMutations'; +import useEventParticipants from './useEventParticipants'; + +type useEventContacReturn = { + removeContact: () => void; + setContact: (contactId: number) => void; +}; + +export default function useEventContact( + orgId: number, + eventId: number +): useEventContacReturn { + const { updateEvent } = useEventMutations(orgId, eventId); + const { addParticipant, getEventParticipants } = useEventParticipants( + orgId, + eventId + ); + + const removeContact = () => { + updateEvent({ + contact_id: null, + }); + }; + + const setContact = async (contactId: number) => { + const eventParticipantsList = getEventParticipants().data; + if (!eventParticipantsList?.find((item) => item.id == contactId)) { + await addParticipant(orgId, eventId, contactId); + } + updateEvent({ + contact_id: contactId, + }); + }; + + return { + removeContact, + setContact, + }; +} From 01e6554872845b84a3fbd3de7f632bcb7bfb9054 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:51:54 +0200 Subject: [PATCH 08/48] refactor event mutations in its own hook and implement changes --- .../events/components/EventActionButtons.tsx | 16 +-- .../components/EventOverviewCard/index.tsx | 6 +- .../components/EventPopper/SingleEvent.tsx | 10 +- .../events/hooks/useEventMutations.ts | 106 ++++++++++++++++++ src/features/events/layout/EventLayout.tsx | 10 +- 5 files changed, 130 insertions(+), 18 deletions(-) create mode 100644 src/features/events/hooks/useEventMutations.ts diff --git a/src/features/events/components/EventActionButtons.tsx b/src/features/events/components/EventActionButtons.tsx index cf80f811a8..bc11bc8b98 100644 --- a/src/features/events/components/EventActionButtons.tsx +++ b/src/features/events/components/EventActionButtons.tsx @@ -8,9 +8,9 @@ import { } from '@mui/icons-material'; import React, { useContext } from 'react'; -import EventDataModel from '../models/EventDataModel'; import messageIds from '../l10n/messageIds'; import useDuplicateEvent from '../hooks/useDuplicateEvent'; +import useEventMutations from '../hooks/useEventMutations'; import { useMessages } from 'core/i18n'; import { ZetkinEvent } from 'utils/types/zetkin'; import { ZUIConfirmDialogContext } from 'zui/ZUIConfirmDialogProvider'; @@ -28,26 +28,26 @@ const EventActionButtons: React.FunctionComponent = ({ const orgId = event.organization.id; const { showConfirmDialog } = useContext(ZUIConfirmDialogContext); const router = useRouter(); - - const model = useModel((env) => new EventDataModel(env, orgId, event.id)); + const { cancelEvent, deleteEvent, restoreEvent, setPublished } = + useEventMutations(orgId, event.id); const { duplicateEvent } = useDuplicateEvent(orgId, event.id); const published = !!event.published && new Date(event.published) <= new Date(); const handlePublish = () => { - model.setPublished(new Date().toISOString()); + setPublished(new Date().toISOString()); }; const handleUnpublish = () => { - model.setPublished(null); + setPublished(null); }; const handleChangeDate = (date: string | null) => { - model.setPublished(date); + setPublished(date); }; const handleDelete = () => { - model.deleteEvent(); + deleteEvent(); router.push(`/organize/${orgId}/projects/${event.campaign?.id || ''} `); }; @@ -56,7 +56,7 @@ const EventActionButtons: React.FunctionComponent = ({ }; const handleCancel = () => { - event.cancelled ? model.restoreEvent() : model.cancel(); + event.cancelled ? restoreEvent() : cancelEvent(); }; return ( diff --git a/src/features/events/components/EventOverviewCard/index.tsx b/src/features/events/components/EventOverviewCard/index.tsx index bae2d98042..c58d1830b2 100644 --- a/src/features/events/components/EventOverviewCard/index.tsx +++ b/src/features/events/components/EventOverviewCard/index.tsx @@ -20,7 +20,6 @@ import { import dayjs, { Dayjs } from 'dayjs'; import { FC, useMemo, useState } from 'react'; -import EventDataModel from 'features/events/models/EventDataModel'; import { getWorkingUrl } from 'features/events/utils/getWorkingUrl'; import LocationModal from '../LocationModal'; import messageIds from 'features/events/l10n/messageIds'; @@ -28,6 +27,7 @@ import theme from 'theme'; import useEditPreviewBlock from 'zui/hooks/useEditPreviewBlock'; import useEventLocation from 'features/events/hooks/useEventLocation'; import useEventLocationMutations from 'features/events/hooks/useEventLocationMutations'; +import useEventMutations from 'features/events/hooks/useEventMutations'; import { useMessages } from 'core/i18n'; import useParallelEvents from 'features/events/hooks/useParallelEvents'; import ZUIDate from 'zui/ZUIDate'; @@ -42,11 +42,9 @@ import { ZetkinEvent, ZetkinLocation } from 'utils/types/zetkin'; type EventOverviewCardProps = { data: ZetkinEvent; - dataModel: EventDataModel; orgId: number; }; - dataModel, const EventOverviewCard: FC = ({ data, orgId }) => { const { updateEvent } = useEventMutations(orgId, data.id); const locations = useEventLocation(orgId); @@ -78,7 +76,7 @@ const EventOverviewCard: FC = ({ data, orgId }) => { setShowEndDate(false); }, save: () => { - dataModel.updateEventData({ + updateEvent({ end_time: dayjs(endDate) .hour(endDate.hour()) .minute(endDate.minute()) diff --git a/src/features/events/components/EventPopper/SingleEvent.tsx b/src/features/events/components/EventPopper/SingleEvent.tsx index 57a965f6b7..89a8573aae 100644 --- a/src/features/events/components/EventPopper/SingleEvent.tsx +++ b/src/features/events/components/EventPopper/SingleEvent.tsx @@ -24,6 +24,7 @@ import StatusDot from './StatusDot'; import { useAppDispatch } from 'core/hooks'; import useModel from 'core/useModel'; import useDuplicateEvent from 'features/events/hooks/useDuplicateEvent'; +import useEventMutations from 'features/events/hooks/useEventMutations'; import { ZetkinEvent } from 'utils/types/zetkin'; import { ZUIConfirmDialogContext } from 'zui/ZUIConfirmDialogProvider'; import ZUIEllipsisMenu from 'zui/ZUIEllipsisMenu'; @@ -60,6 +61,9 @@ const SingleEvent: FC = ({ event, onClickAway }) => { const model = useModel( (env) => new EventDataModel(env, event.organization.id, event.id) + const { cancelEvent, deleteEvent, publishEvent } = useEventMutations( + orgId, + event.id ); const { duplicateEvent } = useDuplicateEvent(orgId, event.id); const dispatch = useAppDispatch(); @@ -86,7 +90,7 @@ const SingleEvent: FC = ({ event, onClickAway }) => { onSelect: () => showConfirmDialog({ onSubmit: () => { - model.deleteEvent(); + deleteEvent(); dispatch(eventsDeselected([event])); onClickAway(); }, @@ -108,7 +112,7 @@ const SingleEvent: FC = ({ event, onClickAway }) => { onSelect: () => showConfirmDialog({ onSubmit: () => { - model.cancel(); + cancelEvent(); onClickAway(); }, title: messages.eventPopper.confirmCancel(), @@ -273,7 +277,7 @@ const SingleEvent: FC = ({ event, onClickAway }) => { {showPublishButton && (