From 77b6a5b29db0ff4842f28765adad55efa703ee27 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 15 Sep 2025 13:09:59 +0200 Subject: [PATCH] Load data into calendar view (#14494) image --- .../RecordIndexCalendarDataLoaderEffect.tsx | 84 +++++++++++++++++++ .../components/RecordCalendarMonthBodyDay.tsx | 13 ++- .../useRecordCalendarQueryDateRangeFilter.tsx | 60 +++++++++++++ ...lendarDayRecordsComponentFamilySelector.ts | 65 ++++++++++++++ .../RecordIndexCalendarContainer.tsx | 13 +++ .../hooks/useLoadRecordIndexStates.ts | 15 +++- ...recordIndexCalendarFieldMetadataIdState.ts | 8 ++ .../hooks/useSetRecoilComponentState.ts | 21 ++++- 8 files changed, 272 insertions(+), 7 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-calendar/components/RecordIndexCalendarDataLoaderEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState.ts diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordIndexCalendarDataLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordIndexCalendarDataLoaderEffect.tsx new file mode 100644 index 0000000000..f5728d786a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordIndexCalendarDataLoaderEffect.tsx @@ -0,0 +1,84 @@ +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; +import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext'; +import { useRecordCalendarQueryDateRangeFilter } from '@/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter'; +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { combineFilters } from '@/object-record/record-filter/utils/combineFilters'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; +import { turnAnyFieldFilterIntoRecordGqlFilter } from '@/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter'; +import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { useEffect } from 'react'; + +export const RecordIndexCalendarDataLoaderEffect = () => { + const selectedDate = new Date(); + const { objectMetadataItem } = useRecordCalendarContextOrThrow(); + + const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore(); + + const currentRecordFilterGroups = useRecoilComponentValue( + currentRecordFilterGroupsComponentState, + ); + + const currentRecordFilters = useRecoilComponentValue( + currentRecordFiltersComponentState, + ); + + const currentRecordSorts = useRecoilComponentValue( + currentRecordSortsComponentState, + ); + + const setRecordIndexAllRecordIdsSelector = useSetRecoilComponentState( + recordIndexAllRecordIdsComponentSelector, + ); + + const { filterValueDependencies } = useFilterValueDependencies(); + + const requestFilters = computeRecordGqlOperationFilter({ + filterValueDependencies, + recordFilters: currentRecordFilters, + recordFilterGroups: currentRecordFilterGroups, + fields: objectMetadataItem.fields, + }); + + const anyFieldFilterValue = useRecoilComponentValue( + anyFieldFilterValueComponentState, + ); + + const { recordGqlOperationFilter: anyFieldFilter } = + turnAnyFieldFilterIntoRecordGqlFilter({ + objectMetadataItem, + filterValue: anyFieldFilterValue, + }); + + const { dateRangeFilter } = + useRecordCalendarQueryDateRangeFilter(selectedDate); + + const orderBy = turnSortsIntoOrderBy(objectMetadataItem, currentRecordSorts); + + const combinedFilters = combineFilters([ + anyFieldFilter, + requestFilters, + dateRangeFilter, + ]); + + const { records } = useFindManyRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + filter: combinedFilters, + orderBy, + limit: 100, + }); + + useEffect(() => { + upsertRecordsInStore(records); + setRecordIndexAllRecordIdsSelector(records.map((record) => record.id)); + }, [records, setRecordIndexAllRecordIdsSelector, upsertRecordsInStore]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyDay.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyDay.tsx index fa12479b0a..ec9526270f 100644 --- a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyDay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/RecordCalendarMonthBodyDay.tsx @@ -1,3 +1,5 @@ +import { calendarDayRecordIdsComponentFamilySelector } from '@/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector'; +import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import styled from '@emotion/styled'; const StyledContainer = styled.div` @@ -12,5 +14,14 @@ type RecordCalendarMonthBodyDayProps = { export const RecordCalendarMonthBodyDay = ({ day, }: RecordCalendarMonthBodyDayProps) => { - return {day.getDate()}; + const recordIds = useRecoilComponentFamilyValue( + calendarDayRecordIdsComponentFamilySelector, + day.toDateString(), + ); + + return ( + + {day.getDate()}: {recordIds.length} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter.tsx new file mode 100644 index 0000000000..a365fd78cb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter.tsx @@ -0,0 +1,60 @@ +import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext'; +import { useRecordCalendarMonthDaysRange } from '@/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange'; +import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; +import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; +import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; +import { isDefined } from 'twenty-shared/utils'; + +const DATE_RANGE_FILTER_AFTER_ID = 'DATE_RANGE_FILTER_AFTER_ID'; +const DATE_RANGE_FILTER_BEFORE_ID = 'DATE_RANGE_FILTER_BEFORE_ID'; + +export const useRecordCalendarQueryDateRangeFilter = (selectedDate: Date) => { + const { objectMetadataItem } = useRecordCalendarContextOrThrow(); + const { firstDayOfFirstWeek, lastDayOfLastWeek } = + useRecordCalendarMonthDaysRange(selectedDate); + + const { currentView } = useGetCurrentViewOnly(); + + if ( + !isDefined(currentView) || + !isDefined(currentView.calendarFieldMetadataId) + ) { + return { + dateRangeFilter: {}, + }; + } + + const dateRangeFilterFieldMetadataId = currentView.calendarFieldMetadataId; + + const dateRangeFilterAfter: RecordFilter = { + id: DATE_RANGE_FILTER_AFTER_ID, + fieldMetadataId: dateRangeFilterFieldMetadataId, + value: `${firstDayOfFirstWeek.toISOString()}`, + operand: RecordFilterOperand.IsAfter, + type: 'DATE', + label: 'After', + displayValue: `${firstDayOfFirstWeek.toISOString()}`, + }; + + const dateRangeFilterBefore: RecordFilter = { + id: DATE_RANGE_FILTER_BEFORE_ID, + fieldMetadataId: dateRangeFilterFieldMetadataId, + value: `${lastDayOfLastWeek.toISOString()}`, + operand: RecordFilterOperand.IsBefore, + type: 'DATE', + label: 'Before', + displayValue: `${lastDayOfLastWeek.toISOString()}`, + }; + + const dateRangeFilter = computeRecordGqlOperationFilter({ + filterValueDependencies: {}, + recordFilters: [dateRangeFilterAfter, dateRangeFilterBefore], + recordFilterGroups: [], + fields: objectMetadataItem.fields, + }); + + return { + dateRangeFilter, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector.ts new file mode 100644 index 0000000000..ee604de744 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector.ts @@ -0,0 +1,65 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext'; +import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState'; +import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector'; +import { isSameDay } from 'date-fns'; +import { isDefined } from 'twenty-shared/utils'; + +export const calendarDayRecordIdsComponentFamilySelector = + createComponentFamilySelector({ + key: 'calendarDayRecordsComponentFamilySelector', + componentInstanceContext: RecordCalendarComponentInstanceContext, + get: + ({ instanceId, familyKey: dayAsString }) => + ({ get }) => { + const calendarFieldMetadataId = get( + recordIndexCalendarFieldMetadataIdState, + ); + + const objectMetadataItems = get(objectMetadataItemsState); + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.fields.some( + (fieldMetadataItem) => + fieldMetadataItem.id === calendarFieldMetadataId, + ), + ); + + if (!isDefined(objectMetadataItem)) { + return []; + } + + const fieldMetadataItem = objectMetadataItem.fields.find( + (fieldMetadataItem) => + fieldMetadataItem.id === calendarFieldMetadataId, + ); + + if (!isDefined(fieldMetadataItem)) { + return []; + } + + const allRecordIds = get( + recordIndexAllRecordIdsComponentSelector.selectorFamily({ + instanceId, + }), + ); + + const recordIds = allRecordIds.filter((recordId) => { + const record = get(recordStoreFamilyState(recordId)); + const recordDate = record?.[fieldMetadataItem.name]; + + if (!recordDate) { + return false; + } + + const dayDate = new Date(dayAsString); + const recordDateObj = new Date(recordDate); + + return isSameDay(recordDateObj, dayDate); + }); + + return recordIds; + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexCalendarContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexCalendarContainer.tsx index 50998c0613..ab16176efd 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexCalendarContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexCalendarContainer.tsx @@ -1,9 +1,12 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; import { RecordCalendar } from '@/object-record/record-calendar/components/RecordCalendar'; +import { RecordIndexCalendarDataLoaderEffect } from '@/object-record/record-calendar/components/RecordIndexCalendarDataLoaderEffect'; import { RecordCalendarContextProvider } from '@/object-record/record-calendar/contexts/RecordCalendarContext'; import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; +import { isDefined } from 'twenty-shared/utils'; type RecordIndexCalendarContainerProps = { recordCalendarInstanceId: string; @@ -24,6 +27,15 @@ export const RecordIndexCalendarContainer = ({ objectMetadataItem.id, ); + const { currentView } = useGetCurrentViewOnly(); + + if ( + !isDefined(currentView) || + !isDefined(currentView.calendarFieldMetadataId) + ) { + return null; + } + return ( + ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts index e8761d901c..74429d8da3 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts @@ -5,6 +5,7 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups'; +import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; @@ -38,6 +39,10 @@ export const useLoadRecordIndexStates = () => { const setRecordIndexViewKanbanFieldMetadataIdState = useSetRecoilState( recordIndexKanbanFieldMetadataIdState, ); + + const setRecordIndexCalendarFieldMetadataIdState = useSetRecoilState( + recordIndexCalendarFieldMetadataIdState, + ); const setRecordIndexViewKanbanAggregateOperationState = useSetRecoilState( recordIndexKanbanAggregateOperationState, ); @@ -182,6 +187,9 @@ export const useLoadRecordIndexStates = () => { setRecordIndexViewKanbanFieldMetadataIdState( view.viewGroups?.[0]?.fieldMetadataId, ); + setRecordIndexCalendarFieldMetadataIdState( + view.calendarFieldMetadataId ?? null, + ); const kanbanAggregateOperationFieldMetadataType = objectMetadataItem.fields?.find( (field) => @@ -202,11 +210,12 @@ export const useLoadRecordIndexStates = () => { onViewFieldsChange, setRecordGroupsFromViewGroups, setContextStoreTargetedRecordsRuleComponentState, - setRecordIndexIsCompactModeActive, - setRecordIndexViewKanbanAggregateOperationState, - setRecordIndexViewKanbanFieldMetadataIdState, setRecordIndexViewType, setRecordIndexOpenRecordIn, + setRecordIndexViewKanbanFieldMetadataIdState, + setRecordIndexCalendarFieldMetadataIdState, + setRecordIndexViewKanbanAggregateOperationState, + setRecordIndexIsCompactModeActive, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState.ts new file mode 100644 index 0000000000..aa5d603c24 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState.ts @@ -0,0 +1,8 @@ +import { createState } from 'twenty-ui/utilities'; + +export const recordIndexCalendarFieldMetadataIdState = createState< + string | null +>({ + key: 'recordIndexCalendarFieldMetadataIdState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useSetRecoilComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useSetRecoilComponentState.ts index 4324d81465..e44814cc7b 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useSetRecoilComponentState.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useSetRecoilComponentState.ts @@ -1,10 +1,15 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { type ComponentSelector } from '@/ui/utilities/state/component-state/types/ComponentSelector'; import { type ComponentState } from '@/ui/utilities/state/component-state/types/ComponentState'; import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap'; -import { type SetterOrUpdater, useSetRecoilState } from 'recoil'; +import { + type RecoilState, + type SetterOrUpdater, + useSetRecoilState, +} from 'recoil'; export const useSetRecoilComponentState = ( - componentState: ComponentState, + componentState: ComponentState | ComponentSelector, instanceIdFromProps?: string, ): SetterOrUpdater => { const componentInstanceContext = globalComponentInstanceContextMap.get( @@ -22,5 +27,15 @@ export const useSetRecoilComponentState = ( instanceIdFromProps, ); - return useSetRecoilState(componentState.atomFamily({ instanceId })); + let state: RecoilState; + + if (componentState.type === 'ComponentState') { + state = componentState.atomFamily({ instanceId }); + } else if (componentState.type === 'ComponentSelector') { + state = componentState.selectorFamily({ instanceId }); + } else { + throw new Error('Invalid component state type'); + } + + return useSetRecoilState(state); };