Skip to content

Commit f0b60a6

Browse files
🐛 hotfix: someday events handle order field (#517)
* feat(web): fix typo * hotfix(web): set order field for someday events if it is not set, add tests the added tests should help us validate edge cases with the order being potentially missing. * feat(web): re-enable validation see why it was disabled here https://github.com/SwitchbackTech/compass/pull/503/files and why it is now re-enabled here #517
1 parent e53908e commit f0b60a6

File tree

4 files changed

+165
-3
lines changed

4 files changed

+165
-3
lines changed

packages/web/src/common/utils/__tests__/someday.util.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
55
import timezone from "dayjs/plugin/timezone";
66
import utc from "dayjs/plugin/utc";
77
import { Origin, Priorities } from "@core/constants/core.constants";
8+
import { Schema_Event } from "@core/types/event.types";
89
import { COLUMN_MONTH, COLUMN_WEEK } from "@web/common/constants/web.constants";
910
import { Schema_SomedayEvent } from "@web/common/types/web.event.types";
1011
import { categorizeSomedayEvents } from "../someday.util";
12+
import { setSomedayEventsOrder } from "../someday.util";
1113

1214
dayjs.extend(utc);
1315
dayjs.extend(timezone);
@@ -191,3 +193,106 @@ describe("categorizeSomedayEvents", () => {
191193
});
192194
});
193195
});
196+
197+
describe("setSomedayEventsOrder", () => {
198+
const createEvent = (id: string, order?: number): Schema_Event => ({
199+
_id: id,
200+
...(order !== undefined && { order }),
201+
});
202+
203+
it("should return empty array for empty input", () => {
204+
expect(setSomedayEventsOrder([])).toEqual([]);
205+
});
206+
207+
it("should assign sequential orders starting from 0 when no events have orders", () => {
208+
const events = [createEvent("1"), createEvent("2"), createEvent("3")];
209+
210+
const result = setSomedayEventsOrder(events);
211+
212+
expect(result).toEqual([
213+
{ ...events[0], order: 0 },
214+
{ ...events[1], order: 1 },
215+
{ ...events[2], order: 2 },
216+
]);
217+
});
218+
219+
it("should preserve existing valid orders", () => {
220+
const events = [
221+
createEvent("1", 5),
222+
createEvent("2", 2),
223+
createEvent("3", 8),
224+
];
225+
226+
const result = setSomedayEventsOrder(events);
227+
228+
expect(result).toEqual(events);
229+
});
230+
231+
it("should fill gaps in order sequence", () => {
232+
const events = [
233+
createEvent("1", 0),
234+
createEvent("2"), // Should get order 1
235+
createEvent("3", 3),
236+
createEvent("4"), // Should get order 2
237+
createEvent("5", 5),
238+
];
239+
240+
const result = setSomedayEventsOrder(events);
241+
242+
expect(result).toEqual([
243+
{ ...events[0], order: 0 },
244+
{ ...events[1], order: 1 },
245+
{ ...events[2], order: 3 },
246+
{ ...events[3], order: 2 },
247+
{ ...events[4], order: 5 },
248+
]);
249+
});
250+
251+
it("should append to end when no gaps available", () => {
252+
const events = [
253+
createEvent("1", 0),
254+
createEvent("2", 1),
255+
createEvent("3"), // Should get order 3
256+
createEvent("4", 2),
257+
createEvent("5"), // Should get order 4
258+
];
259+
260+
const result = setSomedayEventsOrder(events);
261+
262+
expect(result).toEqual([
263+
{ ...events[0], order: 0 },
264+
{ ...events[1], order: 1 },
265+
{ ...events[2], order: 3 },
266+
{ ...events[3], order: 2 },
267+
{ ...events[4], order: 4 },
268+
]);
269+
});
270+
271+
it("should handle mix of valid and invalid order values", () => {
272+
const events = [
273+
createEvent("1", 1),
274+
{ ...createEvent("2"), order: Number.NaN }, // Should get order 0
275+
{ ...createEvent("3"), order: undefined }, // Should get order 2
276+
createEvent("4", 4),
277+
{ ...createEvent("5"), order: undefined }, // Should get order 3
278+
];
279+
280+
const result = setSomedayEventsOrder(events);
281+
282+
expect(result).toEqual([
283+
{ ...events[0], order: 1 },
284+
{ ...events[1], order: 0 },
285+
{ ...events[2], order: 2 },
286+
{ ...events[3], order: 4 },
287+
{ ...events[4], order: 3 },
288+
]);
289+
});
290+
291+
it("should handle single event without order", () => {
292+
const events = [createEvent("1")];
293+
294+
const result = setSomedayEventsOrder(events);
295+
296+
expect(result).toEqual([{ ...events[0], order: 0 }]);
297+
});
298+
});

