Allow non-compact all-day calendar cards to show full content (#22953)
## Summary - Render all-day calendar items as full `RecordCalendarCard` content in non-compact views, while keeping compact cards clickable as a whole. - Rework the all-day time grid layout so the label and day cells align cleanly in a dedicated grid row. - Add coverage for the new card behavior and for filtering out `DATE_TIME` records from the all-day lane. ### Week (compact) <img width="1308" height="812" alt="Screenshot 2026-07-16 at 15 30 02" src="https://github.com/user-attachments/assets/24f74f22-86c1-4326-8c65-92ee2c3e8c92" /> ### Week (non compact) **NEW** <img width="1311" height="789" alt="Screenshot 2026-07-16 at 15 29 52" src="https://github.com/user-attachments/assets/8445c8c5-c952-47e9-ba24-c21d63352e79" /> ### Month <img width="1310" height="822" alt="Screenshot 2026-07-16 at 15 29 41" src="https://github.com/user-attachments/assets/aa33cf48-c602-49e6-bfb1-b9ab1c798bcb" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22953?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+126
@@ -0,0 +1,126 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { RecordCalendarCard } from '@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCard';
|
||||
|
||||
const mockOpenRecordFromIndexView = jest.fn();
|
||||
const mockUseGetCurrentViewOnly = jest.fn();
|
||||
|
||||
jest.mock('@/views/hooks/useGetCurrentViewOnly', () => ({
|
||||
useGetCurrentViewOnly: () => mockUseGetCurrentViewOnly(),
|
||||
}));
|
||||
jest.mock(
|
||||
'@/object-record/record-index/hooks/useOpenRecordFromIndexView',
|
||||
() => ({
|
||||
useOpenRecordFromIndexView: () => ({
|
||||
openRecordFromIndexView: mockOpenRecordFromIndexView,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
jest.mock(
|
||||
'@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyState',
|
||||
() => ({ useAtomComponentFamilyState: () => [false, jest.fn()] }),
|
||||
);
|
||||
jest.mock(
|
||||
'@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue',
|
||||
() => ({ useAtomComponentStateValue: () => false }),
|
||||
);
|
||||
jest.mock(
|
||||
'@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow',
|
||||
() => ({ useAvailableComponentInstanceIdOrThrow: () => 'calendar-id' }),
|
||||
);
|
||||
jest.mock('@/ui/utilities/state/jotai/hooks/useSetAtomComponentState', () => ({
|
||||
useSetAtomComponentState: () => jest.fn(),
|
||||
}));
|
||||
jest.mock('@/ui/layout/dropdown/hooks/useOpenDropdown', () => ({
|
||||
useOpenDropdown: () => ({ openDropdown: jest.fn() }),
|
||||
}));
|
||||
jest.mock(
|
||||
'@/object-record/record-field-list/contexts/RecordFieldsScopeContext',
|
||||
() => ({
|
||||
RecordFieldsScopeContextProvider: ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => children,
|
||||
}),
|
||||
);
|
||||
jest.mock(
|
||||
'@/object-record/record-calendar/record-calendar-card/anchored-portal/components/RecordCalendarCardCellHoveredPortal',
|
||||
() => ({ RecordCalendarCardCellHoveredPortal: () => null }),
|
||||
);
|
||||
jest.mock(
|
||||
'@/object-record/record-calendar/record-calendar-card/anchored-portal/components/RecordCalendarCardCellEditModePortal',
|
||||
() => ({ RecordCalendarCardCellEditModePortal: () => null }),
|
||||
);
|
||||
jest.mock(
|
||||
'@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardHeader',
|
||||
() => ({
|
||||
RecordCalendarCardHeader: () => <div data-testid="card-header" />,
|
||||
}),
|
||||
);
|
||||
jest.mock(
|
||||
'@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardBody',
|
||||
() => ({
|
||||
RecordCalendarCardBody: () => <div data-testid="card-body" />,
|
||||
}),
|
||||
);
|
||||
jest.mock('@/object-record/record-card/components/RecordCard', () => ({
|
||||
RecordCard: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}) => (
|
||||
<button data-testid="record-card" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
jest.mock('twenty-ui/layout', () => ({
|
||||
AnimatedEaseInOut: ({
|
||||
children,
|
||||
isOpen,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
}) => (isOpen ? children : null),
|
||||
}));
|
||||
|
||||
describe('RecordCalendarCard', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows the full card body without making the whole card clickable', () => {
|
||||
mockUseGetCurrentViewOnly.mockReturnValue({
|
||||
currentView: { isCompact: false },
|
||||
});
|
||||
|
||||
render(<RecordCalendarCard recordId="record-id" />);
|
||||
|
||||
expect(screen.getByTestId('card-header')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('card-body')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('record-card'));
|
||||
|
||||
expect(mockOpenRecordFromIndexView).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides the body and makes the whole compact card clickable', () => {
|
||||
mockUseGetCurrentViewOnly.mockReturnValue({
|
||||
currentView: { isCompact: true },
|
||||
});
|
||||
|
||||
render(<RecordCalendarCard recordId="record-id" />);
|
||||
|
||||
expect(screen.getByTestId('card-header')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('card-body')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('record-card'));
|
||||
|
||||
expect(mockOpenRecordFromIndexView).toHaveBeenCalledWith({
|
||||
recordId: 'record-id',
|
||||
});
|
||||
});
|
||||
});
|
||||
+24
-17
@@ -111,8 +111,17 @@ const StyledDayNumber = styled.span<{ isToday: boolean }>`
|
||||
`;
|
||||
|
||||
const StyledAllDayLabel = styled(StyledHeaderGutter)`
|
||||
border-top: 1px solid ${themeCssVariables.border.color.light};
|
||||
height: 28px;
|
||||
align-items: flex-start;
|
||||
height: auto;
|
||||
min-height: 28px;
|
||||
padding-top: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledAllDayGrid = styled.div<{ dayCount: number }>`
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
${RECORD_CALENDAR_WEEK_DIMENSIONS.timeGutterWidth}px
|
||||
repeat(${({ dayCount }) => dayCount}, minmax(120px, 1fr));
|
||||
`;
|
||||
|
||||
const StyledGrid = styled.div<{ dayCount: number }>`
|
||||
@@ -568,22 +577,20 @@ export const RecordCalendarTimeGrid = ({
|
||||
</StyledDayHeader>
|
||||
);
|
||||
})}
|
||||
{isAllDayView && (
|
||||
<>
|
||||
<StyledAllDayLabel>{t`All day`}</StyledAllDayLabel>
|
||||
{days.map(({ date }) => (
|
||||
<RecordCalendarTimeGridAllDayCell
|
||||
key={`all-day-${date.toString()}`}
|
||||
calendarFieldName={calendarFieldMetadataItem.name}
|
||||
calendarFieldType={calendarFieldMetadataItem.type}
|
||||
day={date}
|
||||
timeFormat={timeFormat}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</StyledHeader>
|
||||
{isAllDayView && (
|
||||
<StyledAllDayGrid dayCount={days.length}>
|
||||
<StyledAllDayLabel>{t`All day`}</StyledAllDayLabel>
|
||||
{days.map(({ date }) => (
|
||||
<RecordCalendarTimeGridAllDayCell
|
||||
key={`all-day-${date.toString()}`}
|
||||
calendarFieldType={calendarFieldMetadataItem.type}
|
||||
day={date}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
))}
|
||||
</StyledAllDayGrid>
|
||||
)}
|
||||
{isTimedView && (
|
||||
<StyledGrid
|
||||
ref={gridRef}
|
||||
|
||||
+9
-16
@@ -1,5 +1,5 @@
|
||||
import { RecordCalendarCard } from '@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCard';
|
||||
import { calendarDayRecordIdsComponentFamilySelector } from '@/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector';
|
||||
import { RecordCalendarWeekEvent } from '@/object-record/record-calendar/week/components/RecordCalendarWeekEvent';
|
||||
import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type Temporal } from 'temporal-polyfill';
|
||||
@@ -8,7 +8,6 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledAllDayCell = styled.div`
|
||||
border-left: 1px solid ${themeCssVariables.border.color.light};
|
||||
border-top: 1px solid ${themeCssVariables.border.color.light};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing['0.5']};
|
||||
@@ -17,19 +16,20 @@ const StyledAllDayCell = styled.div`
|
||||
padding: ${themeCssVariables.spacing['0.5']};
|
||||
`;
|
||||
|
||||
const StyledCardContainer = styled.div`
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type RecordCalendarTimeGridAllDayCellProps = {
|
||||
calendarFieldName: string;
|
||||
calendarFieldType: FieldMetadataType;
|
||||
day: Temporal.PlainDate;
|
||||
timeFormat: string;
|
||||
timeZone: string;
|
||||
};
|
||||
|
||||
export const RecordCalendarTimeGridAllDayCell = ({
|
||||
calendarFieldName,
|
||||
calendarFieldType,
|
||||
day,
|
||||
timeFormat,
|
||||
timeZone,
|
||||
}: RecordCalendarTimeGridAllDayCellProps) => {
|
||||
const recordIds = useAtomComponentFamilySelectorValue(
|
||||
@@ -43,16 +43,9 @@ export const RecordCalendarTimeGridAllDayCell = ({
|
||||
return (
|
||||
<StyledAllDayCell>
|
||||
{allDayRecordIds.map((recordId) => (
|
||||
<RecordCalendarWeekEvent
|
||||
key={recordId}
|
||||
calendarDay={day}
|
||||
calendarFieldName={calendarFieldName}
|
||||
calendarFieldType={calendarFieldType}
|
||||
isAllDay
|
||||
recordId={recordId}
|
||||
timeFormat={timeFormat}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
<StyledCardContainer key={recordId} data-selectable-id={recordId}>
|
||||
<RecordCalendarCard recordId={recordId} />
|
||||
</StyledCardContainer>
|
||||
))}
|
||||
</StyledAllDayCell>
|
||||
);
|
||||
|
||||
+21
-41
@@ -14,20 +14,10 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
jest.mock(
|
||||
'@/object-record/record-calendar/week/components/RecordCalendarWeekEvent',
|
||||
'@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCard',
|
||||
() => ({
|
||||
RecordCalendarWeekEvent: ({
|
||||
isAllDay,
|
||||
recordId,
|
||||
}: {
|
||||
isAllDay: boolean;
|
||||
recordId: string;
|
||||
}) => (
|
||||
<div
|
||||
data-testid="all-day-event"
|
||||
data-is-all-day={isAllDay}
|
||||
data-record-id={recordId}
|
||||
/>
|
||||
RecordCalendarCard: ({ recordId }: { recordId: string }) => (
|
||||
<div data-testid="all-day-card" data-record-id={recordId} />
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -44,41 +34,31 @@ describe('RecordCalendarTimeGridAllDayCell', () => {
|
||||
|
||||
render(
|
||||
<RecordCalendarTimeGridAllDayCell
|
||||
calendarFieldName="date"
|
||||
calendarFieldType={FieldMetadataType.DATE}
|
||||
day={Temporal.PlainDate.from('2026-07-15')}
|
||||
timeFormat="HH:mm"
|
||||
timeZone="UTC"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getAllByTestId('all-day-event').map((element) => ({
|
||||
isAllDay: element.dataset.isAllDay,
|
||||
recordId: element.dataset.recordId,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
isAllDay: 'true',
|
||||
recordId: 'first',
|
||||
},
|
||||
{
|
||||
isAllDay: 'true',
|
||||
recordId: 'second',
|
||||
},
|
||||
{
|
||||
isAllDay: 'true',
|
||||
recordId: 'third',
|
||||
},
|
||||
{
|
||||
isAllDay: 'true',
|
||||
recordId: 'fourth',
|
||||
},
|
||||
{
|
||||
isAllDay: 'true',
|
||||
recordId: 'fifth',
|
||||
},
|
||||
]);
|
||||
screen
|
||||
.getAllByTestId('all-day-card')
|
||||
.map((element) => element.dataset.recordId),
|
||||
).toEqual(['first', 'second', 'third', 'fourth', 'fifth']);
|
||||
expect(screen.queryByText(/^\+\d+$/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render DATE_TIME records in the all-day cell', () => {
|
||||
mockUseAtomComponentFamilySelectorValue.mockReturnValue(['first']);
|
||||
|
||||
render(
|
||||
<RecordCalendarTimeGridAllDayCell
|
||||
calendarFieldType={FieldMetadataType.DATE_TIME}
|
||||
day={Temporal.PlainDate.from('2026-07-15')}
|
||||
timeZone="UTC"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('all-day-card')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user