Compare commits

...

5 Commits

Author SHA1 Message Date
Weiko bb9add4c48 various UI fixes in calendar view (#14551) 2025-09-17 16:29:22 +02:00
Thomas Trompette 538bd944cd Put back previous empty option (#14542)
Adding that option puts No Value everywhere. Removing it.
2025-09-16 17:38:56 +02:00
Weiko 27a21dea35 Add dnd to calendar view (#14539) 2025-09-16 17:38:51 +02:00
Thomas Trompette fabeeafad6 Add form select field (#14538)
- create a WorkflowFormFieldInput component that takes a fieldMetadataId
as input and display a form field input
- add a select field for forms


https://github.com/user-attachments/assets/42f8ce98-c17a-4809-b1be-032e3066a596
2025-09-16 17:38:47 +02:00
Weiko 2168a678d8 Add compact view to calendar (#14527)
<img width="1263" height="785" alt="Screenshot 2025-09-16 at 11 44 44"
src="https://github.com/user-attachments/assets/dc61e69f-b887-4a4d-9f9c-e50ffa076082"
/>
<img width="1263" height="785" alt="Screenshot 2025-09-16 at 11 43 45"
src="https://github.com/user-attachments/assets/2dd917b3-6e6d-40f2-bb10-6c82de83c15d"
/>
2025-09-16 17:38:42 +02:00
35 changed files with 676 additions and 242 deletions
@@ -1,6 +1,5 @@
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState';
@@ -16,12 +15,15 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType';
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useLingui } from '@lingui/react/macro';
import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -40,22 +42,24 @@ import { FeatureFlagKey, ViewCalendarLayout } from '~/generated/graphql';
export const ObjectOptionsDropdownLayoutContent = () => {
const { t } = useLingui();
const { objectMetadataItem, resetContent, onContentChange, dropdownId } =
useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();
const { updateCurrentView } = useUpdateCurrentView();
const {
recordIndexId,
objectMetadataItem,
resetContent,
onContentChange,
dropdownId,
} = useObjectOptionsDropdown();
const isCompactModeActive = currentView?.isCompact ?? false;
const { isCompactModeActive, setAndPersistIsCompactModeActive } =
useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
});
const setAndPersistIsCompactModeActive = useCallback(
(isCompactModeActive: boolean, view: GraphQLView | undefined) => {
if (!view) return;
updateCurrentView({
isCompact: isCompactModeActive,
});
},
[updateCurrentView],
);
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
const recordIndexCalendarLayout = useRecoilValue(
@@ -116,10 +120,11 @@ export const ObjectOptionsDropdownLayoutContent = () => {
...(isDefaultView ? [] : [ViewType.Kanban]),
...(!isDefaultView && hasCalendarViewEnabled ? [ViewType.Calendar] : []),
ViewOpenRecordInType.SIDE_PANEL,
...(currentView?.type === ViewType.Kanban ? ['Group', 'Compact view'] : []),
...(currentView?.type === ViewType.Kanban ? ['Group'] : []),
...(currentView?.type === ViewType.Calendar
? ['CalendarView', 'CalendarDateField']
: []),
...(currentView?.type !== ViewType.Table ? ['Compact view'] : []),
];
const selectedItemId = useRecoilComponentValue(
@@ -273,53 +278,52 @@ export const ObjectOptionsDropdownLayoutContent = () => {
/>
</SelectableListItem>
{currentView?.type === ViewType.Kanban && (
<>
<SelectableListItem
itemId="Group"
onEnter={() => {
<SelectableListItem
itemId="Group"
onEnter={() => {
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields');
}}
>
<MenuItem
focused={selectedItemId === 'Group'}
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields');
}}
>
<MenuItem
focused={selectedItemId === 'Group'}
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text={t`Group`}
contextualText={recordGroupFieldMetadata?.label}
hasSubMenu
/>
</SelectableListItem>
<SelectableListItem
itemId="Compact view"
onEnter={() => {
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text={t`Group`}
contextualText={recordGroupFieldMetadata?.label}
hasSubMenu
/>
</SelectableListItem>
)}
{currentView?.type !== ViewType.Table && (
<SelectableListItem
itemId="Compact view"
onEnter={() => {
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
);
}}
>
<MenuItemToggle
focused={selectedItemId === 'Compact view'}
LeftIcon={IconBaselineDensitySmall}
onToggleChange={() =>
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
);
}}
>
<MenuItemToggle
focused={selectedItemId === 'Compact view'}
LeftIcon={IconBaselineDensitySmall}
onToggleChange={() =>
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
)
}
toggled={isCompactModeActive}
text={t`Compact view`}
toggleSize="small"
/>
</SelectableListItem>
</>
)
}
toggled={isCompactModeActive}
text={t`Compact view`}
toggleSize="small"
/>
</SelectableListItem>
)}
</DropdownMenuItemsContainer>
</SelectableList>
@@ -4,7 +4,6 @@ import { useRecoilState } from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { useReorderVisibleRecordFields } from '@/object-record/record-field/hooks/useReorderVisibleRecordFields';
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
import { useUpsertRecordField } from '@/object-record/record-field/hooks/useUpsertRecordField';
@@ -13,11 +12,8 @@ import { type RecordField } from '@/object-record/record-field/types/RecordField
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField';
import { produce } from 'immer';
import { findByProperty, isDefined } from 'twenty-shared/utils';
@@ -39,12 +35,6 @@ export const useObjectOptionsForBoard = ({
useRecoilState(recordIndexFieldDefinitionsState);
const { saveViewFields } = useSaveCurrentViewFields();
const { updateCurrentView } = useUpdateCurrentView();
const [isCompactModeActive, setIsCompactModeActive] = useRecoilComponentState(
isRecordBoardCompactModeActiveComponentState,
recordBoardId,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -252,23 +242,10 @@ export const useObjectOptionsForBoard = ({
],
);
const setAndPersistIsCompactModeActive = useCallback(
(isCompactModeActive: boolean, view: GraphQLView | undefined) => {
if (!view) return;
setIsCompactModeActive(isCompactModeActive);
updateCurrentView({
isCompact: isCompactModeActive,
});
},
[setIsCompactModeActive, updateCurrentView],
);
return {
handleReorderBoardFields,
handleBoardFieldVisibilityChange,
visibleBoardFields,
hiddenBoardFields,
isCompactModeActive,
setAndPersistIsCompactModeActive,
};
};
@@ -5,7 +5,6 @@ import { RecordBoardCardContext } from '@/object-record/record-board/record-boar
import { isRecordBoardCardActiveComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardActiveComponentFamilyState';
import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState';
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard';
@@ -26,8 +25,8 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { InView, useInView } from 'react-intersection-observer';
@@ -82,9 +81,9 @@ export const RecordBoardCard = () => {
multiDragState.originalSelection.includes(recordId) &&
recordId !== multiDragState.primaryDraggedRecordId;
const isCompactModeActive = useRecoilComponentValue(
isRecordBoardCompactModeActiveComponentState,
);
const { currentView } = useGetCurrentViewOnly();
const isCompactModeActive = currentView?.isCompact ?? false;
const [isCardExpanded, setIsCardExpanded] = useRecoilComponentState(
recordBoardCardIsExpandedComponentState,
@@ -2,7 +2,6 @@ import { RecordBoardContext } from '@/object-record/record-board/contexts/Record
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { RecordChip } from '@/object-record/components/RecordChip';
import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard';
@@ -15,7 +14,7 @@ import { recordIndexOpenRecordInState } from '@/object-record/record-index/state
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import styled from '@emotion/styled';
import { useContext } from 'react';
@@ -50,9 +49,9 @@ export const RecordBoardCardHeader = () => {
const { activateBoardCard } = useActiveRecordBoardCard(recordBoardId);
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId);
const isCompactModeActive = useRecoilComponentValue(
isRecordBoardCompactModeActiveComponentState,
);
const { currentView } = useGetCurrentViewOnly();
const isCompactModeActive = currentView?.isCompact ?? false;
const [isCardExpanded, setIsCardExpanded] = useRecoilComponentState(
recordBoardCardIsExpandedComponentState,
@@ -9,10 +9,10 @@ import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/r
import { RecordBoardColumnNewRecordButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading';
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
const StyledColumnCardsContainer = styled.div`
display: flex;
@@ -58,9 +58,9 @@ export const RecordBoardColumnCardsContainer = ({
const numberOfFields = visibleRecordFields.length;
const isCompactModeActive = useRecoilComponentValue(
isRecordBoardCompactModeActiveComponentState,
);
const { currentView } = useGetCurrentViewOnly();
const isCompactModeActive = currentView?.isCompact ?? false;
return (
<StyledColumnCardsContainer
@@ -1,9 +0,0 @@
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const isRecordBoardCompactModeActiveComponentState =
createComponentState<boolean>({
key: 'isRecordBoardCompactModeActiveComponentState',
defaultValue: false,
componentInstanceContext: RecordBoardComponentInstanceContext,
});
@@ -1,82 +1,31 @@
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 { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
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 { useRecordFieldGqlFields } from '@/object-record/record-field/hooks/useRecordTableRecordGqlFields';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
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 recordCalendarSelectedDate = useRecoilComponentValue(
recordCalendarSelectedDateComponentState,
);
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 objectNameSingular = objectMetadataItem.nameSingular;
const requestFilters = computeRecordGqlOperationFilter({
filterValueDependencies,
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
fields: objectMetadataItem.fields,
});
const params = useFindManyRecordIndexTableParams(objectNameSingular);
const anyFieldFilterValue = useRecoilComponentValue(
anyFieldFilterValueComponentState,
);
const { recordGqlOperationFilter: anyFieldFilter } =
turnAnyFieldFilterIntoRecordGqlFilter({
objectMetadataItem,
filterValue: anyFieldFilterValue,
});
const { dateRangeFilter } = useRecordCalendarQueryDateRangeFilter(
recordCalendarSelectedDate,
);
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, currentRecordSorts);
const combinedFilters = combineFilters([
anyFieldFilter,
requestFilters,
dateRangeFilter,
]);
const recordGqlFields = useRecordFieldGqlFields({ objectMetadataItem });
const { records } = useFindManyRecords({
objectNameSingular: objectMetadataItem.nameSingular,
filter: combinedFilters,
orderBy,
...params,
limit: 100,
recordGqlFields,
});
useEffect(() => {
@@ -2,9 +2,19 @@ import { RecordCalendarMonthBody } from '@/object-record/record-calendar/month/c
import { RecordCalendarMonthHeader } from '@/object-record/record-calendar/month/components/RecordCalendarMonthHeader';
import { RecordCalendarMonthContextProvider } from '@/object-record/record-calendar/month/contexts/RecordCalendarMonthContext';
import { useRecordCalendarMonthDaysRange } from '@/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange';
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
import { useHandleDragOneCalendarCard } from '@/object-record/record-drag/calendar/hooks/useHandleDragOneCalendarCard';
import { useEndRecordDrag } from '@/object-record/record-drag/shared/hooks/useEndRecordDrag';
import { useStartRecordDrag } from '@/object-record/record-drag/shared/hooks/useStartRecordDrag';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import {
DragDropContext,
type DragStart,
type OnDragEndResponder,
} from '@hello-pangea/dnd';
const StyledContainer = styled.div`
display: flex;
@@ -13,10 +23,18 @@ const StyledContainer = styled.div`
`;
export const RecordCalendarMonth = () => {
const recordCalendarId = useAvailableComponentInstanceIdOrThrow(
RecordCalendarComponentInstanceContext,
);
const recordCalendarSelectedDate = useRecoilComponentValue(
recordCalendarSelectedDateComponentState,
);
const { processDragOperation } = useHandleDragOneCalendarCard();
const { startDrag } = useStartRecordDrag('calendar', recordCalendarId);
const { endDrag } = useEndRecordDrag('calendar', recordCalendarId);
const {
firstDayOfMonth,
lastDayOfMonth,
@@ -27,6 +45,16 @@ export const RecordCalendarMonth = () => {
weekStartsOnDayIndex,
} = useRecordCalendarMonthDaysRange(recordCalendarSelectedDate);
const handleDragStart = (start: DragStart) => {
startDrag(start, []);
};
const handleDragEnd: OnDragEndResponder = (result) => {
endDrag();
if (!result.destination) return;
processDragOperation(result);
};
return (
<RecordCalendarMonthContextProvider
value={{
@@ -39,10 +67,12 @@ export const RecordCalendarMonth = () => {
weekStartsOnDayIndex,
}}
>
<StyledContainer>
<RecordCalendarMonthHeader />
<RecordCalendarMonthBody />
</StyledContainer>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<StyledContainer>
<RecordCalendarMonthHeader />
<RecordCalendarMonthBody />
</StyledContainer>
</DragDropContext>
</RecordCalendarMonthContextProvider>
);
};
@@ -1,6 +1,7 @@
import { RecordCalendarMonthBodyWeek } from '@/object-record/record-calendar/month/components/RecordCalendarMonthBodyWeek';
import { useRecordCalendarMonthContextOrThrow } from '@/object-record/record-calendar/month/contexts/RecordCalendarMonthContext';
import styled from '@emotion/styled';
import { format } from 'date-fns';
const StyledContainer = styled.div`
display: flex;
@@ -17,7 +18,7 @@ export const RecordCalendarMonthBody = () => {
<StyledContainer>
{weekFirstDays.map((weekFirstDay) => (
<RecordCalendarMonthBodyWeek
key={`week-${weekFirstDay.toDateString()}`}
key={`week-${format(weekFirstDay, 'yyyy-MM-dd')}`}
startDayOfWeek={weekFirstDay}
/>
))}
@@ -1,13 +1,17 @@
import { RecordCalendarCard } from '@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCard';
import { RecordCalendarCardDraggableContainer } from '@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardDraggableContainer';
import { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
import { calendarDayRecordIdsComponentFamilySelector } from '@/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { isSameDay, isSameMonth } from 'date-fns';
import { Droppable } from '@hello-pangea/dnd';
import { format, isSameDay, isSameMonth, isWeekend } from 'date-fns';
const StyledContainer = styled.div<{ isOtherMonth: boolean }>`
const StyledContainer = styled.div<{
isOtherMonth: boolean;
isDayOfWeekend: boolean;
}>`
display: flex;
width: calc(100% / 7);
flex-direction: column;
@@ -15,6 +19,7 @@ const StyledContainer = styled.div<{ isOtherMonth: boolean }>`
padding: ${({ theme }) => theme.spacing(1)};
background: ${({ theme }) => theme.background.primary};
min-width: 0;
color: ${({ theme }) => theme.font.color.primary};
&:not(:last-child) {
border-right: 0.5px solid ${({ theme }) => theme.border.color.light};
@@ -26,31 +31,65 @@ const StyledContainer = styled.div<{ isOtherMonth: boolean }>`
background: ${theme.background.secondary};
color: ${theme.font.color.light};
`}
${({ isDayOfWeekend, theme }) =>
isDayOfWeekend &&
css`
background: ${theme.background.secondary};
`}
`;
const StyledDayHeader = styled.div<{ isToday: boolean }>`
display: flex;
text-align: right;
justify-content: center;
const StyledDayHeader = styled.div`
align-items: center;
display: flex;
flex-direction: column;
height: 24px;
justify-content: center;
margin-bottom: ${({ theme }) => theme.spacing(0.5)};
margin-left: auto;
width: 20px;
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(1)};
width: 24px;
`;
const StyledDayHeaderDayContainer = styled.div`
display: flex;
margin-left: auto;
padding: ${({ theme }) => theme.spacing(0.5, 0.5)};
`;
const StyledDayHeaderDay = styled.span<{ isToday: boolean }>`
align-items: center;
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
justify-content: center;
line-height: 140%;
width: 20px;
${({ isToday, theme }) =>
isToday &&
css`
border-radius: 4px;
background: ${theme.color.red};
background: ${theme.color.blue};
color: ${theme.font.color.inverted};
font-weight: ${theme.font.weight.medium};
`}
`;
const StyledCardsContainer = styled.div`
const StyledCardsContainer = styled.div<{ isDraggedOver?: boolean }>`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(0.5)};
flex: 1;
min-height: 60px;
border-radius: ${({ theme }) => theme.border.radius.sm};
transition: background-color 0.1s ease;
${({ isDraggedOver, theme }) =>
isDraggedOver &&
css`
background: ${theme.background.transparent.lighter};
border: 1px dashed ${theme.border.color.medium};
`}
`;
type RecordCalendarMonthBodyDayProps = {
@@ -64,23 +103,50 @@ export const RecordCalendarMonthBodyDay = ({
recordCalendarSelectedDateComponentState,
);
const dayKey = format(day, 'yyyy-MM-dd');
const recordIds = useRecoilComponentFamilyValue(
calendarDayRecordIdsComponentFamilySelector,
day.toDateString(),
dayKey,
);
const isToday = isSameDay(day, new Date());
const isOtherMonth = !isSameMonth(day, recordCalendarSelectedDate);
const isDayOfWeekend = isWeekend(day);
return (
<StyledContainer isOtherMonth={isOtherMonth}>
<StyledDayHeader isToday={isToday}>{day.getDate()}</StyledDayHeader>
<StyledCardsContainer>
{recordIds.slice(0, 5).map((recordId) => (
<RecordCalendarCard key={recordId} recordId={recordId} />
))}
</StyledCardsContainer>
<StyledContainer
isOtherMonth={isOtherMonth}
isDayOfWeekend={isDayOfWeekend}
>
<StyledDayHeader>
<StyledDayHeaderDayContainer>
<StyledDayHeaderDay isToday={isToday}>
{day.getDate()}
</StyledDayHeaderDay>
</StyledDayHeaderDayContainer>
</StyledDayHeader>
<Droppable droppableId={dayKey}>
{(droppableProvided, droppableSnapshot) => (
<StyledCardsContainer
// eslint-disable-next-line react/jsx-props-no-spreading
{...droppableProvided.droppableProps}
ref={droppableProvided.innerRef}
isDraggedOver={droppableSnapshot.isDraggingOver}
>
{recordIds.slice(0, 5).map((recordId, index) => (
<RecordCalendarCardDraggableContainer
key={recordId}
recordId={recordId}
index={index}
/>
))}
{droppableProvided.placeholder}
</StyledCardsContainer>
)}
</Droppable>
</StyledContainer>
);
};
@@ -10,7 +10,7 @@ const StyledLabel = styled.div`
font-size: ${({ theme }) => theme.font.size.sm};
height: 24px;
justify-content: flex-end;
padding: ${({ theme }) => theme.spacing(0.5)};
padding: ${({ theme }) => theme.spacing(0, 1)};
width: calc(100% / 7);
`;
@@ -4,15 +4,15 @@ import { RecordCalendarCardBody } from '@/object-record/record-calendar/record-c
import { RecordCalendarCardHeader } from '@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardHeader';
import { RecordCalendarCardComponentInstanceContext } from '@/object-record/record-calendar/record-calendar-card/states/contexts/RecordCalendarCardComponentInstanceContext';
import { RecordCard } from '@/object-record/record-card/components/RecordCard';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import styled from '@emotion/styled';
import { AnimatedEaseInOut } from 'twenty-ui/utilities';
const StyledContainer = styled.div`
display: flex;
`;
const StyledRecordCard = styled(RecordCard)`
margin-bottom: ${({ theme }) => theme.spacing(1)};
width: calc(100% - 2px);
`;
@@ -21,11 +21,9 @@ type RecordCalendarCardProps = {
};
export const RecordCalendarCard = ({ recordId }: RecordCalendarCardProps) => {
const { openRecordFromIndexView } = useOpenRecordFromIndexView();
const { currentView } = useGetCurrentViewOnly();
const handleCardClick = () => {
openRecordFromIndexView({ recordId });
};
const isCompactModeActive = currentView?.isCompact ?? false;
return (
<RecordCalendarCardComponentInstanceContext.Provider
@@ -34,12 +32,14 @@ export const RecordCalendarCard = ({ recordId }: RecordCalendarCardProps) => {
}}
>
<StyledContainer>
<StyledRecordCard onClick={handleCardClick}>
<StyledRecordCard>
<RecordCalendarCardHeader recordId={recordId} />
<RecordCalendarCardBody
recordId={recordId}
isRecordReadOnly={false}
/>
<AnimatedEaseInOut isOpen={!isCompactModeActive} initial={false}>
<RecordCalendarCardBody
recordId={recordId}
isRecordReadOnly={false}
/>
</AnimatedEaseInOut>
</StyledRecordCard>
<RecordCalendarCardCellHoveredPortal recordId={recordId} />
<RecordCalendarCardCellEditModePortal recordId={recordId} />
@@ -17,6 +17,11 @@ import { RecordInlineCell } from '@/object-record/record-inline-cell/components/
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import styled from '@emotion/styled';
const StyledRecordCardBodyContainer = styled(RecordCardBodyContainer)`
padding: ${({ theme }) => theme.spacing(1)};
`;
type RecordCalendarCardBodyProps = {
recordId: string;
@@ -68,7 +73,7 @@ export const RecordCalendarCardBody = ({
};
return (
<RecordCardBodyContainer>
<StyledRecordCardBodyContainer>
{visibleRecordFieldsExceptLabelIdentifier.map((recordField, index) => {
const correspondingFieldDefinition =
fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId];
@@ -115,6 +120,6 @@ export const RecordCalendarCardBody = ({
</StopPropagationContainer>
);
})}
</RecordCardBodyContainer>
</StyledRecordCardBodyContainer>
);
};
@@ -0,0 +1,56 @@
import styled from '@emotion/styled';
import { Draggable } from '@hello-pangea/dnd';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
import { RecordCalendarCard } from '@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCard';
import { RecordCalendarCardComponentInstanceContext } from '@/object-record/record-calendar/record-calendar-card/states/contexts/RecordCalendarCardComponentInstanceContext';
const StyledDraggableContainer = styled.div`
position: relative;
scroll-margin-left: 8px;
scroll-margin-right: 8px;
scroll-margin-top: 8px;
`;
export const RecordCalendarCardDraggableContainer = ({
recordId,
index,
}: {
recordId: string;
index: number;
}) => {
const { objectMetadataItem } = useRecordCalendarContextOrThrow();
const isRecordReadOnly = useIsRecordReadOnly({
recordId,
objectMetadataId: objectMetadataItem.id,
});
return (
<RecordCalendarCardComponentInstanceContext.Provider
value={{ instanceId: recordId }}
>
<Draggable
key={recordId}
draggableId={recordId}
index={index}
isDragDisabled={isRecordReadOnly}
>
{(draggableProvided) => (
<StyledDraggableContainer
id={`record-calendar-card-${recordId}`}
ref={draggableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.dragHandleProps}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
data-selectable-id={recordId}
>
<RecordCalendarCard recordId={recordId} />
</StyledDraggableContainer>
)}
</Draggable>
</RecordCalendarCardComponentInstanceContext.Provider>
);
};
@@ -2,7 +2,10 @@ import { RecordChip } from '@/object-record/components/RecordChip';
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
import { RecordCardHeaderContainer } from '@/object-record/record-card/components/RecordCardHeaderContainer';
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
@@ -18,6 +21,11 @@ const StyledRecordChipContainer = styled.div`
display: flex;
flex: 1 1 auto;
overflow: hidden;
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledRecordCardHeaderContainer = styled(RecordCardHeaderContainer)`
padding: ${({ theme }) => theme.spacing(1)};
`;
type RecordCalendarCardHeaderProps = {
@@ -27,15 +35,30 @@ type RecordCalendarCardHeaderProps = {
export const RecordCalendarCardHeader = ({
recordId,
}: RecordCalendarCardHeaderProps) => {
const { objectMetadataItem } = useRecordCalendarContextOrThrow();
const { objectMetadataItem, viewBarInstanceId } =
useRecordCalendarContextOrThrow();
const record = useRecoilValue(recordStoreFamilyState(recordId));
const { openRecordFromIndexView } = useOpenRecordFromIndexView();
const { currentView } = useGetCurrentViewOnly();
const isCompactModeActive = currentView?.isCompact ?? false;
const dragState = useRecordDragState('calendar', viewBarInstanceId);
const handleChipClick = () => {
if (dragState.isDragging) {
return;
}
openRecordFromIndexView({ recordId });
};
if (!isDefined(record)) {
return null;
}
return (
<RecordCardHeaderContainer isCompact={true}>
<StyledRecordCardHeaderContainer isCompact={isCompactModeActive}>
<StyledRecordChipContainer>
<StopPropagationContainer>
<RecordChip
@@ -43,6 +66,8 @@ export const RecordCalendarCardHeader = ({
record={record}
variant={ChipVariant.Transparent}
isIconHidden={true}
onClick={handleChipClick}
triggerEvent={'CLICK'}
/>
</StopPropagationContainer>
</StyledRecordChipContainer>
@@ -56,6 +81,6 @@ export const RecordCalendarCardHeader = ({
/>
</StopPropagationContainer>
</StyledCheckboxContainer>
</RecordCardHeaderContainer>
</StyledRecordCardHeaderContainer>
);
};
@@ -4,7 +4,7 @@ import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-
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 { isSameDay, parse } from 'date-fns';
import { isDefined } from 'twenty-shared/utils';
export const calendarDayRecordIdsComponentFamilySelector =
@@ -54,7 +54,7 @@ export const calendarDayRecordIdsComponentFamilySelector =
return false;
}
const dayDate = new Date(dayAsString);
const dayDate = parse(dayAsString, 'yyyy-MM-dd', new Date());
const recordDateObj = new Date(recordDate);
return isSameDay(recordDateObj, dayDate);
@@ -0,0 +1,113 @@
import { type DropResult } from '@hello-pangea/dnd';
import { useRecoilCallback } from 'recoil';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
import { calendarDayRecordIdsComponentFamilySelector } from '@/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector';
import { calculateDragPositions } from '@/object-record/record-drag/shared/utils/calculateDragPositions';
import { extractRecordPositions } from '@/object-record/record-drag/shared/utils/extractRecordPositions';
import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/isFieldDateTime';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import {
formatISO,
getHours,
getMilliseconds,
getMinutes,
getSeconds,
parse,
set,
} from 'date-fns';
import { isDefined } from 'twenty-shared/utils';
export const useHandleDragOneCalendarCard = () => {
const { objectMetadataItem } = useRecordCalendarContextOrThrow();
const { currentView } = useGetCurrentViewOnly();
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const calendarDayRecordIdsSelector = useRecoilComponentCallbackState(
calendarDayRecordIdsComponentFamilySelector,
);
const processDragOperation = useRecoilCallback(
({ snapshot }) =>
async (result: DropResult) => {
if (!result.destination || !currentView?.calendarFieldMetadataId)
return;
const { draggableId: recordId } = result;
const destinationDate = result.destination.droppableId;
const destinationIndex = result.destination.index;
const record = snapshot
.getLoadable(recordStoreFamilyState(recordId))
.getValue();
if (!record) return;
const calendarFieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarFieldMetadataId,
);
if (!calendarFieldMetadata) return;
const destinationRecordIds = snapshot
.getLoadable(calendarDayRecordIdsSelector(destinationDate))
.getValue() as string[];
const recordPositionData = extractRecordPositions(
destinationRecordIds,
snapshot,
);
const positions = calculateDragPositions({
recordIds: destinationRecordIds,
recordsToMove: [recordId],
destinationIndex,
recordPositionData,
});
const newPosition = positions[recordId];
const targetDate = parse(destinationDate, 'yyyy-MM-dd', new Date());
const currentFieldValue = record[calendarFieldMetadata.name];
let newDate: Date;
if (
isDefined(currentFieldValue) &&
isFieldDateTime(calendarFieldMetadata)
) {
const currentDateTime = new Date(currentFieldValue);
newDate = set(targetDate, {
hours: getHours(currentDateTime),
minutes: getMinutes(currentDateTime),
seconds: getSeconds(currentDateTime),
milliseconds: getMilliseconds(currentDateTime),
});
} else {
newDate = targetDate;
}
await updateOneRecord({
idToUpdate: recordId,
updateOneRecordInput: {
[calendarFieldMetadata.name]: formatISO(newDate),
position: newPosition,
},
});
},
[
objectMetadataItem,
currentView,
updateOneRecord,
calendarDayRecordIdsSelector,
],
);
return {
processDragOperation,
};
};
@@ -1 +1 @@
export type RecordDragContext = 'board' | 'table';
export type RecordDragContext = 'board' | 'table' | 'calendar';
@@ -10,7 +10,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const useRecordTableRecordGqlFields = ({
export const useRecordFieldGqlFields = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
@@ -92,10 +92,10 @@ export const FormSelectFieldInput = ({
(option) => option.value === draftValue.value,
);
const emptyOption = {
label: `No ${label}`,
const defaultEmptyOption = {
label: `No ${label ?? 'value'}`,
value: '',
Icon: IconCircleOff,
icon: IconCircleOff,
};
const handleUnlinkVariable = () => {
@@ -135,7 +135,7 @@ export const FormSelectFieldInput = ({
options={options}
value={selectedOption?.value}
onChange={onSelect}
emptyOption={emptyOption}
emptyOption={defaultEmptyOption}
fullWidth
hasRightElement={isDefined(VariablePicker) && !readonly}
withSearchInput
@@ -1,10 +1,7 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
@@ -15,24 +12,11 @@ type RecordIndexBoardDataLoaderEffectProps = {
export const RecordIndexBoardDataLoaderEffect = ({
recordBoardId,
}: RecordIndexBoardDataLoaderEffectProps) => {
const recordIndexIsCompactModeActive = useRecoilValue(
recordIndexIsCompactModeActiveState,
);
const selectedRecordIds = useRecoilComponentValue(
recordBoardSelectedRecordIdsComponentSelector,
recordBoardId,
);
const setIsCompactModeActive = useSetRecoilComponentState(
isRecordBoardCompactModeActiveComponentState,
recordBoardId,
);
useEffect(() => {
setIsCompactModeActive(recordIndexIsCompactModeActive);
}, [recordIndexIsCompactModeActive, setIsCompactModeActive]);
const setContextStoreTargetedRecords = useSetRecoilComponentState(
contextStoreTargetedRecordsRuleComponentState,
);
@@ -7,7 +7,6 @@ import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldM
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';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
@@ -29,9 +28,6 @@ export const useLoadRecordIndexStates = () => {
const setContextStoreTargetedRecordsRuleComponentState =
useSetRecoilComponentState(contextStoreTargetedRecordsRuleComponentState);
const setRecordIndexIsCompactModeActive = useSetRecoilState(
recordIndexIsCompactModeActiveState,
);
const setRecordIndexViewType = useSetRecoilState(recordIndexViewTypeState);
const setRecordIndexOpenRecordIn = useSetRecoilState(
recordIndexOpenRecordInState,
@@ -204,7 +200,6 @@ export const useLoadRecordIndexStates = () => {
: view.kanbanAggregateOperation,
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
});
setRecordIndexIsCompactModeActive(view.isCompact);
},
[
onViewFieldsChange,
@@ -215,7 +210,6 @@ export const useLoadRecordIndexStates = () => {
setRecordIndexViewKanbanFieldMetadataIdState,
setRecordIndexCalendarFieldMetadataIdState,
setRecordIndexViewKanbanAggregateOperationState,
setRecordIndexIsCompactModeActive,
],
);
@@ -1,7 +1,7 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
import { useRecordFieldGqlFields } from '@/object-record/record-field/hooks/useRecordTableRecordGqlFields';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
export const useRecordIndexTableFetchMore = (objectNameSingular: string) => {
const { objectMetadataItem } = useObjectMetadataItem({
@@ -10,7 +10,7 @@ export const useRecordIndexTableFetchMore = (objectNameSingular: string) => {
const params = useFindManyRecordIndexTableParams(objectNameSingular);
const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem });
const recordGqlFields = useRecordFieldGqlFields({ objectMetadataItem });
const { fetchMoreRecordsLazy, queryIdentifier } = useLazyFindManyRecords({
...params,
@@ -1,7 +1,7 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useRecordFieldGqlFields } from '@/object-record/record-field/hooks/useRecordTableRecordGqlFields';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
@@ -14,7 +14,7 @@ export const useRecordIndexTableQuery = (objectNameSingular: string) => {
const params = useFindManyRecordIndexTableParams(objectNameSingular);
const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem });
const recordGqlFields = useRecordFieldGqlFields({ objectMetadataItem });
const { records, hasNextPage, queryIdentifier, loading } = useFindManyRecords(
{
@@ -1,5 +0,0 @@
import { createState } from 'twenty-ui/utilities';
export const recordIndexIsCompactModeActiveState = createState<boolean>({
key: 'recordIndexIsCompactModeActiveState',
defaultValue: false,
});
@@ -23,7 +23,7 @@ export const convertUpdateViewInputToCore = (
...(view.name && { name: view.name }),
...(view.icon && { icon: view.icon }),
...(view.position && { position: view.position }),
...(view.isCompact && { isCompact: view.isCompact }),
...(isDefined(view.isCompact) && { isCompact: view.isCompact }),
...(view.kanbanAggregateOperation && {
kanbanAggregateOperation: view.kanbanAggregateOperation,
}),
@@ -0,0 +1,45 @@
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { isDefined } from 'twenty-shared/utils';
import { type JsonValue } from 'type-fest';
type WorkflowFormFieldInputProps = {
fieldMetadataId: string;
defaultValue: JsonValue;
readonly: boolean;
onChange: (value: JsonValue) => void;
VariablePicker?: VariablePickerComponent;
};
export const WorkflowFormFieldInput = ({
fieldMetadataId,
defaultValue,
readonly,
onChange,
VariablePicker,
}: WorkflowFormFieldInputProps) => {
const { fieldMetadataItem, objectMetadataItem } =
useFieldMetadataItemById(fieldMetadataId);
if (!isDefined(fieldMetadataItem) || !isDefined(objectMetadataItem)) {
return null;
}
const fieldDefinition = formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItem,
objectMetadataItem,
});
return (
<FormFieldInput
key={fieldDefinition.metadata.fieldName}
defaultValue={defaultValue}
field={fieldDefinition}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
);
};
@@ -9,6 +9,7 @@ import { type WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
import { WorkflowFormFieldInput } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowFormFieldInput';
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
@@ -142,6 +143,29 @@ export const WorkflowEditActionFormFiller = ({
);
}
if (field.type === 'SELECT') {
const selectedFieldId = field.settings?.selectedFieldId;
if (!isDefined(selectedFieldId)) {
return null;
}
return (
<WorkflowFormFieldInput
key={field.id}
fieldMetadataId={selectedFieldId}
defaultValue={field.value}
readonly={actionOptions.readonly}
onChange={(value) => {
onFieldUpdate({
fieldId: field.id,
value,
});
}}
/>
);
}
return (
<FormFieldInput
key={field.id}
@@ -1,5 +1,6 @@
import { WorkflowFormFieldSettingsDate } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsDate';
import { WorkflowFormFieldSettingsRecordPicker } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsRecordPicker';
import { WorkflowFormFieldSettingsSelect } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsSelect';
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
import { FieldMetadataType } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
@@ -26,6 +27,10 @@ export const WorkflowFormFieldSettingsByType = ({
return (
<WorkflowFormFieldSettingsDate field={field} onChange={onChange} />
);
case FieldMetadataType.SELECT:
return (
<WorkflowFormFieldSettingsSelect field={field} onChange={onChange} />
);
case 'RECORD':
return (
<WorkflowFormFieldSettingsRecordPicker
@@ -0,0 +1,125 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
import styled from '@emotion/styled';
import camelCase from 'lodash.camelcase';
import { FieldMetadataType } from 'twenty-shared/types';
type WorkflowFormFieldSettingsSelectProps = {
field: WorkflowFormActionField;
onChange: (updatedField: WorkflowFormActionField) => void;
};
const StyledFormFieldSettingsSelect = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledRowContainer = styled.div`
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`;
export const WorkflowFormFieldSettingsSelect = ({
field,
onChange,
}: WorkflowFormFieldSettingsSelectProps) => {
const selectTypeOptions = [
{
label: 'Existing Field',
value: 'EXISTING_FIELD',
},
];
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const selectFieldId = field.settings?.selectedFieldId;
const fieldOptions = activeNonSystemObjectMetadataItems.reduce(
(acc, objectMetadataItem) => {
return acc.concat(
objectMetadataItem.fields
.filter(
(field) =>
field.isActive &&
!field.isSystem &&
field.type === FieldMetadataType.SELECT,
)
.map((field) => ({
label: `${objectMetadataItem.labelSingular} > ${field.label}`,
value: field.id,
})),
);
},
[] as { label: string; value: string }[],
);
return (
<StyledFormFieldSettingsSelect>
<StyledRowContainer>
<FormFieldInputContainer>
<InputLabel>Label</InputLabel>
<FormTextFieldInput
onChange={(newLabel: string) => {
onChange({
...field,
label: newLabel,
name: camelCase(newLabel),
});
}}
defaultValue={field.label}
placeholder={
getDefaultFormFieldSettings(FieldMetadataType.SELECT).label
}
/>
</FormFieldInputContainer>
<FormFieldInputContainer>
<InputLabel>Select Type</InputLabel>
<FormSelectFieldInput
onChange={(newSelectType: string | null) => {
if (newSelectType === null) {
return;
}
onChange({
...field,
settings: {
...field.settings,
selectType: newSelectType,
},
});
}}
defaultValue={'EXISTING_FIELD'}
options={selectTypeOptions}
readonly
/>
</FormFieldInputContainer>
</StyledRowContainer>
<FormFieldInputContainer>
<InputLabel>Field</InputLabel>
<FormSelectFieldInput
onChange={(newSelectFieldId: string | null) => {
if (newSelectFieldId === null) {
return;
}
onChange({
...field,
settings: {
...field.settings,
selectedFieldId: newSelectFieldId,
},
});
}}
defaultValue={selectFieldId}
options={fieldOptions}
/>
</FormFieldInputContainer>
</StyledFormFieldSettingsSelect>
);
};
@@ -145,3 +145,31 @@ export const DateFieldSettings: Story = {
expect(args.onClose).toHaveBeenCalled();
},
};
export const SelectFieldSettings: Story = {
args: {
field: {
id: 'field-5',
name: 'select',
label: 'Select Field',
type: FieldMetadataType.SELECT,
settings: {
selectedFieldId: 'field-1',
},
},
onClose: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const fieldTypeSelect = await canvas.findByText('Select');
expect(fieldTypeSelect).toBeVisible();
const selectTypeSelect = await canvas.findByText('Select Type');
expect(selectTypeSelect).toBeVisible();
const closeButton = await canvas.findByTestId('close-button');
await userEvent.click(closeButton);
expect(args.onClose).toHaveBeenCalled();
},
};
@@ -5,6 +5,7 @@ import {
IllustrationIconCalendarEvent,
IllustrationIconNumbers,
IllustrationIconOneToMany,
IllustrationIconTag,
IllustrationIconText,
} from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
@@ -31,4 +32,9 @@ export const FORM_SELECT_FIELD_TYPE_OPTIONS: SelectOption<WorkflowFormFieldType>
value: 'RECORD',
Icon: IllustrationIconOneToMany,
},
{
label: 'Select',
value: FieldMetadataType.SELECT,
Icon: IllustrationIconTag,
},
];
@@ -4,4 +4,5 @@ export type WorkflowFormFieldType =
| FieldMetadataType.TEXT
| FieldMetadataType.NUMBER
| FieldMetadataType.DATE
| FieldMetadataType.SELECT
| 'RECORD';
@@ -36,6 +36,17 @@ export const getDefaultFormFieldSettings = (type: WorkflowFormFieldType) => {
objectName: 'company',
},
};
case FieldMetadataType.SELECT:
return {
id: v4(),
name: 'select',
label: 'Select',
placeholder: 'Choose a value',
settings: {
selectType: 'EXISTING_FIELD',
selectedFieldId: undefined,
},
};
default:
assertUnreachable(type);
}
@@ -158,6 +158,7 @@ export const workflowFormActionSettingsSchema =
z.literal(FieldMetadataType.TEXT),
z.literal(FieldMetadataType.NUMBER),
z.literal(FieldMetadataType.DATE),
z.literal(FieldMetadataType.SELECT),
z.literal('RECORD'),
]),
placeholder: z.string().optional(),