Load data into calendar view (#14494)

<img width="1919" height="956" alt="image"
src="https://github.com/user-attachments/assets/1389b1e9-63d5-4967-9587-681d8b151b07"
/>
This commit is contained in:
Charles Bochet
2025-09-15 13:09:59 +02:00
committed by GitHub
parent 53aa60baf4
commit 77b6a5b29d
8 changed files with 272 additions and 7 deletions
@@ -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 <></>;
};
@@ -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 <StyledContainer>{day.getDate()}</StyledContainer>;
const recordIds = useRecoilComponentFamilyValue(
calendarDayRecordIdsComponentFamilySelector,
day.toDateString(),
);
return (
<StyledContainer>
{day.getDate()}: {recordIds.length}
</StyledContainer>
);
};
@@ -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,
};
};
@@ -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<string[], string>({
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;
},
});
@@ -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 (
<RecordCalendarComponentInstanceContext.Provider
value={{
@@ -40,6 +52,7 @@ export const RecordIndexCalendarContainer = ({
}}
>
<RecordCalendar />
<RecordIndexCalendarDataLoaderEffect />
</RecordCalendarContextProvider>
</RecordCalendarComponentInstanceContext.Provider>
);
@@ -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,
],
);
@@ -0,0 +1,8 @@
import { createState } from 'twenty-ui/utilities';
export const recordIndexCalendarFieldMetadataIdState = createState<
string | null
>({
key: 'recordIndexCalendarFieldMetadataIdState',
defaultValue: null,
});
@@ -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 = <ValueType>(
componentState: ComponentState<ValueType>,
componentState: ComponentState<ValueType> | ComponentSelector<ValueType>,
instanceIdFromProps?: string,
): SetterOrUpdater<ValueType> => {
const componentInstanceContext = globalComponentInstanceContextMap.get(
@@ -22,5 +27,15 @@ export const useSetRecoilComponentState = <ValueType>(
instanceIdFromProps,
);
return useSetRecoilState(componentState.atomFamily({ instanceId }));
let state: RecoilState<ValueType>;
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);
};