packages/web/src/common/utils/someday.util.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Schema_SomedayEvent,
88
Schema_SomedayEventsColumn,
99
} from "@web/common/types/web.event.types";
10+
import { validateSomedayEvents } from "@web/common/validators/someday.event.validator";
1011

1112
export const getSomedayEventCategory = (
1213
event: Schema_Event,
@@ -34,7 +35,9 @@ export const categorizeSomedayEvents = (
3435
): Schema_SomedayEventsColumn => {
3536
const { start: weekStart, end: weekEnd } = weekDates;
3637

37-
const events = Object.values(somedayEvents) as Schema_SomedayEvent[];
38+
let events = Object.values(somedayEvents) as Schema_SomedayEvent[];
39+
40+
events = validateSomedayEvents(events);
3841

3942
const sortedEvents = events.sort((a, b) => a.order - b.order);
4043

@@ -85,3 +88,54 @@ export const categorizeSomedayEvents = (
8588
};
8689
return sortedData;
8790
};
91+
92+
/**
93+
* See https://github.com/SwitchbackTech/compass/issues/512 for more context.
94+
* Should be removed after we ensure that backend sets the order field for all someday events.
95+
*/
96+
export const setSomedayEventsOrder = (
97+
events: Schema_Event[],
98+
): Schema_Event[] => {
99+
if (events.length === 0) return [];
100+
101+
// Get existing valid orders
102+
const existingOrders = events
103+
.map((e) => e.order)
104+
.filter(
105+
(order): order is number => typeof order === "number" && !isNaN(order),
106+
)
107+
.sort((a, b) => a - b);
108+
109+
// If no valid orders exist, assign sequential orders starting from 0
110+
if (existingOrders.length === 0) {
111+
return events.map((event, index) => ({ ...event, order: index }));
112+
}
113+
114+
const lowestOrder = Math.min(0, existingOrders[0]); // Ensure we start at least from 0
115+
const highestOrder = existingOrders[existingOrders.length - 1];
116+
117+
// Create a set of used orders for faster lookup
118+
const usedOrders = new Set(existingOrders);
119+
120+
// Find all available orders in the range
121+
const availableOrders: number[] = [];
122+
for (let i = lowestOrder; i <= highestOrder; i++) {
123+
if (!usedOrders.has(i)) {
124+
availableOrders.push(i);
125+
}
126+
}
127+
128+
// Process each event that needs an order
129+
let nextNewOrder = highestOrder + 1;
130+
return events.map((event) => {
131+
// Keep existing valid orders
132+
if (typeof event.order === "number" && !isNaN(event.order)) {
133+
return event;
134+
}
135+
136+
// Assign next available order or append to end
137+
const order =
138+
availableOrders.length > 0 ? availableOrders.shift()! : nextNewOrder++;
139+
return { ...event, order };
140+
});
141+
};

packages/web/src/ducks/events/sagas/somday.sagas.ts renamed to packages/web/src/ducks/events/sagas/someday.sagas.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
handleError,
99
replaceIdWithOptimisticId,
1010
} from "@web/common/utils/event.util";
11+
import { setSomedayEventsOrder } from "@web/common/utils/someday.util";
1112
import { validateGridEvent } from "@web/common/validators/grid.event.validator";
1213
import { EventApi } from "../event.api";
1314
import {
@@ -70,7 +71,9 @@ export function* getSomedayEvents({ payload }: Action_GetEvents) {
7071
endDate: payload.endDate,
7172
})) as Response_GetEventsSuccess;
7273

73-
const normalizedEvents = normalize<Schema_Event>(res.data, [
74+
const events = setSomedayEventsOrder(res.data);
75+
76+
const normalizedEvents = normalize<Schema_Event>(events, [
7477
normalizedEventsSchema(),
7578
]);
7679
yield put(

packages/web/src/store/sagas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
deleteSomedayEvent,
1313
getSomedayEvents,
1414
reorderSomedayEvents,
15-
} from "@web/ducks/events/sagas/somday.sagas";
15+
} from "@web/ducks/events/sagas/someday.sagas";
1616
import {
1717
createEventSlice,
1818
deleteEventSlice,

0 commit comments

Comments
 (0)