From b5390c311cb59528a2b4e82903fce255ac9e745c Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Tue, 20 Aug 2024 20:04:51 -0700
Subject: [PATCH 01/33] Duplicate "Manage Schedule" tab as "Manage Venues"
both point to the same react component
---
app/controllers/competitions_controller.rb | 5 ++++
app/views/competitions/_nav.html.erb | 1 +
app/views/competitions/edit_venues.html.erb | 28 +++++++++++++++++++++
config/locales/en.yml | 1 +
config/routes.rb | 1 +
5 files changed, 36 insertions(+)
create mode 100644 app/views/competitions/edit_venues.html.erb
diff --git a/app/controllers/competitions_controller.rb b/app/controllers/competitions_controller.rb
index cf16da3cb0b..c04c03e43f4 100644
--- a/app/controllers/competitions_controller.rb
+++ b/app/controllers/competitions_controller.rb
@@ -38,6 +38,7 @@ class CompetitionsController < ApplicationController
before_action -> { redirect_to_root_unless_user(:can_manage_competition?, competition_from_params) }, only: [
:edit,
:edit_events,
+ :edit_venues,
:edit_schedule,
:payment_integration_setup,
]
@@ -253,6 +254,10 @@ def edit_events
@competition = competition_from_params(includes: associations)
end
+ def edit_venues
+ @competition = competition_from_params(includes: [competition_events: { rounds: { competition_event: [:event] } }, competition_venues: { venue_rooms: { schedule_activities: [:child_activities] } }])
+ end
+
def edit_schedule
@competition = competition_from_params(includes: [competition_events: { rounds: { competition_event: [:event] } }, competition_venues: { venue_rooms: { schedule_activities: [:child_activities] } }])
end
diff --git a/app/views/competitions/_nav.html.erb b/app/views/competitions/_nav.html.erb
index 6c169113269..7a136c5563f 100644
--- a/app/views/competitions/_nav.html.erb
+++ b/app/views/competitions/_nav.html.erb
@@ -42,6 +42,7 @@
children: [
{ text: t('.menu.orga_view'), path: edit_competition_path(@competition), icon: "lock" },
{ text: t('.menu.event_view'), path: edit_events_path(@competition), icon: "cubes" },
+ { text: t('.menu.venue_view'), path: edit_venues_path(@competition), icon: "building" },
{ text: t('.menu.schedule_view'), path: edit_schedule_path(@competition), icon: "calendar" },
{ text: t('.menu.payment_view'), path: competition_payment_integration_setup_path(@competition), icon: "money bill alternate" },
] +
diff --git a/app/views/competitions/edit_venues.html.erb b/app/views/competitions/edit_venues.html.erb
new file mode 100644
index 00000000000..63393f753f6
--- /dev/null
+++ b/app/views/competitions/edit_venues.html.erb
@@ -0,0 +1,28 @@
+<% provide(:title, "Manage venue for #{@competition.name}") %>
+<% add_to_packs("wca_maps") %>
+
+<%= render layout: 'nav' do %>
+ <% if !@competition.has_defined_dates? %>
+
+ There is no start and/or end date assigned to this competition yet.
+ Before you can manage your venues, you have to add them
+ <%= link_to "here", edit_competition_path(@competition, anchor: "competition_start_date") %>.
+
+ <% elsif @competition.country.blank? %>
+
+ There is no country assigned to this competition yet.
+ Before you can manage your venues, you have to add one
+ <%= link_to "here", edit_competition_path(@competition, anchor: "competition_countryId") %>.
+
+ <% else %>
+ <%= react_component("EditSchedule", {
+ competitionId: @competition.id,
+ wcifEvents: @competition.events_wcif,
+ wcifSchedule: @competition.schedule_wcif,
+ countryZones: @competition.country_zones,
+ calendarLocale: I18n.locale,
+ }, {
+ id: 'edit-schedule-area'
+ }) %>
+ <% end %>
+<% end %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3eb4823151c..ff4543e7af0 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -2086,6 +2086,7 @@ en:
clone: "Clone"
orga_view: "Organizer view"
event_view: "Manage events"
+ venue_view: "Manage venues"
schedule_view: "Manage schedule"
payment_view: "Setup payments"
admin_view: "Admin view"
diff --git a/config/routes.rb b/config/routes.rb
index 1a6a7bf3225..c70d2e9dbff 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -138,6 +138,7 @@
# Stripe needs this special redirect URL during OAuth, see the linked controller method for details
get 'stripe-connect' => 'competitions#stripe_connect', as: :competitions_stripe_connect
get 'competitions/:id/events/edit' => 'competitions#edit_events', as: :edit_events
+ get 'competitions/:id/venues/edit' => 'competitions#edit_venues', as: :edit_venues
get 'competitions/:id/schedule/edit' => 'competitions#edit_schedule', as: :edit_schedule
get 'competitions/edit/nearby_competitions' => 'competitions#nearby_competitions', as: :nearby_competitions
get 'competitions/edit/series_eligible_competitions' => 'competitions#series_eligible_competitions', as: :series_eligible_competitions
From 2e8c982cc840b4b6555e94ce252cdd9637f31f73 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 15:23:06 -0700
Subject: [PATCH 02/33] Duplicate EditSchedule forlder as EditVenues
---
.../EditActivities/ActionsHeader.js | 129 +++++
.../EditActivities/ActivityPicker.js | 106 ++++
.../EditActivities/EditActivityModal.js | 122 +++++
.../EditVenues/EditActivities/index.js | 507 ++++++++++++++++++
.../EditVenues/EditVenues/RoomPanel.js | 81 +++
.../EditVenues/EditVenues/VenueLocationMap.js | 128 +++++
.../EditVenues/EditVenues/VenuePanel.js | 184 +++++++
.../components/EditVenues/EditVenues/index.js | 51 ++
app/webpacker/components/EditVenues/index.js | 188 +++++++
.../components/EditVenues/store/actions.js | 219 ++++++++
.../components/EditVenues/store/reducer.js | 318 +++++++++++
app/webpacker/components/EditVenues/utils.js | 106 ++++
12 files changed, 2139 insertions(+)
create mode 100644 app/webpacker/components/EditVenues/EditActivities/ActionsHeader.js
create mode 100644 app/webpacker/components/EditVenues/EditActivities/ActivityPicker.js
create mode 100644 app/webpacker/components/EditVenues/EditActivities/EditActivityModal.js
create mode 100644 app/webpacker/components/EditVenues/EditActivities/index.js
create mode 100644 app/webpacker/components/EditVenues/EditVenues/RoomPanel.js
create mode 100644 app/webpacker/components/EditVenues/EditVenues/VenueLocationMap.js
create mode 100644 app/webpacker/components/EditVenues/EditVenues/VenuePanel.js
create mode 100644 app/webpacker/components/EditVenues/EditVenues/index.js
create mode 100644 app/webpacker/components/EditVenues/index.js
create mode 100644 app/webpacker/components/EditVenues/store/actions.js
create mode 100644 app/webpacker/components/EditVenues/store/reducer.js
create mode 100644 app/webpacker/components/EditVenues/utils.js
diff --git a/app/webpacker/components/EditVenues/EditActivities/ActionsHeader.js b/app/webpacker/components/EditVenues/EditActivities/ActionsHeader.js
new file mode 100644
index 00000000000..b5ea6cd3371
--- /dev/null
+++ b/app/webpacker/components/EditVenues/EditActivities/ActionsHeader.js
@@ -0,0 +1,129 @@
+import React, { useState } from 'react';
+import {
+ Button,
+ Checkbox,
+ Container,
+ Form,
+ Icon,
+ Modal,
+} from 'semantic-ui-react';
+
+import { copyRoomActivities } from '../store/actions';
+import { useDispatch, useStore } from '../../../lib/providers/StoreProvider';
+import { useConfirm } from '../../../lib/providers/ConfirmProvider';
+import { venueWcifFromRoomId } from '../../../lib/utils/wcif';
+import useInputState from '../../../lib/hooks/useInputState';
+
+function ActionsHeader({
+ selectedRoomId,
+ shouldUpdateMatches,
+ setShouldUpdateMatches,
+}) {
+ const { wcifSchedule } = useStore();
+
+ const [isCopyModalOpen, setIsCopyModalOpen] = useState(false);
+
+ const otherRoomsWithNonEmptySchedules = wcifSchedule.venues.flatMap(
+ (venue) => venue.rooms.filter(
+ (room) => room.activities.length > 0 && room.id !== selectedRoomId,
+ ).map((room) => ({
+ key: room.id,
+ text: `${venue.name} - ${room.name}`,
+ value: room.id,
+ })),
+ );
+
+ return (
+ otherRoomsWithNonEmptySchedules.length > 0 && (
+
+ setIsCopyModalOpen(false)}
+ />
+
+ setIsCopyModalOpen(true)}>
+
+ Copy another room
+
+
+
+ )
+ );
+}
+
+function CopyRoomScheduleModal({
+ isOpen,
+ selectedRoomId,
+ roomOptions,
+ close,
+}) {
+ const { wcifSchedule } = useStore();
+ const dispatch = useDispatch();
+ const confirm = useConfirm();
+
+ const [toCopyRoomId, setToCopyRoomId] = useInputState();
+ const selectedRoomVenue = venueWcifFromRoomId(wcifSchedule, selectedRoomId);
+ const toCopyRoomVenue = venueWcifFromRoomId(wcifSchedule, toCopyRoomId);
+ const areRoomsInSameVenue = selectedRoomVenue.id === toCopyRoomVenue?.id;
+
+ const onClose = () => {
+ setToCopyRoomId(undefined);
+ close();
+ };
+
+ const dispatchAndClose = () => {
+ dispatch(copyRoomActivities(toCopyRoomId, selectedRoomId));
+ onClose();
+ };
+
+ const handleCopyRoom = () => {
+ if (areRoomsInSameVenue) {
+ dispatchAndClose();
+ } else {
+ confirm({
+ content: 'The room you selected is in a different venue. You should probably only be copying from a different venue for a multi-location fewest moves competition. If so, make sure you correctly set all venue time zones BEFORE proceeding with this copy. Are you sure you want to proceed?',
+ }).then(dispatchAndClose);
+ }
+ };
+
+ return (
+
+ Copy Existing Schedule
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ActionsHeader;
diff --git a/app/webpacker/components/EditVenues/EditActivities/ActivityPicker.js b/app/webpacker/components/EditVenues/EditActivities/ActivityPicker.js
new file mode 100644
index 00000000000..25c68db52bf
--- /dev/null
+++ b/app/webpacker/components/EditVenues/EditActivities/ActivityPicker.js
@@ -0,0 +1,106 @@
+import React, { useMemo } from 'react';
+import {
+ Label,
+ List,
+ Popup,
+ Ref,
+} from 'semantic-ui-react';
+import cn from 'classnames';
+import _ from 'lodash';
+import { shortLabelForActivityCode } from '../../../lib/utils/wcif';
+import { formats } from '../../../lib/wca-data.js.erb';
+import { activityToFcTitle, buildPartialActivityFromCode } from '../../../lib/utils/edit-schedule';
+
+function ActivityPicker({
+ wcifEvents,
+ wcifRoom,
+ listRef,
+}) {
+ return (
+ <>
+ [
+ ]
+ {wcifEvents.map((event) => (
+
+
+
+ {event.rounds.map((round) => (
+
+ ))}
+
+
+ ))}
+
+
+
+ Want to add a custom activity such as lunch or registration?
+ Click and select a timeframe on the calendar!
+
+ >
+ );
+}
+
+function PickerRow({
+ wcifRoom,
+ wcifEvent,
+ wcifRound,
+}) {
+ if (['333fm', '333mbf'].includes(wcifEvent.id)) {
+ const numberOfAttempts = formats.byId[wcifRound.format].expectedSolveCount;
+
+ return _.times(numberOfAttempts, (n) => (
+
+ ));
+ }
+
+ return (
+
+ );
+}
+
+function ActivityLabel({
+ wcifRoom,
+ activityCode,
+}) {
+ const usedActivityCodes = useMemo(
+ () => wcifRoom.activities.map((activity) => activity.activityCode),
+ [wcifRoom.activities],
+ );
+
+ const isEnabled = !usedActivityCodes.includes(activityCode);
+
+ const partialActivity = buildPartialActivityFromCode(activityCode);
+
+ return (
+
+ {shortLabelForActivityCode(activityCode)}
+
+ )}
+ />
+ );
+}
+
+export default ActivityPicker;
diff --git a/app/webpacker/components/EditVenues/EditActivities/EditActivityModal.js b/app/webpacker/components/EditVenues/EditActivities/EditActivityModal.js
new file mode 100644
index 00000000000..b3516ab2419
--- /dev/null
+++ b/app/webpacker/components/EditVenues/EditActivities/EditActivityModal.js
@@ -0,0 +1,122 @@
+import React, { useEffect } from 'react';
+import {
+ Button,
+ Container,
+ Form,
+ Modal,
+} from 'semantic-ui-react';
+import { DateTime } from 'luxon';
+import I18n from '../../../lib/i18n';
+import useInputState from '../../../lib/hooks/useInputState';
+
+const otherActivityCodes = [
+ 'other-registration',
+ 'other-checkin',
+ 'other-tutorial',
+ 'other-multi',
+ 'other-breakfast',
+ 'other-lunch',
+ 'other-dinner',
+ 'other-awards',
+ 'other-misc',
+];
+
+const otherActivityCodeOptions = otherActivityCodes
+ .map((activityCode) => ({
+ key: activityCode,
+ text: I18n.t(`activity.${activityCode.substring(6)}`),
+ value: activityCode,
+ }));
+
+function EditActivityModal({
+ isModalOpen,
+ activity,
+ startLuxon,
+ endLuxon,
+ dateLocale,
+ onModalClose,
+ onModalSave,
+}) {
+ const [activityCode, setActivityCode] = useInputState();
+ const [activityName, setActivityName] = useInputState();
+
+ const setActivityCodeInternal = (evt, data) => {
+ const { value: newActivityCode } = data;
+
+ // only if there is no name yet: assign a default name based on the activity code
+ if (!activityName && newActivityCode) {
+ setActivityName(I18n.t(`activity.${newActivityCode.substring(6)}`));
+ }
+
+ setActivityCode(evt, data);
+ };
+
+ // We have to assign state in this awkward way because of an opinionated conflict
+ // between FullCalendar and SemanticUI. See comment at the beginning of index.js for details.
+ useEffect(() => {
+ setActivityCode(activity?.activityCode);
+ setActivityName(activity?.name);
+ }, [activity, setActivityCode, setActivityName]);
+
+ const closeModalAndCleanUp = () => {
+ onModalClose();
+ setActivityCode(undefined);
+ setActivityName(undefined);
+ };
+
+ return (
+
+ Add a custom activity
+
+
+
+
+ On
+ {' '}
+ {startLuxon?.setLocale(dateLocale)?.toLocaleString(DateTime.DATE_HUGE)}
+ {' '}
+ from
+ {' '}
+ {startLuxon?.setLocale(dateLocale)?.toLocaleString(DateTime.TIME_SIMPLE)}
+ {' '}
+ until
+ {' '}
+ {endLuxon?.setLocale(dateLocale)?.toLocaleString(DateTime.TIME_SIMPLE)}
+
+
+
+ {
+ onModalSave({ activityCode, activityName });
+ closeModalAndCleanUp();
+ }}
+ />
+
+
+
+ );
+}
+
+export default EditActivityModal;
diff --git a/app/webpacker/components/EditVenues/EditActivities/index.js b/app/webpacker/components/EditVenues/EditActivities/index.js
new file mode 100644
index 00000000000..e2edc39abb3
--- /dev/null
+++ b/app/webpacker/components/EditVenues/EditActivities/index.js
@@ -0,0 +1,507 @@
+import React, {
+ useCallback,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+
+import {
+ Button,
+ Container,
+ Divider,
+ Form,
+ Grid,
+ Icon, List,
+ Message,
+ Popup, Ref, Segment,
+ Sticky,
+} from 'semantic-ui-react';
+
+import FullCalendar from '@fullcalendar/react';
+import interactionPlugin, { Draggable } from '@fullcalendar/interaction';
+import timeGridPlugin from '@fullcalendar/timegrid';
+import luxonPlugin, { toLuxonDateTime, toLuxonDuration } from '@fullcalendar/luxon3';
+
+import { useDispatch, useStore } from '../../../lib/providers/StoreProvider';
+import { useConfirm } from '../../../lib/providers/ConfirmProvider';
+import useInputState from '../../../lib/hooks/useInputState';
+import ActivityPicker from './ActivityPicker';
+import {
+ getMatchingActivities,
+ roomWcifFromId,
+ venueWcifFromRoomId,
+} from '../../../lib/utils/wcif';
+import { getTextColor } from '../../../lib/utils/calendar';
+import useCheckboxState from '../../../lib/hooks/useCheckboxState';
+
+import {
+ addActivity,
+ editActivity,
+ moveActivity,
+ removeActivity,
+ scaleActivity,
+} from '../store/actions';
+
+import {
+ activityToFcTitle,
+ buildPartialActivityFromCode,
+ defaultDurationFromActivityCode, FC_ACTIVITY_ATTACHMENT,
+ fcEventToActivityAndDates,
+ luxonToWcifIso,
+} from '../../../lib/utils/edit-schedule';
+import EditActivityModal from './EditActivityModal';
+import ActionsHeader from './ActionsHeader';
+import { getTimeZoneDropdownLabel } from '../../../lib/utils/timezone';
+import { earliestTimeOfDayWithBuffer } from '../../../lib/utils/activities';
+
+function EditActivities({
+ wcifEvents,
+ referenceTime,
+ calendarLocale,
+}) {
+ const { wcifSchedule } = useStore();
+ const dispatch = useDispatch();
+
+ const confirm = useConfirm();
+
+ const [selectedRoomId, setSelectedRoomId] = useInputState();
+
+ const [shouldUpdateMatches, setShouldUpdateMatches] = useCheckboxState(false);
+
+ const [minutesPerRow, setMinutesPerRow] = useInputState(15);
+ const [calendarStart, setCalendarStart] = useInputState(8);
+ const [calendarEnd, setCalendarEnd] = useInputState(20);
+
+ // This part is ugly because Semantic-UI and Fullcalendar disagree
+ // about how modals should be handled.
+ // According to Semantic-UI, modals are "always there" in the DOM,
+ // just that their "isOpen" state is false most of the time.
+ // So they simply don't show but they are already part of the DOM tree.
+ // The (click-)event-based model of Fullcalendar however dictates that
+ // we can only "instantiate" a modal once the user actually clicks somewhere on the calendar.
+ // So we pre-fill the modal with empty state and set it accordingly on every event click.
+ // If somebody has a better idea how to handle this, please shout.
+ const [isActivityModalOpen, setActivityModalOpen] = useState(false);
+
+ const [modalActivity, setModalActivity] = useState();
+
+ const [modalLuxonStart, setModalLuxonStart] = useState();
+ const [modalLuxonEnd, setModalLuxonEnd] = useState();
+ // ------ MODAL HACK END ------
+
+ const fcSlotDuration = useMemo(() => `00:${minutesPerRow.toString().padStart(2, '0')}:00`, [minutesPerRow]);
+
+ const fcSlotMin = useMemo(() => `${calendarStart.toString().padStart(2, '0')}:00:00`, [calendarStart]);
+ const fcSlotMax = useMemo(() => `${calendarEnd.toString().padStart(2, '0')}:00:00`, [calendarEnd]);
+
+ const wcifVenue = useMemo(
+ () => venueWcifFromRoomId(wcifSchedule, selectedRoomId),
+ [selectedRoomId, wcifSchedule],
+ );
+
+ const wcifRoom = useMemo(
+ () => roomWcifFromId(wcifSchedule, selectedRoomId),
+ [selectedRoomId, wcifSchedule],
+ );
+
+ const earliestActivity = useMemo(
+ () => (
+ (wcifRoom && wcifVenue)
+ ? earliestTimeOfDayWithBuffer(wcifRoom.activities, wcifVenue.timezone)
+ : undefined
+ ),
+ [wcifRoom, wcifVenue],
+ );
+
+ const fcActivities = useMemo(() => (
+ wcifRoom?.activities.map((activity) => {
+ const matchCount = getMatchingActivities(wcifSchedule, activity).length - 1;
+ const matchesText = ` (${matchCount} matching activit${matchCount === 1 ? 'y' : 'ies'})`;
+
+ const fcTitle = activityToFcTitle(activity) + (shouldUpdateMatches && matchCount > 0 ? matchesText : '');
+
+ return {
+ title: fcTitle,
+ start: activity.startTime,
+ end: activity.endTime,
+ extendedProps: {
+ [FC_ACTIVITY_ATTACHMENT]: activity,
+ matchCount,
+ },
+ };
+ })
+ ), [wcifRoom?.activities, wcifSchedule, shouldUpdateMatches]);
+
+ // we 'fake' our own ref due to quirks in useRef + useEffect combinations.
+ // See https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780
+ const activityPickerRef = useCallback((node) => {
+ if (!node) return;
+
+ // eslint-disable-next-line no-new
+ new Draggable(node, {
+ itemSelector: '.fc-draggable',
+ eventData: (eventEl) => {
+ const activityCode = eventEl.getAttribute('wcif-ac');
+
+ const partialActivity = buildPartialActivityFromCode(activityCode);
+ const defaultDuration = defaultDurationFromActivityCode(activityCode);
+
+ return {
+ title: activityToFcTitle(partialActivity),
+ duration: `00:${defaultDuration.toString().padStart(2, '0')}:00`,
+ extendedProps: {
+ [FC_ACTIVITY_ATTACHMENT]: partialActivity,
+ },
+ };
+ },
+ });
+ }, []);
+
+ const dropToDeleteRef = useRef(null);
+
+ const removeIfOverDropzone = ({ event: fcEvent, jsEvent }) => {
+ if (!dropToDeleteRef.current) return;
+
+ // Don't bother trying to delete an activity that hasn't even been added yet
+ if (!fcEvent.extendedProps[FC_ACTIVITY_ATTACHMENT]?.id) return;
+
+ const elem = dropToDeleteRef.current;
+ const rect = elem.getBoundingClientRect();
+
+ const top = rect.top + window.scrollY;
+ const bottom = rect.bottom + window.scrollY;
+ const left = rect.left + window.scrollX;
+ const right = rect.right + window.scrollX;
+
+ if (
+ jsEvent.pageX >= left
+ && jsEvent.pageX <= right
+ && jsEvent.pageY >= top
+ && jsEvent.pageY <= bottom
+ ) {
+ const {
+ [FC_ACTIVITY_ATTACHMENT]: {
+ id: activityId,
+ name: activityName,
+ },
+ matchCount,
+ } = fcEvent.extendedProps;
+
+ const matchText = `all ${matchCount + 1} copies of `;
+
+ confirm({
+ content: `Are you sure you want to delete ${shouldUpdateMatches && matchCount > 1 ? matchText : ''}the event ${activityName}? THIS ACTION CANNOT BE UNDONE!`,
+ }).then(() => {
+ dispatch(removeActivity(activityId, shouldUpdateMatches));
+ });
+ }
+ };
+
+ const addActivityFromPicker = ({ event: fcEvent, view: { calendar } }) => {
+ const { activity } = fcEventToActivityAndDates(fcEvent, calendar);
+
+ dispatch(addActivity(activity, wcifRoom.id));
+ };
+
+ const changeActivityTimeslot = ({
+ event: fcEvent,
+ delta,
+ view: { calendar },
+ }) => {
+ const { [FC_ACTIVITY_ATTACHMENT]: { id: activityId } } = fcEvent.extendedProps;
+
+ const duration = toLuxonDuration(delta, calendar);
+ const deltaIso = duration.toISO();
+
+ dispatch(moveActivity(activityId, deltaIso, shouldUpdateMatches));
+ };
+
+ const resizeActivity = ({
+ event: fcEvent,
+ startDelta,
+ endDelta,
+ view: { calendar },
+ }) => {
+ const { [FC_ACTIVITY_ATTACHMENT]: { id: activityId } } = fcEvent.extendedProps;
+
+ const startScaleDuration = toLuxonDuration(startDelta, calendar);
+ const startScaleIso = startScaleDuration.toISO();
+
+ const endScaleDuration = toLuxonDuration(endDelta, calendar);
+ const endScaleIso = endScaleDuration.toISO();
+
+ dispatch(scaleActivity(activityId, startScaleIso, endScaleIso, shouldUpdateMatches));
+ };
+
+ const addActivityFromCalendar = (startLuxon, endLuxon) => {
+ setModalLuxonStart(startLuxon);
+ setModalLuxonEnd(endLuxon);
+
+ setActivityModalOpen(true);
+ };
+
+ const addActivityFromCalendarClick = ({ date, view: { calendar } }) => {
+ const eventStartLuxon = toLuxonDateTime(date, calendar);
+ const eventEndLuxon = eventStartLuxon.plus({ minutes: defaultDurationFromActivityCode('other') });
+
+ addActivityFromCalendar(eventStartLuxon, eventEndLuxon);
+ };
+
+ const addActivityFromCalendarDrag = ({ start, end, view: { calendar } }) => {
+ const eventStartLuxon = toLuxonDateTime(start, calendar);
+ const eventEndLuxon = toLuxonDateTime(end, calendar);
+
+ addActivityFromCalendar(eventStartLuxon, eventEndLuxon);
+ };
+
+ const editCustomEvent = ({ event: fcEvent, view: { calendar } }) => {
+ const {
+ activity,
+ startLuxon,
+ endLuxon,
+ } = fcEventToActivityAndDates(fcEvent, calendar);
+
+ const canEdit = activity.activityCode.startsWith('other-');
+
+ if (canEdit) {
+ setModalActivity(activity);
+
+ setModalLuxonStart(startLuxon);
+ setModalLuxonEnd(endLuxon);
+
+ setActivityModalOpen(true);
+ }
+ };
+
+ const closeActivityModalAndCleanUp = () => {
+ // close
+ setActivityModalOpen(false);
+
+ // cleanup
+ setModalActivity(null);
+
+ setModalLuxonStart(null);
+ setModalLuxonEnd(null);
+ };
+
+ const dispatchActivityModalUpdates = (modalData) => {
+ const { activityCode, activityName } = modalData;
+
+ if (modalActivity) {
+ dispatch(editActivity(modalActivity.id, 'activityCode', activityCode, shouldUpdateMatches));
+ dispatch(editActivity(modalActivity.id, 'name', activityName, shouldUpdateMatches));
+ } else {
+ const utcStartIso = luxonToWcifIso(modalLuxonStart);
+ const utcEndIso = luxonToWcifIso(modalLuxonEnd);
+
+ const activity = {
+ name: activityName,
+ activityCode,
+ startTime: utcStartIso,
+ endTime: utcEndIso,
+ childActivities: [],
+ };
+
+ dispatch(addActivity(activity, wcifRoom.id));
+ }
+ };
+
+ return (
+
+
+
+ {wcifSchedule.venues.map((venue) => (
+
+
+
+ {venue.name}
+
+ {venue.rooms.map((room) => (
+ setSelectedRoomId(room.id)}
+ >
+ {room.id === wcifRoom?.id ? {room.name} : room.name}
+
+ ))}
+
+
+
+ ))}
+
+
+
+ {selectedRoomId === undefined && (
+
Please select a room by clicking one of the labels above
+ )}
+ {selectedRoomId !== undefined && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The timezone for this room is
+ {' '}
+
+ {getTimeZoneDropdownLabel(
+ wcifVenue.timezone,
+ earliestActivity || referenceTime,
+ calendarLocale,
+ )}
+
+
+
+
+
+ }
+ on="click"
+ position="right center"
+ pinned
+ flowing
+ >
+ Calendar settings
+
+
+
+
+
+
+
+
+
+ [
+ ]
+
+ Drop an event here to remove it from the schedule.
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+export default EditActivities;
diff --git a/app/webpacker/components/EditVenues/EditVenues/RoomPanel.js b/app/webpacker/components/EditVenues/EditVenues/RoomPanel.js
new file mode 100644
index 00000000000..4946f4a03d1
--- /dev/null
+++ b/app/webpacker/components/EditVenues/EditVenues/RoomPanel.js
@@ -0,0 +1,81 @@
+import React from 'react';
+import {
+ Button,
+ Card,
+ Form,
+ Icon,
+ Popup,
+} from 'semantic-ui-react';
+import { useDispatch } from '../../../lib/providers/StoreProvider';
+import { useConfirm } from '../../../lib/providers/ConfirmProvider';
+import { copyRoom, editRoom, removeRoom } from '../store/actions';
+
+function RoomPanel({
+ room,
+}) {
+ const dispatch = useDispatch();
+
+ const confirm = useConfirm();
+
+ const handleChange = (evt, { name, value }) => {
+ dispatch(editRoom(room.id, name, value));
+ };
+
+ const handleDeleteRoom = () => {
+ confirm({
+ content: `Are you sure you want to delete the room ${room.name}? This will also delete all associated schedules. THIS ACTION CANNOT BE UNDONE!`,
+ }).then(() => dispatch(removeRoom(room.id)));
+ };
+
+ const handleCopyRoom = () => {
+ dispatch(copyRoom(room.id));
+ };
+
+ return (
+
+
+
+
+
+
+ )}
+ />
+
+
+
+ )}
+ />
+
+
+
+
+
+
+
+
+ );
+}
+
+export default RoomPanel;
diff --git a/app/webpacker/components/EditVenues/EditVenues/VenueLocationMap.js b/app/webpacker/components/EditVenues/EditVenues/VenueLocationMap.js
new file mode 100644
index 00000000000..4bfdaffd767
--- /dev/null
+++ b/app/webpacker/components/EditVenues/EditVenues/VenueLocationMap.js
@@ -0,0 +1,128 @@
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import {
+ MapContainer,
+ Marker,
+ Popup,
+ TileLayer,
+ useMap,
+} from 'react-leaflet';
+import { toDegrees, toMicrodegrees } from '../../../lib/utils/edit-schedule';
+import { userTileProvider } from '../../../lib/leaflet-wca/providers';
+import { useDispatch } from '../../../lib/providers/StoreProvider';
+import { editVenue } from '../store/actions';
+import ResizeMapIFrame from '../../../lib/utils/leaflet-iframe';
+
+function GeoSearchControl({
+ onGeoSearchResult,
+}) {
+ const map = useMap();
+
+ useEffect(() => {
+ const searchControl = window.wca.createSearchInput(map);
+
+ map.on('geosearch/showlocation', onGeoSearchResult);
+ map.zoomControl.setPosition('bottomright');
+
+ return () => {
+ map.removeControl(searchControl);
+ };
+ }, [map, onGeoSearchResult]);
+
+ return null;
+}
+
+export function DraggableMarker({
+ position,
+ setPosition,
+ disabled = false,
+ markerRef = null,
+ children,
+}) {
+ const map = useMap();
+
+ const updatePosition = useCallback((e) => setPosition(e, e.target.getLatLng()), [setPosition]);
+
+ useEffect(() => {
+ map.panTo(position);
+ }, [map, position]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+function VenueLocationMap({
+ venue,
+}) {
+ const dispatch = useDispatch();
+ const markerRef = useRef();
+
+ const [searchResultPopup, setSearchResultPopup] = useState();
+
+ const markerPopup = useMemo(() => {
+ if (searchResultPopup) {
+ return {searchResultPopup} ;
+ }
+
+ return null;
+ }, [searchResultPopup]);
+
+ const venuePosition = useMemo(() => ({
+ lat: toDegrees(venue.latitudeMicrodegrees),
+ lng: toDegrees(venue.longitudeMicrodegrees),
+ }), [venue.latitudeMicrodegrees, venue.longitudeMicrodegrees]);
+
+ const setVenuePosition = useCallback((evt, { lat, lng }) => {
+ dispatch(editVenue(venue.id, 'latitudeMicrodegrees', toMicrodegrees(lat)));
+ dispatch(editVenue(venue.id, 'longitudeMicrodegrees', toMicrodegrees(lng)));
+ }, [dispatch, venue.id]);
+
+ const provider = userTileProvider;
+
+ const onGeoSearchResult = useCallback((evt) => {
+ setVenuePosition(evt, {
+ lat: evt.location.y,
+ lng: evt.location.x,
+ });
+
+ setSearchResultPopup(evt.location.label);
+ }, [setVenuePosition, setSearchResultPopup]);
+
+ return (
+
+
+
+
+
+ {markerPopup}
+
+
+ );
+}
+
+export default VenueLocationMap;
diff --git a/app/webpacker/components/EditVenues/EditVenues/VenuePanel.js b/app/webpacker/components/EditVenues/EditVenues/VenuePanel.js
new file mode 100644
index 00000000000..cc153c561e8
--- /dev/null
+++ b/app/webpacker/components/EditVenues/EditVenues/VenuePanel.js
@@ -0,0 +1,184 @@
+import React, { useCallback, useMemo } from 'react';
+import {
+ Button,
+ Card,
+ Container,
+ DropdownHeader,
+ Form,
+ Icon,
+ Image,
+} from 'semantic-ui-react';
+import _ from 'lodash';
+
+import VenueLocationMap from './VenueLocationMap';
+import { countries, backendTimezones } from '../../../lib/wca-data.js.erb';
+import RoomPanel from './RoomPanel';
+import { useDispatch } from '../../../lib/providers/StoreProvider';
+import { useConfirm } from '../../../lib/providers/ConfirmProvider';
+import {
+ addRoom,
+ editVenue,
+ removeVenue,
+} from '../store/actions';
+import { toDegrees, toMicrodegrees } from '../../../lib/utils/edit-schedule';
+import { getTimeZoneDropdownLabel, sortByOffset } from '../../../lib/utils/timezone';
+
+const countryOptions = countries.real.map((country) => ({
+ key: country.iso2,
+ text: country.name,
+ value: country.iso2,
+ flag: country.iso2.toLowerCase(),
+}));
+
+function VenuePanel({
+ venue,
+ countryZones,
+ referenceTime,
+}) {
+ const dispatch = useDispatch();
+ const confirm = useConfirm();
+
+ const handleCoordinateChange = (evt, { name, value }) => {
+ dispatch(editVenue(venue.id, name, toMicrodegrees(value)));
+ };
+
+ const handleVenueChange = (evt, { name, value }) => {
+ dispatch(editVenue(venue.id, name, value));
+ };
+
+ const handleDeleteVenue = () => {
+ confirm({
+ content: `Are you sure you want to delete the venue ${venue.name}? This will also delete all associated rooms and all associated schedules. THIS ACTION CANNOT BE UNDONE!`,
+ }).then(() => dispatch(removeVenue(venue.id)));
+ };
+
+ const handleAddRoom = () => {
+ dispatch(addRoom(venue.id));
+ };
+
+ const getVenueTzDropdownLabel = useCallback(
+ // The whole page is not localized yet, so we just hard-code US English here as well.
+ (tzId) => getTimeZoneDropdownLabel(tzId, referenceTime, 'en-US'),
+ [referenceTime],
+ );
+
+ const makeTimeZoneOption = useCallback((key) => ({
+ key,
+ text: getVenueTzDropdownLabel(key),
+ value: key,
+ }), [getVenueTzDropdownLabel]);
+
+ // Instead of giving *all* TZInfo, use uniq-fied rails "meaningful" subset
+ // We'll add the "country_zones" to that, because some of our competitions
+ // use TZs not included in this subset.
+ // We want to display the "country_zones" first, so that it's more convenient for the user.
+ // In the end the array should look like that:
+ // - country_zone_a, country_zone_b, [...], other_tz_a, other_tz_b, [...]
+ const timezoneOptions = useMemo(() => {
+ // Stuff that is recommended based on the country list
+ const competitionZoneIds = _.uniq(countryZones);
+ const sortedCompetitionZones = sortByOffset(competitionZoneIds, referenceTime);
+
+ // Stuff that is listed in our `backendTimezones` list but not in the preferred country list
+ const otherZoneIds = _.difference(backendTimezones, competitionZoneIds);
+ const sortedOtherZones = sortByOffset(otherZoneIds, referenceTime);
+
+ // Both merged together, with the countryZone entries listed first.
+ return [
+ {
+ as: DropdownHeader,
+ key: 'local-zones-header',
+ text: 'Local time zones',
+ disabled: true,
+ },
+ ...sortedCompetitionZones.map(makeTimeZoneOption),
+ {
+ as: DropdownHeader,
+ key: 'other-zones-header',
+ text: 'Other time zones',
+ disabled: true,
+ },
+ ...sortedOtherZones.map(makeTimeZoneOption),
+ ];
+ }, [countryZones, referenceTime, makeTimeZoneOption]);
+
+ return (
+
+ { /* Needs the className 'image' so that SemUI fills the top of the card */ }
+
+
+
+
+
+
+
+ Remove
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add room
+
+ Rooms
+
+
+
+ {venue.rooms.map((room) => (
+
+ ))}
+
+
+
+
+ );
+}
+
+export default VenuePanel;
diff --git a/app/webpacker/components/EditVenues/EditVenues/index.js b/app/webpacker/components/EditVenues/EditVenues/index.js
new file mode 100644
index 00000000000..5fba7373db4
--- /dev/null
+++ b/app/webpacker/components/EditVenues/EditVenues/index.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import {
+ Button,
+ Card,
+ Container,
+ Icon,
+ Segment,
+} from 'semantic-ui-react';
+import { useDispatch, useStore } from '../../../lib/providers/StoreProvider';
+import VenuePanel from './VenuePanel';
+import { addVenue } from '../store/actions';
+
+function EditVenues({
+ countryZones,
+ referenceTime,
+}) {
+ const { wcifSchedule } = useStore();
+
+ const dispatch = useDispatch();
+
+ const handleAddVenue = () => {
+ dispatch(addVenue());
+ };
+
+ return (
+
+
+
+
+ Add a venue
+
+ Please add all your venues and rooms below:
+
+
+
+
+ {wcifSchedule.venues.map((venue) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default EditVenues;
diff --git a/app/webpacker/components/EditVenues/index.js b/app/webpacker/components/EditVenues/index.js
new file mode 100644
index 00000000000..8c4cfbfe15f
--- /dev/null
+++ b/app/webpacker/components/EditVenues/index.js
@@ -0,0 +1,188 @@
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import {
+ Accordion,
+ Button,
+ Container,
+ Message,
+} from 'semantic-ui-react';
+
+import _ from 'lodash';
+
+import { useSaveWcifAction } from '../../lib/utils/wcif';
+import { changesSaved } from './store/actions';
+import wcifScheduleReducer from './store/reducer';
+import Store, { useDispatch, useStore } from '../../lib/providers/StoreProvider';
+import ConfirmProvider from '../../lib/providers/ConfirmProvider';
+import EditVenues from './EditVenues';
+import EditActivities from './EditActivities';
+
+function EditSchedule({
+ wcifEvents,
+ countryZones,
+ referenceTime,
+ calendarLocale,
+}) {
+ const {
+ competitionId,
+ wcifSchedule,
+ initialWcifSchedule,
+ } = useStore();
+
+ const dispatch = useDispatch();
+
+ const [openAccordion, setOpenAccordion] = useState(-1);
+
+ const unsavedChanges = useMemo(() => (
+ !_.isEqual(wcifSchedule, initialWcifSchedule)
+ ), [wcifSchedule, initialWcifSchedule]);
+
+ const onUnload = useCallback((e) => {
+ // Prompt the user before letting them navigate away from this page with unsaved changes.
+ if (unsavedChanges) {
+ const confirmationMessage = 'You have unsaved changes, are you sure you want to leave?';
+ e.returnValue = confirmationMessage;
+ return confirmationMessage;
+ }
+
+ return null;
+ }, [unsavedChanges]);
+
+ useEffect(() => {
+ window.addEventListener('beforeunload', onUnload);
+
+ return () => {
+ window.removeEventListener('beforeunload', onUnload);
+ };
+ }, [onUnload]);
+
+ const { saveWcif, saving } = useSaveWcifAction();
+
+ const save = useCallback(() => {
+ saveWcif(
+ competitionId,
+ { schedule: wcifSchedule },
+ () => dispatch(changesSaved()),
+ );
+ }, [competitionId, dispatch, saveWcif, wcifSchedule]);
+
+ const renderUnsavedChangesAlert = () => (
+
+ You have unsaved changes. Don't forget to
+ {' '}
+
+ save your changes!
+
+
+ );
+
+ const renderIntroductionMessage = () => (
+
+
+ Depending on the size and setup of the competition, it may take place in
+ several rooms of several venues.
+ Therefore a schedule is necessarily linked to a specific room.
+ Each room may have its own schedule (with all or a subset of events).
+ So you can start creating the competition’s schedule below by adding at
+ least one venue with one room.
+ Then you will be able to select this room in the "Edit schedules"
+ panel, and drag and drop event rounds (or attempts for some events) on it.
+
+
+ For the typical simple competition, creating one "Main venue"
+ with one "Main room" is enough.
+ If your competition has a single venue but multiple "stages" with different
+ schedules, please input them as different rooms.
+
+
+ );
+
+ const handleAccordionClick = (evt, titleProps) => {
+ const { index } = titleProps;
+ const newIndex = openAccordion === index ? -1 : index;
+
+ setOpenAccordion(newIndex);
+ };
+
+ return (
+ <>
+ {renderIntroductionMessage()}
+
+ {unsavedChanges && renderUnsavedChangesAlert()}
+
+
+ Edit venues information
+
+
+
+
+
+ Edit schedules
+
+
+
+
+
+ {unsavedChanges && renderUnsavedChangesAlert()}
+
+ >
+ );
+}
+
+export default function Wrapper({
+ competitionId,
+ wcifEvents,
+ wcifSchedule,
+ countryZones,
+ referenceTime,
+ calendarLocale,
+}) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/app/webpacker/components/EditVenues/store/actions.js b/app/webpacker/components/EditVenues/store/actions.js
new file mode 100644
index 00000000000..660f946ba89
--- /dev/null
+++ b/app/webpacker/components/EditVenues/store/actions.js
@@ -0,0 +1,219 @@
+export const ChangesSaved = 'saving_started';
+export const AddActivity = 'ADD_ACTIVITY';
+export const EditActivity = 'EDIT_ACTIVITY';
+export const RemoveActivity = 'REMOVE_ACTIVITY';
+export const MoveActivity = 'MOVE_ACTIVITY';
+export const ScaleActivity = 'SCALE_ACTIVITY';
+export const EditVenue = 'EDIT_VENUE';
+export const EditRoom = 'EDIT_ROOM';
+export const RemoveVenue = 'REMOVE_VENUE';
+export const RemoveRoom = 'REMOVE_ROOM';
+export const AddVenue = 'ADD_VENUE';
+export const AddRoom = 'ADD_ROOM';
+export const CopyVenue = 'COPY_VENUE';
+export const CopyRoom = 'COPY_ROOM';
+export const CopyRoomActivities = 'COPY_ROOM_ACTIVITIES';
+
+/**
+ * Action creator for marking changes as saved.
+ * @returns {Action}
+ */
+export const changesSaved = () => ({
+ type: ChangesSaved,
+});
+
+/**
+ * Action creator for adding activity.
+ * @param {Activity} wcifActivity
+ * @param {int} roomId
+ * @returns {Action}
+ */
+export const addActivity = (wcifActivity, roomId) => ({
+ type: AddActivity,
+ payload: {
+ wcifActivity,
+ roomId,
+ },
+});
+
+/**
+ * Action creator for modifying details of an activity.
+ * @param {int} activityId
+ * @param {string} key
+ * @param {string} value
+ * @param {boolean} updateMatches
+ * @returns {Action}
+ */
+export const editActivity = (activityId, key, value, updateMatches) => ({
+ type: EditActivity,
+ payload: {
+ activityId,
+ key,
+ value,
+ updateMatches,
+ },
+});
+
+/**
+ * Action creator for removing activity.
+ * @param {int} activityId
+ * @param {boolean} updateMatches
+ * @returns {Action}
+ */
+export const removeActivity = (activityId, updateMatches) => ({
+ type: RemoveActivity,
+ payload: {
+ activityId,
+ updateMatches,
+ },
+});
+
+/**
+ * Action creator for moving an activity's time.
+ * @param {int} activityId
+ * @param {string} isoDuration
+ * @param {boolean} updateMatches
+ * @returns {Action}
+ */
+export const moveActivity = (activityId, isoDuration, updateMatches = false) => ({
+ type: MoveActivity,
+ payload: {
+ activityId,
+ isoDuration,
+ updateMatches,
+ },
+});
+
+/**
+ * Action creator for scaling an activity's time,
+ * i.e. changing the start and/or end date by some delta.
+ * @param {int} activityId
+ * @param {string} isoDeltaStart
+ * @param {string} isoDeltaEnd
+ * @param {boolean} updateMatches
+ * @returns {Action}
+ */
+export const scaleActivity = (activityId, isoDeltaStart, isoDeltaEnd, updateMatches = false) => ({
+ type: ScaleActivity,
+ payload: {
+ activityId,
+ isoDeltaStart,
+ isoDeltaEnd,
+ updateMatches,
+ },
+});
+
+/**
+ * Action creator for changing a venue's properties.
+ * @param {int} venueId
+ * @param {string} propertyKey
+ * @param {string} newProperty
+ * @returns {Action}
+ */
+export const editVenue = (venueId, propertyKey, newProperty) => ({
+ type: EditVenue,
+ payload: {
+ venueId,
+ propertyKey,
+ newProperty,
+ },
+});
+
+/**
+ * Action creator for changing a room's properties.
+ * @param {int} roomId
+ * @param {string} propertyKey
+ * @param {string} newProperty
+ * @returns {Action}
+ */
+export const editRoom = (roomId, propertyKey, newProperty) => ({
+ type: EditRoom,
+ payload: {
+ roomId,
+ propertyKey,
+ newProperty,
+ },
+});
+
+/**
+ * Action creator for removing a venue.
+ * @param {int} venueId
+ * @returns {Action}
+ */
+export const removeVenue = (venueId) => ({
+ type: RemoveVenue,
+ payload: {
+ venueId,
+ },
+});
+
+/**
+ * Action creator for removing a room.
+ * @param {int} roomId
+ * @returns {Action}
+ */
+export const removeRoom = (roomId) => ({
+ type: RemoveRoom,
+ payload: {
+ roomId,
+ },
+});
+
+/**
+ * Action creator for adding a blank venue.
+ * @returns {Action}
+ */
+export const addVenue = () => ({
+ type: AddVenue,
+ payload: {},
+});
+
+/**
+ * Action creator for adding a blank room.
+ * @param {int} venueId
+ * @returns {Action}
+ */
+export const addRoom = (venueId) => ({
+ type: AddRoom,
+ payload: {
+ venueId,
+ },
+});
+
+/**
+ * Action creator for copying a venue.
+ * @param {int} venueId
+ * @returns {Action}
+ */
+export const copyVenue = (venueId) => ({
+ type: CopyVenue,
+ payload: {
+ venueId,
+ },
+});
+
+/**
+ * Action creator for copying a room.
+ * @param {int} roomId
+ * @returns {Action}
+ */
+export const copyRoom = (roomId) => ({
+ type: CopyRoom,
+ payload: {
+ roomId,
+ },
+});
+
+/**
+ * Action creator for copying a room's activities to another room.
+ * @param {int} sourceRoomId
+ * @param {int} targetRoomId
+ * @returns {Action}
+ */
+export const copyRoomActivities = (sourceRoomId, targetRoomId) => ({
+ type: CopyRoomActivities,
+ payload: {
+ sourceRoomId,
+ targetRoomId,
+ },
+});
diff --git a/app/webpacker/components/EditVenues/store/reducer.js b/app/webpacker/components/EditVenues/store/reducer.js
new file mode 100644
index 00000000000..f9ba3dcb17f
--- /dev/null
+++ b/app/webpacker/components/EditVenues/store/reducer.js
@@ -0,0 +1,318 @@
+import {
+ AddActivity,
+ AddRoom,
+ AddVenue,
+ ChangesSaved,
+ CopyRoom,
+ CopyRoomActivities,
+ CopyVenue,
+ EditActivity,
+ EditRoom,
+ EditVenue,
+ MoveActivity,
+ RemoveActivity,
+ RemoveRoom,
+ RemoveVenue,
+ ScaleActivity,
+} from './actions';
+import {
+ copyActivity, copyRoom, copyVenue, nextActivityId, nextRoomId, nextVenueId,
+} from '../../../lib/utils/edit-schedule';
+import {
+ changeActivityTimezone, moveActivityByDuration, scaleActivitiesByDuration,
+} from '../utils';
+import {
+ activityWcifFromId,
+ doActivitiesMatch,
+ roomWcifFromId,
+ venueWcifFromRoomId,
+} from '../../../lib/utils/wcif';
+import { defaultRoomColor } from '../../../lib/wca-data.js.erb';
+
+const reducers = {
+ [ChangesSaved]: (state) => ({
+ ...state,
+ initialWcifSchedule: state.wcifSchedule,
+ }),
+
+ [AddActivity]: (state, { payload }) => ({
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => ({
+ ...venue,
+ rooms: venue.rooms.map((room) => (room.id === payload.roomId ? ({
+ ...room,
+ activities: [
+ ...room.activities,
+ {
+ ...payload.wcifActivity,
+ id: nextActivityId(state.wcifSchedule),
+ },
+ ],
+ }) : room)),
+ })),
+ },
+ }),
+
+ [EditActivity]: (state, { payload }) => {
+ const selectedActivity = activityWcifFromId(state.wcifSchedule, payload.activityId);
+
+ return {
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => ({
+ ...venue,
+ rooms: venue.rooms.map((room) => ({
+ ...room,
+ activities: room.activities.map((activity) => (
+ (activity.id === selectedActivity.id || (
+ payload.updateMatches && doActivitiesMatch(activity, selectedActivity)
+ ))
+ ? { ...activity, [payload.key]: payload.value }
+ : activity
+ )),
+ })),
+ })),
+ },
+ };
+ },
+
+ [RemoveActivity]: (state, { payload }) => {
+ const selectedActivity = activityWcifFromId(state.wcifSchedule, payload.activityId);
+
+ return {
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => ({
+ ...venue,
+ rooms: venue.rooms.map((room) => ({
+ ...room,
+ activities: room.activities.filter((activity) => (
+ activity.id !== payload.activityId && (
+ !payload.updateMatches || !doActivitiesMatch(activity, selectedActivity)
+ )
+ )),
+ })),
+ })),
+ },
+ };
+ },
+
+ [MoveActivity]: (state, { payload }) => {
+ const selectedActivity = activityWcifFromId(state.wcifSchedule, payload.activityId);
+
+ return {
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => ({
+ ...venue,
+ rooms: venue.rooms.map((room) => ({
+ ...room,
+ activities: room.activities.map((activity) => (
+ (activity.id === selectedActivity.id || (
+ payload.updateMatches && doActivitiesMatch(activity, selectedActivity)
+ ))
+ ? moveActivityByDuration(activity, payload.isoDuration)
+ : activity
+ )),
+ })),
+ })),
+ },
+ };
+ },
+
+ [ScaleActivity]: (state, { payload }) => {
+ const selectedActivity = activityWcifFromId(state.wcifSchedule, payload.activityId);
+
+ return {
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => ({
+ ...venue,
+ rooms: venue.rooms.map((room) => ({
+ ...room,
+ activities: room.activities.map((activity) => (
+ (activity.id === selectedActivity.id || (
+ payload.updateMatches && doActivitiesMatch(activity, selectedActivity)
+ ))
+ ? scaleActivitiesByDuration(activity, payload.isoDeltaStart, payload.isoDeltaEnd)
+ : activity
+ )),
+ })),
+ })),
+ },
+ };
+ },
+
+ [EditVenue]: (state, { payload }) => ({
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => (venue.id === payload.venueId ? {
+ ...venue,
+ [payload.propertyKey]: payload.newProperty,
+ rooms: payload.propertyKey === 'timezone'
+ ? venue.rooms.map((room) => ({
+ ...room,
+ activities: room.activities.map((activity) => (
+ changeActivityTimezone(
+ activity,
+ venue.timezone,
+ payload.newProperty,
+ )
+ )),
+ }))
+ : venue.rooms,
+ } : venue)),
+ },
+ }),
+
+ [EditRoom]: (state, { payload }) => ({
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => ({
+ ...venue,
+ rooms: venue.rooms.map((room) => (room.id === payload.roomId ? {
+ ...room,
+ [payload.propertyKey]: payload.newProperty,
+ } : room)),
+ })),
+ },
+ }),
+
+ [RemoveVenue]: (state, { payload }) => ({
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.filter((venue) => venue.id !== payload.venueId),
+ },
+ }),
+
+ [RemoveRoom]: (state, { payload }) => ({
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => ({
+ ...venue,
+ rooms: venue.rooms.filter((room) => room.id !== payload.roomId),
+ })),
+ },
+ }),
+
+ [AddVenue]: (state) => ({
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: [
+ ...state.wcifSchedule.venues,
+ {
+ id: nextVenueId(state.wcifSchedule),
+ latitudeMicrodegrees: 0,
+ longitudeMicrodegrees: 0,
+ rooms: [],
+ extensions: [],
+ },
+ ],
+ },
+ }),
+
+ [AddRoom]: (state, { payload }) => ({
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => (venue.id === payload.venueId ? {
+ ...venue,
+ rooms: [
+ ...venue.rooms,
+ {
+ id: nextRoomId(state.wcifSchedule),
+ color: defaultRoomColor,
+ activities: [],
+ extensions: [],
+ },
+ ],
+ } : venue)),
+ },
+ }),
+
+ [CopyVenue]: (state, { payload }) => {
+ const venue = state.wcifSchedule.venues.find(({ id }) => id === payload.venueId);
+ if (!venue) return state;
+
+ return {
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: [
+ ...state.wcifSchedule.venues,
+ {
+ ...copyVenue(state.wcifSchedule, venue),
+ name: `Copy of ${venue.name}`,
+ },
+ ],
+ },
+ };
+ },
+
+ [CopyRoom]: (state, { payload }) => {
+ const targetVenue = venueWcifFromRoomId(state.wcifSchedule, payload.roomId);
+ if (!targetVenue) return state;
+ const room = targetVenue.rooms.find(({ id }) => id === payload.roomId);
+ if (!room) return state;
+
+ return {
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => (venue.id === targetVenue.id ? {
+ ...venue,
+ rooms: [
+ ...venue.rooms,
+ {
+ ...copyRoom(state.wcifSchedule, room),
+ name: `Copy of ${room.name}`,
+ },
+ ],
+ } : venue)),
+ },
+ };
+ },
+
+ [CopyRoomActivities]: (state, { payload }) => {
+ const { sourceRoomId, targetRoomId } = payload;
+ const sourceRoomActivities = roomWcifFromId(state.wcifSchedule, sourceRoomId).activities;
+ if (sourceRoomActivities.length === 0) return state;
+ const copiedActivities = sourceRoomActivities.map(
+ (activity) => copyActivity(state.wcifSchedule, activity),
+ );
+ const targetRoomVenueId = venueWcifFromRoomId(state.wcifSchedule, targetRoomId).id;
+
+ return {
+ ...state,
+ wcifSchedule: {
+ ...state.wcifSchedule,
+ venues: state.wcifSchedule.venues.map((venue) => (venue.id === targetRoomVenueId ? {
+ ...venue,
+ rooms: venue.rooms.map((room) => (room.id === targetRoomId ? {
+ ...room,
+ activities: [...room.activities, ...copiedActivities],
+ } : room)),
+ } : venue)),
+ },
+ };
+ },
+};
+
+export default function rootReducer(state, action) {
+ const reducer = reducers[action.type];
+ if (reducer) {
+ return reducer(state, action);
+ }
+ return state;
+}
diff --git a/app/webpacker/components/EditVenues/utils.js b/app/webpacker/components/EditVenues/utils.js
new file mode 100644
index 00000000000..6495a75f474
--- /dev/null
+++ b/app/webpacker/components/EditVenues/utils.js
@@ -0,0 +1,106 @@
+import {
+ addIsoDurations,
+ changeTimezoneKeepingLocalTime,
+ millisecondsBetween,
+ moveByIsoDuration,
+ rescaleIsoDuration,
+} from '../../lib/utils/edit-schedule';
+
+export const moveActivityByDuration = (activity, isoDuration) => ({
+ ...activity,
+ startTime: moveByIsoDuration(activity.startTime, isoDuration),
+ endTime: moveByIsoDuration(activity.endTime, isoDuration),
+ childActivities: activity.childActivities.map((childActivity) => (
+ moveActivityByDuration(childActivity, isoDuration)
+ )),
+});
+
+export const scaleActivitiesByDuration = (activity, isoDeltaStart, isoDeltaEnd) => {
+ const rootActivityLengthMs = millisecondsBetween(
+ activity.startTime,
+ activity.endTime,
+ );
+
+ return ({
+ ...activity,
+ startTime: moveByIsoDuration(activity.startTime, isoDeltaStart),
+ endTime: moveByIsoDuration(activity.endTime, isoDeltaEnd),
+ childActivities: activity.childActivities.map((childActivity) => {
+ // Unfortunately, scaling child activities (properly) is rocket science.
+ const childActivityLengthMs = millisecondsBetween(
+ childActivity.startTime,
+ childActivity.endTime,
+ );
+
+ // Say you scale the start by -1 hour (i.e. 1 hour earlier).
+ // The amount that you have to scale a child by is directly proportional
+ // to the child's length. So we calculate the proportion of durations using milliseconds.
+ // Say you have a parent activity with three equally sized children. In that case,
+ // every child gets scaled down an equal amount, because they are all equally long.
+ // If you plan your schedule with one slow group (long duration) and one fast group
+ // (short duration), the fast and short group only needs to be rescaled a little bit
+ // while the slow and long group gets the "lion share" of the scaling factor.
+ const scalingFactor = childActivityLengthMs / rootActivityLengthMs;
+
+ const childStartScale = rescaleIsoDuration(isoDeltaStart, scalingFactor);
+ const childEndScale = rescaleIsoDuration(isoDeltaEnd, scalingFactor);
+
+ // Of course, this all has to happen recursively because children can have children!
+ const scaledChild = scaleActivitiesByDuration(
+ childActivity,
+ childStartScale,
+ childEndScale,
+ );
+
+ // However, it doesn't end there. When a parent activity _scales_,
+ // the child activities also have to _move_. Think of an activity "shrinking",
+ // i.e. becoming shorter: Then the children also "shrink" as a result.
+ // This "shrinking" will create gaps which can only be filled by the children
+ // _moving_ closer together after shrinking down.
+
+ const ownStartToParentStart = millisecondsBetween(
+ childActivity.startTime,
+ activity.startTime,
+ );
+
+ const ownEndToParentEnd = millisecondsBetween(
+ childActivity.endTime,
+ activity.endTime,
+ );
+
+ // Again, this growth is proportional to the size of the child activity.
+ const scalingStartUp = ownStartToParentStart / rootActivityLengthMs;
+ const scalingEndDown = ownEndToParentEnd / rootActivityLengthMs;
+
+ // Now it gets a little bit crazy:
+ // - When applying a Delta to the END of the activity, we have to move it UP
+ // - When applying a Delta to the START of the activity, we have to move it DOWN
+ // Think of it this way: With two subsequent child activities, removing a few minutes
+ // from the END of either activity creates a gap that needs to be closed by moving
+ // the second, later activity UP closer towards its predecessor.
+ // The same logic applies in reverse for adding minutes instead of removing minutes.
+ const moveUpwardsDuration = rescaleIsoDuration(isoDeltaEnd, scalingStartUp);
+ const moveDownwardsDuration = rescaleIsoDuration(isoDeltaStart, scalingEndDown);
+
+ // Both directional Deltas are added together, and it is possible that they cancel
+ // each other out, most notably if DeltaStart == -DeltaEnd.
+ const totalMovingDuration = addIsoDurations(moveUpwardsDuration, moveDownwardsDuration);
+
+ // Phew, we're done.
+ return moveActivityByDuration(scaledChild, totalMovingDuration);
+ }),
+ });
+};
+
+export const changeActivityTimezone = (activity, oldTimezone, newTimezone) => ({
+ ...activity,
+ startTime: changeTimezoneKeepingLocalTime(activity.startTime, oldTimezone, newTimezone),
+ endTime: changeTimezoneKeepingLocalTime(activity.endTime, oldTimezone, newTimezone),
+ childActivities: activity.childActivities.map((childActivity) => (
+ changeActivityTimezone(
+ childActivity,
+ oldTimezone,
+ newTimezone,
+ )
+ )),
+});
From d5cbc916176bf1975a060a6b35273461fc5a84e4 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 15:48:42 -0700
Subject: [PATCH 03/33] Add missing referenceTime
---
app/views/competitions/edit_venues.html.erb | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/views/competitions/edit_venues.html.erb b/app/views/competitions/edit_venues.html.erb
index 63393f753f6..06db92ee540 100644
--- a/app/views/competitions/edit_venues.html.erb
+++ b/app/views/competitions/edit_venues.html.erb
@@ -20,6 +20,7 @@
wcifEvents: @competition.events_wcif,
wcifSchedule: @competition.schedule_wcif,
countryZones: @competition.country_zones,
+ referenceTime: @competition.start_date.to_fs,
calendarLocale: I18n.locale,
}, {
id: 'edit-schedule-area'
From 2a16a61a0bfc892b7e123c73ce27fe6a976cbec9 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 15:49:19 -0700
Subject: [PATCH 04/33] Use new (copied) EditVenues component in new tab
---
app/views/competitions/edit_venues.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/competitions/edit_venues.html.erb b/app/views/competitions/edit_venues.html.erb
index 06db92ee540..77b651b7d32 100644
--- a/app/views/competitions/edit_venues.html.erb
+++ b/app/views/competitions/edit_venues.html.erb
@@ -15,7 +15,7 @@
<%= link_to "here", edit_competition_path(@competition, anchor: "competition_countryId") %>.
<% else %>
- <%= react_component("EditSchedule", {
+ <%= react_component("EditVenues", {
competitionId: @competition.id,
wcifEvents: @competition.events_wcif,
wcifSchedule: @competition.schedule_wcif,
From e5854acb11443838e1b2365887a4db6f19beaa0a Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 15:54:14 -0700
Subject: [PATCH 05/33] Remove schedule-related things from new EditVenues
---
app/views/competitions/edit_venues.html.erb | 2 -
app/webpacker/components/EditVenues/index.js | 61 +++-----------------
2 files changed, 7 insertions(+), 56 deletions(-)
diff --git a/app/views/competitions/edit_venues.html.erb b/app/views/competitions/edit_venues.html.erb
index 77b651b7d32..4ce9d1dcef2 100644
--- a/app/views/competitions/edit_venues.html.erb
+++ b/app/views/competitions/edit_venues.html.erb
@@ -17,11 +17,9 @@
<% else %>
<%= react_component("EditVenues", {
competitionId: @competition.id,
- wcifEvents: @competition.events_wcif,
wcifSchedule: @competition.schedule_wcif,
countryZones: @competition.country_zones,
referenceTime: @competition.start_date.to_fs,
- calendarLocale: I18n.locale,
}, {
id: 'edit-schedule-area'
}) %>
diff --git a/app/webpacker/components/EditVenues/index.js b/app/webpacker/components/EditVenues/index.js
index 8c4cfbfe15f..c1b5b630318 100644
--- a/app/webpacker/components/EditVenues/index.js
+++ b/app/webpacker/components/EditVenues/index.js
@@ -2,11 +2,9 @@ import React, {
useCallback,
useEffect,
useMemo,
- useState,
} from 'react';
import {
- Accordion,
Button,
Container,
Message,
@@ -20,13 +18,10 @@ import wcifScheduleReducer from './store/reducer';
import Store, { useDispatch, useStore } from '../../lib/providers/StoreProvider';
import ConfirmProvider from '../../lib/providers/ConfirmProvider';
import EditVenues from './EditVenues';
-import EditActivities from './EditActivities';
function EditSchedule({
- wcifEvents,
countryZones,
referenceTime,
- calendarLocale,
}) {
const {
competitionId,
@@ -36,8 +31,6 @@ function EditSchedule({
const dispatch = useDispatch();
- const [openAccordion, setOpenAccordion] = useState(-1);
-
const unsavedChanges = useMemo(() => (
!_.isEqual(wcifSchedule, initialWcifSchedule)
), [wcifSchedule, initialWcifSchedule]);
@@ -93,9 +86,9 @@ function EditSchedule({
several rooms of several venues.
Therefore a schedule is necessarily linked to a specific room.
Each room may have its own schedule (with all or a subset of events).
- So you can start creating the competition’s schedule below by adding at
- least one venue with one room.
- Then you will be able to select this room in the "Edit schedules"
+ To create the competition’s schedule, start by adding at
+ least one venue with one room below.
+ Then you will be able to select this room in the "Manage schedule"
panel, and drag and drop event rounds (or attempts for some events) on it.
@@ -107,51 +100,15 @@ function EditSchedule({
);
- const handleAccordionClick = (evt, titleProps) => {
- const { index } = titleProps;
- const newIndex = openAccordion === index ? -1 : index;
-
- setOpenAccordion(newIndex);
- };
-
return (
<>
{renderIntroductionMessage()}
{unsavedChanges && renderUnsavedChangesAlert()}
-
-
- Edit venues information
-
-
-
-
-
- Edit schedules
-
-
-
-
-
+
{unsavedChanges && renderUnsavedChangesAlert()}
>
@@ -160,11 +117,9 @@ function EditSchedule({
export default function Wrapper({
competitionId,
- wcifEvents,
wcifSchedule,
countryZones,
referenceTime,
- calendarLocale,
}) {
return (
From c0a075a6c8876c218a527056f3134a9881566738 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 15:56:05 -0700
Subject: [PATCH 06/33] Remove unused EditActivities folder in EditVenues
---
.../EditActivities/ActionsHeader.js | 129 -----
.../EditActivities/ActivityPicker.js | 106 ----
.../EditActivities/EditActivityModal.js | 122 -----
.../EditVenues/EditActivities/index.js | 507 ------------------
4 files changed, 864 deletions(-)
delete mode 100644 app/webpacker/components/EditVenues/EditActivities/ActionsHeader.js
delete mode 100644 app/webpacker/components/EditVenues/EditActivities/ActivityPicker.js
delete mode 100644 app/webpacker/components/EditVenues/EditActivities/EditActivityModal.js
delete mode 100644 app/webpacker/components/EditVenues/EditActivities/index.js
diff --git a/app/webpacker/components/EditVenues/EditActivities/ActionsHeader.js b/app/webpacker/components/EditVenues/EditActivities/ActionsHeader.js
deleted file mode 100644
index b5ea6cd3371..00000000000
--- a/app/webpacker/components/EditVenues/EditActivities/ActionsHeader.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import React, { useState } from 'react';
-import {
- Button,
- Checkbox,
- Container,
- Form,
- Icon,
- Modal,
-} from 'semantic-ui-react';
-
-import { copyRoomActivities } from '../store/actions';
-import { useDispatch, useStore } from '../../../lib/providers/StoreProvider';
-import { useConfirm } from '../../../lib/providers/ConfirmProvider';
-import { venueWcifFromRoomId } from '../../../lib/utils/wcif';
-import useInputState from '../../../lib/hooks/useInputState';
-
-function ActionsHeader({
- selectedRoomId,
- shouldUpdateMatches,
- setShouldUpdateMatches,
-}) {
- const { wcifSchedule } = useStore();
-
- const [isCopyModalOpen, setIsCopyModalOpen] = useState(false);
-
- const otherRoomsWithNonEmptySchedules = wcifSchedule.venues.flatMap(
- (venue) => venue.rooms.filter(
- (room) => room.activities.length > 0 && room.id !== selectedRoomId,
- ).map((room) => ({
- key: room.id,
- text: `${venue.name} - ${room.name}`,
- value: room.id,
- })),
- );
-
- return (
- otherRoomsWithNonEmptySchedules.length > 0 && (
-
- setIsCopyModalOpen(false)}
- />
-
- setIsCopyModalOpen(true)}>
-
- Copy another room
-
-
-
- )
- );
-}
-
-function CopyRoomScheduleModal({
- isOpen,
- selectedRoomId,
- roomOptions,
- close,
-}) {
- const { wcifSchedule } = useStore();
- const dispatch = useDispatch();
- const confirm = useConfirm();
-
- const [toCopyRoomId, setToCopyRoomId] = useInputState();
- const selectedRoomVenue = venueWcifFromRoomId(wcifSchedule, selectedRoomId);
- const toCopyRoomVenue = venueWcifFromRoomId(wcifSchedule, toCopyRoomId);
- const areRoomsInSameVenue = selectedRoomVenue.id === toCopyRoomVenue?.id;
-
- const onClose = () => {
- setToCopyRoomId(undefined);
- close();
- };
-
- const dispatchAndClose = () => {
- dispatch(copyRoomActivities(toCopyRoomId, selectedRoomId));
- onClose();
- };
-
- const handleCopyRoom = () => {
- if (areRoomsInSameVenue) {
- dispatchAndClose();
- } else {
- confirm({
- content: 'The room you selected is in a different venue. You should probably only be copying from a different venue for a multi-location fewest moves competition. If so, make sure you correctly set all venue time zones BEFORE proceeding with this copy. Are you sure you want to proceed?',
- }).then(dispatchAndClose);
- }
- };
-
- return (
-
- Copy Existing Schedule
-
-
-
-
-
-
-
-
- );
-}
-
-export default ActionsHeader;
diff --git a/app/webpacker/components/EditVenues/EditActivities/ActivityPicker.js b/app/webpacker/components/EditVenues/EditActivities/ActivityPicker.js
deleted file mode 100644
index 25c68db52bf..00000000000
--- a/app/webpacker/components/EditVenues/EditActivities/ActivityPicker.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import React, { useMemo } from 'react';
-import {
- Label,
- List,
- Popup,
- Ref,
-} from 'semantic-ui-react';
-import cn from 'classnames';
-import _ from 'lodash';
-import { shortLabelForActivityCode } from '../../../lib/utils/wcif';
-import { formats } from '../../../lib/wca-data.js.erb';
-import { activityToFcTitle, buildPartialActivityFromCode } from '../../../lib/utils/edit-schedule';
-
-function ActivityPicker({
- wcifEvents,
- wcifRoom,
- listRef,
-}) {
- return (
- <>
- [
- ]
- {wcifEvents.map((event) => (
-
-
-
- {event.rounds.map((round) => (
-
- ))}
-
-
- ))}
-
-
-
- Want to add a custom activity such as lunch or registration?
- Click and select a timeframe on the calendar!
-
- >
- );
-}
-
-function PickerRow({
- wcifRoom,
- wcifEvent,
- wcifRound,
-}) {
- if (['333fm', '333mbf'].includes(wcifEvent.id)) {
- const numberOfAttempts = formats.byId[wcifRound.format].expectedSolveCount;
-
- return _.times(numberOfAttempts, (n) => (
-
- ));
- }
-
- return (
-
- );
-}
-
-function ActivityLabel({
- wcifRoom,
- activityCode,
-}) {
- const usedActivityCodes = useMemo(
- () => wcifRoom.activities.map((activity) => activity.activityCode),
- [wcifRoom.activities],
- );
-
- const isEnabled = !usedActivityCodes.includes(activityCode);
-
- const partialActivity = buildPartialActivityFromCode(activityCode);
-
- return (
-
- {shortLabelForActivityCode(activityCode)}
-
- )}
- />
- );
-}
-
-export default ActivityPicker;
diff --git a/app/webpacker/components/EditVenues/EditActivities/EditActivityModal.js b/app/webpacker/components/EditVenues/EditActivities/EditActivityModal.js
deleted file mode 100644
index b3516ab2419..00000000000
--- a/app/webpacker/components/EditVenues/EditActivities/EditActivityModal.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import React, { useEffect } from 'react';
-import {
- Button,
- Container,
- Form,
- Modal,
-} from 'semantic-ui-react';
-import { DateTime } from 'luxon';
-import I18n from '../../../lib/i18n';
-import useInputState from '../../../lib/hooks/useInputState';
-
-const otherActivityCodes = [
- 'other-registration',
- 'other-checkin',
- 'other-tutorial',
- 'other-multi',
- 'other-breakfast',
- 'other-lunch',
- 'other-dinner',
- 'other-awards',
- 'other-misc',
-];
-
-const otherActivityCodeOptions = otherActivityCodes
- .map((activityCode) => ({
- key: activityCode,
- text: I18n.t(`activity.${activityCode.substring(6)}`),
- value: activityCode,
- }));
-
-function EditActivityModal({
- isModalOpen,
- activity,
- startLuxon,
- endLuxon,
- dateLocale,
- onModalClose,
- onModalSave,
-}) {
- const [activityCode, setActivityCode] = useInputState();
- const [activityName, setActivityName] = useInputState();
-
- const setActivityCodeInternal = (evt, data) => {
- const { value: newActivityCode } = data;
-
- // only if there is no name yet: assign a default name based on the activity code
- if (!activityName && newActivityCode) {
- setActivityName(I18n.t(`activity.${newActivityCode.substring(6)}`));
- }
-
- setActivityCode(evt, data);
- };
-
- // We have to assign state in this awkward way because of an opinionated conflict
- // between FullCalendar and SemanticUI. See comment at the beginning of index.js for details.
- useEffect(() => {
- setActivityCode(activity?.activityCode);
- setActivityName(activity?.name);
- }, [activity, setActivityCode, setActivityName]);
-
- const closeModalAndCleanUp = () => {
- onModalClose();
- setActivityCode(undefined);
- setActivityName(undefined);
- };
-
- return (
-
- Add a custom activity
-
-
-
-
- On
- {' '}
- {startLuxon?.setLocale(dateLocale)?.toLocaleString(DateTime.DATE_HUGE)}
- {' '}
- from
- {' '}
- {startLuxon?.setLocale(dateLocale)?.toLocaleString(DateTime.TIME_SIMPLE)}
- {' '}
- until
- {' '}
- {endLuxon?.setLocale(dateLocale)?.toLocaleString(DateTime.TIME_SIMPLE)}
-
-
-
- {
- onModalSave({ activityCode, activityName });
- closeModalAndCleanUp();
- }}
- />
-
-
-
- );
-}
-
-export default EditActivityModal;
diff --git a/app/webpacker/components/EditVenues/EditActivities/index.js b/app/webpacker/components/EditVenues/EditActivities/index.js
deleted file mode 100644
index e2edc39abb3..00000000000
--- a/app/webpacker/components/EditVenues/EditActivities/index.js
+++ /dev/null
@@ -1,507 +0,0 @@
-import React, {
- useCallback,
- useMemo,
- useRef,
- useState,
-} from 'react';
-
-import {
- Button,
- Container,
- Divider,
- Form,
- Grid,
- Icon, List,
- Message,
- Popup, Ref, Segment,
- Sticky,
-} from 'semantic-ui-react';
-
-import FullCalendar from '@fullcalendar/react';
-import interactionPlugin, { Draggable } from '@fullcalendar/interaction';
-import timeGridPlugin from '@fullcalendar/timegrid';
-import luxonPlugin, { toLuxonDateTime, toLuxonDuration } from '@fullcalendar/luxon3';
-
-import { useDispatch, useStore } from '../../../lib/providers/StoreProvider';
-import { useConfirm } from '../../../lib/providers/ConfirmProvider';
-import useInputState from '../../../lib/hooks/useInputState';
-import ActivityPicker from './ActivityPicker';
-import {
- getMatchingActivities,
- roomWcifFromId,
- venueWcifFromRoomId,
-} from '../../../lib/utils/wcif';
-import { getTextColor } from '../../../lib/utils/calendar';
-import useCheckboxState from '../../../lib/hooks/useCheckboxState';
-
-import {
- addActivity,
- editActivity,
- moveActivity,
- removeActivity,
- scaleActivity,
-} from '../store/actions';
-
-import {
- activityToFcTitle,
- buildPartialActivityFromCode,
- defaultDurationFromActivityCode, FC_ACTIVITY_ATTACHMENT,
- fcEventToActivityAndDates,
- luxonToWcifIso,
-} from '../../../lib/utils/edit-schedule';
-import EditActivityModal from './EditActivityModal';
-import ActionsHeader from './ActionsHeader';
-import { getTimeZoneDropdownLabel } from '../../../lib/utils/timezone';
-import { earliestTimeOfDayWithBuffer } from '../../../lib/utils/activities';
-
-function EditActivities({
- wcifEvents,
- referenceTime,
- calendarLocale,
-}) {
- const { wcifSchedule } = useStore();
- const dispatch = useDispatch();
-
- const confirm = useConfirm();
-
- const [selectedRoomId, setSelectedRoomId] = useInputState();
-
- const [shouldUpdateMatches, setShouldUpdateMatches] = useCheckboxState(false);
-
- const [minutesPerRow, setMinutesPerRow] = useInputState(15);
- const [calendarStart, setCalendarStart] = useInputState(8);
- const [calendarEnd, setCalendarEnd] = useInputState(20);
-
- // This part is ugly because Semantic-UI and Fullcalendar disagree
- // about how modals should be handled.
- // According to Semantic-UI, modals are "always there" in the DOM,
- // just that their "isOpen" state is false most of the time.
- // So they simply don't show but they are already part of the DOM tree.
- // The (click-)event-based model of Fullcalendar however dictates that
- // we can only "instantiate" a modal once the user actually clicks somewhere on the calendar.
- // So we pre-fill the modal with empty state and set it accordingly on every event click.
- // If somebody has a better idea how to handle this, please shout.
- const [isActivityModalOpen, setActivityModalOpen] = useState(false);
-
- const [modalActivity, setModalActivity] = useState();
-
- const [modalLuxonStart, setModalLuxonStart] = useState();
- const [modalLuxonEnd, setModalLuxonEnd] = useState();
- // ------ MODAL HACK END ------
-
- const fcSlotDuration = useMemo(() => `00:${minutesPerRow.toString().padStart(2, '0')}:00`, [minutesPerRow]);
-
- const fcSlotMin = useMemo(() => `${calendarStart.toString().padStart(2, '0')}:00:00`, [calendarStart]);
- const fcSlotMax = useMemo(() => `${calendarEnd.toString().padStart(2, '0')}:00:00`, [calendarEnd]);
-
- const wcifVenue = useMemo(
- () => venueWcifFromRoomId(wcifSchedule, selectedRoomId),
- [selectedRoomId, wcifSchedule],
- );
-
- const wcifRoom = useMemo(
- () => roomWcifFromId(wcifSchedule, selectedRoomId),
- [selectedRoomId, wcifSchedule],
- );
-
- const earliestActivity = useMemo(
- () => (
- (wcifRoom && wcifVenue)
- ? earliestTimeOfDayWithBuffer(wcifRoom.activities, wcifVenue.timezone)
- : undefined
- ),
- [wcifRoom, wcifVenue],
- );
-
- const fcActivities = useMemo(() => (
- wcifRoom?.activities.map((activity) => {
- const matchCount = getMatchingActivities(wcifSchedule, activity).length - 1;
- const matchesText = ` (${matchCount} matching activit${matchCount === 1 ? 'y' : 'ies'})`;
-
- const fcTitle = activityToFcTitle(activity) + (shouldUpdateMatches && matchCount > 0 ? matchesText : '');
-
- return {
- title: fcTitle,
- start: activity.startTime,
- end: activity.endTime,
- extendedProps: {
- [FC_ACTIVITY_ATTACHMENT]: activity,
- matchCount,
- },
- };
- })
- ), [wcifRoom?.activities, wcifSchedule, shouldUpdateMatches]);
-
- // we 'fake' our own ref due to quirks in useRef + useEffect combinations.
- // See https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780
- const activityPickerRef = useCallback((node) => {
- if (!node) return;
-
- // eslint-disable-next-line no-new
- new Draggable(node, {
- itemSelector: '.fc-draggable',
- eventData: (eventEl) => {
- const activityCode = eventEl.getAttribute('wcif-ac');
-
- const partialActivity = buildPartialActivityFromCode(activityCode);
- const defaultDuration = defaultDurationFromActivityCode(activityCode);
-
- return {
- title: activityToFcTitle(partialActivity),
- duration: `00:${defaultDuration.toString().padStart(2, '0')}:00`,
- extendedProps: {
- [FC_ACTIVITY_ATTACHMENT]: partialActivity,
- },
- };
- },
- });
- }, []);
-
- const dropToDeleteRef = useRef(null);
-
- const removeIfOverDropzone = ({ event: fcEvent, jsEvent }) => {
- if (!dropToDeleteRef.current) return;
-
- // Don't bother trying to delete an activity that hasn't even been added yet
- if (!fcEvent.extendedProps[FC_ACTIVITY_ATTACHMENT]?.id) return;
-
- const elem = dropToDeleteRef.current;
- const rect = elem.getBoundingClientRect();
-
- const top = rect.top + window.scrollY;
- const bottom = rect.bottom + window.scrollY;
- const left = rect.left + window.scrollX;
- const right = rect.right + window.scrollX;
-
- if (
- jsEvent.pageX >= left
- && jsEvent.pageX <= right
- && jsEvent.pageY >= top
- && jsEvent.pageY <= bottom
- ) {
- const {
- [FC_ACTIVITY_ATTACHMENT]: {
- id: activityId,
- name: activityName,
- },
- matchCount,
- } = fcEvent.extendedProps;
-
- const matchText = `all ${matchCount + 1} copies of `;
-
- confirm({
- content: `Are you sure you want to delete ${shouldUpdateMatches && matchCount > 1 ? matchText : ''}the event ${activityName}? THIS ACTION CANNOT BE UNDONE!`,
- }).then(() => {
- dispatch(removeActivity(activityId, shouldUpdateMatches));
- });
- }
- };
-
- const addActivityFromPicker = ({ event: fcEvent, view: { calendar } }) => {
- const { activity } = fcEventToActivityAndDates(fcEvent, calendar);
-
- dispatch(addActivity(activity, wcifRoom.id));
- };
-
- const changeActivityTimeslot = ({
- event: fcEvent,
- delta,
- view: { calendar },
- }) => {
- const { [FC_ACTIVITY_ATTACHMENT]: { id: activityId } } = fcEvent.extendedProps;
-
- const duration = toLuxonDuration(delta, calendar);
- const deltaIso = duration.toISO();
-
- dispatch(moveActivity(activityId, deltaIso, shouldUpdateMatches));
- };
-
- const resizeActivity = ({
- event: fcEvent,
- startDelta,
- endDelta,
- view: { calendar },
- }) => {
- const { [FC_ACTIVITY_ATTACHMENT]: { id: activityId } } = fcEvent.extendedProps;
-
- const startScaleDuration = toLuxonDuration(startDelta, calendar);
- const startScaleIso = startScaleDuration.toISO();
-
- const endScaleDuration = toLuxonDuration(endDelta, calendar);
- const endScaleIso = endScaleDuration.toISO();
-
- dispatch(scaleActivity(activityId, startScaleIso, endScaleIso, shouldUpdateMatches));
- };
-
- const addActivityFromCalendar = (startLuxon, endLuxon) => {
- setModalLuxonStart(startLuxon);
- setModalLuxonEnd(endLuxon);
-
- setActivityModalOpen(true);
- };
-
- const addActivityFromCalendarClick = ({ date, view: { calendar } }) => {
- const eventStartLuxon = toLuxonDateTime(date, calendar);
- const eventEndLuxon = eventStartLuxon.plus({ minutes: defaultDurationFromActivityCode('other') });
-
- addActivityFromCalendar(eventStartLuxon, eventEndLuxon);
- };
-
- const addActivityFromCalendarDrag = ({ start, end, view: { calendar } }) => {
- const eventStartLuxon = toLuxonDateTime(start, calendar);
- const eventEndLuxon = toLuxonDateTime(end, calendar);
-
- addActivityFromCalendar(eventStartLuxon, eventEndLuxon);
- };
-
- const editCustomEvent = ({ event: fcEvent, view: { calendar } }) => {
- const {
- activity,
- startLuxon,
- endLuxon,
- } = fcEventToActivityAndDates(fcEvent, calendar);
-
- const canEdit = activity.activityCode.startsWith('other-');
-
- if (canEdit) {
- setModalActivity(activity);
-
- setModalLuxonStart(startLuxon);
- setModalLuxonEnd(endLuxon);
-
- setActivityModalOpen(true);
- }
- };
-
- const closeActivityModalAndCleanUp = () => {
- // close
- setActivityModalOpen(false);
-
- // cleanup
- setModalActivity(null);
-
- setModalLuxonStart(null);
- setModalLuxonEnd(null);
- };
-
- const dispatchActivityModalUpdates = (modalData) => {
- const { activityCode, activityName } = modalData;
-
- if (modalActivity) {
- dispatch(editActivity(modalActivity.id, 'activityCode', activityCode, shouldUpdateMatches));
- dispatch(editActivity(modalActivity.id, 'name', activityName, shouldUpdateMatches));
- } else {
- const utcStartIso = luxonToWcifIso(modalLuxonStart);
- const utcEndIso = luxonToWcifIso(modalLuxonEnd);
-
- const activity = {
- name: activityName,
- activityCode,
- startTime: utcStartIso,
- endTime: utcEndIso,
- childActivities: [],
- };
-
- dispatch(addActivity(activity, wcifRoom.id));
- }
- };
-
- return (
-
-
-
- {wcifSchedule.venues.map((venue) => (
-
-
-
- {venue.name}
-
- {venue.rooms.map((room) => (
- setSelectedRoomId(room.id)}
- >
- {room.id === wcifRoom?.id ? {room.name} : room.name}
-
- ))}
-
-
-
- ))}
-
-
-
- {selectedRoomId === undefined && (
-
Please select a room by clicking one of the labels above
- )}
- {selectedRoomId !== undefined && (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- The timezone for this room is
- {' '}
-
- {getTimeZoneDropdownLabel(
- wcifVenue.timezone,
- earliestActivity || referenceTime,
- calendarLocale,
- )}
-
-
-
-
-
- }
- on="click"
- position="right center"
- pinned
- flowing
- >
- Calendar settings
-
-
-
-
-
-
-
-
-
- [
- ]
-
- Drop an event here to remove it from the schedule.
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- );
-}
-
-export default EditActivities;
From ec4f55fe83155119a501858b46be60a9351a1cc3 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:06:37 -0700
Subject: [PATCH 07/33] Remove schedule actions from venues files
---
.../components/EditVenues/store/actions.js | 101 -----------
.../components/EditVenues/store/reducer.js | 157 +-----------------
2 files changed, 3 insertions(+), 255 deletions(-)
diff --git a/app/webpacker/components/EditVenues/store/actions.js b/app/webpacker/components/EditVenues/store/actions.js
index 660f946ba89..56745718420 100644
--- a/app/webpacker/components/EditVenues/store/actions.js
+++ b/app/webpacker/components/EditVenues/store/actions.js
@@ -1,9 +1,4 @@
export const ChangesSaved = 'saving_started';
-export const AddActivity = 'ADD_ACTIVITY';
-export const EditActivity = 'EDIT_ACTIVITY';
-export const RemoveActivity = 'REMOVE_ACTIVITY';
-export const MoveActivity = 'MOVE_ACTIVITY';
-export const ScaleActivity = 'SCALE_ACTIVITY';
export const EditVenue = 'EDIT_VENUE';
export const EditRoom = 'EDIT_ROOM';
export const RemoveVenue = 'REMOVE_VENUE';
@@ -12,7 +7,6 @@ export const AddVenue = 'ADD_VENUE';
export const AddRoom = 'ADD_ROOM';
export const CopyVenue = 'COPY_VENUE';
export const CopyRoom = 'COPY_ROOM';
-export const CopyRoomActivities = 'COPY_ROOM_ACTIVITIES';
/**
* Action creator for marking changes as saved.
@@ -22,87 +16,6 @@ export const changesSaved = () => ({
type: ChangesSaved,
});
-/**
- * Action creator for adding activity.
- * @param {Activity} wcifActivity
- * @param {int} roomId
- * @returns {Action}
- */
-export const addActivity = (wcifActivity, roomId) => ({
- type: AddActivity,
- payload: {
- wcifActivity,
- roomId,
- },
-});
-
-/**
- * Action creator for modifying details of an activity.
- * @param {int} activityId
- * @param {string} key
- * @param {string} value
- * @param {boolean} updateMatches
- * @returns {Action}
- */
-export const editActivity = (activityId, key, value, updateMatches) => ({
- type: EditActivity,
- payload: {
- activityId,
- key,
- value,
- updateMatches,
- },
-});
-
-/**
- * Action creator for removing activity.
- * @param {int} activityId
- * @param {boolean} updateMatches
- * @returns {Action}
- */
-export const removeActivity = (activityId, updateMatches) => ({
- type: RemoveActivity,
- payload: {
- activityId,
- updateMatches,
- },
-});
-
-/**
- * Action creator for moving an activity's time.
- * @param {int} activityId
- * @param {string} isoDuration
- * @param {boolean} updateMatches
- * @returns {Action}
- */
-export const moveActivity = (activityId, isoDuration, updateMatches = false) => ({
- type: MoveActivity,
- payload: {
- activityId,
- isoDuration,
- updateMatches,
- },
-});
-
-/**
- * Action creator for scaling an activity's time,
- * i.e. changing the start and/or end date by some delta.
- * @param {int} activityId
- * @param {string} isoDeltaStart
- * @param {string} isoDeltaEnd
- * @param {boolean} updateMatches
- * @returns {Action}
- */
-export const scaleActivity = (activityId, isoDeltaStart, isoDeltaEnd, updateMatches = false) => ({
- type: ScaleActivity,
- payload: {
- activityId,
- isoDeltaStart,
- isoDeltaEnd,
- updateMatches,
- },
-});
-
/**
* Action creator for changing a venue's properties.
* @param {int} venueId
@@ -203,17 +116,3 @@ export const copyRoom = (roomId) => ({
roomId,
},
});
-
-/**
- * Action creator for copying a room's activities to another room.
- * @param {int} sourceRoomId
- * @param {int} targetRoomId
- * @returns {Action}
- */
-export const copyRoomActivities = (sourceRoomId, targetRoomId) => ({
- type: CopyRoomActivities,
- payload: {
- sourceRoomId,
- targetRoomId,
- },
-});
diff --git a/app/webpacker/components/EditVenues/store/reducer.js b/app/webpacker/components/EditVenues/store/reducer.js
index f9ba3dcb17f..b1c81d587bd 100644
--- a/app/webpacker/components/EditVenues/store/reducer.js
+++ b/app/webpacker/components/EditVenues/store/reducer.js
@@ -1,32 +1,19 @@
import {
- AddActivity,
AddRoom,
AddVenue,
ChangesSaved,
CopyRoom,
- CopyRoomActivities,
CopyVenue,
- EditActivity,
EditRoom,
EditVenue,
- MoveActivity,
- RemoveActivity,
RemoveRoom,
RemoveVenue,
- ScaleActivity,
} from './actions';
import {
- copyActivity, copyRoom, copyVenue, nextActivityId, nextRoomId, nextVenueId,
+ copyRoom, copyVenue, nextRoomId, nextVenueId,
} from '../../../lib/utils/edit-schedule';
-import {
- changeActivityTimezone, moveActivityByDuration, scaleActivitiesByDuration,
-} from '../utils';
-import {
- activityWcifFromId,
- doActivitiesMatch,
- roomWcifFromId,
- venueWcifFromRoomId,
-} from '../../../lib/utils/wcif';
+import { changeActivityTimezone } from '../utils';
+import { venueWcifFromRoomId } from '../../../lib/utils/wcif';
import { defaultRoomColor } from '../../../lib/wca-data.js.erb';
const reducers = {
@@ -35,120 +22,6 @@ const reducers = {
initialWcifSchedule: state.wcifSchedule,
}),
- [AddActivity]: (state, { payload }) => ({
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => ({
- ...venue,
- rooms: venue.rooms.map((room) => (room.id === payload.roomId ? ({
- ...room,
- activities: [
- ...room.activities,
- {
- ...payload.wcifActivity,
- id: nextActivityId(state.wcifSchedule),
- },
- ],
- }) : room)),
- })),
- },
- }),
-
- [EditActivity]: (state, { payload }) => {
- const selectedActivity = activityWcifFromId(state.wcifSchedule, payload.activityId);
-
- return {
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => ({
- ...venue,
- rooms: venue.rooms.map((room) => ({
- ...room,
- activities: room.activities.map((activity) => (
- (activity.id === selectedActivity.id || (
- payload.updateMatches && doActivitiesMatch(activity, selectedActivity)
- ))
- ? { ...activity, [payload.key]: payload.value }
- : activity
- )),
- })),
- })),
- },
- };
- },
-
- [RemoveActivity]: (state, { payload }) => {
- const selectedActivity = activityWcifFromId(state.wcifSchedule, payload.activityId);
-
- return {
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => ({
- ...venue,
- rooms: venue.rooms.map((room) => ({
- ...room,
- activities: room.activities.filter((activity) => (
- activity.id !== payload.activityId && (
- !payload.updateMatches || !doActivitiesMatch(activity, selectedActivity)
- )
- )),
- })),
- })),
- },
- };
- },
-
- [MoveActivity]: (state, { payload }) => {
- const selectedActivity = activityWcifFromId(state.wcifSchedule, payload.activityId);
-
- return {
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => ({
- ...venue,
- rooms: venue.rooms.map((room) => ({
- ...room,
- activities: room.activities.map((activity) => (
- (activity.id === selectedActivity.id || (
- payload.updateMatches && doActivitiesMatch(activity, selectedActivity)
- ))
- ? moveActivityByDuration(activity, payload.isoDuration)
- : activity
- )),
- })),
- })),
- },
- };
- },
-
- [ScaleActivity]: (state, { payload }) => {
- const selectedActivity = activityWcifFromId(state.wcifSchedule, payload.activityId);
-
- return {
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => ({
- ...venue,
- rooms: venue.rooms.map((room) => ({
- ...room,
- activities: room.activities.map((activity) => (
- (activity.id === selectedActivity.id || (
- payload.updateMatches && doActivitiesMatch(activity, selectedActivity)
- ))
- ? scaleActivitiesByDuration(activity, payload.isoDeltaStart, payload.isoDeltaEnd)
- : activity
- )),
- })),
- })),
- },
- };
- },
-
[EditVenue]: (state, { payload }) => ({
...state,
wcifSchedule: {
@@ -283,30 +156,6 @@ const reducers = {
},
};
},
-
- [CopyRoomActivities]: (state, { payload }) => {
- const { sourceRoomId, targetRoomId } = payload;
- const sourceRoomActivities = roomWcifFromId(state.wcifSchedule, sourceRoomId).activities;
- if (sourceRoomActivities.length === 0) return state;
- const copiedActivities = sourceRoomActivities.map(
- (activity) => copyActivity(state.wcifSchedule, activity),
- );
- const targetRoomVenueId = venueWcifFromRoomId(state.wcifSchedule, targetRoomId).id;
-
- return {
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => (venue.id === targetRoomVenueId ? {
- ...venue,
- rooms: venue.rooms.map((room) => (room.id === targetRoomId ? {
- ...room,
- activities: [...room.activities, ...copiedActivities],
- } : room)),
- } : venue)),
- },
- };
- },
};
export default function rootReducer(state, action) {
From 1b2ea1e5f91d82387fe1e6bcb9966698e6b74824 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:07:54 -0700
Subject: [PATCH 08/33] Remove unused utils from venues
---
app/webpacker/components/EditVenues/utils.js | 94 +-------------------
1 file changed, 1 insertion(+), 93 deletions(-)
diff --git a/app/webpacker/components/EditVenues/utils.js b/app/webpacker/components/EditVenues/utils.js
index 6495a75f474..6e4ec6d01a6 100644
--- a/app/webpacker/components/EditVenues/utils.js
+++ b/app/webpacker/components/EditVenues/utils.js
@@ -1,96 +1,4 @@
-import {
- addIsoDurations,
- changeTimezoneKeepingLocalTime,
- millisecondsBetween,
- moveByIsoDuration,
- rescaleIsoDuration,
-} from '../../lib/utils/edit-schedule';
-
-export const moveActivityByDuration = (activity, isoDuration) => ({
- ...activity,
- startTime: moveByIsoDuration(activity.startTime, isoDuration),
- endTime: moveByIsoDuration(activity.endTime, isoDuration),
- childActivities: activity.childActivities.map((childActivity) => (
- moveActivityByDuration(childActivity, isoDuration)
- )),
-});
-
-export const scaleActivitiesByDuration = (activity, isoDeltaStart, isoDeltaEnd) => {
- const rootActivityLengthMs = millisecondsBetween(
- activity.startTime,
- activity.endTime,
- );
-
- return ({
- ...activity,
- startTime: moveByIsoDuration(activity.startTime, isoDeltaStart),
- endTime: moveByIsoDuration(activity.endTime, isoDeltaEnd),
- childActivities: activity.childActivities.map((childActivity) => {
- // Unfortunately, scaling child activities (properly) is rocket science.
- const childActivityLengthMs = millisecondsBetween(
- childActivity.startTime,
- childActivity.endTime,
- );
-
- // Say you scale the start by -1 hour (i.e. 1 hour earlier).
- // The amount that you have to scale a child by is directly proportional
- // to the child's length. So we calculate the proportion of durations using milliseconds.
- // Say you have a parent activity with three equally sized children. In that case,
- // every child gets scaled down an equal amount, because they are all equally long.
- // If you plan your schedule with one slow group (long duration) and one fast group
- // (short duration), the fast and short group only needs to be rescaled a little bit
- // while the slow and long group gets the "lion share" of the scaling factor.
- const scalingFactor = childActivityLengthMs / rootActivityLengthMs;
-
- const childStartScale = rescaleIsoDuration(isoDeltaStart, scalingFactor);
- const childEndScale = rescaleIsoDuration(isoDeltaEnd, scalingFactor);
-
- // Of course, this all has to happen recursively because children can have children!
- const scaledChild = scaleActivitiesByDuration(
- childActivity,
- childStartScale,
- childEndScale,
- );
-
- // However, it doesn't end there. When a parent activity _scales_,
- // the child activities also have to _move_. Think of an activity "shrinking",
- // i.e. becoming shorter: Then the children also "shrink" as a result.
- // This "shrinking" will create gaps which can only be filled by the children
- // _moving_ closer together after shrinking down.
-
- const ownStartToParentStart = millisecondsBetween(
- childActivity.startTime,
- activity.startTime,
- );
-
- const ownEndToParentEnd = millisecondsBetween(
- childActivity.endTime,
- activity.endTime,
- );
-
- // Again, this growth is proportional to the size of the child activity.
- const scalingStartUp = ownStartToParentStart / rootActivityLengthMs;
- const scalingEndDown = ownEndToParentEnd / rootActivityLengthMs;
-
- // Now it gets a little bit crazy:
- // - When applying a Delta to the END of the activity, we have to move it UP
- // - When applying a Delta to the START of the activity, we have to move it DOWN
- // Think of it this way: With two subsequent child activities, removing a few minutes
- // from the END of either activity creates a gap that needs to be closed by moving
- // the second, later activity UP closer towards its predecessor.
- // The same logic applies in reverse for adding minutes instead of removing minutes.
- const moveUpwardsDuration = rescaleIsoDuration(isoDeltaEnd, scalingStartUp);
- const moveDownwardsDuration = rescaleIsoDuration(isoDeltaStart, scalingEndDown);
-
- // Both directional Deltas are added together, and it is possible that they cancel
- // each other out, most notably if DeltaStart == -DeltaEnd.
- const totalMovingDuration = addIsoDurations(moveUpwardsDuration, moveDownwardsDuration);
-
- // Phew, we're done.
- return moveActivityByDuration(scaledChild, totalMovingDuration);
- }),
- });
-};
+import { changeTimezoneKeepingLocalTime } from '../../lib/utils/edit-schedule';
export const changeActivityTimezone = (activity, oldTimezone, newTimezone) => ({
...activity,
From 2ba5e1dd5555661bce49ae1d85fa8ac72c2cab22 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:16:19 -0700
Subject: [PATCH 09/33] Update text
---
app/webpacker/components/EditVenues/EditVenues/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/webpacker/components/EditVenues/EditVenues/index.js b/app/webpacker/components/EditVenues/EditVenues/index.js
index 5fba7373db4..2bbe5c40396 100644
--- a/app/webpacker/components/EditVenues/EditVenues/index.js
+++ b/app/webpacker/components/EditVenues/EditVenues/index.js
@@ -29,7 +29,7 @@ function EditVenues({
Add a venue
- Please add all your venues and rooms below:
+ Venues
From 52ba83ba2f2ddd1f3976bf03f87a69fff5454016 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:22:38 -0700
Subject: [PATCH 10/33] Remove EditVenues component from EditSchedule
---
app/views/competitions/edit_schedule.html.erb | 1 -
.../components/EditSchedule/index.js | 68 ++-----------------
2 files changed, 6 insertions(+), 63 deletions(-)
diff --git a/app/views/competitions/edit_schedule.html.erb b/app/views/competitions/edit_schedule.html.erb
index 4faab4c0a9e..c7eae9f91a4 100644
--- a/app/views/competitions/edit_schedule.html.erb
+++ b/app/views/competitions/edit_schedule.html.erb
@@ -19,7 +19,6 @@
competitionId: @competition.id,
wcifEvents: @competition.events_wcif,
wcifSchedule: @competition.schedule_wcif,
- countryZones: @competition.country_zones,
referenceTime: @competition.start_date.to_fs,
calendarLocale: I18n.locale,
}, {
diff --git a/app/webpacker/components/EditSchedule/index.js b/app/webpacker/components/EditSchedule/index.js
index 8c4cfbfe15f..00a426305dd 100644
--- a/app/webpacker/components/EditSchedule/index.js
+++ b/app/webpacker/components/EditSchedule/index.js
@@ -2,11 +2,9 @@ import React, {
useCallback,
useEffect,
useMemo,
- useState,
} from 'react';
import {
- Accordion,
Button,
Container,
Message,
@@ -19,12 +17,10 @@ import { changesSaved } from './store/actions';
import wcifScheduleReducer from './store/reducer';
import Store, { useDispatch, useStore } from '../../lib/providers/StoreProvider';
import ConfirmProvider from '../../lib/providers/ConfirmProvider';
-import EditVenues from './EditVenues';
import EditActivities from './EditActivities';
function EditSchedule({
wcifEvents,
- countryZones,
referenceTime,
calendarLocale,
}) {
@@ -36,8 +32,6 @@ function EditSchedule({
const dispatch = useDispatch();
- const [openAccordion, setOpenAccordion] = useState(-1);
-
const unsavedChanges = useMemo(() => (
!_.isEqual(wcifSchedule, initialWcifSchedule)
), [wcifSchedule, initialWcifSchedule]);
@@ -89,69 +83,21 @@ function EditSchedule({
const renderIntroductionMessage = () => (
- Depending on the size and setup of the competition, it may take place in
- several rooms of several venues.
- Therefore a schedule is necessarily linked to a specific room.
- Each room may have its own schedule (with all or a subset of events).
- So you can start creating the competition’s schedule below by adding at
- least one venue with one room.
- Then you will be able to select this room in the "Edit schedules"
- panel, and drag and drop event rounds (or attempts for some events) on it.
-
-
- For the typical simple competition, creating one "Main venue"
- with one "Main room" is enough.
- If your competition has a single venue but multiple "stages" with different
- schedules, please input them as different rooms.
+ To create a schedule, first visit the "Manage venues" panel to add venues and rooms/stages.
);
- const handleAccordionClick = (evt, titleProps) => {
- const { index } = titleProps;
- const newIndex = openAccordion === index ? -1 : index;
-
- setOpenAccordion(newIndex);
- };
-
return (
<>
{renderIntroductionMessage()}
{unsavedChanges && renderUnsavedChangesAlert()}
-
-
- Edit venues information
-
-
-
-
-
- Edit schedules
-
-
-
-
-
+
{unsavedChanges && renderUnsavedChangesAlert()}
>
@@ -162,7 +108,6 @@ export default function Wrapper({
competitionId,
wcifEvents,
wcifSchedule,
- countryZones,
referenceTime,
calendarLocale,
}) {
@@ -178,7 +123,6 @@ export default function Wrapper({
From 78b605fe7a7e61fed5fb6841d9abcf0d51e7533f Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:23:47 -0700
Subject: [PATCH 11/33] Delete EditVenues folder from EditSchedule folder
---
.../EditSchedule/EditVenues/RoomPanel.js | 81 --------
.../EditVenues/VenueLocationMap.js | 128 ------------
.../EditSchedule/EditVenues/VenuePanel.js | 184 ------------------
.../EditSchedule/EditVenues/index.js | 51 -----
4 files changed, 444 deletions(-)
delete mode 100644 app/webpacker/components/EditSchedule/EditVenues/RoomPanel.js
delete mode 100644 app/webpacker/components/EditSchedule/EditVenues/VenueLocationMap.js
delete mode 100644 app/webpacker/components/EditSchedule/EditVenues/VenuePanel.js
delete mode 100644 app/webpacker/components/EditSchedule/EditVenues/index.js
diff --git a/app/webpacker/components/EditSchedule/EditVenues/RoomPanel.js b/app/webpacker/components/EditSchedule/EditVenues/RoomPanel.js
deleted file mode 100644
index 4946f4a03d1..00000000000
--- a/app/webpacker/components/EditSchedule/EditVenues/RoomPanel.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import React from 'react';
-import {
- Button,
- Card,
- Form,
- Icon,
- Popup,
-} from 'semantic-ui-react';
-import { useDispatch } from '../../../lib/providers/StoreProvider';
-import { useConfirm } from '../../../lib/providers/ConfirmProvider';
-import { copyRoom, editRoom, removeRoom } from '../store/actions';
-
-function RoomPanel({
- room,
-}) {
- const dispatch = useDispatch();
-
- const confirm = useConfirm();
-
- const handleChange = (evt, { name, value }) => {
- dispatch(editRoom(room.id, name, value));
- };
-
- const handleDeleteRoom = () => {
- confirm({
- content: `Are you sure you want to delete the room ${room.name}? This will also delete all associated schedules. THIS ACTION CANNOT BE UNDONE!`,
- }).then(() => dispatch(removeRoom(room.id)));
- };
-
- const handleCopyRoom = () => {
- dispatch(copyRoom(room.id));
- };
-
- return (
-
-
-
-
-
-
- )}
- />
-
-
-
- )}
- />
-
-
-
-
-
-
-
-
- );
-}
-
-export default RoomPanel;
diff --git a/app/webpacker/components/EditSchedule/EditVenues/VenueLocationMap.js b/app/webpacker/components/EditSchedule/EditVenues/VenueLocationMap.js
deleted file mode 100644
index 4bfdaffd767..00000000000
--- a/app/webpacker/components/EditSchedule/EditVenues/VenueLocationMap.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import React, {
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from 'react';
-import {
- MapContainer,
- Marker,
- Popup,
- TileLayer,
- useMap,
-} from 'react-leaflet';
-import { toDegrees, toMicrodegrees } from '../../../lib/utils/edit-schedule';
-import { userTileProvider } from '../../../lib/leaflet-wca/providers';
-import { useDispatch } from '../../../lib/providers/StoreProvider';
-import { editVenue } from '../store/actions';
-import ResizeMapIFrame from '../../../lib/utils/leaflet-iframe';
-
-function GeoSearchControl({
- onGeoSearchResult,
-}) {
- const map = useMap();
-
- useEffect(() => {
- const searchControl = window.wca.createSearchInput(map);
-
- map.on('geosearch/showlocation', onGeoSearchResult);
- map.zoomControl.setPosition('bottomright');
-
- return () => {
- map.removeControl(searchControl);
- };
- }, [map, onGeoSearchResult]);
-
- return null;
-}
-
-export function DraggableMarker({
- position,
- setPosition,
- disabled = false,
- markerRef = null,
- children,
-}) {
- const map = useMap();
-
- const updatePosition = useCallback((e) => setPosition(e, e.target.getLatLng()), [setPosition]);
-
- useEffect(() => {
- map.panTo(position);
- }, [map, position]);
-
- return (
-
- {children}
-
- );
-}
-
-function VenueLocationMap({
- venue,
-}) {
- const dispatch = useDispatch();
- const markerRef = useRef();
-
- const [searchResultPopup, setSearchResultPopup] = useState();
-
- const markerPopup = useMemo(() => {
- if (searchResultPopup) {
- return {searchResultPopup} ;
- }
-
- return null;
- }, [searchResultPopup]);
-
- const venuePosition = useMemo(() => ({
- lat: toDegrees(venue.latitudeMicrodegrees),
- lng: toDegrees(venue.longitudeMicrodegrees),
- }), [venue.latitudeMicrodegrees, venue.longitudeMicrodegrees]);
-
- const setVenuePosition = useCallback((evt, { lat, lng }) => {
- dispatch(editVenue(venue.id, 'latitudeMicrodegrees', toMicrodegrees(lat)));
- dispatch(editVenue(venue.id, 'longitudeMicrodegrees', toMicrodegrees(lng)));
- }, [dispatch, venue.id]);
-
- const provider = userTileProvider;
-
- const onGeoSearchResult = useCallback((evt) => {
- setVenuePosition(evt, {
- lat: evt.location.y,
- lng: evt.location.x,
- });
-
- setSearchResultPopup(evt.location.label);
- }, [setVenuePosition, setSearchResultPopup]);
-
- return (
-
-
-
-
-
- {markerPopup}
-
-
- );
-}
-
-export default VenueLocationMap;
diff --git a/app/webpacker/components/EditSchedule/EditVenues/VenuePanel.js b/app/webpacker/components/EditSchedule/EditVenues/VenuePanel.js
deleted file mode 100644
index cc153c561e8..00000000000
--- a/app/webpacker/components/EditSchedule/EditVenues/VenuePanel.js
+++ /dev/null
@@ -1,184 +0,0 @@
-import React, { useCallback, useMemo } from 'react';
-import {
- Button,
- Card,
- Container,
- DropdownHeader,
- Form,
- Icon,
- Image,
-} from 'semantic-ui-react';
-import _ from 'lodash';
-
-import VenueLocationMap from './VenueLocationMap';
-import { countries, backendTimezones } from '../../../lib/wca-data.js.erb';
-import RoomPanel from './RoomPanel';
-import { useDispatch } from '../../../lib/providers/StoreProvider';
-import { useConfirm } from '../../../lib/providers/ConfirmProvider';
-import {
- addRoom,
- editVenue,
- removeVenue,
-} from '../store/actions';
-import { toDegrees, toMicrodegrees } from '../../../lib/utils/edit-schedule';
-import { getTimeZoneDropdownLabel, sortByOffset } from '../../../lib/utils/timezone';
-
-const countryOptions = countries.real.map((country) => ({
- key: country.iso2,
- text: country.name,
- value: country.iso2,
- flag: country.iso2.toLowerCase(),
-}));
-
-function VenuePanel({
- venue,
- countryZones,
- referenceTime,
-}) {
- const dispatch = useDispatch();
- const confirm = useConfirm();
-
- const handleCoordinateChange = (evt, { name, value }) => {
- dispatch(editVenue(venue.id, name, toMicrodegrees(value)));
- };
-
- const handleVenueChange = (evt, { name, value }) => {
- dispatch(editVenue(venue.id, name, value));
- };
-
- const handleDeleteVenue = () => {
- confirm({
- content: `Are you sure you want to delete the venue ${venue.name}? This will also delete all associated rooms and all associated schedules. THIS ACTION CANNOT BE UNDONE!`,
- }).then(() => dispatch(removeVenue(venue.id)));
- };
-
- const handleAddRoom = () => {
- dispatch(addRoom(venue.id));
- };
-
- const getVenueTzDropdownLabel = useCallback(
- // The whole page is not localized yet, so we just hard-code US English here as well.
- (tzId) => getTimeZoneDropdownLabel(tzId, referenceTime, 'en-US'),
- [referenceTime],
- );
-
- const makeTimeZoneOption = useCallback((key) => ({
- key,
- text: getVenueTzDropdownLabel(key),
- value: key,
- }), [getVenueTzDropdownLabel]);
-
- // Instead of giving *all* TZInfo, use uniq-fied rails "meaningful" subset
- // We'll add the "country_zones" to that, because some of our competitions
- // use TZs not included in this subset.
- // We want to display the "country_zones" first, so that it's more convenient for the user.
- // In the end the array should look like that:
- // - country_zone_a, country_zone_b, [...], other_tz_a, other_tz_b, [...]
- const timezoneOptions = useMemo(() => {
- // Stuff that is recommended based on the country list
- const competitionZoneIds = _.uniq(countryZones);
- const sortedCompetitionZones = sortByOffset(competitionZoneIds, referenceTime);
-
- // Stuff that is listed in our `backendTimezones` list but not in the preferred country list
- const otherZoneIds = _.difference(backendTimezones, competitionZoneIds);
- const sortedOtherZones = sortByOffset(otherZoneIds, referenceTime);
-
- // Both merged together, with the countryZone entries listed first.
- return [
- {
- as: DropdownHeader,
- key: 'local-zones-header',
- text: 'Local time zones',
- disabled: true,
- },
- ...sortedCompetitionZones.map(makeTimeZoneOption),
- {
- as: DropdownHeader,
- key: 'other-zones-header',
- text: 'Other time zones',
- disabled: true,
- },
- ...sortedOtherZones.map(makeTimeZoneOption),
- ];
- }, [countryZones, referenceTime, makeTimeZoneOption]);
-
- return (
-
- { /* Needs the className 'image' so that SemUI fills the top of the card */ }
-
-
-
-
-
-
-
- Remove
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add room
-
- Rooms
-
-
-
- {venue.rooms.map((room) => (
-
- ))}
-
-
-
-
- );
-}
-
-export default VenuePanel;
diff --git a/app/webpacker/components/EditSchedule/EditVenues/index.js b/app/webpacker/components/EditSchedule/EditVenues/index.js
deleted file mode 100644
index 5fba7373db4..00000000000
--- a/app/webpacker/components/EditSchedule/EditVenues/index.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react';
-import {
- Button,
- Card,
- Container,
- Icon,
- Segment,
-} from 'semantic-ui-react';
-import { useDispatch, useStore } from '../../../lib/providers/StoreProvider';
-import VenuePanel from './VenuePanel';
-import { addVenue } from '../store/actions';
-
-function EditVenues({
- countryZones,
- referenceTime,
-}) {
- const { wcifSchedule } = useStore();
-
- const dispatch = useDispatch();
-
- const handleAddVenue = () => {
- dispatch(addVenue());
- };
-
- return (
-
-
-
-
- Add a venue
-
- Please add all your venues and rooms below:
-
-
-
-
- {wcifSchedule.venues.map((venue) => (
-
- ))}
-
-
-
- );
-}
-
-export default EditVenues;
From 604fb6e32b3814fa81f2a44c030501d2e3b5b247 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:26:54 -0700
Subject: [PATCH 12/33] Remove venue actions from schedule reducer
---
.../components/EditSchedule/store/actions.js | 109 -------------
.../components/EditSchedule/store/reducer.js | 148 +-----------------
2 files changed, 2 insertions(+), 255 deletions(-)
diff --git a/app/webpacker/components/EditSchedule/store/actions.js b/app/webpacker/components/EditSchedule/store/actions.js
index 660f946ba89..dbf81e76d01 100644
--- a/app/webpacker/components/EditSchedule/store/actions.js
+++ b/app/webpacker/components/EditSchedule/store/actions.js
@@ -4,14 +4,6 @@ export const EditActivity = 'EDIT_ACTIVITY';
export const RemoveActivity = 'REMOVE_ACTIVITY';
export const MoveActivity = 'MOVE_ACTIVITY';
export const ScaleActivity = 'SCALE_ACTIVITY';
-export const EditVenue = 'EDIT_VENUE';
-export const EditRoom = 'EDIT_ROOM';
-export const RemoveVenue = 'REMOVE_VENUE';
-export const RemoveRoom = 'REMOVE_ROOM';
-export const AddVenue = 'ADD_VENUE';
-export const AddRoom = 'ADD_ROOM';
-export const CopyVenue = 'COPY_VENUE';
-export const CopyRoom = 'COPY_ROOM';
export const CopyRoomActivities = 'COPY_ROOM_ACTIVITIES';
/**
@@ -103,107 +95,6 @@ export const scaleActivity = (activityId, isoDeltaStart, isoDeltaEnd, updateMatc
},
});
-/**
- * Action creator for changing a venue's properties.
- * @param {int} venueId
- * @param {string} propertyKey
- * @param {string} newProperty
- * @returns {Action}
- */
-export const editVenue = (venueId, propertyKey, newProperty) => ({
- type: EditVenue,
- payload: {
- venueId,
- propertyKey,
- newProperty,
- },
-});
-
-/**
- * Action creator for changing a room's properties.
- * @param {int} roomId
- * @param {string} propertyKey
- * @param {string} newProperty
- * @returns {Action}
- */
-export const editRoom = (roomId, propertyKey, newProperty) => ({
- type: EditRoom,
- payload: {
- roomId,
- propertyKey,
- newProperty,
- },
-});
-
-/**
- * Action creator for removing a venue.
- * @param {int} venueId
- * @returns {Action}
- */
-export const removeVenue = (venueId) => ({
- type: RemoveVenue,
- payload: {
- venueId,
- },
-});
-
-/**
- * Action creator for removing a room.
- * @param {int} roomId
- * @returns {Action}
- */
-export const removeRoom = (roomId) => ({
- type: RemoveRoom,
- payload: {
- roomId,
- },
-});
-
-/**
- * Action creator for adding a blank venue.
- * @returns {Action}
- */
-export const addVenue = () => ({
- type: AddVenue,
- payload: {},
-});
-
-/**
- * Action creator for adding a blank room.
- * @param {int} venueId
- * @returns {Action}
- */
-export const addRoom = (venueId) => ({
- type: AddRoom,
- payload: {
- venueId,
- },
-});
-
-/**
- * Action creator for copying a venue.
- * @param {int} venueId
- * @returns {Action}
- */
-export const copyVenue = (venueId) => ({
- type: CopyVenue,
- payload: {
- venueId,
- },
-});
-
-/**
- * Action creator for copying a room.
- * @param {int} roomId
- * @returns {Action}
- */
-export const copyRoom = (roomId) => ({
- type: CopyRoom,
- payload: {
- roomId,
- },
-});
-
/**
* Action creator for copying a room's activities to another room.
* @param {int} sourceRoomId
diff --git a/app/webpacker/components/EditSchedule/store/reducer.js b/app/webpacker/components/EditSchedule/store/reducer.js
index f9ba3dcb17f..51c1faa6d03 100644
--- a/app/webpacker/components/EditSchedule/store/reducer.js
+++ b/app/webpacker/components/EditSchedule/store/reducer.js
@@ -1,25 +1,17 @@
import {
AddActivity,
- AddRoom,
- AddVenue,
ChangesSaved,
- CopyRoom,
CopyRoomActivities,
- CopyVenue,
EditActivity,
- EditRoom,
- EditVenue,
MoveActivity,
RemoveActivity,
- RemoveRoom,
- RemoveVenue,
ScaleActivity,
} from './actions';
import {
- copyActivity, copyRoom, copyVenue, nextActivityId, nextRoomId, nextVenueId,
+ copyActivity, nextActivityId,
} from '../../../lib/utils/edit-schedule';
import {
- changeActivityTimezone, moveActivityByDuration, scaleActivitiesByDuration,
+ moveActivityByDuration, scaleActivitiesByDuration,
} from '../utils';
import {
activityWcifFromId,
@@ -27,7 +19,6 @@ import {
roomWcifFromId,
venueWcifFromRoomId,
} from '../../../lib/utils/wcif';
-import { defaultRoomColor } from '../../../lib/wca-data.js.erb';
const reducers = {
[ChangesSaved]: (state) => ({
@@ -149,141 +140,6 @@ const reducers = {
};
},
- [EditVenue]: (state, { payload }) => ({
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => (venue.id === payload.venueId ? {
- ...venue,
- [payload.propertyKey]: payload.newProperty,
- rooms: payload.propertyKey === 'timezone'
- ? venue.rooms.map((room) => ({
- ...room,
- activities: room.activities.map((activity) => (
- changeActivityTimezone(
- activity,
- venue.timezone,
- payload.newProperty,
- )
- )),
- }))
- : venue.rooms,
- } : venue)),
- },
- }),
-
- [EditRoom]: (state, { payload }) => ({
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => ({
- ...venue,
- rooms: venue.rooms.map((room) => (room.id === payload.roomId ? {
- ...room,
- [payload.propertyKey]: payload.newProperty,
- } : room)),
- })),
- },
- }),
-
- [RemoveVenue]: (state, { payload }) => ({
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.filter((venue) => venue.id !== payload.venueId),
- },
- }),
-
- [RemoveRoom]: (state, { payload }) => ({
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => ({
- ...venue,
- rooms: venue.rooms.filter((room) => room.id !== payload.roomId),
- })),
- },
- }),
-
- [AddVenue]: (state) => ({
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: [
- ...state.wcifSchedule.venues,
- {
- id: nextVenueId(state.wcifSchedule),
- latitudeMicrodegrees: 0,
- longitudeMicrodegrees: 0,
- rooms: [],
- extensions: [],
- },
- ],
- },
- }),
-
- [AddRoom]: (state, { payload }) => ({
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => (venue.id === payload.venueId ? {
- ...venue,
- rooms: [
- ...venue.rooms,
- {
- id: nextRoomId(state.wcifSchedule),
- color: defaultRoomColor,
- activities: [],
- extensions: [],
- },
- ],
- } : venue)),
- },
- }),
-
- [CopyVenue]: (state, { payload }) => {
- const venue = state.wcifSchedule.venues.find(({ id }) => id === payload.venueId);
- if (!venue) return state;
-
- return {
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: [
- ...state.wcifSchedule.venues,
- {
- ...copyVenue(state.wcifSchedule, venue),
- name: `Copy of ${venue.name}`,
- },
- ],
- },
- };
- },
-
- [CopyRoom]: (state, { payload }) => {
- const targetVenue = venueWcifFromRoomId(state.wcifSchedule, payload.roomId);
- if (!targetVenue) return state;
- const room = targetVenue.rooms.find(({ id }) => id === payload.roomId);
- if (!room) return state;
-
- return {
- ...state,
- wcifSchedule: {
- ...state.wcifSchedule,
- venues: state.wcifSchedule.venues.map((venue) => (venue.id === targetVenue.id ? {
- ...venue,
- rooms: [
- ...venue.rooms,
- {
- ...copyRoom(state.wcifSchedule, room),
- name: `Copy of ${room.name}`,
- },
- ],
- } : venue)),
- },
- };
- },
-
[CopyRoomActivities]: (state, { payload }) => {
const { sourceRoomId, targetRoomId } = payload;
const sourceRoomActivities = roomWcifFromId(state.wcifSchedule, sourceRoomId).activities;
From f6a2bc9bd7193f6c5b0e8712c7ade3c8749976dc Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:28:40 -0700
Subject: [PATCH 13/33] Remove unused schedule utils
---
app/webpacker/components/EditSchedule/utils.js | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/app/webpacker/components/EditSchedule/utils.js b/app/webpacker/components/EditSchedule/utils.js
index 6495a75f474..99d9407df3f 100644
--- a/app/webpacker/components/EditSchedule/utils.js
+++ b/app/webpacker/components/EditSchedule/utils.js
@@ -1,6 +1,5 @@
import {
addIsoDurations,
- changeTimezoneKeepingLocalTime,
millisecondsBetween,
moveByIsoDuration,
rescaleIsoDuration,
@@ -91,16 +90,3 @@ export const scaleActivitiesByDuration = (activity, isoDeltaStart, isoDeltaEnd)
}),
});
};
-
-export const changeActivityTimezone = (activity, oldTimezone, newTimezone) => ({
- ...activity,
- startTime: changeTimezoneKeepingLocalTime(activity.startTime, oldTimezone, newTimezone),
- endTime: changeTimezoneKeepingLocalTime(activity.endTime, oldTimezone, newTimezone),
- childActivities: activity.childActivities.map((childActivity) => (
- changeActivityTimezone(
- childActivity,
- oldTimezone,
- newTimezone,
- )
- )),
-});
From 35dc828cf89b5d4d05e2a9a39b9d9f6cf8ff88bb Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:30:17 -0700
Subject: [PATCH 14/33] Rename folders
---
.../{EditActivities => ManageActivities}/ActionsHeader.js | 0
.../{EditActivities => ManageActivities}/ActivityPicker.js | 0
.../{EditActivities => ManageActivities}/EditActivityModal.js | 0
.../{EditActivities => ManageActivities}/index.js | 0
app/webpacker/components/EditSchedule/index.js | 4 ++--
.../EditVenues/{EditVenues => ManageVenues}/RoomPanel.js | 0
.../{EditVenues => ManageVenues}/VenueLocationMap.js | 0
.../EditVenues/{EditVenues => ManageVenues}/VenuePanel.js | 0
.../EditVenues/{EditVenues => ManageVenues}/index.js | 0
app/webpacker/components/EditVenues/index.js | 4 ++--
10 files changed, 4 insertions(+), 4 deletions(-)
rename app/webpacker/components/EditSchedule/{EditActivities => ManageActivities}/ActionsHeader.js (100%)
rename app/webpacker/components/EditSchedule/{EditActivities => ManageActivities}/ActivityPicker.js (100%)
rename app/webpacker/components/EditSchedule/{EditActivities => ManageActivities}/EditActivityModal.js (100%)
rename app/webpacker/components/EditSchedule/{EditActivities => ManageActivities}/index.js (100%)
rename app/webpacker/components/EditVenues/{EditVenues => ManageVenues}/RoomPanel.js (100%)
rename app/webpacker/components/EditVenues/{EditVenues => ManageVenues}/VenueLocationMap.js (100%)
rename app/webpacker/components/EditVenues/{EditVenues => ManageVenues}/VenuePanel.js (100%)
rename app/webpacker/components/EditVenues/{EditVenues => ManageVenues}/index.js (100%)
diff --git a/app/webpacker/components/EditSchedule/EditActivities/ActionsHeader.js b/app/webpacker/components/EditSchedule/ManageActivities/ActionsHeader.js
similarity index 100%
rename from app/webpacker/components/EditSchedule/EditActivities/ActionsHeader.js
rename to app/webpacker/components/EditSchedule/ManageActivities/ActionsHeader.js
diff --git a/app/webpacker/components/EditSchedule/EditActivities/ActivityPicker.js b/app/webpacker/components/EditSchedule/ManageActivities/ActivityPicker.js
similarity index 100%
rename from app/webpacker/components/EditSchedule/EditActivities/ActivityPicker.js
rename to app/webpacker/components/EditSchedule/ManageActivities/ActivityPicker.js
diff --git a/app/webpacker/components/EditSchedule/EditActivities/EditActivityModal.js b/app/webpacker/components/EditSchedule/ManageActivities/EditActivityModal.js
similarity index 100%
rename from app/webpacker/components/EditSchedule/EditActivities/EditActivityModal.js
rename to app/webpacker/components/EditSchedule/ManageActivities/EditActivityModal.js
diff --git a/app/webpacker/components/EditSchedule/EditActivities/index.js b/app/webpacker/components/EditSchedule/ManageActivities/index.js
similarity index 100%
rename from app/webpacker/components/EditSchedule/EditActivities/index.js
rename to app/webpacker/components/EditSchedule/ManageActivities/index.js
diff --git a/app/webpacker/components/EditSchedule/index.js b/app/webpacker/components/EditSchedule/index.js
index 00a426305dd..f2f6268811f 100644
--- a/app/webpacker/components/EditSchedule/index.js
+++ b/app/webpacker/components/EditSchedule/index.js
@@ -17,7 +17,7 @@ import { changesSaved } from './store/actions';
import wcifScheduleReducer from './store/reducer';
import Store, { useDispatch, useStore } from '../../lib/providers/StoreProvider';
import ConfirmProvider from '../../lib/providers/ConfirmProvider';
-import EditActivities from './EditActivities';
+import ManageActivities from './ManageActivities';
function EditSchedule({
wcifEvents,
@@ -93,7 +93,7 @@ function EditSchedule({
{renderIntroductionMessage()}
{unsavedChanges && renderUnsavedChangesAlert()}
-
{unsavedChanges && renderUnsavedChangesAlert()}
-
From 21cf6172d698c779bde5f905751c3534050eb9f9 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:35:18 -0700
Subject: [PATCH 15/33] Remove id from EditVenues react_component
---
app/views/competitions/edit_venues.html.erb | 2 --
1 file changed, 2 deletions(-)
diff --git a/app/views/competitions/edit_venues.html.erb b/app/views/competitions/edit_venues.html.erb
index 4ce9d1dcef2..943a2ed027f 100644
--- a/app/views/competitions/edit_venues.html.erb
+++ b/app/views/competitions/edit_venues.html.erb
@@ -20,8 +20,6 @@
wcifSchedule: @competition.schedule_wcif,
countryZones: @competition.country_zones,
referenceTime: @competition.start_date.to_fs,
- }, {
- id: 'edit-schedule-area'
}) %>
<% end %>
<% end %>
From fda75f27c75826cd8c03add9eebe837a66f89bbe Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:37:29 -0700
Subject: [PATCH 16/33] Remove id from EditSchedule react_component
---
app/views/competitions/edit_schedule.html.erb | 2 --
1 file changed, 2 deletions(-)
diff --git a/app/views/competitions/edit_schedule.html.erb b/app/views/competitions/edit_schedule.html.erb
index c7eae9f91a4..7f056b0fe81 100644
--- a/app/views/competitions/edit_schedule.html.erb
+++ b/app/views/competitions/edit_schedule.html.erb
@@ -21,8 +21,6 @@
wcifSchedule: @competition.schedule_wcif,
referenceTime: @competition.start_date.to_fs,
calendarLocale: I18n.locale,
- }, {
- id: 'edit-schedule-area'
}) %>
<% end %>
<% end %>
From 4ca7687fdad9a8254693114e2b8e87169212e047 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:39:42 -0700
Subject: [PATCH 17/33] Remove edit_schedule.scss
not actually doing anything, as far as I can tell
---
app/assets/stylesheets/application.css.scss | 1 -
app/assets/stylesheets/edit_schedule.scss | 194 --------------------
2 files changed, 195 deletions(-)
delete mode 100644 app/assets/stylesheets/edit_schedule.scss
diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss
index 218aedbf5f0..2b4ab0044ba 100644
--- a/app/assets/stylesheets/application.css.scss
+++ b/app/assets/stylesheets/application.css.scss
@@ -50,7 +50,6 @@
@import "server_status";
@import "static_pages";
@import "edit_events";
-@import "edit_schedule";
@import "media";
@import "incidents";
@import "translations";
diff --git a/app/assets/stylesheets/edit_schedule.scss b/app/assets/stylesheets/edit_schedule.scss
deleted file mode 100644
index 9673da238b2..00000000000
--- a/app/assets/stylesheets/edit_schedule.scss
+++ /dev/null
@@ -1,194 +0,0 @@
-#edit-schedule-area {
- .panel-primary {
- .collapse-indicator {
- color: #fff;
- }
- }
-
- #venues-edit-panel {
- .leaflet-container {
- height: 300px;
- }
- .venue-form-label {
- @extend label;
- }
- .panel-venue {
- margin-bottom: 15px;
- .new-venue-link {
- width: 100%;
- }
- .venue-title {
- padding-top: 7px;
- }
- .panel-body {
- .row {
- margin-bottom: 10px;
- .room-row {
- margin-bottom: 20px;
- .room-color-cell {
- padding-top: 5px;
- }
- }
- }
- }
- }
- }
-
- #schedules-edit-panel {
- #activity-picker-panel {
- &.affix {
- top: 10px;
- }
- &.affix-bottom {
- position: absolute;
- bottom: auto;
- }
- .selected-activity {
- border: 3px solid $selected-activity-border-color;
- margin-top: 1px;
- margin-bottom: 1px;
- }
- .panel-heading {
- @extend .text-center;
- }
- .panel-body {
- padding: 5px;
- overflow-y: auto;
- }
- .event-picker-line {
- .row {
- min-height: 32px;
- .activity-icon {
- padding: 0;
- .cubing-icon {
- font-size: 22px;
- &::before {
- margin-top: 2px;
- }
- }
- }
- .activity-in-picker {
- padding: 0;
- }
- }
- }
- }
- .room-selector {
- label {
- padding-top: 7px;
- }
- }
- #schedule-editor {
- #schedule-menu {
- width: 150px;
- position: fixed;
- top: 100px;
- left: 100px;
- display: block;
- &.hide-element {
- display: none;
- }
- &.delete-only {
- .edit-option {
- display: none;
- }
- }
- a {
- padding: 3px 20px;
- i {
- margin-right: 10px;
- }
- }
- }
-
- #schedule-calendar {
- &:target {
- // Override the flashy yellow we use for targets elsewhere
- background-color: transparent;
- }
-
- // Events get added to 'fc-helper-container' when they are dragged/resized,
- // so we need to consider any event there as selected.
- .fc-helper-container > .fc-event,
- .fc-event.selected-fc-event {
- border: 2px solid $selected-activity-border-color;
- }
-
- }
- #drop-event-area {
- z-index: 0;
-
- display: flex;
- align-items: center;
- justify-content: space-around;
- padding: 10px;
-
- margin-bottom: 15px;
- border-radius: 0.25em;
- border: 2px dashed #a94442;
- &.event-on-top {
- border-color: #f2dede;
- color: #f2dede;
- background-color: #a94442;
- }
- }
- }
- }
-}
-
-#tooltip-enable-keyboard {
- .tooltip-inner {
- max-width: none;
- }
-}
-
-#calendar-settings-popover {
- .setting-label {
- padding-top: 7px;
- padding-right: 5px;
- padding-left: 0;
- font-size: 12px;
- text-align: right;
- }
- select {
- @extend .input-sm;
- margin-bottom: 5px;
- }
-}
-
-// This needs to be here, to overcome an issue with draggable activity + overflow container
-.schedule-activity {
- background-color: $activity-bg-color;
- color: $activity-text-color;
- font-weight: bold;
- border-radius: 0.25em;
- margin: 3px 2px;
- padding-left: 2px;
- padding-right: 2px;
- text-align: center;
- cursor: pointer;
- z-index: 3;
- &.activity-used {
- opacity: 0.5;
- }
-}
-
-// The popover gets appended to , so we can't nest this style
-#calendar-help-popover {
- width: 400px;
- dl {
- dt {
- margin-bottom: 10px;
- text-align: right;
- padding-right: 0;
- padding-left: 5px;
- &::after {
- content: ":";
- }
- }
- dd {
- margin-bottom: 10px;
- padding-left: 5px;
- }
- }
-}
From aff6d0df94fb68d43e7fcc7ee38b338f1ac14540 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 16:50:06 -0700
Subject: [PATCH 18/33] Rename new component to EditVenues
---
app/webpacker/components/EditVenues/index.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/webpacker/components/EditVenues/index.js b/app/webpacker/components/EditVenues/index.js
index 6d19e0d4aaf..0a00d83406c 100644
--- a/app/webpacker/components/EditVenues/index.js
+++ b/app/webpacker/components/EditVenues/index.js
@@ -19,7 +19,7 @@ import Store, { useDispatch, useStore } from '../../lib/providers/StoreProvider'
import ConfirmProvider from '../../lib/providers/ConfirmProvider';
import ManageVenues from './ManageVenues';
-function EditSchedule({
+function EditVenues({
countryZones,
referenceTime,
}) {
@@ -131,7 +131,7 @@ export default function Wrapper({
}}
>
-
From 66333fd48d1132f6cf6c6018aafeccc115aad6ee Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Fri, 4 Oct 2024 17:18:45 -0700
Subject: [PATCH 19/33] eslint
---
app/webpacker/components/EditSchedule/index.js | 13 +++++++------
.../components/EditVenues/store/reducer.js | 2 +-
app/webpacker/components/EditVenues/utils.js | 4 +++-
3 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/app/webpacker/components/EditSchedule/index.js b/app/webpacker/components/EditSchedule/index.js
index f2f6268811f..62417d74984 100644
--- a/app/webpacker/components/EditSchedule/index.js
+++ b/app/webpacker/components/EditSchedule/index.js
@@ -83,7 +83,8 @@ function EditSchedule({
const renderIntroductionMessage = () => (
- To create a schedule, first visit the "Manage venues" panel to add venues and rooms/stages.
+ To create a schedule, first visit the "Manage venues" panel to add venues and
+ rooms/stages.
);
@@ -93,11 +94,11 @@ function EditSchedule({
{renderIntroductionMessage()}
{unsavedChanges && renderUnsavedChangesAlert()}
-
+
{unsavedChanges && renderUnsavedChangesAlert()}
>
diff --git a/app/webpacker/components/EditVenues/store/reducer.js b/app/webpacker/components/EditVenues/store/reducer.js
index b1c81d587bd..6d4800a77eb 100644
--- a/app/webpacker/components/EditVenues/store/reducer.js
+++ b/app/webpacker/components/EditVenues/store/reducer.js
@@ -12,7 +12,7 @@ import {
import {
copyRoom, copyVenue, nextRoomId, nextVenueId,
} from '../../../lib/utils/edit-schedule';
-import { changeActivityTimezone } from '../utils';
+import changeActivityTimezone from '../utils';
import { venueWcifFromRoomId } from '../../../lib/utils/wcif';
import { defaultRoomColor } from '../../../lib/wca-data.js.erb';
diff --git a/app/webpacker/components/EditVenues/utils.js b/app/webpacker/components/EditVenues/utils.js
index 6e4ec6d01a6..638b64d9ab0 100644
--- a/app/webpacker/components/EditVenues/utils.js
+++ b/app/webpacker/components/EditVenues/utils.js
@@ -1,6 +1,6 @@
import { changeTimezoneKeepingLocalTime } from '../../lib/utils/edit-schedule';
-export const changeActivityTimezone = (activity, oldTimezone, newTimezone) => ({
+const changeActivityTimezone = (activity, oldTimezone, newTimezone) => ({
...activity,
startTime: changeTimezoneKeepingLocalTime(activity.startTime, oldTimezone, newTimezone),
endTime: changeTimezoneKeepingLocalTime(activity.endTime, oldTimezone, newTimezone),
@@ -12,3 +12,5 @@ export const changeActivityTimezone = (activity, oldTimezone, newTimezone) => ({
)
)),
});
+
+export default changeActivityTimezone;
From c6475428969cd791b8dd7392ffc2f6f57e3869e6 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Sat, 5 Oct 2024 11:46:57 -0700
Subject: [PATCH 20/33] Rename RSpec.feature
---
spec/features/competition_manage_schedule_spec.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spec/features/competition_manage_schedule_spec.rb b/spec/features/competition_manage_schedule_spec.rb
index 0db74ec84c1..6404a3a36e9 100644
--- a/spec/features/competition_manage_schedule_spec.rb
+++ b/spec/features/competition_manage_schedule_spec.rb
@@ -2,7 +2,7 @@
require "rails_helper"
-RSpec.feature "Competition events management" do
+RSpec.feature "Competition schedule management" do
before :each do
# Enable CSRF protection just for these tests.
# See https://blog.tomoyukikashiro.me/post/test-csrf-in-feature-test-using-capybara/
From ba28af6af65496207882b6dcfa3866ad639ef72f Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Sat, 5 Oct 2024 11:48:29 -0700
Subject: [PATCH 21/33] Duplicate manage schedule spec for manage venues
---
.../competition_manage_venues_spec.rb | 68 +++++++++++++++++++
1 file changed, 68 insertions(+)
create mode 100644 spec/features/competition_manage_venues_spec.rb
diff --git a/spec/features/competition_manage_venues_spec.rb b/spec/features/competition_manage_venues_spec.rb
new file mode 100644
index 00000000000..312744fe327
--- /dev/null
+++ b/spec/features/competition_manage_venues_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.feature "Competition venues management" do
+ before :each do
+ # Enable CSRF protection just for these tests.
+ # See https://blog.tomoyukikashiro.me/post/test-csrf-in-feature-test-using-capybara/
+ allow_any_instance_of(ActionController::Base).to receive(:protect_against_forgery?).and_return(true)
+ end
+
+ context "unconfirmed competition without schedule" do
+ let!(:competition) { FactoryBot.create(:competition, :with_delegate, :registration_open, event_ids: ["333", "444"], with_rounds: true) }
+ background do
+ sign_in competition.delegates.first
+ visit "/competitions/#{competition.id}/schedule/edit"
+ end
+
+ scenario "can add a venue and a room", js: true do
+ find("div", class: 'title', text: 'Edit venues information').click
+
+ within(:css, "#venues-edit-panel-body") do
+ click_button "Add a venue"
+ fill_in("venue-name", with: "Venue")
+ click_button "Add room"
+ fill_in("room-name", with: "Youpitralala")
+ within(:css, "div[name='timezone'][role='listbox']>div.menu", visible: :all) do
+ # Using a timezone that does not follow Daylight Savings, so that we get consistent results all year round
+ find("div", class: "item", text: "Asia/Tokyo (Japan Standard Time, UTC+9)", visible: :all).trigger(:click)
+ end
+ within(:css, "div[name='countryIso2'][role='combobox']>div.menu[role='listbox']", visible: :all) do
+ find("div", class: "item", text: "United States", visible: :all).trigger(:click)
+ end
+ end
+
+ save_schedule_react
+
+ expect(competition.competition_venues.map(&:name)).to match_array %w(Venue)
+ expect(competition.competition_venues.flat_map(&:venue_rooms).map(&:name)).to match_array %w(Youpitralala)
+ end
+ end
+
+ context "unconfirmed competition with schedule" do
+ let!(:competition) { FactoryBot.create(:competition, :with_delegate, :registration_open, :with_valid_schedule, event_ids: ["333", "444"]) }
+ background do
+ sign_in competition.delegates.first
+ visit "/competitions/#{competition.id}/schedule/edit"
+ end
+
+ scenario "room calendar is rendered", js: true do
+ find("div", class: 'title', text: 'Edit schedules').click
+
+ within(:css, "#schedules-edit-panel-body") do
+ # click_link doesn't work because Capybara expects links to always have an href
+ find("a", class: 'item', text: "Room 1 for venue 1").click
+ # 2 is the number of non-nested activities created by the factory
+ # Nested activity are not supported (yet) in the schedule manager
+ expect(all('.fc-event').size).to eq(2)
+ end
+ end
+ end
+end
+
+def save_schedule_react
+ first(:button, "save your changes!", visible: true).click
+ # Wait for ajax to complete.
+ expect(page).to have_no_content("You have unsaved changes")
+end
From 2850f2420e27cec0ac280f0c86aefbd92abf103e Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Sat, 5 Oct 2024 11:50:07 -0700
Subject: [PATCH 22/33] Remove venue tests from schedule spec
---
.../competition_manage_schedule_spec.rb | 31 -------------------
1 file changed, 31 deletions(-)
diff --git a/spec/features/competition_manage_schedule_spec.rb b/spec/features/competition_manage_schedule_spec.rb
index 6404a3a36e9..da8c8a0cfaf 100644
--- a/spec/features/competition_manage_schedule_spec.rb
+++ b/spec/features/competition_manage_schedule_spec.rb
@@ -9,37 +9,6 @@
allow_any_instance_of(ActionController::Base).to receive(:protect_against_forgery?).and_return(true)
end
- context "unconfirmed competition without schedule" do
- let!(:competition) { FactoryBot.create(:competition, :with_delegate, :registration_open, event_ids: ["333", "444"], with_rounds: true) }
- background do
- sign_in competition.delegates.first
- visit "/competitions/#{competition.id}/schedule/edit"
- end
-
- scenario "can add a venue and a room", js: true do
- find("div", class: 'title', text: 'Edit venues information').click
-
- within(:css, "#venues-edit-panel-body") do
- click_button "Add a venue"
- fill_in("venue-name", with: "Venue")
- click_button "Add room"
- fill_in("room-name", with: "Youpitralala")
- within(:css, "div[name='timezone'][role='listbox']>div.menu", visible: :all) do
- # Using a timezone that does not follow Daylight Savings, so that we get consistent results all year round
- find("div", class: "item", text: "Asia/Tokyo (Japan Standard Time, UTC+9)", visible: :all).trigger(:click)
- end
- within(:css, "div[name='countryIso2'][role='combobox']>div.menu[role='listbox']", visible: :all) do
- find("div", class: "item", text: "United States", visible: :all).trigger(:click)
- end
- end
-
- save_schedule_react
-
- expect(competition.competition_venues.map(&:name)).to match_array %w(Venue)
- expect(competition.competition_venues.flat_map(&:venue_rooms).map(&:name)).to match_array %w(Youpitralala)
- end
- end
-
context "unconfirmed competition with schedule" do
let!(:competition) { FactoryBot.create(:competition, :with_delegate, :registration_open, :with_valid_schedule, event_ids: ["333", "444"]) }
background do
From bdb801cd434bf7743e47bb47a0f13fca7b5f98ac Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Sat, 5 Oct 2024 11:51:06 -0700
Subject: [PATCH 23/33] Remove schedule tests from venues spec
---
.../competition_manage_venues_spec.rb | 23 ++-----------------
1 file changed, 2 insertions(+), 21 deletions(-)
diff --git a/spec/features/competition_manage_venues_spec.rb b/spec/features/competition_manage_venues_spec.rb
index 312744fe327..537d52f0639 100644
--- a/spec/features/competition_manage_venues_spec.rb
+++ b/spec/features/competition_manage_venues_spec.rb
@@ -33,35 +33,16 @@
end
end
- save_schedule_react
+ save_venues_react
expect(competition.competition_venues.map(&:name)).to match_array %w(Venue)
expect(competition.competition_venues.flat_map(&:venue_rooms).map(&:name)).to match_array %w(Youpitralala)
end
end
- context "unconfirmed competition with schedule" do
- let!(:competition) { FactoryBot.create(:competition, :with_delegate, :registration_open, :with_valid_schedule, event_ids: ["333", "444"]) }
- background do
- sign_in competition.delegates.first
- visit "/competitions/#{competition.id}/schedule/edit"
- end
-
- scenario "room calendar is rendered", js: true do
- find("div", class: 'title', text: 'Edit schedules').click
-
- within(:css, "#schedules-edit-panel-body") do
- # click_link doesn't work because Capybara expects links to always have an href
- find("a", class: 'item', text: "Room 1 for venue 1").click
- # 2 is the number of non-nested activities created by the factory
- # Nested activity are not supported (yet) in the schedule manager
- expect(all('.fc-event').size).to eq(2)
- end
- end
- end
end
-def save_schedule_react
+def save_venues_react
first(:button, "save your changes!", visible: true).click
# Wait for ajax to complete.
expect(page).to have_no_content("You have unsaved changes")
From dbaac69d371a54de44b3516a4c59454837531817 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Sat, 5 Oct 2024 11:52:43 -0700
Subject: [PATCH 24/33] Remove clicking accord from tests
since it doesn't exist anymore
---
spec/features/competition_manage_schedule_spec.rb | 2 --
spec/features/competition_manage_venues_spec.rb | 2 --
2 files changed, 4 deletions(-)
diff --git a/spec/features/competition_manage_schedule_spec.rb b/spec/features/competition_manage_schedule_spec.rb
index da8c8a0cfaf..a011b54bcbb 100644
--- a/spec/features/competition_manage_schedule_spec.rb
+++ b/spec/features/competition_manage_schedule_spec.rb
@@ -17,8 +17,6 @@
end
scenario "room calendar is rendered", js: true do
- find("div", class: 'title', text: 'Edit schedules').click
-
within(:css, "#schedules-edit-panel-body") do
# click_link doesn't work because Capybara expects links to always have an href
find("a", class: 'item', text: "Room 1 for venue 1").click
diff --git a/spec/features/competition_manage_venues_spec.rb b/spec/features/competition_manage_venues_spec.rb
index 537d52f0639..db222c078dd 100644
--- a/spec/features/competition_manage_venues_spec.rb
+++ b/spec/features/competition_manage_venues_spec.rb
@@ -17,8 +17,6 @@
end
scenario "can add a venue and a room", js: true do
- find("div", class: 'title', text: 'Edit venues information').click
-
within(:css, "#venues-edit-panel-body") do
click_button "Add a venue"
fill_in("venue-name", with: "Venue")
From ce584894c705f57f175ef1773e1d91c2a6a0b1b2 Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Sat, 5 Oct 2024 11:55:02 -0700
Subject: [PATCH 25/33] Remove extra empty line
---
spec/features/competition_manage_venues_spec.rb | 1 -
1 file changed, 1 deletion(-)
diff --git a/spec/features/competition_manage_venues_spec.rb b/spec/features/competition_manage_venues_spec.rb
index db222c078dd..8576008cb0a 100644
--- a/spec/features/competition_manage_venues_spec.rb
+++ b/spec/features/competition_manage_venues_spec.rb
@@ -37,7 +37,6 @@
expect(competition.competition_venues.flat_map(&:venue_rooms).map(&:name)).to match_array %w(Youpitralala)
end
end
-
end
def save_venues_react
From 8f0a4f64d7b9388e6046cc6e3e382fb7fba659de Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Sun, 6 Oct 2024 11:45:03 -0700
Subject: [PATCH 26/33] Rename components to match folder name
---
.../components/EditSchedule/ManageActivities/index.js | 4 ++--
app/webpacker/components/EditVenues/ManageVenues/index.js | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/webpacker/components/EditSchedule/ManageActivities/index.js b/app/webpacker/components/EditSchedule/ManageActivities/index.js
index e2edc39abb3..a6985119277 100644
--- a/app/webpacker/components/EditSchedule/ManageActivities/index.js
+++ b/app/webpacker/components/EditSchedule/ManageActivities/index.js
@@ -54,7 +54,7 @@ import ActionsHeader from './ActionsHeader';
import { getTimeZoneDropdownLabel } from '../../../lib/utils/timezone';
import { earliestTimeOfDayWithBuffer } from '../../../lib/utils/activities';
-function EditActivities({
+function ManageActivities({
wcifEvents,
referenceTime,
calendarLocale,
@@ -504,4 +504,4 @@ function EditActivities({
);
}
-export default EditActivities;
+export default ManageActivities;
diff --git a/app/webpacker/components/EditVenues/ManageVenues/index.js b/app/webpacker/components/EditVenues/ManageVenues/index.js
index 2bbe5c40396..6451455dac0 100644
--- a/app/webpacker/components/EditVenues/ManageVenues/index.js
+++ b/app/webpacker/components/EditVenues/ManageVenues/index.js
@@ -10,7 +10,7 @@ import { useDispatch, useStore } from '../../../lib/providers/StoreProvider';
import VenuePanel from './VenuePanel';
import { addVenue } from '../store/actions';
-function EditVenues({
+function ManageVenues({
countryZones,
referenceTime,
}) {
@@ -48,4 +48,4 @@ function EditVenues({
);
}
-export default EditVenues;
+export default ManageVenues;
From a2a4934a8f210a9ddbf0865bc16f0cc89bc8390b Mon Sep 17 00:00:00 2001
From: Kevin Matthews
Date: Sun, 6 Oct 2024 11:45:32 -0700
Subject: [PATCH 27/33] Fix url in manage venues spec
---
spec/features/competition_manage_venues_spec.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spec/features/competition_manage_venues_spec.rb b/spec/features/competition_manage_venues_spec.rb
index 8576008cb0a..1eff31bd8aa 100644
--- a/spec/features/competition_manage_venues_spec.rb
+++ b/spec/features/competition_manage_venues_spec.rb
@@ -13,7 +13,7 @@
let!(:competition) { FactoryBot.create(:competition, :with_delegate, :registration_open, event_ids: ["333", "444"], with_rounds: true) }
background do
sign_in competition.delegates.first
- visit "/competitions/#{competition.id}/schedule/edit"
+ visit "/competitions/#{competition.id}/venues/edit"
end
scenario "can add a venue and a room", js: true do
From d715d9314dc845eb1b74b0ff2c66a63cc8b43dc5 Mon Sep 17 00:00:00 2001
From: Gregor Billing
Date: Mon, 13 Jan 2025 20:42:51 +0900
Subject: [PATCH 28/33] Add backend param to prevent schedule updates in WCIF
patch
---
app/controllers/api/v0/competitions_controller.rb | 6 ++++--
app/models/competition.rb | 8 ++++----
app/models/competition_venue.rb | 4 ++--
app/models/venue_room.rb | 12 +++++++-----
4 files changed, 17 insertions(+), 13 deletions(-)
diff --git a/app/controllers/api/v0/competitions_controller.rb b/app/controllers/api/v0/competitions_controller.rb
index 9bbba881f6e..ec586018ce9 100644
--- a/app/controllers/api/v0/competitions_controller.rb
+++ b/app/controllers/api/v0/competitions_controller.rb
@@ -166,9 +166,11 @@ def show_wcif_public
def update_wcif
competition = competition_from_params
require_can_manage!(competition)
- wcif = params.permit!.to_h
+ skip_schedule = params[:'skip-schedule']
+ skip_schedule = ActiveRecord::Type::Boolean.new.cast(skip_schedule)
+ wcif = params.except("skip-schedule").permit!.to_h
wcif = wcif["_json"] || wcif
- competition.set_wcif!(wcif, require_user!)
+ competition.set_wcif!(wcif, require_user!, skip_schedule: skip_schedule)
render json: {
status: "Successfully saved WCIF",
}
diff --git a/app/models/competition.rb b/app/models/competition.rb
index 67a4989a041..81cee3c1711 100644
--- a/app/models/competition.rb
+++ b/app/models/competition.rb
@@ -1963,13 +1963,13 @@ def schedule_wcif
}
end
- def set_wcif!(wcif, current_user)
+ def set_wcif!(wcif, current_user, skip_schedule: false)
JSON::Validator.validate!(Competition.wcif_json_schema, wcif)
ActiveRecord::Base.transaction do
set_wcif_series!(wcif["series"], current_user) if wcif["series"]
set_wcif_events!(wcif["events"], current_user) if wcif["events"]
- set_wcif_schedule!(wcif["schedule"], current_user) if wcif["schedule"]
+ set_wcif_schedule!(wcif["schedule"], current_user, skip_schedule: skip_schedule) if wcif["schedule"]
update_persons_wcif!(wcif["persons"], current_user) if wcif["persons"]
WcifExtension.update_wcif_extensions!(self, wcif["extensions"]) if wcif["extensions"]
set_wcif_competitor_limit!(wcif["competitorLimit"], current_user) if wcif["competitorLimit"]
@@ -2134,7 +2134,7 @@ def update_persons_wcif!(wcif_persons, current_user)
Assignment.upsert_all(new_assignments) if new_assignments.any?
end
- def set_wcif_schedule!(wcif_schedule, current_user)
+ def set_wcif_schedule!(wcif_schedule, current_user, skip_schedule: false)
if wcif_schedule["startDate"] != start_date.strftime("%F")
raise WcaExceptions::BadApiParameter.new("Wrong start date for competition")
elsif wcif_schedule["numberOfDays"] != number_of_days
@@ -2153,7 +2153,7 @@ def set_wcif_schedule!(wcif_schedule, current_user)
# using this find instead of ActiveRecord's find_or_create_by avoid several queries
# (despite having the association included :()
venue = competition_venues.find { |v| v.wcif_id == venue_wcif["id"] } || competition_venues.build
- venue.load_wcif!(venue_wcif)
+ venue.load_wcif!(venue_wcif, skip_schedule: skip_schedule)
end
self.competition_venues = new_venues
diff --git a/app/models/competition_venue.rb b/app/models/competition_venue.rb
index 21aec40fd93..7b8dbc784f6 100644
--- a/app/models/competition_venue.rb
+++ b/app/models/competition_venue.rb
@@ -22,11 +22,11 @@ def country
Country.find_by_iso2(self.country_iso2)
end
- def load_wcif!(wcif)
+ def load_wcif!(wcif, skip_schedule: false)
update!(CompetitionVenue.wcif_to_attributes(wcif))
new_rooms = wcif["rooms"].map do |room_wcif|
room = venue_rooms.find { |r| r.wcif_id == room_wcif["id"] } || venue_rooms.build
- room.load_wcif!(room_wcif)
+ room.load_wcif!(room_wcif, skip_schedule: skip_schedule)
end
self.venue_rooms = new_rooms
WcifExtension.update_wcif_extensions!(self, wcif["extensions"]) if wcif["extensions"]
diff --git a/app/models/venue_room.rb b/app/models/venue_room.rb
index d6f5152d693..8c337cfb15a 100644
--- a/app/models/venue_room.rb
+++ b/app/models/venue_room.rb
@@ -53,13 +53,15 @@ def self.wcif_json_schema
}
end
- def load_wcif!(wcif)
+ def load_wcif!(wcif, skip_schedule: false)
update!(VenueRoom.wcif_to_attributes(wcif))
- new_activities = wcif["activities"].map do |activity_wcif|
- activity = schedule_activities.find { |a| a.wcif_id == activity_wcif["id"] } || schedule_activities.build
- activity.load_wcif!(activity_wcif)
+ unless skip_schedule
+ new_activities = wcif["activities"].map do |activity_wcif|
+ activity = schedule_activities.find { |a| a.wcif_id == activity_wcif["id"] } || schedule_activities.build
+ activity.load_wcif!(activity_wcif)
+ end
+ self.schedule_activities = new_activities
end
- self.schedule_activities = new_activities
WcifExtension.update_wcif_extensions!(self, wcif["extensions"]) if wcif["extensions"]
self
end
From 66cf8c6e4693801f4d87ca2c63bb5092c6cfc13f Mon Sep 17 00:00:00 2001
From: Gregor Billing
Date: Mon, 13 Jan 2025 20:48:21 +0900
Subject: [PATCH 29/33] Pull through skip_schedule param in frontend
---
app/webpacker/components/EditVenues/index.js | 2 +-
app/webpacker/lib/requests/routes.js.erb | 2 ++
app/webpacker/lib/utils/wcif.js | 7 ++++---
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/app/webpacker/components/EditVenues/index.js b/app/webpacker/components/EditVenues/index.js
index 0a00d83406c..c49805e42a3 100644
--- a/app/webpacker/components/EditVenues/index.js
+++ b/app/webpacker/components/EditVenues/index.js
@@ -54,7 +54,7 @@ function EditVenues({
};
}, [onUnload]);
- const { saveWcif, saving } = useSaveWcifAction();
+ const { saveWcif, saving } = useSaveWcifAction(true);
const save = useCallback(() => {
saveWcif(
diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb
index 8430672929e..a22056b138f 100644
--- a/app/webpacker/lib/requests/routes.js.erb
+++ b/app/webpacker/lib/requests/routes.js.erb
@@ -298,3 +298,5 @@ export const updateRegistrationUrl = `<%= CGI.unescape(Rails.application.routes.
export const bulkUpdateRegistrationUrl = `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v1_registrations_bulk_update_path) %>`;
export const paymentTicketUrl = (competitionId, donationIso) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v1_registrations_payment_ticket_path(competition_id: "${competitionId}", donation_iso: "${donationIso}")) %>`;
+
+export const patchWcifUrl = (competitionId, skipSchedule = false) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v0_competition_update_wcif_path(competition_id: "${competitionId}")) %>?skip-schedule=${skipSchedule}`;
diff --git a/app/webpacker/lib/utils/wcif.js b/app/webpacker/lib/utils/wcif.js
index 5e8a483f081..c66fba9e4c5 100644
--- a/app/webpacker/lib/utils/wcif.js
+++ b/app/webpacker/lib/utils/wcif.js
@@ -5,8 +5,9 @@ import I18n from '../i18n';
import { attemptResultToString, attemptResultToMbPoints } from './edit-events';
import useSaveAction from '../hooks/useSaveAction';
import { centisecondsToClockFormat } from '../wca-live/attempts';
+import { patchWcifUrl } from '../requests/routes.js.erb';
-export function useSaveWcifAction() {
+export function useSaveWcifAction(skipSchedule = false) {
const { save, saving } = useSaveAction();
const alertWcifError = (err) => {
@@ -22,11 +23,11 @@ export function useSaveWcifAction() {
options = {},
onError = alertWcifError,
) => {
- const url = `/api/v0/competitions/${competitionId}/wcif`;
+ const url = patchWcifUrl(competitionId, skipSchedule);
save(url, wcifData, onSuccess, options, onError);
},
- [save],
+ [save, skipSchedule],
);
return {
From 2f8af2454b20475d1eecdf6bb3f5090e58e5ab0e Mon Sep 17 00:00:00 2001
From: Gregor Billing
Date: Mon, 13 Jan 2025 20:57:30 +0900
Subject: [PATCH 30/33] Add another parameter for venue details
---
app/controllers/api/v0/competitions_controller.rb | 6 ++++--
app/models/competition.rb | 8 ++++----
app/models/competition_venue.rb | 6 +++---
app/models/venue_room.rb | 4 ++--
4 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/app/controllers/api/v0/competitions_controller.rb b/app/controllers/api/v0/competitions_controller.rb
index ec586018ce9..7d8d41545e3 100644
--- a/app/controllers/api/v0/competitions_controller.rb
+++ b/app/controllers/api/v0/competitions_controller.rb
@@ -168,9 +168,11 @@ def update_wcif
require_can_manage!(competition)
skip_schedule = params[:'skip-schedule']
skip_schedule = ActiveRecord::Type::Boolean.new.cast(skip_schedule)
- wcif = params.except("skip-schedule").permit!.to_h
+ skip_venue_details = params[:'skip-venue-details']
+ skip_venue_details = ActiveRecord::Type::Boolean.new.cast(skip_venue_details)
+ wcif = params.except("skip-schedule", "skip-venue-details").permit!.to_h
wcif = wcif["_json"] || wcif
- competition.set_wcif!(wcif, require_user!, skip_schedule: skip_schedule)
+ competition.set_wcif!(wcif, require_user!, skip_schedule: skip_schedule, skip_venue_details: skip_venue_details)
render json: {
status: "Successfully saved WCIF",
}
diff --git a/app/models/competition.rb b/app/models/competition.rb
index 81cee3c1711..b6d1bc198a2 100644
--- a/app/models/competition.rb
+++ b/app/models/competition.rb
@@ -1963,13 +1963,13 @@ def schedule_wcif
}
end
- def set_wcif!(wcif, current_user, skip_schedule: false)
+ def set_wcif!(wcif, current_user, skip_schedule: false, skip_venue_details: false)
JSON::Validator.validate!(Competition.wcif_json_schema, wcif)
ActiveRecord::Base.transaction do
set_wcif_series!(wcif["series"], current_user) if wcif["series"]
set_wcif_events!(wcif["events"], current_user) if wcif["events"]
- set_wcif_schedule!(wcif["schedule"], current_user, skip_schedule: skip_schedule) if wcif["schedule"]
+ set_wcif_schedule!(wcif["schedule"], current_user, skip_schedule: skip_schedule, skip_venue_details: skip_venue_details) if wcif["schedule"]
update_persons_wcif!(wcif["persons"], current_user) if wcif["persons"]
WcifExtension.update_wcif_extensions!(self, wcif["extensions"]) if wcif["extensions"]
set_wcif_competitor_limit!(wcif["competitorLimit"], current_user) if wcif["competitorLimit"]
@@ -2134,7 +2134,7 @@ def update_persons_wcif!(wcif_persons, current_user)
Assignment.upsert_all(new_assignments) if new_assignments.any?
end
- def set_wcif_schedule!(wcif_schedule, current_user, skip_schedule: false)
+ def set_wcif_schedule!(wcif_schedule, current_user, skip_schedule: false, skip_venue_details: false)
if wcif_schedule["startDate"] != start_date.strftime("%F")
raise WcaExceptions::BadApiParameter.new("Wrong start date for competition")
elsif wcif_schedule["numberOfDays"] != number_of_days
@@ -2153,7 +2153,7 @@ def set_wcif_schedule!(wcif_schedule, current_user, skip_schedule: false)
# using this find instead of ActiveRecord's find_or_create_by avoid several queries
# (despite having the association included :()
venue = competition_venues.find { |v| v.wcif_id == venue_wcif["id"] } || competition_venues.build
- venue.load_wcif!(venue_wcif, skip_schedule: skip_schedule)
+ venue.load_wcif!(venue_wcif, skip_schedule: skip_schedule, skip_venue_details: skip_venue_details)
end
self.competition_venues = new_venues
diff --git a/app/models/competition_venue.rb b/app/models/competition_venue.rb
index 7b8dbc784f6..5c7c95d64dc 100644
--- a/app/models/competition_venue.rb
+++ b/app/models/competition_venue.rb
@@ -22,11 +22,11 @@ def country
Country.find_by_iso2(self.country_iso2)
end
- def load_wcif!(wcif, skip_schedule: false)
- update!(CompetitionVenue.wcif_to_attributes(wcif))
+ def load_wcif!(wcif, skip_schedule: false, skip_venue_details: false)
+ update!(CompetitionVenue.wcif_to_attributes(wcif)) unless skip_venue_details
new_rooms = wcif["rooms"].map do |room_wcif|
room = venue_rooms.find { |r| r.wcif_id == room_wcif["id"] } || venue_rooms.build
- room.load_wcif!(room_wcif, skip_schedule: skip_schedule)
+ room.load_wcif!(room_wcif, skip_schedule: skip_schedule, skip_venue_details: skip_venue_details)
end
self.venue_rooms = new_rooms
WcifExtension.update_wcif_extensions!(self, wcif["extensions"]) if wcif["extensions"]
diff --git a/app/models/venue_room.rb b/app/models/venue_room.rb
index 8c337cfb15a..7248c6729ad 100644
--- a/app/models/venue_room.rb
+++ b/app/models/venue_room.rb
@@ -53,8 +53,8 @@ def self.wcif_json_schema
}
end
- def load_wcif!(wcif, skip_schedule: false)
- update!(VenueRoom.wcif_to_attributes(wcif))
+ def load_wcif!(wcif, skip_schedule: false, skip_venue_details: false)
+ update!(VenueRoom.wcif_to_attributes(wcif)) unless skip_venue_details
unless skip_schedule
new_activities = wcif["activities"].map do |activity_wcif|
activity = schedule_activities.find { |a| a.wcif_id == activity_wcif["id"] } || schedule_activities.build
From a7e9a8da23aa22f18c3fa30504e237c065a11df2 Mon Sep 17 00:00:00 2001
From: Gregor Billing
Date: Mon, 13 Jan 2025 20:59:34 +0900
Subject: [PATCH 31/33] Split PATCH URL config between EditVenues and
EditSchedules
---
app/webpacker/components/EditSchedule/index.js | 2 +-
app/webpacker/components/EditVenues/index.js | 2 +-
app/webpacker/lib/requests/routes.js.erb | 2 +-
app/webpacker/lib/utils/wcif.js | 6 +++---
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/app/webpacker/components/EditSchedule/index.js b/app/webpacker/components/EditSchedule/index.js
index 62417d74984..4ee53f4c494 100644
--- a/app/webpacker/components/EditSchedule/index.js
+++ b/app/webpacker/components/EditSchedule/index.js
@@ -55,7 +55,7 @@ function EditSchedule({
};
}, [onUnload]);
- const { saveWcif, saving } = useSaveWcifAction();
+ const { saveWcif, saving } = useSaveWcifAction({ skipVenueDetails: true });
const save = useCallback(() => {
saveWcif(
diff --git a/app/webpacker/components/EditVenues/index.js b/app/webpacker/components/EditVenues/index.js
index c49805e42a3..3f526aaff4c 100644
--- a/app/webpacker/components/EditVenues/index.js
+++ b/app/webpacker/components/EditVenues/index.js
@@ -54,7 +54,7 @@ function EditVenues({
};
}, [onUnload]);
- const { saveWcif, saving } = useSaveWcifAction(true);
+ const { saveWcif, saving } = useSaveWcifAction({ skipSchedule: true });
const save = useCallback(() => {
saveWcif(
diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb
index a22056b138f..b65bcecc136 100644
--- a/app/webpacker/lib/requests/routes.js.erb
+++ b/app/webpacker/lib/requests/routes.js.erb
@@ -299,4 +299,4 @@ export const bulkUpdateRegistrationUrl = `<%= CGI.unescape(Rails.application.rou
export const paymentTicketUrl = (competitionId, donationIso) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v1_registrations_payment_ticket_path(competition_id: "${competitionId}", donation_iso: "${donationIso}")) %>`;
-export const patchWcifUrl = (competitionId, skipSchedule = false) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v0_competition_update_wcif_path(competition_id: "${competitionId}")) %>?skip-schedule=${skipSchedule}`;
+export const patchWcifUrl = (competitionId, patchOpts = {}) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v0_competition_update_wcif_path(competition_id: "${competitionId}")) %>?skip-schedule=${patchOpts["skipSchedule"]}&skip-venue-details=${patchOpts["skipVenueDetails"]}`;
diff --git a/app/webpacker/lib/utils/wcif.js b/app/webpacker/lib/utils/wcif.js
index c66fba9e4c5..b76dc934031 100644
--- a/app/webpacker/lib/utils/wcif.js
+++ b/app/webpacker/lib/utils/wcif.js
@@ -7,7 +7,7 @@ import useSaveAction from '../hooks/useSaveAction';
import { centisecondsToClockFormat } from '../wca-live/attempts';
import { patchWcifUrl } from '../requests/routes.js.erb';
-export function useSaveWcifAction(skipSchedule = false) {
+export function useSaveWcifAction(patchOpts = {}) {
const { save, saving } = useSaveAction();
const alertWcifError = (err) => {
@@ -23,11 +23,11 @@ export function useSaveWcifAction(skipSchedule = false) {
options = {},
onError = alertWcifError,
) => {
- const url = patchWcifUrl(competitionId, skipSchedule);
+ const url = patchWcifUrl(competitionId, patchOpts);
save(url, wcifData, onSuccess, options, onError);
},
- [save, skipSchedule],
+ [save, patchOpts],
);
return {
From c198c7b2c8be554f27557746fa11a23984cd49dd Mon Sep 17 00:00:00 2001
From: Gregor Billing
Date: Tue, 14 Jan 2025 20:30:37 +0900
Subject: [PATCH 32/33] Move patchOptions in frontend to avoid rerender
---
app/webpacker/components/EditSchedule/index.js | 6 +++++-
app/webpacker/components/EditVenues/index.js | 6 +++++-
app/webpacker/lib/utils/wcif.js | 11 ++++++-----
3 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/app/webpacker/components/EditSchedule/index.js b/app/webpacker/components/EditSchedule/index.js
index 4ee53f4c494..2439bdd6ec9 100644
--- a/app/webpacker/components/EditSchedule/index.js
+++ b/app/webpacker/components/EditSchedule/index.js
@@ -19,6 +19,8 @@ import Store, { useDispatch, useStore } from '../../lib/providers/StoreProvider'
import ConfirmProvider from '../../lib/providers/ConfirmProvider';
import ManageActivities from './ManageActivities';
+const PATCH_OPTIONS = { skipVenueDetails: true };
+
function EditSchedule({
wcifEvents,
referenceTime,
@@ -55,13 +57,15 @@ function EditSchedule({
};
}, [onUnload]);
- const { saveWcif, saving } = useSaveWcifAction({ skipVenueDetails: true });
+ const { saveWcif, saving } = useSaveWcifAction();
const save = useCallback(() => {
saveWcif(
competitionId,
{ schedule: wcifSchedule },
() => dispatch(changesSaved()),
+ {},
+ PATCH_OPTIONS,
);
}, [competitionId, dispatch, saveWcif, wcifSchedule]);
diff --git a/app/webpacker/components/EditVenues/index.js b/app/webpacker/components/EditVenues/index.js
index 3f526aaff4c..1f4167e748d 100644
--- a/app/webpacker/components/EditVenues/index.js
+++ b/app/webpacker/components/EditVenues/index.js
@@ -19,6 +19,8 @@ import Store, { useDispatch, useStore } from '../../lib/providers/StoreProvider'
import ConfirmProvider from '../../lib/providers/ConfirmProvider';
import ManageVenues from './ManageVenues';
+const PATCH_OPTIONS = { skipSchedule: true };
+
function EditVenues({
countryZones,
referenceTime,
@@ -54,13 +56,15 @@ function EditVenues({
};
}, [onUnload]);
- const { saveWcif, saving } = useSaveWcifAction({ skipSchedule: true });
+ const { saveWcif, saving } = useSaveWcifAction();
const save = useCallback(() => {
saveWcif(
competitionId,
{ schedule: wcifSchedule },
() => dispatch(changesSaved()),
+ {},
+ PATCH_OPTIONS,
);
}, [competitionId, dispatch, saveWcif, wcifSchedule]);
diff --git a/app/webpacker/lib/utils/wcif.js b/app/webpacker/lib/utils/wcif.js
index b76dc934031..e7da6f73c68 100644
--- a/app/webpacker/lib/utils/wcif.js
+++ b/app/webpacker/lib/utils/wcif.js
@@ -7,7 +7,7 @@ import useSaveAction from '../hooks/useSaveAction';
import { centisecondsToClockFormat } from '../wca-live/attempts';
import { patchWcifUrl } from '../requests/routes.js.erb';
-export function useSaveWcifAction(patchOpts = {}) {
+export function useSaveWcifAction() {
const { save, saving } = useSaveAction();
const alertWcifError = (err) => {
@@ -20,14 +20,15 @@ export function useSaveWcifAction(patchOpts = {}) {
competitionId,
wcifData,
onSuccess,
- options = {},
+ fetchOptions = {},
+ patchWcifOptions = {},
onError = alertWcifError,
) => {
- const url = patchWcifUrl(competitionId, patchOpts);
+ const url = patchWcifUrl(competitionId, patchWcifOptions);
- save(url, wcifData, onSuccess, options, onError);
+ save(url, wcifData, onSuccess, fetchOptions, onError);
},
- [save, patchOpts],
+ [save],
);
return {
From b2b1d110d8310ad5457cd40a59e68ee65647e084 Mon Sep 17 00:00:00 2001
From: Gregor Billing
Date: Tue, 14 Jan 2025 21:02:40 +0900
Subject: [PATCH 33/33] Fix 'undefined' parameter in WCIF patch
---
app/controllers/api/v0/competitions_controller.rb | 6 +++---
app/webpacker/lib/requests/routes.js.erb | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/controllers/api/v0/competitions_controller.rb b/app/controllers/api/v0/competitions_controller.rb
index 7d8d41545e3..3605cb59309 100644
--- a/app/controllers/api/v0/competitions_controller.rb
+++ b/app/controllers/api/v0/competitions_controller.rb
@@ -166,11 +166,11 @@ def show_wcif_public
def update_wcif
competition = competition_from_params
require_can_manage!(competition)
- skip_schedule = params[:'skip-schedule']
+ skip_schedule = params[:skipSchedule]
skip_schedule = ActiveRecord::Type::Boolean.new.cast(skip_schedule)
- skip_venue_details = params[:'skip-venue-details']
+ skip_venue_details = params[:skipVenueDetails]
skip_venue_details = ActiveRecord::Type::Boolean.new.cast(skip_venue_details)
- wcif = params.except("skip-schedule", "skip-venue-details").permit!.to_h
+ wcif = params.except("skipSchedule", "skipVenueDetails").permit!.to_h
wcif = wcif["_json"] || wcif
competition.set_wcif!(wcif, require_user!, skip_schedule: skip_schedule, skip_venue_details: skip_venue_details)
render json: {
diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb
index b65bcecc136..e53a5aa572f 100644
--- a/app/webpacker/lib/requests/routes.js.erb
+++ b/app/webpacker/lib/requests/routes.js.erb
@@ -299,4 +299,4 @@ export const bulkUpdateRegistrationUrl = `<%= CGI.unescape(Rails.application.rou
export const paymentTicketUrl = (competitionId, donationIso) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v1_registrations_payment_ticket_path(competition_id: "${competitionId}", donation_iso: "${donationIso}")) %>`;
-export const patchWcifUrl = (competitionId, patchOpts = {}) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v0_competition_update_wcif_path(competition_id: "${competitionId}")) %>?skip-schedule=${patchOpts["skipSchedule"]}&skip-venue-details=${patchOpts["skipVenueDetails"]}`;
+export const patchWcifUrl = (competitionId, patchOpts = {}) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v0_competition_update_wcif_path(competition_id: "${competitionId}")) %>?${jsonToQueryString(patchOpts)}`;