Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 644a06f24d | |||
| d44b7ca275 | |||
| 0112d19eac | |||
| 8f49664c87 | |||
| bb9add4c48 | |||
| 538bd944cd | |||
| 27a21dea35 | |||
| fabeeafad6 | |||
| 2168a678d8 |
@@ -1040,6 +1040,7 @@ export enum FeatureFlagKey {
|
||||
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
|
||||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||
IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED',
|
||||
IS_GROUP_BY_ENABLED = 'IS_GROUP_BY_ENABLED',
|
||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_MESSAGE_FOLDER_CONTROL_ENABLED = 'IS_MESSAGE_FOLDER_CONTROL_ENABLED',
|
||||
|
||||
@@ -1004,6 +1004,7 @@ export enum FeatureFlagKey {
|
||||
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
|
||||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||
IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED',
|
||||
IS_GROUP_BY_ENABLED = 'IS_GROUP_BY_ENABLED',
|
||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_MESSAGE_FOLDER_CONTROL_ENABLED = 'IS_MESSAGE_FOLDER_CONTROL_ENABLED',
|
||||
|
||||
+1
-10
@@ -77,14 +77,6 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const limitPerMetadataItemArray =
|
||||
queryOperationSignatureWithObjectMetadataItemArray
|
||||
.map(
|
||||
({ objectMetadataItem }) =>
|
||||
`$limit${capitalize(objectMetadataItem.nameSingular)}: Int`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
if (isEmpty(queryOperationSignatureWithObjectMetadataItemArray)) {
|
||||
return null;
|
||||
}
|
||||
@@ -93,8 +85,7 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
|
||||
query CombinedFindManyRecords(
|
||||
${filterPerMetadataItemArray},
|
||||
${orderByPerMetadataItemArray},
|
||||
${cursorFilteringPerMetadataItemArray},
|
||||
${limitPerMetadataItemArray}
|
||||
${cursorFilteringPerMetadataItemArray},
|
||||
) {
|
||||
${queryOperationSignatureWithObjectMetadataItemArray
|
||||
.map(
|
||||
|
||||
-8
@@ -51,13 +51,6 @@ export const usePerformCombinedFindManyRecords = () => {
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const limitPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$limit${capitalize(objectNameSingular)}: Int`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const queryOperationSignatureWithObjectMetadataItemArray =
|
||||
operationSignatures.map((operationSignature) => {
|
||||
const objectMetadataItem = objectMetadataItemsValue.find(
|
||||
@@ -80,7 +73,6 @@ export const usePerformCombinedFindManyRecords = () => {
|
||||
${filterPerMetadataItemArray},
|
||||
${orderByPerMetadataItemArray},
|
||||
${cursorFilteringPerMetadataItemArray},
|
||||
${limitPerMetadataItemArray}
|
||||
) {
|
||||
${queryOperationSignatureWithObjectMetadataItemArray
|
||||
.map(
|
||||
|
||||
+1
-2
@@ -10,6 +10,5 @@ export const getCombinedFindManyRecordsQueryFilteringPart = (
|
||||
after: $after${capitalize(objectMetadataItem.nameSingular)},
|
||||
before: $before${capitalize(objectMetadataItem.nameSingular)},
|
||||
first: $first${capitalize(objectMetadataItem.nameSingular)},
|
||||
last: $last${capitalize(objectMetadataItem.nameSingular)},
|
||||
limit: $limit${capitalize(objectMetadataItem.nameSingular)})`;
|
||||
last: $last${capitalize(objectMetadataItem.nameSingular)})`;
|
||||
};
|
||||
|
||||
+61
-57
@@ -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>
|
||||
|
||||
-23
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+4
-5
@@ -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,
|
||||
|
||||
+4
-5
@@ -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,
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
-9
@@ -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,
|
||||
});
|
||||
+7
-58
@@ -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(() => {
|
||||
|
||||
+34
-4
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
+85
-19
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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);
|
||||
`;
|
||||
|
||||
|
||||
+11
-11
@@ -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} />
|
||||
|
||||
+7
-2
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+56
@@ -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>
|
||||
);
|
||||
};
|
||||
+28
-3
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+113
@@ -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
@@ -1 +1 @@
|
||||
export type RecordDragContext = 'board' | 'table';
|
||||
export type RecordDragContext = 'board' | 'table' | 'calendar';
|
||||
|
||||
+1
-1
@@ -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;
|
||||
+4
-4
@@ -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
|
||||
|
||||
-16
@@ -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,
|
||||
);
|
||||
|
||||
-6
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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(
|
||||
{
|
||||
|
||||
-5
@@ -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,
|
||||
}),
|
||||
|
||||
+45
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+24
@@ -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}
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
+125
@@ -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>
|
||||
);
|
||||
};
|
||||
+28
@@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
+6
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
+1
@@ -4,4 +4,5 @@ export type WorkflowFormFieldType =
|
||||
| FieldMetadataType.TEXT
|
||||
| FieldMetadataType.NUMBER
|
||||
| FieldMetadataType.DATE
|
||||
| FieldMetadataType.SELECT
|
||||
| 'RECORD';
|
||||
|
||||
+11
@@ -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);
|
||||
}
|
||||
|
||||
+2
@@ -13,6 +13,7 @@ import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/gr
|
||||
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
|
||||
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
|
||||
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
|
||||
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-group-by-resolver.service';
|
||||
import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service';
|
||||
import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service';
|
||||
import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service';
|
||||
@@ -35,6 +36,7 @@ const graphqlQueryResolvers = [
|
||||
GraphqlQueryFindDuplicatesResolverService,
|
||||
GraphqlQueryFindManyResolverService,
|
||||
GraphqlQueryFindOneResolverService,
|
||||
GraphqlQueryGroupByResolverService,
|
||||
GraphqlQueryMergeManyResolverService,
|
||||
GraphqlQueryRestoreManyResolverService,
|
||||
GraphqlQueryRestoreOneResolverService,
|
||||
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import {
|
||||
ObjectRecord,
|
||||
ObjectRecordFilter,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
||||
import { IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/group-by-connection.interface';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolverService<
|
||||
GroupByResolverArgs,
|
||||
IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[]
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<GroupByResolverArgs>,
|
||||
_featureFlagsMap: Record<FeatureFlagKey, boolean>,
|
||||
): Promise<IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[]> {
|
||||
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
|
||||
|
||||
const groupByFields = this.parseGroupByArgs(
|
||||
executionArgs.args,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
const aggregateMetrics = Object.keys(
|
||||
executionArgs.graphqlQuerySelectedFieldsResult.aggregate,
|
||||
);
|
||||
|
||||
const mockedGroupByValues = Array.from({ length: 3 }, (_, index) => ({
|
||||
groupByDimensionValues: groupByFields.map((groupByField) =>
|
||||
this.getFakeDimensionValues(groupByField, index),
|
||||
),
|
||||
...aggregateMetrics.reduce(
|
||||
(acc, aggregateMetric, subIndex) => {
|
||||
acc[aggregateMetric] = this.getFakeAggregateValues(subIndex + index);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
}));
|
||||
|
||||
return mockedGroupByValues as unknown as IGroupByConnection<
|
||||
ObjectRecord,
|
||||
IEdge<ObjectRecord>
|
||||
>[];
|
||||
}
|
||||
|
||||
private parseGroupByArgs(
|
||||
args: GroupByResolverArgs,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
): { fieldMetadata: FieldMetadataEntity; subFieldName?: string }[] {
|
||||
const groupByFieldNames = args.groupBy;
|
||||
|
||||
const groupByFields = [];
|
||||
|
||||
for (const fieldNames of groupByFieldNames) {
|
||||
if (Object.keys(fieldNames).length > 1) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'You cannot provide multiple fields in one GroupByInput, split them into multiple GroupByInput',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
for (const fieldName of Object.keys(fieldNames)) {
|
||||
const fieldMetadataId =
|
||||
objectMetadataItemWithFieldMaps.fieldIdByName[fieldName];
|
||||
const fieldMetadata =
|
||||
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
|
||||
|
||||
if (fieldNames[fieldName] === true) {
|
||||
groupByFields.push({
|
||||
fieldMetadata,
|
||||
subFieldName: undefined,
|
||||
});
|
||||
continue;
|
||||
} else if (typeof fieldNames[fieldName] === 'object') {
|
||||
if (Object.keys(fieldNames[fieldName]).length > 1) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'You cannot provide multiple subfields in one GroupByInput, split them into multiple GroupByInput',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
for (const subFieldName of Object.keys(fieldNames[fieldName])) {
|
||||
if (fieldNames[fieldName][subFieldName] === true) {
|
||||
groupByFields.push({
|
||||
fieldMetadata,
|
||||
subFieldName,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupByFields;
|
||||
}
|
||||
|
||||
async validate(
|
||||
_args: GroupByResolverArgs<ObjectRecordFilter>,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {}
|
||||
|
||||
//todo : remove these methods after the groupBy logic implementation
|
||||
private getFakeDimensionValues(
|
||||
groupByField: {
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
subFieldName?: string;
|
||||
},
|
||||
rank: number,
|
||||
): string {
|
||||
const fakeDimensionValuesByField = {
|
||||
[FieldMetadataType.TEXT]: [
|
||||
'Text 1',
|
||||
'Long long long long and long text 2',
|
||||
'Text 3',
|
||||
],
|
||||
[FieldMetadataType.NUMERIC]: [2000, 3000, 4000],
|
||||
[FieldMetadataType.NUMBER]: [1, 2, 3],
|
||||
[FieldMetadataType.BOOLEAN]: [true, false, true],
|
||||
[FieldMetadataType.DATE_TIME]: [
|
||||
new Date(2025, 5, 10, 12, 1, 0),
|
||||
new Date(2025, 5, 10, 12, 1, 0),
|
||||
new Date(2018, 6, 14, 12, 2, 0),
|
||||
],
|
||||
[FieldMetadataType.DATE]: [
|
||||
new Date(2025, 1, 1),
|
||||
new Date(2025, 1, 2),
|
||||
new Date(2025, 1, 3),
|
||||
],
|
||||
[FieldMetadataType.UUID]: [
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
],
|
||||
[FieldMetadataType.SELECT]: ['Option 1', 'Option 2', 'Option 3'],
|
||||
[FieldMetadataType.MULTI_SELECT]: [
|
||||
['Tag 1', 'Tag 2'],
|
||||
['Tag 3'],
|
||||
['Tag 4', 'Tag 5', 'Tag 6'],
|
||||
],
|
||||
[FieldMetadataType.RELATION]: [
|
||||
{ id: 'relation-1', name: 'Relation 1' },
|
||||
{ id: 'relation-2', name: 'Relation 2' },
|
||||
{ id: 'relation-3', name: 'Relation 3' },
|
||||
],
|
||||
[FieldMetadataType.MORPH_RELATION]: [
|
||||
{ id: 'morph-1', name: 'Morph Relation 1' },
|
||||
{ id: 'morph-2', name: 'Morph Relation 2' },
|
||||
{ id: 'morph-3', name: 'Morph Relation 3' },
|
||||
],
|
||||
[FieldMetadataType.RATING]: ['RATING_3', 'RATING_4', 'RATING_5'],
|
||||
[FieldMetadataType.RAW_JSON]: [
|
||||
{ key: 'value1' },
|
||||
{ key: 'value2', key2: 'value2' },
|
||||
{},
|
||||
],
|
||||
[FieldMetadataType.ARRAY]: [['value1', 'value2'], ['value3'], []],
|
||||
[FieldMetadataType.POSITION]: [1, 2, 3],
|
||||
[FieldMetadataType.TS_VECTOR]: ['vector1', 'vector2', 'vector3'],
|
||||
[FieldMetadataType.EMAILS]: [
|
||||
{
|
||||
primaryEmail: 'tim@twenty.com',
|
||||
additionalEmails: [
|
||||
'tim@twenty.com',
|
||||
'timapple@twenty.com',
|
||||
'johnappletim@twenty.com',
|
||||
],
|
||||
},
|
||||
{
|
||||
primaryEmail: 'jane@twenty.com',
|
||||
additionalEmails: ['jane@twenty.com', 'jane.doe@twenty.com'],
|
||||
},
|
||||
{
|
||||
primaryEmail: 'john@twenty.com',
|
||||
additionalEmails: ['john.doe@twenty.com'],
|
||||
},
|
||||
],
|
||||
[FieldMetadataType.PHONES]: [
|
||||
{
|
||||
primaryPhoneCallingCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneNumber: '789012345',
|
||||
additionalPhones: [
|
||||
{ number: '617272323', callingCode: '+33', countryCode: 'FR' },
|
||||
],
|
||||
},
|
||||
{
|
||||
primaryPhoneCallingCode: '+1',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneNumber: '612345789',
|
||||
additionalPhones: [
|
||||
{ number: '123456789', callingCode: '+1', countryCode: 'US' },
|
||||
{ number: '617272323', callingCode: '+1', countryCode: 'US' },
|
||||
],
|
||||
},
|
||||
{
|
||||
primaryPhoneCallingCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneNumber: '123456789',
|
||||
additionalPhones: [],
|
||||
},
|
||||
],
|
||||
[FieldMetadataType.CURRENCY]: [
|
||||
{ amountMicros: 1000000, currencyCode: 'USD' },
|
||||
{ amountMicros: 2000000, currencyCode: 'EUR' },
|
||||
{ amountMicros: 3000000, currencyCode: 'GBP' },
|
||||
],
|
||||
[FieldMetadataType.LINKS]: [
|
||||
{
|
||||
primaryLinkUrl: 'twenty.com',
|
||||
primaryLinkLabel: '',
|
||||
secondaryLinks: [{ url: 'twenty.com', label: 'Twenty' }],
|
||||
},
|
||||
{
|
||||
primaryLinkUrl: 'github.com/twentyhq/twenty',
|
||||
primaryLinkLabel: 'Twenty Repo',
|
||||
secondaryLinks: [{ url: 'twenty.com', label: '' }],
|
||||
},
|
||||
{
|
||||
primaryLinkUrl: 'react.dev',
|
||||
primaryLinkLabel: '',
|
||||
secondaryLinks: [],
|
||||
},
|
||||
],
|
||||
[FieldMetadataType.FULL_NAME]: [
|
||||
{ firstName: 'John', lastName: 'Doe' },
|
||||
{ firstName: 'Jane', lastName: 'Doe' },
|
||||
{ firstName: 'John', lastName: 'Smith' },
|
||||
],
|
||||
[FieldMetadataType.ADDRESS]: [
|
||||
{
|
||||
addressStreet1: '456 Oak Street',
|
||||
addressStreet2: '',
|
||||
addressCity: 'Springfield',
|
||||
addressState: 'California',
|
||||
addressCountry: 'United States',
|
||||
addressPostcode: '90210',
|
||||
addressLat: 34.0522,
|
||||
addressLng: -118.2437,
|
||||
},
|
||||
{
|
||||
addressStreet1: '123 Main Street',
|
||||
addressStreet2: '',
|
||||
addressCity: 'New York',
|
||||
addressState: 'New York',
|
||||
addressCountry: 'United States',
|
||||
addressPostcode: '10001',
|
||||
addressLat: 40.7128,
|
||||
addressLng: -74.006,
|
||||
},
|
||||
{
|
||||
addressStreet1: '8 rue Saint-Anne',
|
||||
addressStreet2: '',
|
||||
addressCity: 'Paris',
|
||||
addressState: 'Ile-de-France',
|
||||
addressCountry: 'France',
|
||||
addressPostcode: '75001',
|
||||
addressLat: 40.7128,
|
||||
addressLng: -74.006,
|
||||
},
|
||||
],
|
||||
[FieldMetadataType.ACTOR]: [
|
||||
{
|
||||
source: 'IMPORT',
|
||||
name: 'name',
|
||||
workspaceMemberId: 'id',
|
||||
context: { provider: 'GOOGLE' },
|
||||
},
|
||||
{
|
||||
source: 'MANUAL',
|
||||
name: 'name',
|
||||
workspaceMemberId: 'id',
|
||||
context: { provider: 'MICROSOFT' },
|
||||
},
|
||||
{
|
||||
source: 'WEBHOOK',
|
||||
name: 'name',
|
||||
workspaceMemberId: 'id',
|
||||
context: {},
|
||||
},
|
||||
],
|
||||
[FieldMetadataType.RICH_TEXT_V2]: [
|
||||
{
|
||||
blocknote: '[{"type":"heading","content":"Hello"}]',
|
||||
markdown: '# Hello',
|
||||
},
|
||||
{
|
||||
blocknote: '[{"type":"heading","content":"Hello World"}]',
|
||||
markdown: '# Hello World',
|
||||
},
|
||||
{
|
||||
blocknote: '[{"type":"heading","content":"Hello Again"}]',
|
||||
markdown: '# Hello Again',
|
||||
},
|
||||
],
|
||||
[FieldMetadataType.RICH_TEXT]: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as Record<FieldMetadataType, any>;
|
||||
|
||||
const fakeDimensionValues =
|
||||
fakeDimensionValuesByField[groupByField.fieldMetadata.type][rank % 3];
|
||||
|
||||
return isDefined(groupByField.subFieldName)
|
||||
? fakeDimensionValues[groupByField.subFieldName].toString()
|
||||
: fakeDimensionValues.toString();
|
||||
}
|
||||
|
||||
private getFakeAggregateValues(rank: number) {
|
||||
const fakeAggregateValues = [39, 20, 56, 88, 2];
|
||||
|
||||
return fakeAggregateValues[rank % fakeAggregateValues.length];
|
||||
}
|
||||
}
|
||||
+12
@@ -12,6 +12,18 @@ export type ObjectRecordFilter = Partial<{
|
||||
[Property in keyof ObjectRecord]: any;
|
||||
}>;
|
||||
|
||||
export type ObjectRecordGroupBy = Array<
|
||||
ObjectRecordGroupByForAtomicField | ObjectRecordGroupByForCompositeField
|
||||
>;
|
||||
|
||||
export type ObjectRecordGroupByForAtomicField = Partial<{
|
||||
[Property in keyof ObjectRecord]: boolean;
|
||||
}>;
|
||||
|
||||
export type ObjectRecordGroupByForCompositeField = Partial<{
|
||||
[Property in keyof ObjectRecord]: Record<string, boolean>;
|
||||
}>;
|
||||
|
||||
export enum OrderByDirection {
|
||||
AscNullsFirst = 'AscNullsFirst',
|
||||
AscNullsLast = 'AscNullsLast',
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
||||
|
||||
export type IGroupByConnection<
|
||||
T,
|
||||
CustomEdge extends IEdge<T> = IEdge<T>,
|
||||
> = IConnection<T, CustomEdge> & {
|
||||
groupByDimensionValues: string[];
|
||||
};
|
||||
+1
@@ -2,6 +2,7 @@ export const RESOLVER_METHOD_NAMES = {
|
||||
FIND_MANY: 'findMany',
|
||||
FIND_ONE: 'findOne',
|
||||
FIND_DUPLICATES: 'findDuplicates',
|
||||
GROUP_BY: 'groupBy',
|
||||
CREATE_MANY: 'createMany',
|
||||
CREATE_ONE: 'createOne',
|
||||
UPDATE_MANY: 'updateMany',
|
||||
|
||||
+3
@@ -1,5 +1,6 @@
|
||||
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
|
||||
import { GroupByResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory';
|
||||
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
||||
import { RestoreOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory';
|
||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||
@@ -29,6 +30,7 @@ export const workspaceResolverBuilderFactories = [
|
||||
RestoreOneResolverFactory,
|
||||
RestoreManyResolverFactory,
|
||||
MergeManyResolverFactory,
|
||||
GroupByResolverFactory,
|
||||
];
|
||||
|
||||
export const workspaceResolverBuilderMethodNames = {
|
||||
@@ -36,6 +38,7 @@ export const workspaceResolverBuilderMethodNames = {
|
||||
FindManyResolverFactory.methodName,
|
||||
FindOneResolverFactory.methodName,
|
||||
FindDuplicatesResolverFactory.methodName,
|
||||
GroupByResolverFactory.methodName,
|
||||
],
|
||||
mutations: [
|
||||
CreateManyResolverFactory.methodName,
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
GroupByResolverArgs,
|
||||
type Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-group-by-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class GroupByResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = RESOLVER_METHOD_NAMES.GROUP_BY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryGroupByResolverService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<GroupByResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
GroupByResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
@@ -3,6 +3,7 @@ import { type GraphQLFieldResolver } from 'graphql';
|
||||
import {
|
||||
type ObjectRecord,
|
||||
type ObjectRecordFilter,
|
||||
type ObjectRecordGroupBy,
|
||||
type ObjectRecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
@@ -64,6 +65,13 @@ export interface CreateManyResolverArgs<
|
||||
upsert?: boolean;
|
||||
}
|
||||
|
||||
export interface GroupByResolverArgs<Filter = ObjectRecordFilter> {
|
||||
filter?: Filter;
|
||||
groupBy: ObjectRecordGroupBy;
|
||||
viewId?: string;
|
||||
orderBy?: ObjectRecordOrderBy;
|
||||
}
|
||||
|
||||
export interface UpdateOneResolverArgs<
|
||||
Data extends Partial<ObjectRecord> = Partial<ObjectRecord>,
|
||||
> {
|
||||
@@ -131,6 +139,7 @@ export interface WorkspaceResolverBuilderMethods {
|
||||
export type ResolverArgs =
|
||||
| CreateManyResolverArgs
|
||||
| CreateOneResolverArgs
|
||||
| GroupByResolverArgs
|
||||
| DeleteManyResolverArgs
|
||||
| DeleteOneResolverArgs
|
||||
| DestroyManyResolverArgs
|
||||
|
||||
+11
@@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
||||
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
|
||||
import { GroupByResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory';
|
||||
import { MergeManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory';
|
||||
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
||||
import { RestoreOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory';
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||
@@ -54,6 +56,7 @@ export class WorkspaceResolverFactory {
|
||||
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
|
||||
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
|
||||
private readonly mergeManyResolverFactory: MergeManyResolverFactory,
|
||||
private readonly groupByResolverFactory: GroupByResolverFactory,
|
||||
private readonly workspaceResolverBuilderService: WorkspaceResolverBuilderService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
@@ -81,6 +84,7 @@ export class WorkspaceResolverFactory {
|
||||
['restoreOne', this.restoreOneResolverFactory],
|
||||
['updateMany', this.updateManyResolverFactory],
|
||||
['updateOne', this.updateOneResolverFactory],
|
||||
['groupBy', this.groupByResolverFactory],
|
||||
]);
|
||||
const resolvers: IResolvers = {
|
||||
Query: {},
|
||||
@@ -112,8 +116,15 @@ export class WorkspaceResolverFactory {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isGroupByEnabled =
|
||||
workspaceFeatureFlagsMap[FeatureFlagKey.IS_GROUP_BY_ENABLED];
|
||||
|
||||
// Generate query resolvers
|
||||
for (const methodName of workspaceResolverBuilderMethods.queries) {
|
||||
if (methodName === 'groupBy' && !isGroupByEnabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolverName = getResolverName(objectMetadata, methodName);
|
||||
const resolverFactory = factories.get(methodName);
|
||||
|
||||
|
||||
+1
@@ -3,4 +3,5 @@ export enum GqlInputTypeDefinitionKind {
|
||||
Update = 'Update',
|
||||
Filter = 'Filter',
|
||||
OrderBy = 'OrderBy',
|
||||
GroupBy = 'GroupBy',
|
||||
}
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
export enum ObjectTypeDefinitionKind {
|
||||
GroupByConnection = 'GroupByConnection',
|
||||
Connection = 'Connection',
|
||||
Edge = 'Edge',
|
||||
Plain = '',
|
||||
|
||||
+13
-2
@@ -10,6 +10,7 @@ import { RelationConnectGqlInputTypeGenerator } from 'src/engine/api/graphql/wor
|
||||
import { CompositeFieldMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/composite-field-metadata-gql-object-type.generator';
|
||||
import { ConnectionGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator';
|
||||
import { EdgeGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator';
|
||||
import { GroupByConnectionGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator';
|
||||
import { ObjectMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator';
|
||||
import { ObjectMetadataWithRelationsGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator';
|
||||
import { MutationTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator';
|
||||
@@ -32,18 +33,25 @@ export class GqlTypeGenerator {
|
||||
private readonly compositeFieldMetadataGqlInputTypeGenerator: CompositeFieldMetadataGqlInputTypeGenerator,
|
||||
private readonly edgeGqlObjectTypeGenerator: EdgeGqlObjectTypeGenerator,
|
||||
private readonly connectionGqlObjectTypeGenerator: ConnectionGqlObjectTypeGenerator,
|
||||
private readonly groupByConnectionGqlObjectTypeGenerator: GroupByConnectionGqlObjectTypeGenerator,
|
||||
private readonly objectMetadataWithRelationsGqlObjectTypeGenerator: ObjectMetadataWithRelationsGqlObjectTypeGenerator,
|
||||
private readonly relationConnectGqlInputTypeGenerator: RelationConnectGqlInputTypeGenerator,
|
||||
private readonly queryTypeGenerator: QueryTypeGenerator,
|
||||
private readonly mutationTypeGenerator: MutationTypeGenerator,
|
||||
) {}
|
||||
|
||||
buildAndStore(objectMetadataCollection: ObjectMetadataEntity[]) {
|
||||
async buildAndStore(
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
workspaceId: string,
|
||||
) {
|
||||
const compositeTypeCollection = [...compositeTypeDefinitions.values()];
|
||||
|
||||
this.buildAndStoreCompositeFieldMetadataGqlTypes(compositeTypeCollection);
|
||||
this.buildAndStoreObjectMetadataGqlTypes(objectMetadataCollection);
|
||||
this.queryTypeGenerator.buildAndStore(objectMetadataCollection);
|
||||
await this.queryTypeGenerator.buildAndStore(
|
||||
objectMetadataCollection,
|
||||
workspaceId,
|
||||
);
|
||||
this.mutationTypeGenerator.buildAndStore(objectMetadataCollection);
|
||||
}
|
||||
|
||||
@@ -77,6 +85,9 @@ export class GqlTypeGenerator {
|
||||
this.objectMetadataGqlObjectTypeGenerator.buildAndStore(objectMetadata);
|
||||
this.edgeGqlObjectTypeGenerator.buildAndStore(objectMetadata);
|
||||
this.connectionGqlObjectTypeGenerator.buildAndStore(objectMetadata);
|
||||
this.groupByConnectionGqlObjectTypeGenerator.buildAndStore(
|
||||
objectMetadata,
|
||||
);
|
||||
this.relationConnectGqlInputTypeGenerator.buildAndStore(objectMetadata);
|
||||
this.objectMetadataGqlInputTypeGenerator.buildAndStore(objectMetadata);
|
||||
|
||||
|
||||
+5
@@ -4,6 +4,7 @@ import { type CompositeType } from 'src/engine/metadata-modules/field-metadata/i
|
||||
|
||||
import { CompositeFieldMetadataCreateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/composite-field-metadata-create-gql-input-type.generator';
|
||||
import { CompositeFieldMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/composite-field-metadata-filter-gql-input-types.generator';
|
||||
import { CompositeFieldMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/composite-field-metadata-group-by-gql-input-type.generator';
|
||||
import { CompositeFieldMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/composite-field-metadata-order-by-gql-input-type.generator';
|
||||
import { CompositeFieldMetadataUpdateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/composite-field-metadata-update-gql-input-type.generator';
|
||||
|
||||
@@ -14,6 +15,7 @@ export class CompositeFieldMetadataGqlInputTypeGenerator {
|
||||
private readonly compositeFieldMetadataUpdateGqlInputTypeGenerator: CompositeFieldMetadataUpdateGqlInputTypeGenerator,
|
||||
private readonly compositeFieldMetadataFilterGqlInputTypeGenerator: CompositeFieldMetadataFilterGqlInputTypeGenerator,
|
||||
private readonly compositeFieldMetadataOrderByGqlInputTypeGenerator: CompositeFieldMetadataOrderByGqlInputTypeGenerator,
|
||||
private readonly compositeFieldMetadataGroupByGqlInputTypeGenerator: CompositeFieldMetadataGroupByGqlInputTypeGenerator,
|
||||
) {}
|
||||
|
||||
public buildAndStore(compositeType: CompositeType) {
|
||||
@@ -29,5 +31,8 @@ export class CompositeFieldMetadataGqlInputTypeGenerator {
|
||||
this.compositeFieldMetadataOrderByGqlInputTypeGenerator.buildAndStore(
|
||||
compositeType,
|
||||
);
|
||||
this.compositeFieldMetadataGroupByGqlInputTypeGenerator.buildAndStore(
|
||||
compositeType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+123
-70
@@ -1,25 +1,27 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import {
|
||||
GraphQLEnumType,
|
||||
GraphQLInputFieldConfigMap,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLScalarType,
|
||||
isEnumType,
|
||||
isInputObjectType,
|
||||
isObjectType,
|
||||
} from 'graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
|
||||
import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator';
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
|
||||
import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util';
|
||||
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
|
||||
import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util';
|
||||
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
|
||||
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -79,77 +81,128 @@ export class ObjectMetadataCreateGqlInputTypeGenerator {
|
||||
},
|
||||
),
|
||||
};
|
||||
} else if (isEnumFieldMetadataType(fieldMetadata.type)) {
|
||||
generatedFields = this.generateEnumFieldCreateInputType(
|
||||
objectMetadata,
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
);
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
generatedFields = this.generateCompositeFieldCreateInputType(
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
);
|
||||
} else {
|
||||
let type:
|
||||
| GraphQLInputObjectType
|
||||
| GraphQLEnumType
|
||||
| GraphQLScalarType
|
||||
| undefined;
|
||||
|
||||
if (isEnumFieldMetadataType(fieldMetadata.type)) {
|
||||
const key = computeEnumFieldGqlTypeKey(
|
||||
objectMetadata.nameSingular,
|
||||
fieldMetadata.name,
|
||||
);
|
||||
|
||||
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(enumType) || !isEnumType(enumType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
type = enumType;
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const key = computeCompositeFieldInputTypeKey(
|
||||
fieldMetadata.type,
|
||||
GqlInputTypeDefinitionKind.Create,
|
||||
);
|
||||
|
||||
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
type = compositeType;
|
||||
} else {
|
||||
type = this.typeMapperService.mapToScalarType(
|
||||
fieldMetadata.type,
|
||||
typeOptions,
|
||||
);
|
||||
|
||||
if (!isDefined(type) || isObjectType(type)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
generatedFields = {
|
||||
[fieldMetadata.name]: {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
generatedFields = this.generateAtomicFieldCreateInputType(
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(allGeneratedFields, generatedFields);
|
||||
}
|
||||
|
||||
return allGeneratedFields;
|
||||
}
|
||||
|
||||
private generateEnumFieldCreateInputType(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
typeOptions: TypeOptions,
|
||||
) {
|
||||
const key = computeEnumFieldGqlTypeKey(
|
||||
objectMetadata.nameSingular,
|
||||
fieldMetadata.name,
|
||||
);
|
||||
|
||||
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(enumType) || !isEnumType(enumType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const modifiedEnumType = this.typeMapperService.applyTypeOptions(
|
||||
enumType,
|
||||
typeOptions,
|
||||
);
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type: modifiedEnumType,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private generateCompositeFieldCreateInputType(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
typeOptions: TypeOptions,
|
||||
) {
|
||||
const key = computeCompositeFieldInputTypeKey(
|
||||
fieldMetadata.type,
|
||||
GqlInputTypeDefinitionKind.Create,
|
||||
);
|
||||
|
||||
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const modifiedCompositeType = this.typeMapperService.applyTypeOptions(
|
||||
compositeType,
|
||||
typeOptions,
|
||||
);
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type: modifiedCompositeType,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private generateAtomicFieldCreateInputType(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
typeOptions: TypeOptions,
|
||||
) {
|
||||
const type = this.typeMapperService.mapToScalarType(
|
||||
fieldMetadata.type,
|
||||
typeOptions,
|
||||
);
|
||||
|
||||
if (!isDefined(type) || isObjectType(type)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const modifiedType = this.typeMapperService.applyTypeOptions(
|
||||
type,
|
||||
typeOptions,
|
||||
);
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type: modifiedType,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+109
-65
@@ -3,7 +3,6 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
GraphQLInputFieldConfigMap,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInputType,
|
||||
isEnumType,
|
||||
isInputObjectType,
|
||||
isObjectType,
|
||||
@@ -12,7 +11,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
|
||||
import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator';
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
|
||||
import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util';
|
||||
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
|
||||
@@ -20,6 +22,7 @@ import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-sch
|
||||
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
|
||||
import { createGqlEnumFilterType } from 'src/engine/api/graphql/workspace-schema-builder/utils/create-gql-enum-filter-type.util';
|
||||
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -67,7 +70,7 @@ export class ObjectMetadataFilterGqlInputTypeGenerator {
|
||||
|
||||
const typeOptions = computeFieldInputTypeOptions(
|
||||
fieldMetadata,
|
||||
GqlInputTypeDefinitionKind.Update,
|
||||
GqlInputTypeDefinitionKind.Filter,
|
||||
);
|
||||
|
||||
let generatedFields;
|
||||
@@ -80,69 +83,22 @@ export class ObjectMetadataFilterGqlInputTypeGenerator {
|
||||
typeOptions,
|
||||
},
|
||||
);
|
||||
} else if (isEnumFieldMetadataType(fieldMetadata.type)) {
|
||||
generatedFields = this.generateEnumFieldFilterInputType(
|
||||
objectMetadata,
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
);
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
generatedFields = this.generateCompositeFieldFilterInputType(
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
);
|
||||
} else {
|
||||
let type: GraphQLInputType | undefined;
|
||||
|
||||
if (isEnumFieldMetadataType(fieldMetadata.type)) {
|
||||
const key = computeEnumFieldGqlTypeKey(
|
||||
objectMetadata.nameSingular,
|
||||
fieldMetadata.name,
|
||||
);
|
||||
|
||||
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(enumType) || !isEnumType(enumType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
type = createGqlEnumFilterType(enumType);
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const key = computeCompositeFieldInputTypeKey(
|
||||
fieldMetadata.type,
|
||||
GqlInputTypeDefinitionKind.Filter,
|
||||
);
|
||||
|
||||
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
type = compositeType;
|
||||
} else {
|
||||
type = this.typeMapperService.mapToFilterType(
|
||||
fieldMetadata.type,
|
||||
typeOptions,
|
||||
);
|
||||
|
||||
if (!isDefined(type) || isObjectType(type)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
generatedFields = {
|
||||
[fieldMetadata.name]: {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
generatedFields = this.generateAtomicFieldFilterInputType(
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
);
|
||||
}
|
||||
Object.assign(allGeneratedFields, generatedFields);
|
||||
}
|
||||
@@ -162,4 +118,92 @@ export class ObjectMetadataFilterGqlInputTypeGenerator {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private generateEnumFieldFilterInputType(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
typeOptions: TypeOptions,
|
||||
) {
|
||||
const key = computeEnumFieldGqlTypeKey(
|
||||
objectMetadata.nameSingular,
|
||||
fieldMetadata.name,
|
||||
);
|
||||
|
||||
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(enumType) || !isEnumType(enumType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const type = createGqlEnumFilterType(enumType);
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private generateCompositeFieldFilterInputType(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
typeOptions: TypeOptions,
|
||||
) {
|
||||
const key = computeCompositeFieldInputTypeKey(
|
||||
fieldMetadata.type,
|
||||
GqlInputTypeDefinitionKind.Filter,
|
||||
);
|
||||
|
||||
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type: compositeType,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private generateAtomicFieldFilterInputType(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
typeOptions: TypeOptions,
|
||||
) {
|
||||
const type = this.typeMapperService.mapToFilterType(
|
||||
fieldMetadata.type,
|
||||
typeOptions,
|
||||
);
|
||||
|
||||
if (!isDefined(type) || isObjectType(type)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLInputFieldConfigMap,
|
||||
GraphQLInputObjectType,
|
||||
} from 'graphql';
|
||||
|
||||
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
|
||||
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
|
||||
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
|
||||
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
@Injectable()
|
||||
export class CompositeFieldMetadataGroupByGqlInputTypeGenerator {
|
||||
private readonly logger = new Logger(
|
||||
CompositeFieldMetadataGroupByGqlInputTypeGenerator.name,
|
||||
);
|
||||
constructor(
|
||||
private readonly gqlTypesStorage: GqlTypesStorage,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public buildAndStore(compositeType: CompositeType) {
|
||||
const key = computeCompositeFieldInputTypeKey(
|
||||
compositeType.type,
|
||||
GqlInputTypeDefinitionKind.GroupBy,
|
||||
);
|
||||
|
||||
const type = new GraphQLInputObjectType({
|
||||
name: `${pascalCase(compositeType.type)}${GqlInputTypeDefinitionKind.GroupBy.toString()}Input`,
|
||||
fields: this.generateFields(compositeType),
|
||||
});
|
||||
|
||||
this.gqlTypesStorage.addGqlType(key, type);
|
||||
}
|
||||
|
||||
public generateFields(
|
||||
compositeType: CompositeType,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const property of compositeType.properties) {
|
||||
// Relation fields are not supported in composite types
|
||||
if (isMorphOrRelationFieldMetadataType(property.type)) {
|
||||
this.logger.error(
|
||||
'Relation fields are not supported in composite types',
|
||||
{ compositeType, property },
|
||||
);
|
||||
|
||||
throw new Error('Relation fields are not supported in composite types');
|
||||
}
|
||||
|
||||
// Skip hidden fields
|
||||
if (property.hidden === true || property.hidden === 'input') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.typeMapperService.applyTypeOptions(GraphQLBoolean, {});
|
||||
|
||||
fields[property.name] = {
|
||||
type,
|
||||
description: property.description,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLInputFieldConfigMap,
|
||||
GraphQLInputObjectType,
|
||||
isInputObjectType,
|
||||
} from 'graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
|
||||
import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator';
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
|
||||
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
|
||||
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
|
||||
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataGroupByGqlInputTypeGenerator {
|
||||
private readonly logger = new Logger(
|
||||
ObjectMetadataGroupByGqlInputTypeGenerator.name,
|
||||
);
|
||||
constructor(
|
||||
private readonly gqlTypesStorage: GqlTypesStorage,
|
||||
private readonly relationFieldMetadataGqlInputTypeGenerator: RelationFieldMetadataGqlInputTypeGenerator,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public buildAndStore(objectMetadata: ObjectMetadataEntity) {
|
||||
const inputType = new GraphQLInputObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.GroupBy.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => this.generateFields(objectMetadata),
|
||||
}) as GraphQLInputObjectType;
|
||||
|
||||
const key = computeObjectMetadataInputTypeKey(
|
||||
objectMetadata.nameSingular,
|
||||
GqlInputTypeDefinitionKind.GroupBy,
|
||||
);
|
||||
|
||||
this.gqlTypesStorage.addGqlType(key, inputType);
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const allGeneratedFields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
const generatedField = isFieldMetadataRelationOrMorphRelation(
|
||||
fieldMetadata,
|
||||
)
|
||||
? this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldGroupByInputType(
|
||||
fieldMetadata,
|
||||
)
|
||||
: this.generateField(fieldMetadata);
|
||||
|
||||
Object.assign(allGeneratedFields, generatedField);
|
||||
}
|
||||
|
||||
return allGeneratedFields;
|
||||
}
|
||||
|
||||
private generateField(fieldMetadata: FieldMetadataEntity) {
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type))
|
||||
return this.generateCompositeFieldGroupByInputType(fieldMetadata);
|
||||
|
||||
const type = this.typeMapperService.applyTypeOptions(GraphQLBoolean, {});
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private generateCompositeFieldGroupByInputType(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
) {
|
||||
const key = computeCompositeFieldInputTypeKey(
|
||||
fieldMetadata.type,
|
||||
GqlInputTypeDefinitionKind.GroupBy,
|
||||
);
|
||||
|
||||
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
fieldMetadata,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type: compositeType,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+5
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataCreateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator';
|
||||
import { ObjectMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator';
|
||||
import { ObjectMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator';
|
||||
import { ObjectMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator';
|
||||
import { ObjectMetadataUpdateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -13,6 +14,7 @@ export class ObjectMetadataGqlInputTypeGenerator {
|
||||
private readonly objectMetadataUpdateGqlInputTypeGenerator: ObjectMetadataUpdateGqlInputTypeGenerator,
|
||||
private readonly objectMetadataFilterGqlInputTypeGenerator: ObjectMetadataFilterGqlInputTypeGenerator,
|
||||
private readonly objectMetadataOrderByGqlInputTypeGenerator: ObjectMetadataOrderByGqlInputTypeGenerator,
|
||||
private readonly objectMetadataGroupByGqlInputTypeGenerator: ObjectMetadataGroupByGqlInputTypeGenerator,
|
||||
) {}
|
||||
|
||||
public buildAndStore(objectMetadata: ObjectMetadataEntity) {
|
||||
@@ -28,5 +30,8 @@ export class ObjectMetadataGqlInputTypeGenerator {
|
||||
this.objectMetadataOrderByGqlInputTypeGenerator.buildAndStore(
|
||||
objectMetadata,
|
||||
);
|
||||
this.objectMetadataGroupByGqlInputTypeGenerator.buildAndStore(
|
||||
objectMetadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-11
@@ -13,9 +13,7 @@ import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-sch
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
|
||||
import { computeCompositeFieldTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-field-type-options.util';
|
||||
import { computeCompositeFieldEnumTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-enum-type-key.util';
|
||||
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
|
||||
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
@@ -61,20 +59,13 @@ export class CompositeFieldMetadataOrderByGqlInputTypeGenerator {
|
||||
}
|
||||
|
||||
// Skip hidden fields
|
||||
if (property.hidden === true || property.hidden === 'input') {
|
||||
if (property.hidden === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = computeCompositeFieldEnumTypeKey(
|
||||
compositeType.type,
|
||||
property.name,
|
||||
);
|
||||
|
||||
const typeOptions = computeCompositeFieldTypeOptions(property);
|
||||
|
||||
const type = isEnumFieldMetadataType(property.type)
|
||||
? this.gqlTypesStorage.getGqlTypeByKey(key)
|
||||
: this.typeMapperService.mapToOrderByType(property.type);
|
||||
const type = this.typeMapperService.mapToOrderByType(property.type);
|
||||
|
||||
if (!isDefined(type) || isObjectType(type)) {
|
||||
const message = `Could not find a GraphQL input type for ${compositeType.type} ${property.name}`;
|
||||
|
||||
+73
-86
@@ -1,28 +1,25 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
GraphQLEnumType,
|
||||
GraphQLInputFieldConfigMap,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInputType,
|
||||
GraphQLList,
|
||||
GraphQLScalarType,
|
||||
isEnumType,
|
||||
isInputObjectType,
|
||||
} from 'graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
|
||||
import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator';
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
|
||||
import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util';
|
||||
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
|
||||
import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util';
|
||||
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
|
||||
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
@@ -68,93 +65,83 @@ export class ObjectMetadataOrderByGqlInputTypeGenerator {
|
||||
let generatedFields;
|
||||
|
||||
if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) {
|
||||
generatedFields = {
|
||||
...this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldCreateOrUpdateInputType(
|
||||
generatedFields =
|
||||
this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldOrderByInputType(
|
||||
{
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
},
|
||||
),
|
||||
...this.relationFieldMetadataGqlInputTypeGenerator.generateConnectRelationFieldInputType(
|
||||
{
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
},
|
||||
),
|
||||
};
|
||||
);
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
generatedFields = this.generateCompositeFieldOrderByInputType(
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
);
|
||||
} else {
|
||||
let type:
|
||||
| GraphQLInputObjectType
|
||||
| GraphQLEnumType
|
||||
| GraphQLScalarType
|
||||
| GraphQLList<GraphQLInputType>
|
||||
| undefined;
|
||||
|
||||
if (isEnumFieldMetadataType(fieldMetadata.type)) {
|
||||
const key = computeEnumFieldGqlTypeKey(
|
||||
objectMetadata.nameSingular,
|
||||
fieldMetadata.name,
|
||||
);
|
||||
|
||||
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(enumType) || !isEnumType(enumType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
type = enumType;
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const key = computeCompositeFieldInputTypeKey(
|
||||
fieldMetadata.type,
|
||||
GqlInputTypeDefinitionKind.OrderBy,
|
||||
);
|
||||
|
||||
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
type = compositeType;
|
||||
} else {
|
||||
const orderByType = this.typeMapperService.mapToOrderByType(
|
||||
fieldMetadata.type,
|
||||
);
|
||||
|
||||
if (!isDefined(orderByType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
type,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
type = orderByType;
|
||||
}
|
||||
generatedFields = {
|
||||
[fieldMetadata.name]: {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
generatedFields = this.generateAtomicFieldOrderByInputType(
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(allGeneratedFields, generatedFields);
|
||||
}
|
||||
|
||||
return allGeneratedFields;
|
||||
}
|
||||
|
||||
private generateCompositeFieldOrderByInputType(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
typeOptions: TypeOptions,
|
||||
) {
|
||||
const key = computeCompositeFieldInputTypeKey(
|
||||
fieldMetadata.type,
|
||||
GqlInputTypeDefinitionKind.OrderBy,
|
||||
);
|
||||
|
||||
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
|
||||
|
||||
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type: compositeType,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private generateAtomicFieldOrderByInputType(
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
typeOptions: TypeOptions,
|
||||
) {
|
||||
const orderByType = this.typeMapperService.mapToOrderByType(
|
||||
fieldMetadata.type,
|
||||
);
|
||||
|
||||
if (!isDefined(orderByType)) {
|
||||
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
|
||||
|
||||
this.logger.error(message, {
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return {
|
||||
[fieldMetadata.name]: {
|
||||
type: orderByType,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+25
-1
@@ -1,6 +1,10 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isObjectType } from 'graphql';
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLInputFieldConfigMap,
|
||||
isObjectType,
|
||||
} from 'graphql';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -147,6 +151,26 @@ export class RelationFieldMetadataGqlInputTypeGenerator {
|
||||
};
|
||||
}
|
||||
|
||||
public generateSimpleRelationFieldGroupByInputType(
|
||||
fieldMetadata: FieldMetadataEntity<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||
>,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY)
|
||||
return {};
|
||||
|
||||
const { joinColumnName } = extractGraphQLRelationFieldNames(fieldMetadata);
|
||||
|
||||
const type = this.typeMapperService.applyTypeOptions(GraphQLBoolean, {});
|
||||
|
||||
return {
|
||||
[joinColumnName]: {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public generateConnectRelationFieldInputType({
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
|
||||
+7
-1
@@ -142,9 +142,15 @@ export class ObjectMetadataUpdateGqlInputTypeGenerator {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
const modifiedType = this.typeMapperService.applyTypeOptions(
|
||||
type,
|
||||
typeOptions,
|
||||
);
|
||||
|
||||
generatedFields = {
|
||||
[fieldMetadata.name]: {
|
||||
type,
|
||||
type: modifiedType,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLObjectType, isObjectType } from 'graphql';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/object-type-definition-kind.enum';
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
|
||||
import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types';
|
||||
import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
@Injectable()
|
||||
export class GroupByConnectionGqlObjectTypeGenerator {
|
||||
private readonly logger = new Logger(
|
||||
GroupByConnectionGqlObjectTypeGenerator.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly gqlTypesStorage: GqlTypesStorage,
|
||||
) {}
|
||||
|
||||
public buildAndStore(objectMetadata: ObjectMetadataEntity) {
|
||||
const kind = ObjectTypeDefinitionKind.GroupByConnection;
|
||||
|
||||
const key = computeObjectMetadataObjectTypeKey(
|
||||
objectMetadata.nameSingular,
|
||||
kind,
|
||||
);
|
||||
|
||||
this.gqlTypesStorage.addGqlType(
|
||||
key,
|
||||
new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => this.generateFields(objectMetadata),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
): GraphQLOutputTypeFieldConfigMap {
|
||||
const fields: GraphQLOutputTypeFieldConfigMap = {};
|
||||
|
||||
const connection = this.gqlTypesStorage.getGqlTypeByKey(
|
||||
computeObjectMetadataObjectTypeKey(
|
||||
objectMetadata.nameSingular,
|
||||
ObjectTypeDefinitionKind.Connection,
|
||||
),
|
||||
);
|
||||
|
||||
if (!isDefined(connection) || !isObjectType(connection)) {
|
||||
this.logger.error(
|
||||
`Connection type for ${objectMetadata.nameSingular} was not found.`,
|
||||
{
|
||||
objectMetadata,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Connection type for ${objectMetadata.nameSingular} was not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(fields, connection.toConfig().fields);
|
||||
|
||||
fields.groupByDimensionValues = {
|
||||
type: this.typeMapperService.applyTypeOptions(GraphQLJSON, {
|
||||
nullable: false,
|
||||
}),
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
+15
-2
@@ -6,6 +6,8 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work
|
||||
import { GqlOperation } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-operation.enum';
|
||||
import { RootTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator';
|
||||
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
@@ -13,12 +15,23 @@ export class QueryTypeGenerator {
|
||||
constructor(
|
||||
private readonly rootTypeGenerator: RootTypeGenerator,
|
||||
private readonly gqlTypesStorage: GqlTypesStorage,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
buildAndStore(objectMetadataCollection: ObjectMetadataEntity[]) {
|
||||
async buildAndStore(
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
workspaceId: string,
|
||||
) {
|
||||
const isGroupByEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_GROUP_BY_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return this.rootTypeGenerator.buildAndStore(
|
||||
objectMetadataCollection,
|
||||
[...workspaceResolverBuilderMethodNames.queries],
|
||||
[...workspaceResolverBuilderMethodNames.queries].filter(
|
||||
(methodName) => methodName !== 'groupBy' || isGroupByEnabled,
|
||||
),
|
||||
GqlOperation.Query,
|
||||
);
|
||||
}
|
||||
|
||||
+5
-2
@@ -100,19 +100,20 @@ export class RootTypeGenerator {
|
||||
);
|
||||
}
|
||||
|
||||
const allowedMethodNames = [
|
||||
const isMethodReturningArrayObjectType = [
|
||||
'updateMany',
|
||||
'deleteMany',
|
||||
'createMany',
|
||||
'findDuplicates',
|
||||
'restoreMany',
|
||||
'destroyMany',
|
||||
'groupBy',
|
||||
];
|
||||
|
||||
const outputType = this.typeMapperService.applyTypeOptions(
|
||||
objectType,
|
||||
{
|
||||
isArray: allowedMethodNames.includes(methodName),
|
||||
isArray: isMethodReturningArrayObjectType.includes(methodName),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -135,6 +136,8 @@ export class RootTypeGenerator {
|
||||
case 'findMany':
|
||||
case 'findDuplicates':
|
||||
return ObjectTypeDefinitionKind.Connection;
|
||||
case 'groupBy':
|
||||
return ObjectTypeDefinitionKind.GroupByConnection;
|
||||
default:
|
||||
return ObjectTypeDefinitionKind.Plain;
|
||||
}
|
||||
|
||||
+6
@@ -6,6 +6,8 @@ import { CompositeFieldMetadataCreateGqlInputTypeGenerator } from 'src/engine/ap
|
||||
import { ObjectMetadataCreateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator';
|
||||
import { CompositeFieldMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/composite-field-metadata-filter-gql-input-types.generator';
|
||||
import { ObjectMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator';
|
||||
import { CompositeFieldMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/composite-field-metadata-group-by-gql-input-type.generator';
|
||||
import { ObjectMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator';
|
||||
import { ObjectMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator';
|
||||
import { CompositeFieldMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/composite-field-metadata-order-by-gql-input-type.generator';
|
||||
import { ObjectMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator';
|
||||
@@ -17,6 +19,7 @@ import { AggregationObjectTypeGenerator } from 'src/engine/api/graphql/workspace
|
||||
import { CompositeFieldMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/composite-field-metadata-gql-object-type.generator';
|
||||
import { ConnectionGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator';
|
||||
import { EdgeGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator';
|
||||
import { GroupByConnectionGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator';
|
||||
import { ObjectMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator';
|
||||
import { ObjectMetadataWithRelationsGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator';
|
||||
import { RelationFieldMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator';
|
||||
@@ -33,6 +36,7 @@ export const workspaceSchemaBuilderTypeGenerators = [
|
||||
CompositeFieldMetadataUpdateGqlInputTypeGenerator,
|
||||
CompositeFieldMetadataFilterGqlInputTypeGenerator,
|
||||
CompositeFieldMetadataOrderByGqlInputTypeGenerator,
|
||||
CompositeFieldMetadataGroupByGqlInputTypeGenerator,
|
||||
RelationFieldMetadataGqlInputTypeGenerator,
|
||||
RelationFieldMetadataGqlObjectTypeGenerator,
|
||||
ObjectMetadataGqlInputTypeGenerator,
|
||||
@@ -40,11 +44,13 @@ export const workspaceSchemaBuilderTypeGenerators = [
|
||||
ObjectMetadataUpdateGqlInputTypeGenerator,
|
||||
ObjectMetadataFilterGqlInputTypeGenerator,
|
||||
ObjectMetadataOrderByGqlInputTypeGenerator,
|
||||
ObjectMetadataGroupByGqlInputTypeGenerator,
|
||||
CompositeFieldMetadataGqlObjectTypeGenerator,
|
||||
ObjectMetadataGqlObjectTypeGenerator,
|
||||
RelationConnectGqlInputTypeGenerator,
|
||||
ConnectionGqlObjectTypeGenerator,
|
||||
EdgeGqlObjectTypeGenerator,
|
||||
GroupByConnectionGqlObjectTypeGenerator,
|
||||
ObjectMetadataWithRelationsGqlObjectTypeGenerator,
|
||||
AggregationObjectTypeGenerator,
|
||||
ArgsTypeGenerator,
|
||||
|
||||
+21
@@ -161,6 +161,27 @@ export const getResolverArgs = (
|
||||
isNullable: true,
|
||||
},
|
||||
};
|
||||
case 'groupBy':
|
||||
return {
|
||||
groupBy: {
|
||||
kind: GqlInputTypeDefinitionKind.GroupBy,
|
||||
isNullable: false,
|
||||
isArray: true,
|
||||
},
|
||||
filter: {
|
||||
kind: GqlInputTypeDefinitionKind.Filter,
|
||||
isNullable: true,
|
||||
},
|
||||
orderBy: {
|
||||
kind: GqlInputTypeDefinitionKind.OrderBy,
|
||||
isNullable: true,
|
||||
isArray: true,
|
||||
},
|
||||
viewId: {
|
||||
type: UUIDScalarType,
|
||||
isNullable: true,
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown resolver type: ${type}`);
|
||||
}
|
||||
|
||||
+7
-3
@@ -17,10 +17,14 @@ export class WorkspaceGraphQLSchemaGenerator {
|
||||
private readonly orphanedTypesGenerator: OrphanedTypesGenerator,
|
||||
) {}
|
||||
|
||||
generateSchema(
|
||||
async generateSchema(
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
): GraphQLSchema {
|
||||
this.gqlTypeGenerator.buildAndStore(objectMetadataCollection);
|
||||
workspaceId: string,
|
||||
): Promise<GraphQLSchema> {
|
||||
await this.gqlTypeGenerator.buildAndStore(
|
||||
objectMetadataCollection,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
// Assemble schema
|
||||
const schema = new GraphQLSchema({
|
||||
|
||||
@@ -106,8 +106,9 @@ export class WorkspaceSchemaFactory {
|
||||
// If typeDefs are not cached, generate them
|
||||
if (!typeDefs || !usedScalarNames) {
|
||||
const autoGeneratedSchema =
|
||||
this.workspaceGraphQLSchemaGenerator.generateSchema(
|
||||
await this.workspaceGraphQLSchemaGenerator.generateSchema(
|
||||
objectMetadataCollection,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
usedScalarNames =
|
||||
|
||||
+1
@@ -16,4 +16,5 @@ export enum FeatureFlagKey {
|
||||
IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED',
|
||||
IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED',
|
||||
IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED',
|
||||
IS_GROUP_BY_ENABLED = 'IS_GROUP_BY_ENABLED',
|
||||
}
|
||||
|
||||
+1
@@ -139,6 +139,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_WORKFLOW_ITERATOR_ENABLED: false,
|
||||
IS_DATABASE_EVENT_TRIGGER_ENABLED: false,
|
||||
IS_CALENDAR_VIEW_ENABLED: false,
|
||||
IS_GROUP_BY_ENABLED: false,
|
||||
},
|
||||
eventEmitterService: {
|
||||
emitMutationEvent: jest.fn(),
|
||||
|
||||
@@ -44,6 +44,9 @@ export const getResolverName = (
|
||||
case 'mergeMany':
|
||||
return `merge${pascalCase(objectMetadata.namePlural)}`;
|
||||
|
||||
case 'groupBy':
|
||||
return `${camelCase(objectMetadata.namePlural)}GroupBy`;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown resolver type: ${type}`);
|
||||
}
|
||||
|
||||
+5
@@ -75,6 +75,11 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_GROUP_BY_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: false,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user