Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/web/src/__tests__/utils/event.util/test.event.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { faker } from "@faker-js/faker";
import { Origin, Priorities } from "@core/constants/core.constants";
import dayjs from "@core/util/date/dayjs";
import { Schema_DraftEvent } from "@web/common/schemas/events/draft.event.schemas";
import { Schema_WebEvent } from "@web/common/schemas/events/web.event.schemas";

/**
* These utils focus on generating web-specific schemas.
* For generating API-compatible events, see utils in `@core`
*/

export const createWebEvent = (
overrides: Partial<Schema_WebEvent> = {},
): Schema_WebEvent => {
const start = faker.date.future();
const end = dayjs(start).add(1, "hour");

return {
_id: faker.string.uuid(),
origin: Origin.COMPASS,
title: faker.lorem.sentence(),
description: faker.lorem.paragraph(),
startDate: start.toISOString(),
endDate: end.toISOString(),
priority: faker.helpers.arrayElement(Object.values(Priorities)),
recurrence: undefined,
user: faker.string.uuid(),
...overrides,
};
};
259 changes: 259 additions & 0 deletions packages/web/src/common/parsers/event.parser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import { faker } from "@faker-js/faker";
import { Priorities } from "@core/constants/core.constants";
import { createWebEvent } from "../../__tests__/utils/event.util/test.event.util";
import { WebEventParser, isEventDirty } from "./event.parser";

describe("WebEventParser", () => {
it("should return false when draft and original events are identical", () => {
const originalEvent = createWebEvent();
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: originalEvent.recurrence,
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(false);
});

it("should return true when title has changed", () => {
const originalEvent = createWebEvent();
const draftEvent = createWebEvent({
title: "Different Title",
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: originalEvent.recurrence,
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return true when description has changed", () => {
const originalEvent = createWebEvent();
const draftEvent = createWebEvent({
title: originalEvent.title,
description: "Different Description",
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: originalEvent.recurrence,
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return true when startDate has changed", () => {
const originalEvent = createWebEvent();
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: faker.date.future().toISOString(),
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: originalEvent.recurrence,
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return true when endDate has changed", () => {
const originalEvent = createWebEvent();
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: faker.date.future().toISOString(),
priority: originalEvent.priority,
recurrence: originalEvent.recurrence,
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return true when priority has changed", () => {
const originalEvent = createWebEvent({ priority: Priorities.WORK });
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: Priorities.SELF,
recurrence: originalEvent.recurrence,
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return true when recurrence is added to non-recurring event", () => {
const originalEvent = createWebEvent({ recurrence: undefined });
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return true when recurrence is removed from recurring event", () => {
const originalEvent = createWebEvent({
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
});
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: undefined,
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return true when recurrence rules have changed", () => {
const originalEvent = createWebEvent({
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
});
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: { rule: ["RRULE:FREQ=DAILY"] },
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return true when recurrence rules array length has changed", () => {
const originalEvent = createWebEvent({
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
});
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: { rule: ["RRULE:FREQ=WEEKLY", "RRULE:BYDAY=MO"] },
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return true when dates change in recurring event", () => {
const originalEvent = createWebEvent({
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
startDate: "2024-01-01T10:00:00Z",
endDate: "2024-01-01T11:00:00Z",
});
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: "2024-01-01T11:00:00Z", // Different start time
endDate: "2024-01-01T12:00:00Z", // Different end time
priority: originalEvent.priority,
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] }, // Same recurrence
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(true);
});

it("should return false when only non-tracked fields change", () => {
const originalEvent = createWebEvent();
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: originalEvent.recurrence,
user: "different-user", // This field is not tracked
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(false);
});

it("should handle undefined recurrence gracefully", () => {
const originalEvent = createWebEvent({ recurrence: undefined });
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: undefined,
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(false);
});

it("should handle empty recurrence rules", () => {
const originalEvent = createWebEvent({
recurrence: { rule: [] },
});
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: { rule: [] },
});

const parser = new WebEventParser(draftEvent, originalEvent);
expect(parser.isDirty()).toBe(false);
});
});

describe("isDraftDirty", () => {
it("should work as a standalone function", () => {
const originalEvent = createWebEvent();
const draftEvent = createWebEvent({
title: "Different Title",
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: originalEvent.recurrence,
});

expect(isEventDirty(draftEvent, originalEvent)).toBe(true);
});

it("should return false for identical events", () => {
const originalEvent = createWebEvent();
const draftEvent = createWebEvent({
title: originalEvent.title,
description: originalEvent.description,
startDate: originalEvent.startDate,
endDate: originalEvent.endDate,
priority: originalEvent.priority,
recurrence: originalEvent.recurrence,
});

expect(isEventDirty(draftEvent, originalEvent)).toBe(false);
});
});
99 changes: 99 additions & 0 deletions packages/web/src/common/parsers/event.parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Schema_WebEvent } from "../types/web.event.types";

/**
* Parser for determining if an event has been modified (is dirty)
*/
export class WebEventParser {
private readonly curr: Schema_WebEvent;
private readonly orig: Schema_WebEvent;

constructor(curr: Schema_WebEvent, orig: Schema_WebEvent) {
this.curr = curr;
this.orig = orig;
}

/**
* Public method to check if the event is dirty (has been modified)
*/
public isDirty(): boolean {
// Compare relevant fields that can change in the form
const fieldsToCompare = [
"title",
"description",
"startDate",
"endDate",
"priority",
"recurrence",
] as const;

return fieldsToCompare.some((field) => {
const current = this.curr[field];
const original = this.orig[field];
const isRecurrenceField = field === "recurrence";

return isRecurrenceField
? this.isRecurrenceChanged()
: current !== original;
});
}

/**
* Private method to check if recurrence has changed
*/
private isRecurrenceChanged(): boolean {
const isDateChanged = this.isDateChanged();
const oldRecurrence = this.orig?.recurrence?.rule ?? [];
const newRecurrence = this.curr?.recurrence?.rule ?? [];

// Check if recurrence existence has changed
const oldHasRecurrence =
Array.isArray(oldRecurrence) && oldRecurrence.length > 0;
const newHasRecurrence =
Array.isArray(newRecurrence) && newRecurrence.length > 0;

if (oldHasRecurrence !== newHasRecurrence) {
return true;
}

// If both have recurrence, compare the rules
if (oldHasRecurrence && newHasRecurrence) {
// First check if the arrays are different in length or content
if (oldRecurrence.length !== newRecurrence.length) {
return true;
}

// Check if any rule is different
for (let i = 0; i < oldRecurrence.length; i++) {
if (oldRecurrence[i] !== newRecurrence[i]) {
return true;
}
}

// Also check for date changes
return isDateChanged;
}

// If neither has recurrence, only check date changes
return isDateChanged;
}

/**
* Private method to check if start or end dates have changed
*/
private isDateChanged(): boolean {
const oldStartDate = this.orig?.startDate;
const newStartDate = this.curr?.startDate;
const oldEndDate = this.orig?.endDate;
const newEndDate = this.curr?.endDate;

return oldStartDate !== newStartDate || oldEndDate !== newEndDate;
}
}

export const isEventDirty = (
curr: Schema_WebEvent,
orig: Schema_WebEvent,
): boolean => {
const parser = new WebEventParser(curr, orig);
return parser.isDirty();
};
2 changes: 2 additions & 0 deletions packages/web/src/common/utils/event.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ export const handleError = (error: Error) => {
return;
}

console.error(error);

if (code === Status.INTERNAL_SERVER) {
alert("Something went wrong behind the scenes. Please try again later.");
window.location.reload();
Expand Down
Loading