Skip to content

Commit b1726aa

Browse files
committed
🐛 fix(recurring-events): fix ui recurring event transitions
1 parent d1dbf9d commit b1726aa

File tree

8 files changed

+125
-97
lines changed

8 files changed

+125
-97
lines changed

packages/web/src/common/parsers/dirty.parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ export class DirtyParser {
4343
}
4444

4545
/**
46-
* Private Static method to check if recurrence has changed
46+
* Public Static method to check if recurrence has changed
4747
*/
48-
private static recurrenceChanged(
48+
public static recurrenceChanged(
4949
curr: Schema_WebEvent,
5050
orig: Schema_WebEvent,
5151
): boolean {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from "react";
2+
3+
export function ConditionalRender({
4+
condition,
5+
children,
6+
}: {
7+
condition: boolean;
8+
children: React.ReactNode;
9+
}) {
10+
if (!condition) return null;
11+
12+
return <React.Fragment>{children}</React.Fragment>;
13+
}

packages/web/src/views/Calendar/Calendar.tsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,24 @@ import { ContextMenuWrapper } from "@web/components/ContextMenu/GridContextMenuW
44
import { FlexDirections } from "@web/components/Flex/styled";
55
import { selectIsSidebarOpen } from "@web/ducks/events/selectors/view.selectors";
66
import { useAppSelector } from "@web/store/store.hooks";
7+
import { RootProps } from "@web/views/Calendar/calendarView.types";
8+
import { Dedication } from "@web/views/Calendar/components/Dedication";
9+
import { Draft } from "@web/views/Calendar/components/Draft/Draft";
10+
import { DraftProvider } from "@web/views/Calendar/components/Draft/context/DraftProvider";
11+
import { SidebarDraftProvider } from "@web/views/Calendar/components/Draft/sidebar/context/SidebarDraftProvider";
12+
import { Grid } from "@web/views/Calendar/components/Grid/";
13+
import { Header } from "@web/views/Calendar/components/Header/Header";
14+
import { Shortcuts } from "@web/views/Calendar/components/Shortcuts";
15+
import { Sidebar } from "@web/views/Calendar/components/Sidebar/Sidebar";
16+
import { useDateCalcs } from "@web/views/Calendar/hooks/grid/useDateCalcs";
17+
import { useGridLayout } from "@web/views/Calendar/hooks/grid/useGridLayout";
18+
import { useScroll } from "@web/views/Calendar/hooks/grid/useScroll";
19+
import { useRefresh } from "@web/views/Calendar/hooks/useRefresh";
20+
import { useToday } from "@web/views/Calendar/hooks/useToday";
21+
import { useWeek } from "@web/views/Calendar/hooks/useWeek";
22+
import { Styled, StyledCalendar } from "@web/views/Calendar/styled";
23+
import { CmdPalette } from "@web/views/CmdPalette";
724
import { RecurringEventUpdateScopeDialog } from "@web/views/Forms/EventForm/RecurringEventUpdateScopeDialog";
8-
import { CmdPalette } from "../CmdPalette";
9-
import { RootProps } from "./calendarView.types";
10-
import { Dedication } from "./components/Dedication";
11-
import { Draft } from "./components/Draft/Draft";
12-
import { DraftProvider } from "./components/Draft/context/DraftProvider";
13-
import { SidebarDraftProvider } from "./components/Draft/sidebar/context/SidebarDraftProvider";
14-
import { Grid } from "./components/Grid/";
15-
import { Header } from "./components/Header/Header";
16-
import { Shortcuts } from "./components/Shortcuts";
17-
import { Sidebar } from "./components/Sidebar/Sidebar";
18-
import { useDateCalcs } from "./hooks/grid/useDateCalcs";
19-
import { useGridLayout } from "./hooks/grid/useGridLayout";
20-
import { useScroll } from "./hooks/grid/useScroll";
21-
import { useRefresh } from "./hooks/useRefresh";
22-
import { useToday } from "./hooks/useToday";
23-
import { useWeek } from "./hooks/useWeek";
24-
import { Styled, StyledCalendar } from "./styled";
2525

2626
export const CalendarView = () => {
2727
const isSidebarOpen = useAppSelector(selectIsSidebarOpen);
@@ -104,9 +104,9 @@ export const CalendarView = () => {
104104
</ContextMenuWrapper>
105105
</StyledCalendar>
106106
</Shortcuts>
107-
</SidebarDraftProvider>
108107

109-
<RecurringEventUpdateScopeDialog />
108+
<RecurringEventUpdateScopeDialog />
109+
</SidebarDraftProvider>
110110
</DraftProvider>
111111
</Styled>
112112
);

packages/web/src/views/Calendar/components/Draft/hooks/state/useDraftConfirmation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export const useDraftConfirmation = ({
4040
const isRecurringEvent = isRecurrence();
4141
const instanceEvent = isInstance();
4242
const toStandAlone = instanceEvent && rule === null;
43+
const applyTo = toStandAlone
44+
? RecurringEventUpdateScope.ALL_EVENTS
45+
: RecurringEventUpdateScope.THIS_EVENT;
4346

4447
if (!toStandAlone && isRecurringEvent) {
4548
setFinalDraft(_draft);
@@ -54,7 +57,7 @@ export const useDraftConfirmation = ({
5457
if (!confirmed) return;
5558
}
5659

57-
submit(_draft);
60+
submit(_draft, applyTo);
5861
discard();
5962
},
6063
[

packages/web/src/views/Calendar/components/Draft/sidebar/hooks/useSidebarActions.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useCallback } from "react";
1+
import { ObjectId } from "bson";
2+
import { useCallback, useMemo } from "react";
23
import { DropResult } from "@hello-pangea/dnd";
34
import {
45
SOMEDAY_MONTH_LIMIT_MSG,
@@ -18,7 +19,9 @@ import {
1819
COLUMN_WEEK,
1920
ID_SOMEDAY_DRAFT,
2021
} from "@web/common/constants/web.constants";
22+
import { DirtyParser } from "@web/common/parsers/dirty.parser";
2123
import { Coordinates } from "@web/common/types/util.types";
24+
import { Schema_WebEvent } from "@web/common/types/web.event.types";
2225
import {
2326
computeCurrentEventDateRange,
2427
computeRelativeEventDateRange,
@@ -82,6 +85,10 @@ export const useSidebarActions = (
8285
const draftType = useAppSelector(selectDraftCategory);
8386
const activity = useAppSelector(selectDraftActivity);
8487

88+
const isInstance = useMemo((): boolean => {
89+
return ObjectId.isValid(reduxDraft?.recurrence?.eventId ?? "");
90+
}, [reduxDraft?.recurrence?.eventId]);
91+
8592
const viewStart = dayjs(start);
8693
const viewEnd = dayjs(end);
8794

@@ -385,11 +392,21 @@ export const useSidebarActions = (
385392

386393
const isExisting = _event._id;
387394
if (isExisting) {
395+
const recurrenceChanged = DirtyParser.recurrenceChanged(
396+
_event as Schema_WebEvent,
397+
reduxDraft!,
398+
);
399+
400+
const applyTo =
401+
isInstance && recurrenceChanged
402+
? RecurringEventUpdateScope.ALL_EVENTS
403+
: RecurringEventUpdateScope.THIS_EVENT;
404+
388405
dispatch(
389406
editEventSlice.actions.request({
390407
_id: _event._id,
391408
event: _event,
392-
applyTo: RecurringEventUpdateScope.THIS_EVENT,
409+
applyTo,
393410
}),
394411
);
395412
} else {
Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
1+
import React, { Dispatch, SetStateAction } from "react";
22
import { Priorities } from "@core/constants/core.constants";
3-
import { Schema_Event } from "@core/types/event.types";
43
import { hoverColorByPriority } from "@web/common/styles/theme.util";
4+
import {
5+
Schema_GridEvent,
6+
Schema_WebEvent,
7+
} from "@web/common/types/web.event.types";
8+
import { ConditionalRender } from "@web/components/ConditionalRender/ConditionalRender";
59
import { FlexDirections } from "@web/components/Flex/styled";
10+
import { EndsOnDate } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate";
11+
import { RecurrenceIntervalSelect } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceIntervalSelect";
12+
import { RecurrenceToggle } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceToggle";
13+
import { WeekDays } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDays";
614
import { StyledRepeatRow } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled";
715
import { useRecurrence } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/useRecurrence/useRecurrence";
8-
import { EndsOnDate } from "./components/EndsOnDate";
9-
import { RecurrenceIntervalSelect } from "./components/RecurrenceIntervalSelect";
10-
import { RecurrenceToggle } from "./components/RecurrenceToggle";
11-
import { WeekDays } from "./components/WeekDays";
1216

1317
export interface RecurrenceSectionProps {
1418
bgColor: string;
15-
event: Schema_Event;
16-
setEvent: Dispatch<SetStateAction<Schema_Event | null>>;
19+
event: Schema_WebEvent | Schema_GridEvent;
20+
setEvent: Dispatch<SetStateAction<Schema_WebEvent | Schema_GridEvent | null>>;
1721
}
1822

1923
export const RecurrenceSection = ({
@@ -25,48 +29,37 @@ export const RecurrenceSection = ({
2529
const { setInterval, setFreq, setWeekDays, setUntil } = recurrenceHook;
2630
const { weekDays, interval, freq, until, toggleRecurrence } = recurrenceHook;
2731
const { hasRecurrence } = recurrenceHook;
28-
const [showForm, setShowForm] = useState(() => hasRecurrence);
29-
30-
useEffect(() => {
31-
setShowForm(hasRecurrence);
32-
}, [hasRecurrence, event?._id]);
33-
34-
const shouldShowForm = hasRecurrence && showForm;
3532

3633
return (
3734
<StyledRepeatRow direction={FlexDirections.COLUMN}>
3835
<RecurrenceToggle
3936
hasRecurrence={hasRecurrence}
4037
toggleRecurrence={toggleRecurrence}
41-
showForm={showForm}
42-
onToggleForm={() => setShowForm((value) => !value)}
4338
/>
4439

45-
{shouldShowForm && (
46-
<>
47-
<RecurrenceIntervalSelect
48-
bgColor={bgColor}
49-
initialValue={interval}
50-
frequency={freq}
51-
onChange={setInterval}
52-
onFreqSelect={setFreq}
53-
min={1}
54-
max={12}
55-
/>
40+
<ConditionalRender condition={hasRecurrence}>
41+
<RecurrenceIntervalSelect
42+
bgColor={bgColor}
43+
initialValue={interval}
44+
frequency={freq}
45+
onChange={setInterval}
46+
onFreqSelect={setFreq}
47+
min={1}
48+
max={12}
49+
/>
5650

57-
<WeekDays bgColor={bgColor} value={weekDays} onChange={setWeekDays} />
51+
<WeekDays bgColor={bgColor} value={weekDays} onChange={setWeekDays} />
5852

59-
<EndsOnDate
60-
bgColor={bgColor}
61-
inputColor={
62-
hoverColorByPriority[event.priority ?? Priorities.UNASSIGNED]
63-
}
64-
until={until}
65-
minDate={event.endDate}
66-
setUntil={setUntil}
67-
/>
68-
</>
69-
)}
53+
<EndsOnDate
54+
bgColor={bgColor}
55+
inputColor={
56+
hoverColorByPriority[event.priority ?? Priorities.UNASSIGNED]
57+
}
58+
until={until}
59+
minDate={event.endDate}
60+
setUntil={setUntil}
61+
/>
62+
</ConditionalRender>
7063
</StyledRepeatRow>
7164
);
7265
};
Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React from "react";
1+
import React, { useCallback } from "react";
2+
import { ConditionalRender } from "@web/components/ConditionalRender/ConditionalRender";
23
import { RepeatIcon } from "@web/components/Icons/Repeat";
34
import {
45
StyledRepeatContainer,
@@ -10,59 +11,49 @@ import {
1011
export interface RecurrenceToggleProps {
1112
hasRecurrence: boolean;
1213
toggleRecurrence: () => void;
13-
showForm: boolean;
14-
onToggleForm: () => void;
1514
}
1615

1716
export const RecurrenceToggle = ({
1817
hasRecurrence,
1918
toggleRecurrence,
20-
showForm,
21-
onToggleForm,
2219
}: RecurrenceToggleProps) => {
23-
const handleClick = () => {
24-
if (!hasRecurrence) {
25-
// Enable recurrence and show form
26-
toggleRecurrence();
27-
onToggleForm();
28-
} else {
29-
// Toggle form visibility when recurrence is already enabled
30-
onToggleForm();
31-
}
32-
};
33-
34-
const handleKeyDown = (event: React.KeyboardEvent) => {
35-
if (event.key === "Enter" || event.key === " ") {
36-
event.preventDefault();
37-
handleClick();
38-
}
39-
};
20+
const handleKeyDown = useCallback(
21+
(event: React.KeyboardEvent) => {
22+
if (event.key === "Enter" || event.key === " ") {
23+
event.preventDefault();
24+
toggleRecurrence();
25+
}
26+
},
27+
[toggleRecurrence],
28+
);
4029

4130
return (
4231
<StyledRepeatRow>
43-
{!hasRecurrence || showForm ? (
44-
<StyledRepeatContainer onClick={handleClick}>
32+
<ConditionalRender condition={hasRecurrence}>
33+
<StyledRepeatContainer onClick={toggleRecurrence}>
4534
<StyledRepeatText
4635
hasRepeat={hasRecurrence}
47-
tabIndex={0}
4836
onKeyDown={handleKeyDown}
37+
tabIndex={0}
4938
>
5039
<RepeatIcon size={18} />
5140
<span>Repeat</span>
5241
</StyledRepeatText>
5342
</StyledRepeatContainer>
54-
) : (
43+
</ConditionalRender>
44+
45+
<ConditionalRender condition={!hasRecurrence}>
5546
<StyledRepeatTextContainer
5647
aria-label="Edit recurrence"
57-
onClick={handleClick}
48+
onClick={toggleRecurrence}
5849
onKeyDown={handleKeyDown}
5950
role="button"
6051
tabIndex={0}
6152
>
6253
<RepeatIcon size={18} />
6354
<span>Repeat</span>
6455
</StyledRepeatTextContainer>
65-
)}
56+
</ConditionalRender>
6657
</StyledRepeatRow>
6758
);
6859
};

packages/web/src/views/Forms/EventForm/RecurringEventUpdateScopeDialog.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,31 @@ import {
1111
} from "@floating-ui/react";
1212
import { Priorities } from "@core/constants/core.constants";
1313
import { RecurringEventUpdateScope } from "@core/types/event.types";
14+
import { DirtyParser } from "@web/common/parsers/dirty.parser";
1415
import { theme } from "@web/common/styles/theme";
16+
import { Schema_WebEvent } from "@web/common/types/web.event.types";
1517
import { selectDraft } from "@web/ducks/events/selectors/draft.selectors";
1618
import { useAppSelector } from "@web/store/store.hooks";
1719
import { useDraftContext } from "@web/views/Calendar/components/Draft/context/useDraftContext";
1820
import { SaveSection } from "@web/views/Forms/EventForm/SaveSection/SaveSection";
1921
import { StyledEventForm } from "@web/views/Forms/EventForm/styled";
2022

2123
export function RecurringEventUpdateScopeDialog() {
22-
const { confirmation, state } = useDraftContext();
24+
const {
25+
confirmation,
26+
state: { draft },
27+
} = useDraftContext();
2328
const { isRecurrenceUpdateScopeDialogOpen } = confirmation;
2429
const { setRecurrenceUpdateScopeDialogOpen } = confirmation;
25-
const { onUpdateScopeChange, recurrenceChanged } = confirmation;
30+
const { onUpdateScopeChange } = confirmation;
2631
const reduxDraft = useAppSelector(selectDraft);
27-
const draft = state.draft ?? reduxDraft;
32+
const { UNASSIGNED } = Priorities;
33+
const priority = draft?.priority ?? reduxDraft?.priority ?? UNASSIGNED;
34+
35+
const recurrenceChanged = DirtyParser.recurrenceChanged(
36+
(draft as Schema_WebEvent) ?? reduxDraft,
37+
reduxDraft!,
38+
);
2839

2940
const { context, refs, floatingStyles } = useFloating({
3041
open: isRecurrenceUpdateScopeDialogOpen,
@@ -75,7 +86,7 @@ export function RecurringEventUpdateScopeDialog() {
7586
style={{ ...floatingStyles, top: "50%", left: "50%" }}
7687
{...interactions.getFloatingProps()}
7788
>
78-
<StyledEventForm role="form" priority={draft?.priority}>
89+
<StyledEventForm role="form" priority={priority}>
7990
<fieldset style={{ borderRadius: theme.shape.borderRadius }}>
8091
<legend>Apply Changes To</legend>
8192

@@ -101,7 +112,7 @@ export function RecurringEventUpdateScopeDialog() {
101112

102113
<SaveSection
103114
saveText="Ok"
104-
priority={draft?.priority ?? Priorities.UNASSIGNED}
115+
priority={priority}
105116
onSubmit={onSubmitHandler}
106117
onCancel={() => setRecurrenceUpdateScopeDialogOpen(false)}
107118
/>

0 commit comments

Comments
 (0)