Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants";
import dayjs from "@core/util/date/dayjs";
import {
getDraggedEventDateRange,
getIsValidResizeMovement,
} from "./draft.movement";
import { describe, expect, it } from "bun:test";

describe("draft movement helpers", () => {
it("keeps timed drag ranges within the same day when the end would overflow", () => {
const start = dayjs("2024-01-15T23:45:00.000");

const result = getDraggedEventDateRange({
eventStart: start,
durationMin: 60,
isAllDay: false,
});

expect(dayjs(result.startDate).format("HH:mm")).toBe("23:00");
expect(dayjs(result.endDate).format("HH:mm")).toBe("00:00");
});

it("formats all-day drag ranges as date-only values", () => {
const start = dayjs("2024-01-15T09:00:00.000Z");

const result = getDraggedEventDateRange({
eventStart: start,
durationMin: 1440,
isAllDay: true,
});

expect(result.startDate).toBe(start.format(YEAR_MONTH_DAY_FORMAT));
expect(result.endDate).toBe(
start.add(1440, "minutes").format(YEAR_MONTH_DAY_FORMAT),
);
});

it("rejects resize movement that changes a timed event to another day", () => {
expect(
getIsValidResizeMovement({
currTime: dayjs("2024-01-16T10:00:00.000Z"),
draftStartDate: "2024-01-15T09:00:00.000Z",
currentValue: "2024-01-15T10:00:00.000Z",
dateBeingChanged: "endDate",
isAllDay: false,
}),
).toBe(false);
});

it("accepts all-day resize movement across dates", () => {
expect(
getIsValidResizeMovement({
currTime: dayjs("2024-01-16T00:00:00.000Z"),
draftStartDate: "2024-01-15",
currentValue: "2024-01-15",
dateBeingChanged: "endDate",
isAllDay: true,
}),
).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants";
import dayjs, { type Dayjs } from "@core/util/date/dayjs";

interface Params_GetDraggedEventDateRange {
eventStart: Dayjs;
durationMin: number;
isAllDay: boolean;
}

export const getDraggedEventDateRange = ({
eventStart,
durationMin,
isAllDay,
}: Params_GetDraggedEventDateRange) => {
let adjustedStart = eventStart;
let adjustedEnd = eventStart.add(durationMin, "minutes");

if (!isAllDay && adjustedEnd.date() !== adjustedStart.date()) {
adjustedEnd = adjustedEnd.hour(0).minute(0);
adjustedStart = adjustedEnd.subtract(durationMin, "minutes");
}

return {
startDate: isAllDay
? adjustedStart.format(YEAR_MONTH_DAY_FORMAT)
: adjustedStart.format(),
endDate: isAllDay
? adjustedEnd.format(YEAR_MONTH_DAY_FORMAT)
: adjustedEnd.format(),
};
};

interface Params_GetIsValidResizeMovement {
currTime: Dayjs;
draftStartDate: string;
currentValue?: string;
dateBeingChanged: "startDate" | "endDate" | null;
isAllDay: boolean;
}

export const getIsValidResizeMovement = ({
currTime,
draftStartDate,
currentValue,
dateBeingChanged,
isAllDay,
}: Params_GetIsValidResizeMovement) => {
if (!dateBeingChanged) return false;
if (isAllDay) return true;

const formatted = currTime.format();
if (currentValue === formatted) return false;

const isDifferentDay = currTime.day() !== dayjs(draftStartDate).day();
if (isDifferentDay) return false;

return formatted !== draftStartDate;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getDraftSubmitAction } from "./draft.submit-decision";
import { describe, expect, it } from "bun:test";

describe("getDraftSubmitAction", () => {
it("creates a new event when the draft has no id", () => {
expect(
getDraftSubmitAction({
draft: {},
pendingEventIds: [],
isFormOpenBeforeDragging: false,
isDirty: false,
}),
).toBe("CREATE");
});

it("discards a pending event update", () => {
expect(
getDraftSubmitAction({
draft: { _id: "pending-id" },
pendingEventIds: ["pending-id"],
isFormOpenBeforeDragging: false,
isDirty: true,
}),
).toBe("DISCARD");
});

it("opens the form again after a drag that started from an open form", () => {
expect(
getDraftSubmitAction({
draft: { _id: "event-id" },
pendingEventIds: [],
isFormOpenBeforeDragging: true,
isDirty: true,
}),
).toBe("OPEN_FORM");
});

it("discards unchanged existing events", () => {
expect(
getDraftSubmitAction({
draft: { _id: "event-id" },
pendingEventIds: [],
isFormOpenBeforeDragging: false,
isDirty: false,
}),
).toBe("DISCARD");
});

it("updates changed existing events", () => {
expect(
getDraftSubmitAction({
draft: { _id: "event-id" },
pendingEventIds: [],
isFormOpenBeforeDragging: false,
isDirty: true,
}),
).toBe("UPDATE");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type DraftSubmitAction = "CREATE" | "DISCARD" | "OPEN_FORM" | "UPDATE";

type DraftIdentity = {
_id?: string | null;
};

interface Params_GetDraftSubmitAction {
draft: DraftIdentity;
pendingEventIds: string[];
isFormOpenBeforeDragging: boolean | null;
isDirty: boolean;
}

export const getDraftSubmitAction = ({
draft,
pendingEventIds,
isFormOpenBeforeDragging,
isDirty,
}: Params_GetDraftSubmitAction): DraftSubmitAction => {
if (!draft._id) return "CREATE";
if (pendingEventIds.includes(draft._id)) return "DISCARD";
if (isFormOpenBeforeDragging) return "OPEN_FORM";
if (!isDirty) return "DISCARD";
return "UPDATE";
};
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ mock.module("@web/common/validators/grid.event.validator", () => ({

mock.module("@web/common/validators/someday.event.validator", () => ({
validateSomedayEvent,
validateSomedayEvents: mock((events: Schema_SomedayEvent[]) => events),
}));

mock.module("@web/common/utils/event/event.util", () => ({
assembleDefaultEvent: mock(async () => createMockGridEvent()),
assembleGridEvent,
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ import {
import { type DateCalcs } from "@web/views/Calendar/hooks/grid/useDateCalcs";
import { type WeekProps } from "@web/views/Calendar/hooks/useWeek";
import { GRID_TIME_STEP } from "@web/views/Calendar/layout.constants";
import {
getDraggedEventDateRange,
getIsValidResizeMovement,
} from "./draft.movement";
import { getDraftSubmitAction } from "./draft.submit-decision";
import { getDragDurationMinutes } from "./drag-duration.util";

export const useDraftActions = (
Expand Down Expand Up @@ -238,31 +243,16 @@ export const useDraftActions = (

const determineSubmitAction = useCallback(
(draft: Schema_WebEvent) => {
const isExisting = !!draft._id;
if (!isExisting) return "CREATE";

if (isExisting) {
// Prevent updates if event is pending (waiting for backend confirmation)
const isPending = draft._id
? pendingEventIds.includes(draft._id)
: false;
if (isPending) {
// Event is pending, discard the change and return to original position
return "DISCARD";
}

if (isFormOpenBeforeDragging) {
return "OPEN_FORM";
}
const isSame = reduxDraft
? !DirtyParser.isEventDirty(draft, reduxDraft)
: false;
if (isSame) {
// no need to make HTTP request
return "DISCARD";
}
}
return "UPDATE";
const isDirty = reduxDraft
? DirtyParser.isEventDirty(draft, reduxDraft)
: true;

return getDraftSubmitAction({
draft,
pendingEventIds,
isFormOpenBeforeDragging,
isDirty,
});
},
[reduxDraft, isFormOpenBeforeDragging, pendingEventIds],
);
Expand Down Expand Up @@ -390,31 +380,22 @@ export const useDraftActions = (

const y = e.clientY - draft.position.dragOffset.y;

let eventStart = dateCalcs.getDateByXY(
const eventStart = dateCalcs.getDateByXY(
x,
y,
weekProps.component.startOfView,
);

let eventEnd = eventStart.add(startEndDurationMin, "minutes");

if (!draft.isAllDay) {
// Edge case: timed events' end times can overflow past midnight at the bottom of the grid.
// Below logic prevents that from occurring.
if (eventEnd.date() !== eventStart.date()) {
eventEnd = eventEnd.hour(0).minute(0);
eventStart = eventEnd.subtract(startEndDurationMin, "minutes");
}
}
const { startDate, endDate } = getDraggedEventDateRange({
eventStart,
durationMin: startEndDurationMin,
isAllDay: Boolean(draft.isAllDay),
});

const _draft: Schema_GridEvent = {
...draft,
startDate: draft.isAllDay
? eventStart.format(YEAR_MONTH_DAY_FORMAT)
: eventStart.format(),
endDate: draft.isAllDay
? eventEnd.format(YEAR_MONTH_DAY_FORMAT)
: eventEnd.format(),
startDate,
endDate,
priority: draft.priority || Priorities.UNASSIGNED,
};

Expand Down Expand Up @@ -458,22 +439,13 @@ export const useDraftActions = (
(currTime: dayjs.Dayjs) => {
if (!draft || !dateBeingChanged) return false;

if (draft.isAllDay) {
return true;
}

const _currTime = currTime.format();
const noChange = draft[dateBeingChanged] === _currTime;

if (noChange) return false;

const diffDay = currTime.day() !== dayjs(draft.startDate).day();
if (diffDay) return false;

const sameStart = currTime.format() === draft.startDate;
if (sameStart) return false;

return true;
return getIsValidResizeMovement({
currTime,
draftStartDate: draft.startDate,
currentValue: draft[dateBeingChanged],
dateBeingChanged,
isAllDay: Boolean(draft.isAllDay),
});
},
[dateBeingChanged, draft],
);
Expand Down