diff --git a/app/models.ts b/app/models.ts index a2415f825c..8bc8af9a6e 100644 --- a/app/models.ts +++ b/app/models.ts @@ -9,7 +9,7 @@ import type { EntityId } from '@reduxjs/toolkit'; import type Comment from 'app/store/models/Comment'; import type { ListCompany } from 'app/store/models/Company'; import type { ReactionsGrouped } from 'app/store/models/Reaction'; -import type { DetailedUser, PhotoConsent } from 'app/store/models/User'; +import type { PhotoConsent } from 'app/store/models/User'; import type { RoleType } from 'app/utils/constants'; import type { Moment } from 'moment'; @@ -27,10 +27,6 @@ export enum EventTime { activate = 'activationTime', start = 'startTime', } -type SelectInput = { - label: string; - value: string; -}; export type EventStatusType = 'NORMAL' | 'OPEN' | 'TBA' | 'INFINITE'; export type Grade = { name: string; @@ -236,23 +232,6 @@ export type Event = EventBase & { isForeignLanguage: boolean; }; -type EventTransformPool = EventPoolBase & { - permissionGroups: Array; -}; - -export type TransformEvent = EventBase & { - pools: Array; - company: SelectInput; - responsibleGroup: SelectInput; - eventStatusType: SelectInput; - eventType: SelectInput; - mazemapPoi: Record; - useMazemap: boolean; - hasFeedbackQuestion: boolean; - responsibleUsers: DetailedUser[]; - isForeignLanguage: boolean; -}; - export type Workplace = { town: string; }; diff --git a/app/routes/events/components/EventEditor/EditorSection/Descriptions.tsx b/app/routes/events/components/EventEditor/EditorSection/Descriptions.tsx index 13bfcec232..b52e2d700d 100644 --- a/app/routes/events/components/EventEditor/EditorSection/Descriptions.tsx +++ b/app/routes/events/components/EventEditor/EditorSection/Descriptions.tsx @@ -1,17 +1,13 @@ -import { Flex } from '@webkom/lego-bricks'; import { Field } from 'react-final-form'; import { EditorField, TextEditor } from 'app/components/Form'; -import Tag from 'app/components/Tags/Tag'; import styles from '../EventEditor.css'; import type { UploadArgs } from 'app/actions/FileActions'; -import type { EditingEvent } from 'app/routes/events/utils'; type Props = { uploadFile: (uploadArgs: UploadArgs) => void; - values: EditingEvent; }; -const Descriptions: React.FC = ({ uploadFile, values }) => { +const Descriptions = ({ uploadFile }: Props) => { return ( <> = ({ uploadFile, values }) => { uploadFile={uploadFile} required /> - - {(values.tags || []).map((tag, i) => ( - - ))} - ); }; diff --git a/app/routes/events/components/EventEditor/EditorSection/Details.tsx b/app/routes/events/components/EventEditor/EditorSection/Details.tsx index f73bbb1f71..7dcc12675a 100644 --- a/app/routes/events/components/EventEditor/EditorSection/Details.tsx +++ b/app/routes/events/components/EventEditor/EditorSection/Details.tsx @@ -12,13 +12,13 @@ import MazemapLink from 'app/components/MazemapEmbed/MazemapLink'; import Tooltip from 'app/components/Tooltip'; import { EventTypeConfig } from 'app/routes/events/utils'; import styles from '../EventEditor.css'; -import type { EditingEvent } from 'app/routes/events/utils'; +import type { EventEditorFormValues } from 'app/routes/events/components/EventEditor'; type Props = { - values: EditingEvent; + values: EventEditorFormValues; }; -const Details: React.FC = ({ values }) => { +const Details = ({ values }: Props) => { return ( <> diff --git a/app/routes/events/components/EventEditor/EditorSection/Header.tsx b/app/routes/events/components/EventEditor/EditorSection/Header.tsx index a3a3e76b93..ef99b2db04 100644 --- a/app/routes/events/components/EventEditor/EditorSection/Header.tsx +++ b/app/routes/events/components/EventEditor/EditorSection/Header.tsx @@ -7,6 +7,7 @@ import { Image, } from '@webkom/lego-bricks'; import { FolderOpen, Trash2 } from 'lucide-react'; +import { useState } from 'react'; import { Field } from 'react-final-form'; import { setSaveForUse } from 'app/actions/FileActions'; import EmptyState from 'app/components/EmptyState'; @@ -16,32 +17,29 @@ import { Button, ImageUploadField, } from 'app/components/Form'; +import { selectAllImageGalleryEntries } from 'app/reducers/imageGallery'; import { colorForEventType } from 'app/routes/events/utils'; +import { useAppSelector } from 'app/store/hooks'; +import { spyForm } from 'app/utils/formSpyUtils'; import styles from '../EventEditor.css'; -import type { EditingEvent } from 'app/routes/events/utils'; -import type { FormApi } from 'final-form'; +import type { EventEditorFormValues } from 'app/routes/events/components/EventEditor'; type Props = { - form: FormApi>; - values: EditingEvent; - useImageGallery: boolean; - imageGalleryUrl: string; - event: any; - imageGallery: any; - setUseImageGallery: React.Dispatch>; - setImageGalleryUrl: React.Dispatch>; + values: EventEditorFormValues; }; -const Header = ({ - form, - values, - useImageGallery, - imageGalleryUrl, - event, - imageGallery, - setUseImageGallery, - setImageGalleryUrl, -}: Props) => { +const Header = ({ values }: Props) => { + const [useImageGallery, setUseImageGallery] = useState(false); + const [imageGalleryUrl, setImageGalleryUrl] = useState(''); + + const imageGalleryEntries = useAppSelector(selectAllImageGalleryEntries); + const imageGallery = imageGalleryEntries?.map((image) => ({ + key: image.key, + cover: image.cover, + token: image.token, + coverPlaceholder: image.coverPlaceholder, + })); + return ( <> @@ -92,49 +90,53 @@ const Header = ({ justifyContent="space-around" gap="var(--spacing-md)" > - {imageGallery?.map((e) => ( - - {`Forsidebildet { - form.change('cover', `${e.key}:${e.token}`); - close(); - setUseImageGallery(true); - setImageGalleryUrl(e.cover); - }} - className={styles.imageGalleryEntry} - /> - { - setSaveForUse(e.key, e.token, false); - }} - > - {({ openConfirmModal }) => ( - } - danger + {spyForm((form) => ( + <> + {imageGallery?.map((e) => ( + + {`Forsidebildet { + form.change('cover', `${e.key}:${e.token}`); + close(); + setUseImageGallery(true); + setImageGalleryUrl(e.cover); + }} + className={styles.imageGalleryEntry} /> - )} - - + { + setSaveForUse(e.key, e.token, false); + }} + > + {({ openConfirmModal }) => ( + } + danger + /> + )} + + + ))} + {imageGallery.length === 0 && ( + } + header="Bildegalleriet er tomt ..." + body="Hvorfor ikke laste opp et bilde?" + /> + )} + ))} - {imageGallery.length === 0 && ( - } - header="Bildegalleriet er tomt ..." - body="Hvorfor ikke laste opp et bilde?" - /> - )} )} diff --git a/app/routes/events/components/EventEditor/EditorSection/Registration.tsx b/app/routes/events/components/EventEditor/EditorSection/Registration.tsx index 5d29d692e0..81e1907c0c 100644 --- a/app/routes/events/components/EventEditor/EditorSection/Registration.tsx +++ b/app/routes/events/components/EventEditor/EditorSection/Registration.tsx @@ -18,14 +18,14 @@ import { } from 'app/routes/events/utils'; import { spyValues } from 'app/utils/formSpyUtils'; import styles from '../EventEditor.css'; -import renderPools from '../renderPools'; -import type { EditingEvent } from 'app/routes/events/utils'; +import PoolsField from '../PoolsField'; +import type { EventEditorFormValues } from 'app/routes/events/components/EventEditor'; type Props = { - values: EditingEvent; + values: EventEditorFormValues; }; -const Registrations: React.FC = ({ values }) => { +const Registrations = ({ values }: Props) => { const initialPool = { name: 'Pool #1', registrations: [], @@ -35,13 +35,17 @@ const Registrations: React.FC = ({ values }) => { .minute(0) .toISOString(), permissionGroups: [], - }; + } satisfies EventEditorFormValues['pools'][number]; return ( <> - {spyValues((values: EditingEvent) => { + {spyValues>((values) => { // Adding an initial pool if the event status type allows for it and there are no current pools - if (['NORMAL', 'INFINITE'].includes(values.eventStatusType?.value)) { + values.pools ??= []; + if ( + values.eventStatusType && + ['NORMAL', 'INFINITE'].includes(values.eventStatusType?.value) + ) { if (values.pools.length === 0) { values.pools = [initialPool]; } @@ -227,9 +231,13 @@ const NormalOrInfiniteStatusType: React.FC = ({
( + + )} />
{values.pools?.length > 1 && ( diff --git a/app/routes/events/components/EventEditor/EditorSection/index.tsx b/app/routes/events/components/EventEditor/EditorSection/index.tsx index fcfa48a789..cb7a8ea9a6 100644 --- a/app/routes/events/components/EventEditor/EditorSection/index.tsx +++ b/app/routes/events/components/EventEditor/EditorSection/index.tsx @@ -2,20 +2,21 @@ import { Flex, Icon } from '@webkom/lego-bricks'; import cx from 'classnames'; import { useState } from 'react'; import styles from '../EventEditor.css'; -import type { PropsWithChildren } from 'react'; +import type { ReactNode } from 'react'; type Props = { title: string; collapsible?: boolean; initiallyExpanded?: boolean; + children: ReactNode; }; -const EditorSection: React.FC> = ({ +const EditorSection = ({ children, title, collapsible = true, initiallyExpanded = false, -}) => { +}: Props) => { const [expanded, setExpanded] = useState(!collapsible || initiallyExpanded); return ( @@ -37,9 +38,7 @@ const EditorSection: React.FC> = ({ )}

{title}

- {expanded && ( -
{children}
- )} + {expanded &&
{children}
} ); }; diff --git a/app/routes/events/components/EventEditor/renderPools.tsx b/app/routes/events/components/EventEditor/PoolsField.tsx similarity index 79% rename from app/routes/events/components/EventEditor/renderPools.tsx rename to app/routes/events/components/EventEditor/PoolsField.tsx index 9ab493304d..edcc0fe841 100644 --- a/app/routes/events/components/EventEditor/renderPools.tsx +++ b/app/routes/events/components/EventEditor/PoolsField.tsx @@ -8,26 +8,31 @@ import { } from 'app/components/Form'; import styles from './EventEditor.css'; import PoolSuggestion from './PoolSuggestions'; -import type { Dateish, EventStatusType } from 'app/models'; +import type { Dateish } from 'app/models'; +import type { EventEditorFormValues } from 'app/routes/events/components/EventEditor/index'; +import type { EventStatusType } from 'app/store/models/Event'; +import type { FieldArrayRenderProps } from 'react-final-form-arrays'; -type poolProps = { - fields: Record; +type Props = FieldArrayRenderProps< + EventEditorFormValues['pools'][number], + HTMLElement +> & { startTime: Dateish; eventStatusType: EventStatusType; }; -const renderPools = ({ fields, startTime, eventStatusType }: poolProps) => ( +const PoolsField = ({ fields, startTime, eventStatusType }: Props) => (
    - {fields.map((pool, index) => ( -
  • + {fields.map((fieldName, index) => ( +
  • Pool #{index + 1}

    { if (!value || value === '') { return 'Navn er påkrevd'; @@ -41,7 +46,7 @@ const renderPools = ({ fields, startTime, eventStatusType }: poolProps) => ( {['NORMAL'].includes(eventStatusType) && ( { if (!value || isNaN(parseInt(value, 10))) { return 'Kapasitet er påkrevd og må være et tall'; @@ -63,14 +68,14 @@ const renderPools = ({ fields, startTime, eventStatusType }: poolProps) => ( )} { if (!value || value.length === 0) { return 'Rettighetsgruppe er påkrevd'; @@ -104,7 +109,7 @@ const renderPools = ({ fields, startTime, eventStatusType }: poolProps) => (
); -export default renderPools; +export default PoolsField; diff --git a/app/routes/events/components/EventEditor/index.tsx b/app/routes/events/components/EventEditor/index.tsx index 0d518019c8..58d2a8d7e2 100644 --- a/app/routes/events/components/EventEditor/index.tsx +++ b/app/routes/events/components/EventEditor/index.tsx @@ -7,29 +7,29 @@ import { import { usePreparedEffect } from '@webkom/react-prepare'; import arrayMutators from 'final-form-arrays'; import moment from 'moment-timezone'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { Field } from 'react-final-form'; import { Helmet } from 'react-helmet-async'; -import { useNavigate, useParams, Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { createEvent, editEvent, fetchEvent } from 'app/actions/EventActions'; import { - uploadFile as _uploadFile, fetchImageGallery, setSaveForUse, + uploadFile as _uploadFile, } from 'app/actions/FileActions'; -import { Form, CheckBox, LegoFinalForm } from 'app/components/Form'; +import { CheckBox, Form, LegoFinalForm } from 'app/components/Form'; import { SubmitButton } from 'app/components/Form/SubmitButton'; import { - selectPoolsWithRegistrationsForEvent, selectEventByIdOrSlug, + selectPoolsWithRegistrationsForEvent, } from 'app/reducers/events'; -import { selectAllImageGalleryEntries } from 'app/reducers/imageGallery'; +import { transformEvent } from 'app/routes/events/components/EventEditor/utils'; import { - transformEvent, - transformEventStatusType, displayNameForEventType, + transformEventStatusType, } from 'app/routes/events/utils'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import { EventStatusType } from 'app/store/models/Event'; import { guardLogin } from 'app/utils/replaceUnlessLoggedIn'; import time from 'app/utils/time'; import { @@ -48,19 +48,93 @@ import { } from 'app/utils/validation'; import Admin from '../Admin'; import EditorSection, { - Header, + Descriptions, Details, + Header, Registration, - Descriptions, } from './EditorSection'; import styles from './EventEditor.css'; +import type { EntityId } from '@reduxjs/toolkit'; import type { UploadArgs } from 'app/actions/FileActions'; import type { ActionGrant } from 'app/models'; -import type { EditingEvent } from 'app/routes/events/utils'; -import type { AdministrateEvent } from 'app/store/models/Event'; +import type { PoolRegistrationWithUser } from 'app/reducers/events'; +import type { AdministrateEvent, EventType } from 'app/store/models/Event'; import type { DetailedUser } from 'app/store/models/User'; -const TypedLegoForm = LegoFinalForm; +export type EventEditorFormValues = { + id?: EntityId; + slug?: string; + title: string; + startTime: string; + endTime: string; + description: string; + text: string; + eventType: { + label: string; + value: EventType; + }; + eventStatusType: { + label: string; + value: EventStatusType; + }; + company?: { + label: string; + value: EntityId; + }; + responsibleGroup?: { + label: string; + value: EntityId; + }; + location: string; + isPriced: boolean; + useStripe: boolean; + priceMember: number; + paymentDueDate?: string; + mergeTime: string; + useCaptcha: boolean; + youtubeUrl: string; + heedPenalties: boolean; + isGroupOnly: boolean; + canViewGroups: { + label: string; + value: EntityId; + id: EntityId; + }[]; + useConsent: boolean; + feedbackDescription: string; + pools: { + id?: EntityId; + name: string; + registrations: PoolRegistrationWithUser[]; + capacity?: number; + permissionGroups: { + label: string; + value: EntityId; + }[]; + activationDate: string; + }[]; + useMazemap: boolean; + mazemapPoi?: { + label: string; + value: number; + }; + separateDeadlines: boolean; + registrationDeadlineHours: number; + unregistrationDeadline?: string; + unregistrationDeadlineHours: number; + responsibleUsers: { + label: string; + value: EntityId; + }[]; + isForeignLanguage: boolean; + cover: string; + saveToImageGallery?: boolean; + hasFeedbackQuestion: boolean; + feedbackRequired: boolean; + isClarified: boolean; +}; + +const TypedLegoForm = LegoFinalForm; const validate = createValidator({ youtubeUrl: [validYoutubeUrl()], @@ -94,12 +168,7 @@ const validate = createValidator({ ), ], isClarified: [ - requiredIf( - (allValues) => - // Only require if we are creating a new event - allValues.id === undefined, - 'Arrangementet må være avklart i arrangementskalenderen', - ), + required('Arrangementet må være avklart i arrangementskalenderen'), ], feedbackDescription: [ requiredIf( @@ -144,13 +213,6 @@ const EventEditor = () => { const pools = useAppSelector((state) => selectPoolsWithRegistrationsForEvent(state, eventId), ); - const imageGalleryEntries = useAppSelector(selectAllImageGalleryEntries); - const imageGallery = imageGalleryEntries?.map((image) => ({ - key: image.key, - cover: image.cover, - token: image.token, - coverPlaceholder: image.coverPlaceholder, - })); const dispatch = useAppDispatch(); @@ -180,9 +242,6 @@ const EventEditor = () => { } }, [event?.slug, navigate, eventIdOrSlug, isEditPage]); - const [useImageGallery, setUseImageGallery] = useState(false); - const [imageGalleryUrl, setImageGalleryUrl] = useState(''); - if (isEditPage && (!event || !event.title)) { return ; } @@ -191,7 +250,7 @@ const EventEditor = () => { return null; } - const onSubmit = (values: EditingEvent) => { + const onSubmit = (values: EventEditorFormValues) => { (isEditPage ? dispatch(editEvent(transformEvent(values))) : dispatch(createEvent(transformEvent(values))) @@ -209,14 +268,18 @@ const EventEditor = () => { }); }; - const initialValues = event + const initialValues: Partial = event ? { ...event, + startTime: moment(event.startTime).toISOString(), + endTime: moment(event.endTime).toISOString(), mergeTime: event.mergeTime - ? event.mergeTime + ? moment(event.mergeTime).toISOString() : time({ hours: 12, }), + paymentDueDate: + event.paymentDueDate && moment(event.paymentDueDate).toISOString(), priceMember: event.priceMember / 100, pools: pools.map((pool) => ({ ...pool, @@ -224,6 +287,7 @@ const EventEditor = () => { label: group.name, value: group.id, })), + activationDate: moment(pool.activationDate).toISOString(), })), canViewGroups: (event.canViewGroups || []).map((group) => ({ label: group.name, @@ -256,14 +320,20 @@ const EventEditor = () => { location: event.location, useMazemap: (event.mazemapPoi && event.mazemapPoi > 0) || !event.location, - mazemapPoi: event.mazemapPoi && { - label: event.location, - //if mazemapPoi has a value, location will be its display name - value: event.mazemapPoi, - }, + mazemapPoi: event.mazemapPoi + ? { + label: event.location, + //if mazemapPoi has a value, location will be its display name + value: event.mazemapPoi, + } + : undefined, separateDeadlines: event.registrationDeadlineHours !== event.unregistrationDeadlineHours, + unregistrationDeadline: + event.unregistrationDeadline && + moment(event.unregistrationDeadline).toISOString(), hasFeedbackQuestion: !!event.feedbackDescription, + isClarified: true, } : { title: '', @@ -277,13 +347,9 @@ const EventEditor = () => { }), description: '', text: '', - eventType: '', - eventStatusType: { - value: 'TBA', - label: 'Ikke bestemt (TBA)', - }, - company: null, - responsibleGroup: null, + eventStatusType: transformEventStatusType(EventStatusType.TBA), + company: undefined, + responsibleGroup: undefined, location: 'TBA', isPriced: false, useStripe: true, @@ -302,6 +368,7 @@ const EventEditor = () => { canViewGroups: [], useConsent: false, feedbackDescription: '', + feedbackRequired: false, pools: [], useMazemap: false, separateDeadlines: false, @@ -312,6 +379,7 @@ const EventEditor = () => { unregistrationDeadlineHours: 2, responsibleUsers: [], isForeignLanguage: false, + isClarified: false, }; const title = isEditPage ? `Redigerer: ${event.title}` : 'Nytt arrangement'; @@ -334,22 +402,13 @@ const EventEditor = () => { ...arrayMutators, }} > - {({ form, handleSubmit, values }) => ( + {({ handleSubmit, values }) => (
-
+
@@ -361,7 +420,7 @@ const EventEditor = () => { - + {!isEditPage && ( @@ -415,7 +474,12 @@ const EventEditor = () => { - {isEditPage && } + {isEditPage && ( + + )} )} diff --git a/app/routes/events/components/EventEditor/utils.ts b/app/routes/events/components/EventEditor/utils.ts new file mode 100644 index 0000000000..1533c51651 --- /dev/null +++ b/app/routes/events/components/EventEditor/utils.ts @@ -0,0 +1,152 @@ +// Takes the full data-object and input and transforms the event to the API format. +import { pick } from 'lodash'; +import moment from 'moment-timezone'; +import config from 'app/config'; +import type { EventEditorFormValues } from 'app/routes/events/components/EventEditor/index'; + +// Event fields that should be created or updated based on the API. +const eventCreateAndUpdateFields = [ + 'id', + 'title', + 'cover', + 'description', + 'text', + 'company', + 'feedbackDescription', + 'feedbackRequired', + 'eventType', + 'eventStatusType', + 'location', + 'isPriced', + 'priceMember', + 'priceGuest', + 'useStripe', + 'paymentDueDate', + 'useCaptcha', + 'tags', + 'pools', + 'registrationDeadlineHours', + 'pinned', + 'heedPenalties', + 'useConsent', + 'separateDeadlines', + 'responsibleUsers', +]; + +// Pool fields that should be created or updated based on the API +const poolCreateAndUpdateFields = [ + 'id', + 'name', + 'capacity', + 'activationDate', + 'permissionGroups', +]; + +export const transformEvent = (data: EventEditorFormValues) => ({ + ...pick(data, eventCreateAndUpdateFields), + startTime: moment(data.startTime).toISOString(), + endTime: moment(data.endTime).toISOString(), + mergeTime: calculateMergeTime(data), + company: data.company && data.company.value, + eventStatusType: data.eventStatusType && data.eventStatusType.value, + eventType: data.eventType && data.eventType.value, + responsibleGroup: data.responsibleGroup && data.responsibleGroup.value, + responsibleUsers: data.responsibleUsers + ? data.responsibleUsers.map((user) => user.value) + : [], + priceMember: calculatePrice(data), + location: calculateLocation(data), + paymentDueDate: calculatePaymentDueDate(data), + canViewGroups: data.isGroupOnly + ? data.canViewGroups.map((group) => group.id) + : [], + requireAuth: data.canViewGroups.length > 0, + unregistrationDeadline: calculateUnregistrationDeadline(data), + unregistrationDeadlineHours: calculateUnregistrationDeadlineHours(data), + pools: calculatePools(data), + useCaptcha: config.environment === 'ci' ? false : data.useCaptcha, + youtubeUrl: data.youtubeUrl, + mazemapPoi: calculateMazemapPoi(data), + feedbackDescription: + (data.hasFeedbackQuestion && data.feedbackDescription) || '', + feedbackRequired: data.hasFeedbackQuestion && data.feedbackRequired, + isForeignLanguage: data.isForeignLanguage, +}); + +/* Calculate the event price + * @param isPriced: If the event is priced + */ +const calculatePrice = (data: EventEditorFormValues) => + data.isPriced ? data.priceMember * 100 : 0; + +/* Calculate the event location + * @param eventStatusType: what kind of registrationmode this event has + */ +const calculateLocation = (data: EventEditorFormValues) => + data.useMazemap ? data.mazemapPoi?.label : data.location; + +const calculateMazemapPoi = (data) => { + if (!data.useMazemap || data.mazemapPoi.value === '') { + return null; + } + + return data.mazemapPoi.value; +}; + +/* Calculate the event pools + * @param eventStatusType: what kind of registrationmode this event has + * @param pools: the event groups as specified by the CreateEvent forms + */ +const calculatePools = (data: EventEditorFormValues) => { + switch (data.eventStatusType?.value) { + case 'TBA': + case 'OPEN': + return []; + + case 'INFINITE': + return [ + { + ...pick(data.pools[0], poolCreateAndUpdateFields), + activationDate: moment(data.pools[0].activationDate).toISOString(), + permissionGroups: data.pools[0].permissionGroups.map( + (group) => group.value, + ), + }, + ]; + + case 'NORMAL': + return data.pools.map((pool) => ({ + ...pick(pool, poolCreateAndUpdateFields), + activationDate: moment(pool.activationDate).toISOString(), + permissionGroups: pool.permissionGroups.map((group) => group.value), + })); + + default: + break; + } +}; + +/* Calculte and convert to payment due date + * @param paymentDueDate: date from form + */ +const calculatePaymentDueDate = (data: EventEditorFormValues) => + data.isPriced ? moment(data.paymentDueDate).toISOString() : null; + +/* Calcualte and convert the registation deadline + * @param unregistationDeadline: data from form + */ +const calculateUnregistrationDeadline = (data: EventEditorFormValues) => + data.unregistrationDeadline + ? moment(data.unregistrationDeadline).toISOString() + : null; + +const calculateUnregistrationDeadlineHours = (data: EventEditorFormValues) => + data.separateDeadlines + ? data.unregistrationDeadlineHours + : data.registrationDeadlineHours; + +/* Calculate the merge time for the pools. Only set if there are more then one pool + * @param mergeTime: date from form + */ +const calculateMergeTime = (data: EventEditorFormValues) => + data.pools.length > 1 ? moment(data.mergeTime).toISOString() : null; diff --git a/app/routes/events/utils.ts b/app/routes/events/utils.ts index b3b36cf77c..9552e9e71b 100644 --- a/app/routes/events/utils.ts +++ b/app/routes/events/utils.ts @@ -1,14 +1,7 @@ -import { pick, sumBy, find } from 'lodash'; +import { find, sumBy } from 'lodash'; import moment from 'moment-timezone'; -import config from 'app/config'; -import { EventType } from 'app/store/models/Event'; -import type { - Event, - TransformEvent, - EventSemester, - Dateish, - EventStatusType, -} from 'app/models'; +import { EventStatusType, EventType } from 'app/store/models/Event'; +import type { Dateish, Event, EventSemester } from 'app/models'; import type { CompleteEvent } from 'app/store/models/Event'; import type Penalty from 'app/store/models/Penalty'; import type { @@ -115,152 +108,6 @@ export type EditingEvent = Event & { saveToImageGallery: boolean; }; -// Event fields that should be created or updated based on the API. -const eventCreateAndUpdateFields = [ - 'id', - 'title', - 'cover', - 'description', - 'text', - 'company', - 'feedbackDescription', - 'feedbackRequired', - 'eventType', - 'eventStatusType', - 'location', - 'isPriced', - 'priceMember', - 'priceGuest', - 'useStripe', - 'paymentDueDate', - 'useCaptcha', - 'tags', - 'pools', - 'registrationDeadlineHours', - 'pinned', - 'heedPenalties', - 'useConsent', - 'separateDeadlines', - 'responsibleUsers', -]; -// Pool fields that should be created or updated based on the API -const poolCreateAndUpdateFields = [ - 'id', - 'name', - 'capacity', - 'activationDate', - 'permissionGroups', -]; - -/* Calculate the event price - * @param isPriced: If the event is priced - */ -const calculatePrice = (data) => (data.isPriced ? data.priceMember * 100 : 0); - -/* Calculate the event location - * @param eventStatusType: what kind of registrationmode this event has - */ -const calculateLocation = (data) => - data.useMazemap ? data.mazemapPoi.label : data.location; - -const calculateMazemapPoi = (data) => { - if (!data.useMazemap || data.mazemapPoi.value === '') { - return null; - } - - return data.mazemapPoi.value; -}; - -/* Calculate the event pools - * @param eventStatusType: what kind of registrationmode this event has - * @param pools: the event groups as specified by the CreateEvent forms - */ -const calculatePools = (data) => { - switch (data.eventStatusType?.value) { - case 'TBA': - case 'OPEN': - return []; - - case 'INFINITE': - return [ - { - ...pick(data.pools[0], poolCreateAndUpdateFields), - activationDate: moment(data.pools[0].activationDate).toISOString(), - permissionGroups: data.pools[0].permissionGroups.map( - (group) => group.value, - ), - }, - ]; - - case 'NORMAL': - return data.pools.map((pool) => ({ - ...pick(pool, poolCreateAndUpdateFields), - activationDate: moment(pool.activationDate).toISOString(), - permissionGroups: pool.permissionGroups.map((group) => group.value), - })); - - default: - break; - } -}; - -/* Calculte and convert to payment due date - * @param paymentDueDate: date from form - */ -const calculatePaymentDueDate = (data) => - data.isPriced ? moment(data.paymentDueDate).toISOString() : null; - -/* Calcualte and convert the registation deadline - * @param unregistationDeadline: data from form - */ -const calculateUnregistrationDeadline = (data) => - data.unregistrationDeadline - ? moment(data.unregistrationDeadline).toISOString() - : null; - -const calculateUnregistrationDeadlineHours = (data) => - data.separateDeadlines - ? data.unregistrationDeadlineHours - : data.registrationDeadlineHours; - -/* Calculate the merge time for the pools. Only set if there are more then one pool - * @param mergeTime: date from form - */ -const calculateMergeTime = (data) => - data.pools.length > 1 ? moment(data.mergeTime).toISOString() : null; - -// Takes the full data-object and input and transforms the event to the API format. -export const transformEvent = (data: TransformEvent) => ({ - ...pick(data, eventCreateAndUpdateFields), - startTime: moment(data.startTime).toISOString(), - endTime: moment(data.endTime).toISOString(), - mergeTime: calculateMergeTime(data), - company: data.company && data.company.value, - eventStatusType: data.eventStatusType && data.eventStatusType.value, - eventType: data.eventType && data.eventType.value, - responsibleGroup: data.responsibleGroup && data.responsibleGroup.value, - responsibleUsers: data.responsibleUsers - ? data.responsibleUsers.map((user) => user.value) - : [], - priceMember: calculatePrice(data), - location: calculateLocation(data), - paymentDueDate: calculatePaymentDueDate(data), - canViewGroups: data.isGroupOnly - ? data.canViewGroups.map((group) => group.id) - : [], - requireAuth: data.canViewGroups.length > 0, - unregistrationDeadline: calculateUnregistrationDeadline(data), - unregistrationDeadlineHours: calculateUnregistrationDeadlineHours(data), - pools: calculatePools(data), - useCaptcha: config.environment === 'ci' ? false : data.useCaptcha, - youtubeUrl: data.youtubeUrl, - mazemapPoi: calculateMazemapPoi(data), - feedbackDescription: - (data.hasFeedbackQuestion && data.feedbackDescription) || '', - feedbackRequired: data.hasFeedbackQuestion && data.feedbackRequired, - isForeignLanguage: data.isForeignLanguage, -}); - export const registrationCloseTime = ( event: Pick, ) => moment(event.startTime).subtract(event.registrationDeadlineHours, 'hours'); @@ -292,26 +139,26 @@ export const penaltyHours = (penalties: Penalty[]) => { } }; -export const eventStatusTypes = [ +export const eventStatusTypes: { value: EventStatusType; label: string }[] = [ { - value: 'TBA', + value: EventStatusType.TBA, label: 'Ikke bestemt (TBA)', }, { - value: 'NORMAL', + value: EventStatusType.NORMAL, label: 'Vanlig påmelding (med pools)', }, { - value: 'OPEN', + value: EventStatusType.OPEN, label: 'Åpen (uten påmelding)', }, { - value: 'INFINITE', + value: EventStatusType.INFINITE, label: 'Åpen (med påmelding)', }, ]; -export const transformEventStatusType = (eventStatusType: string) => { +export const transformEventStatusType = (eventStatusType: EventStatusType) => { return ( find(eventStatusTypes, { value: eventStatusType, diff --git a/app/store/models/Event.ts b/app/store/models/Event.ts index 76f7bfb515..5a27f29bc9 100644 --- a/app/store/models/Event.ts +++ b/app/store/models/Event.ts @@ -52,10 +52,10 @@ export interface CompleteEvent { pools: EntityId[]; totalCapacity: number; registrationCloseTime?: Dateish; - registrationDeadlineHours?: number; + registrationDeadlineHours: number; unregistrationCloseTime?: Dateish; unregistrationDeadline?: Dateish; - unregistrationDeadlineHours?: number; + unregistrationDeadlineHours: number; company?: ListCompany; responsibleGroup?: PublicGroup; activeCapacity?: number; diff --git a/app/utils/formSpyUtils.tsx b/app/utils/formSpyUtils.tsx index 0259fb5bd0..a421d1ac65 100644 --- a/app/utils/formSpyUtils.tsx +++ b/app/utils/formSpyUtils.tsx @@ -1,4 +1,5 @@ import { FormSpy } from 'react-final-form'; +import type { FormApi } from 'final-form'; import type { ReactNode } from 'react'; import type { FormSpyRenderProps } from 'react-final-form'; @@ -14,6 +15,14 @@ export const spyValues = ( ); +export const spyForm = ( + render: (form: FormApi) => ReactNode, +) => ( + + {({ form }: FormSpyRenderProps) => render(form)} + +); + export const spyFormError = (render: (error: any) => ReactNode) => (