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:
Weiko
2026-07-16 15:59:12 +02:00
committed by GitHub
parent 7939cd8684
commit a7de3ce3a5
4 changed files with 180 additions and 74 deletions
@@ -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',
});
});
});
@@ -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}
@@ -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>
);
@@ -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();
});
});