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:
+71
@@ -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} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+17
-6
@@ -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()}
|
||||
|
||||
Reference in New Issue
Block a user