Feat/add add icon to calendar view (#14696)

# Calendar View: Add "+" Button to Calendar Day Cards

## Description
This PR implements the "+" button on calendar day cards in the Month
view, allowing users to create new records directly from the calendar.

- The "+" button appears **on hover** only.  
- Newly created records include the **day representing the card** in the
payload (UTC, with only the date part).
- Respects **object permissions** and **soft delete filters**.  
- Automatically integrates with **active filters** used in Table and
Kanban views.

## Implementation Notes
- `RecordCalendarAddNew` component handles button visibility and record
creation.
- UTC date is passed to the create function to ensure proper timezone
handling.
- No unit tests added yet (need senior guidance).  

---
[Screencast from 2025-09-24
20-10-18.webm](https://github.com/user-attachments/assets/bd117b43-6c1b-4267-874f-b36844733d9e)

###  Video ->



## Related Issues
Fixes: https://github.com/twentyhq/core-team-issues/issues/1552

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Saurav Jain
2025-09-30 23:34:47 +05:30
committed by GitHub
parent 1c572294f9
commit 3c0a197525
2 changed files with 88 additions and 6 deletions
@@ -0,0 +1,71 @@
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { useTheme } from '@emotion/react';
import { useRecoilValue } from 'recoil';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { useRecordCalendarContextOrThrow } from '../contexts/RecordCalendarContext';
const StyledButton = styled(Button)`
padding: ${({ theme }) => theme.spacing(0.5)};
min-width: unset;
height: auto;
`;
type RecordCalendarAddNewProps = {
cardDate: string;
};
export const RecordCalendarAddNew = ({
cardDate,
}: RecordCalendarAddNewProps) => {
const { objectMetadataItem } = useRecordCalendarContextOrThrow();
const theme = useTheme();
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const hasAnySoftDeleteFilterOnView = useRecoilComponentValue(
hasAnySoftDeleteFilterOnViewComponentSelector,
);
const recordIndexCalendarFieldMetadataId = useRecoilValue(
recordIndexCalendarFieldMetadataIdState,
);
const calendarFieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.id === recordIndexCalendarFieldMetadataId,
);
if (
hasAnySoftDeleteFilterOnView ||
!hasObjectUpdatePermissions ||
!calendarFieldMetadataItem
) {
return null;
}
return (
<StyledButton
onClick={() => {
createNewIndexRecord({
[calendarFieldMetadataItem.name]: cardDate,
});
}}
variant="tertiary"
Icon={() => <IconPlus size={theme.icon.size.sm} />}
/>
);
};
@@ -7,6 +7,8 @@ import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { Droppable } from '@hello-pangea/dnd';
import { format, isSameDay, isSameMonth, isWeekend } from 'date-fns';
import { useState } from 'react';
import { RecordCalendarAddNew } from '../../components/RecordCalendarAddNew';
const StyledContainer = styled.div<{
isOtherMonth: boolean;
@@ -42,13 +44,13 @@ const StyledContainer = styled.div<{
const StyledDayHeader = styled.div`
align-items: center;
display: flex;
flex-direction: column;
flex-direction: row;
height: 24px;
justify-content: center;
margin-bottom: ${({ theme }) => theme.spacing(0.5)};
margin-left: auto;
width: 24px;
justify-content: space-between;
margin-bottom: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(1.5)};
margin-left: none;
width: 100%;
`;
const StyledDayHeaderDayContainer = styled.div`
@@ -110,18 +112,27 @@ export const RecordCalendarMonthBodyDay = ({
dayKey,
);
const [hovered, setHovered] = useState(false);
const isToday = isSameDay(day, new Date());
const isOtherMonth = !isSameMonth(day, recordCalendarSelectedDate);
const isDayOfWeekend = isWeekend(day);
const utcDate = new Date(
Date.UTC(day.getFullYear(), day.getMonth(), day.getDate()),
);
return (
<StyledContainer
isOtherMonth={isOtherMonth}
isDayOfWeekend={isDayOfWeekend}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<StyledDayHeader>
{hovered && <RecordCalendarAddNew cardDate={utcDate.toISOString()} />}
<StyledDayHeaderDayContainer>
<StyledDayHeaderDay isToday={isToday}>
{day.getDate()}