= {},
+): Schema_GridEvent => {
+ const standaloneEvent = createMockStandaloneEvent();
+ return {
+ ...standaloneEvent,
+ position: gridEventDefaultPosition,
+ isAllDay: true,
+ ...overrides,
+ } as Schema_GridEvent;
+};
+
+const mockMeasurements: Measurements_Grid = {
+ mainGrid: {
+ top: 0,
+ left: 0,
+ height: 1000,
+ width: 1000,
+ x: 0,
+ y: 0,
+ bottom: 1000,
+ right: 1000,
+ toJSON: () => ({}),
+ },
+ hourHeight: 60,
+ colWidths: Array(7).fill(100),
+ allDayRow: null,
+ remeasure: jest.fn(),
+};
+
+const mockOnMouseDown = jest.fn();
+const mockOnScalerMouseDown = jest.fn();
+
+describe("AllDayEvent - Repeat Icon", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("recurring event rendering", () => {
+ it("should render repeat icon for base recurring event with rule", () => {
+ const baseEvent = createMockBaseEvent({
+ _id: "base-123",
+ title: "Weekly Team Standup",
+ });
+
+ const event = createMockGridEvent(baseEvent);
+
+ const initialState = createInitialState({
+ events: {
+ pendingEvents: {
+ eventIds: [],
+ },
+ },
+ });
+
+ render(
+ ,
+ { state: initialState },
+ );
+
+ const icon = screen.getByLabelText("Recurring event");
+ expect(icon).toBeInTheDocument();
+ expect(screen.getByText("Weekly Team Standup")).toBeInTheDocument();
+ });
+
+ it("should render repeat icon for recurring instance with eventId", () => {
+ const baseEventId = "base-456";
+ const gBaseId = "gbase-456";
+ const instance = createMockInstance(baseEventId, gBaseId, {
+ _id: "instance-1",
+ title: "Weekly Team Standup Instance",
+ isAllDay: true,
+ });
+
+ const event = createMockGridEvent(instance);
+
+ const initialState = createInitialState({
+ events: {
+ pendingEvents: {
+ eventIds: [],
+ },
+ },
+ });
+
+ render(
+ ,
+ { state: initialState },
+ );
+
+ const icon = screen.getByLabelText("Recurring event");
+ expect(icon).toBeInTheDocument();
+ });
+
+ it("should not render repeat icon for non-recurring all-day event", () => {
+ const event = createMockGridEvent({
+ _id: "standalone-1",
+ title: "Company Holiday",
+ recurrence: undefined,
+ });
+
+ const initialState = createInitialState({
+ events: {
+ pendingEvents: {
+ eventIds: [],
+ },
+ },
+ });
+
+ render(
+ ,
+ { state: initialState },
+ );
+
+ expect(
+ screen.queryByLabelText("Recurring event"),
+ ).not.toBeInTheDocument();
+ expect(screen.getByText("Company Holiday")).toBeInTheDocument();
+ });
+
+ it("should not render repeat icon when recurrence is explicitly disabled", () => {
+ const event = createMockGridEvent({
+ _id: "standalone-2",
+ title: "One-time Event",
+ recurrence: {
+ rule: null,
+ eventId: undefined,
+ },
+ });
+
+ const initialState = createInitialState({
+ events: {
+ pendingEvents: {
+ eventIds: [],
+ },
+ },
+ });
+
+ render(
+ ,
+ { state: initialState },
+ );
+
+ expect(
+ screen.queryByLabelText("Recurring event"),
+ ).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvent.tsx b/packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvent.tsx
index f15c0c7b0..00fe692f4 100644
--- a/packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvent.tsx
+++ b/packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvent.tsx
@@ -9,6 +9,7 @@ import { type Schema_GridEvent } from "@web/common/types/web.event.types";
import { getEventPosition } from "@web/common/utils/position/position.util";
import { Flex } from "@web/components/Flex";
import { AlignItems, FlexDirections } from "@web/components/Flex/styled";
+import { RepeatIcon } from "@web/components/Icons/Repeat";
import { SpaceCharacter } from "@web/components/SpaceCharacter";
import { Text } from "@web/components/Text";
import { selectIsEventPending } from "@web/ducks/events/selectors/pending.selectors";
@@ -53,7 +54,10 @@ const AllDayEvent = ({
const isPending = useAppSelector((state) =>
selectIsEventPending(state, event._id!),
);
- const isRecurring = event.recurrence && event.recurrence?.eventId !== null;
+ const rule = event.recurrence?.rule;
+ const recurrenceEventId = event.recurrence?.eventId;
+ const isRecurring =
+ Array.isArray(rule) || typeof recurrenceEventId === "string";
const styledEventProps = {
[DATA_EVENT_ELEMENT_ID]: event._id,
@@ -96,6 +100,13 @@ const AllDayEvent = ({
direction={FlexDirections.COLUMN}
>
+ {isRecurring && (
+
+ )}
{event.title}
diff --git a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.test.tsx b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.test.tsx
new file mode 100644
index 000000000..622352870
--- /dev/null
+++ b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.test.tsx
@@ -0,0 +1,170 @@
+import "@testing-library/jest-dom";
+import { screen } from "@testing-library/react";
+import { Categories_Event, type Schema_Event } from "@core/types/event.types";
+import {
+ createMockBaseEvent,
+ createMockInstance,
+ createMockStandaloneEvent,
+} from "@core/util/test/ccal.event.factory";
+import { render } from "@web/__tests__/__mocks__/mock.render";
+import { type Props_DraftForm } from "@web/views/Calendar/components/Draft/context/DraftContext";
+import { SomedayEventRectangle } from "./SomedayEventRectangle";
+
+const mockFormProps: Props_DraftForm = {
+ context: {} as any,
+ refs: {
+ setFloating: jest.fn(),
+ setReference: jest.fn(),
+ },
+ strategy: "absolute" as const,
+ x: 0,
+ y: 0,
+ getReferenceProps: () => ({}),
+ getFloatingProps: () => ({}),
+} as any;
+
+const mockOnMigrate = jest.fn();
+
+describe("SomedayEventRectangle - Repeat Icon", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("recurring event rendering", () => {
+ it("should render repeat icon for base recurring event with rule", () => {
+ const baseEvent = createMockBaseEvent({
+ _id: "base-789",
+ title: "Weekly Review",
+ }) as Schema_Event;
+
+ render(
+ ,
+ );
+
+ const icon = screen.getByLabelText("Recurring event");
+ expect(icon).toBeInTheDocument();
+ expect(screen.getByText("Weekly Review")).toBeInTheDocument();
+ });
+
+ it("should render repeat icon for recurring instance with eventId", () => {
+ const baseEventId = "base-101";
+ const gBaseId = "gbase-101";
+ const instance = createMockInstance(baseEventId, gBaseId, {
+ _id: "instance-2",
+ title: "Weekly Review Instance",
+ }) as Schema_Event;
+
+ render(
+ ,
+ );
+
+ const icon = screen.getByLabelText("Recurring event");
+ expect(icon).toBeInTheDocument();
+ expect(screen.getByText("Weekly Review Instance")).toBeInTheDocument();
+ });
+
+ it("should not render repeat icon for non-recurring someday event", () => {
+ const event = createMockStandaloneEvent({
+ _id: "standalone-3",
+ title: "Read Book",
+ recurrence: undefined,
+ }) as Schema_Event;
+
+ render(
+ ,
+ );
+
+ expect(
+ screen.queryByLabelText("Recurring event"),
+ ).not.toBeInTheDocument();
+ expect(screen.getByText("Read Book")).toBeInTheDocument();
+ });
+
+ it("should not render repeat icon when recurrence is explicitly disabled", () => {
+ const event = createMockStandaloneEvent({
+ _id: "standalone-4",
+ title: "One-time Task",
+ recurrence: {
+ rule: null,
+ eventId: undefined,
+ },
+ }) as Schema_Event;
+
+ render(
+ ,
+ );
+
+ expect(
+ screen.queryByLabelText("Recurring event"),
+ ).not.toBeInTheDocument();
+ expect(screen.getByText("One-time Task")).toBeInTheDocument();
+ });
+ });
+
+ describe("migration controls", () => {
+ it("should show migration arrows for non-recurring events", () => {
+ const event = createMockStandaloneEvent({
+ _id: "standalone-5",
+ title: "Regular Task",
+ recurrence: undefined,
+ }) as Schema_Event;
+
+ render(
+ ,
+ );
+
+ const arrows = screen.getAllByRole("button");
+ expect(arrows).toHaveLength(2);
+ expect(arrows[0]).toHaveTextContent("<");
+ expect(arrows[1]).toHaveTextContent(">");
+ });
+
+ it("should show warning indicator for recurring events", () => {
+ const baseEvent = createMockBaseEvent({
+ _id: "base-202",
+ title: "Recurring Task",
+ }) as Schema_Event;
+
+ render(
+ ,
+ );
+
+ const warning = screen.getByText("☝️");
+ expect(warning).toBeInTheDocument();
+ expect(warning).toHaveAttribute(
+ "title",
+ "Can't migrate recurring events",
+ );
+ });
+ });
+});
diff --git a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx
index 15fd46b16..054ad94c3 100644
--- a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx
+++ b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx
@@ -5,10 +5,11 @@ import {
FlexDirections,
JustifyContent,
} from "@web/components/Flex/styled";
+import { RepeatIcon } from "@web/components/Icons/Repeat";
import { Text } from "@web/components/Text";
import { type Props_DraftForm } from "@web/views/Calendar/components/Draft/context/DraftContext";
import { type Actions_Sidebar } from "@web/views/Calendar/components/Draft/sidebar/hooks/useSidebarActions";
-import { StyledMigrateArrow, StyledRecurrenceText } from "./styled";
+import { StyledMigrateArrow, StyledRecurringWarning } from "./styled";
interface Props {
category: Categories_Event;
@@ -24,8 +25,11 @@ export const SomedayEventRectangle = ({
onMigrate,
}: Props) => {
const target = category === Categories_Event.SOMEDAY_WEEK ? "week" : "month";
- const canMigrate =
- !event.recurrence?.rule || event.recurrence?.rule.length === 0;
+ const rule = event.recurrence?.rule;
+ const recurrenceEventId = event.recurrence?.eventId;
+ const isRecurring =
+ Array.isArray(rule) || typeof recurrenceEventId === "string";
+ const canMigrate = !isRecurring;
return (
@@ -34,7 +38,16 @@ export const SomedayEventRectangle = ({
direction={FlexDirections.ROW}
justifyContent={JustifyContent.SPACE_BETWEEN}
>
- {event.title}
+
+ {isRecurring && (
+
+ )}
+ {event.title}
+
{canMigrate ? (
@@ -61,7 +74,7 @@ export const SomedayEventRectangle = ({
) : (
- {
e.stopPropagation();
alert("Can't migrate recurring events");
@@ -69,7 +82,7 @@ export const SomedayEventRectangle = ({
title="Can't migrate recurring events"
>
☝️
-
+
)}
diff --git a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/styled.ts b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/styled.ts
index a363f106f..2762b5634 100644
--- a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/styled.ts
+++ b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEventContainer/styled.ts
@@ -18,16 +18,17 @@ export const StyledMigrateArrow = styled.span`
export const StyledMigrateArrowInForm = styled(StyledMigrateArrow)`
font-size: 27px;
`;
-export const StyledRecurrenceText = styled.span`
- border: 1px solid ${({ theme }) => theme.color.border.primary};
+
+export const StyledRecurringWarning = styled.span`
+ border: 1px solid transparent;
border-radius: 2px;
+ cursor: not-allowed;
font-size: 10px;
- opacity: 0;
- transition: opacity 0.2s;
- width: 43px;
+ opacity: 0.5;
+ padding: 2px 4px;
&:hover {
- opacity: 1;
- transition: border ease-in 0.2s;
+ opacity: 0.7;
+ transition: opacity 0.2s;
}
`;