diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts index 923ab48db2..d29ec98be8 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts @@ -1,75 +1,35 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { isSwitchingToKanbanViewTypeComponentState } from '@/object-record/record-board/states/isSwitchingToKanbanViewTypeComponentState'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; -import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView'; import { coreViewsState } from '@/views/states/coreViewState'; import { type GraphQLView } from '@/views/types/GraphQLView'; -import { type ViewGroup } from '@/views/types/ViewGroup'; import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType'; import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; import { convertViewTypeToCore } from '@/views/utils/convertViewTypeToCore'; import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar'; import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; -import { useCallback } from 'react'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; -import { v4 } from 'uuid'; import { ViewCalendarLayout } from '~/generated/graphql'; export const useSetViewTypeFromLayoutOptionsMenu = () => { const { updateCurrentView } = useUpdateCurrentView(); const setRecordIndexViewType = useSetRecoilState(recordIndexViewTypeState); + const isSwitchingToKanbanViewTypeCallbackState = + useRecoilComponentCallbackState(isSwitchingToKanbanViewTypeComponentState); const { availableFieldsForKanban } = useGetAvailableFieldsForKanban(); const { objectMetadataItem } = useRecordIndexContextOrThrow(); const { loadRecordIndexStates } = useLoadRecordIndexStates(); - const { createViewGroups } = usePersistViewGroupRecords(); - const { availableFieldsForCalendar } = useGetAvailableFieldsForCalendar(); - const createViewGroupAssociatedWithKanbanField = useCallback( - async (randomFieldForKanban: string, currentViewId: string) => { - const viewGroupsToCreate = - objectMetadataItem.fields - ?.find((field) => field.id === randomFieldForKanban) - ?.options?.map( - (option, index) => - ({ - id: v4(), - __typename: 'ViewGroup', - fieldValue: option.value, - isVisible: true, - position: index, - }) satisfies ViewGroup, - ) ?? []; - - viewGroupsToCreate.push({ - __typename: 'ViewGroup', - id: v4(), - fieldValue: '', - position: viewGroupsToCreate.length, - isVisible: true, - } satisfies ViewGroup); - - await createViewGroups({ - createCoreViewGroupInputs: { - inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ - ...viewGroup, - viewId: currentViewId, - })), - }, - }); - - return viewGroupsToCreate; - }, - [objectMetadataItem, createViewGroups], - ); - const setAndPersistViewType = useRecoilCallback( ({ snapshot, set }) => async (viewType: ViewType) => { @@ -104,20 +64,20 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { switch (viewType) { case ViewType.Kanban: { + set(isSwitchingToKanbanViewTypeCallbackState, true); if (availableFieldsForKanban.length === 0) { throw new Error('No fields for kanban - should not happen'); } - if (currentView.viewGroups.length === 0) { - const viewGroups = await createViewGroupAssociatedWithKanbanField( - availableFieldsForKanban[0].id, - currentView.id, - ); - loadRecordIndexStates( - { ...currentView, viewGroups }, - objectMetadataItem, - ); + const mainGroupByFieldMetadataId = availableFieldsForKanban[0].id; + updateCurrentViewParams.mainGroupByFieldMetadataId = + mainGroupByFieldMetadataId; + + if (shouldChangeIcon(currentView.icon, currentView.type)) { + updateCurrentViewParams.icon = + viewTypeIconMapping(viewType).displayName; } + setRecordIndexViewType(viewType); set(coreViewsState, [ ...existingCoreViews.filter( @@ -126,16 +86,19 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { { ...currentCoreView, type: convertViewTypeToCore(viewType), + mainGroupByFieldMetadataId, }, ]); - - if (shouldChangeIcon(currentView.icon, currentView.type)) { - updateCurrentViewParams.icon = - viewTypeIconMapping(viewType).displayName; - } - return await updateCurrentView(updateCurrentViewParams); + await updateCurrentView(updateCurrentViewParams); + return; } case ViewType.Table: { + if (shouldChangeIcon(currentView.icon, currentView.type)) { + updateCurrentViewParams.icon = + viewTypeIconMapping(viewType).displayName; + } + updateCurrentViewParams.mainGroupByFieldMetadataId = null; + await updateCurrentView(updateCurrentViewParams); setRecordIndexViewType(viewType); set(coreViewsState, [ ...existingCoreViews.filter( @@ -143,15 +106,11 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { ), { ...currentCoreView, + mainGroupByFieldMetadataId: null, type: convertViewTypeToCore(viewType), }, ]); - - if (shouldChangeIcon(currentView.icon, currentView.type)) { - updateCurrentViewParams.icon = - viewTypeIconMapping(viewType).displayName; - } - return await updateCurrentView(updateCurrentViewParams); + return; } case ViewType.Calendar: { if (availableFieldsForCalendar.length === 0) { @@ -167,6 +126,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { ), { ...currentCoreView, + mainGroupByFieldMetadataId: null, type: convertViewTypeToCore(viewType), calendarLayout: ViewCalendarLayout.MONTH, calendarFieldMetadataId, @@ -190,6 +150,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { updateCurrentViewParams.calendarLayout = ViewCalendarLayout.MONTH; updateCurrentViewParams.calendarFieldMetadataId = calendarFieldMetadataId; + updateCurrentViewParams.mainGroupByFieldMetadataId = null; return await updateCurrentView(updateCurrentViewParams); } default: { @@ -198,13 +159,13 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { } }, [ + isSwitchingToKanbanViewTypeCallbackState, availableFieldsForKanban, - objectMetadataItem, - updateCurrentView, setRecordIndexViewType, - createViewGroupAssociatedWithKanbanField, - loadRecordIndexStates, + updateCurrentView, availableFieldsForCalendar, + loadRecordIndexStates, + objectMetadataItem, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardQueryEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardQueryEffect.tsx index f162185b2a..a9f99eb80a 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardQueryEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardQueryEffect.tsx @@ -1,5 +1,6 @@ import { useTriggerRecordBoardFetchMore } from '@/object-record/record-board/hooks/useTriggerRecordBoardFetchMore'; import { useTriggerRecordBoardInitialQuery } from '@/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery'; +import { isSwitchingToKanbanViewTypeComponentState } from '@/object-record/record-board/states/isSwitchingToKanbanViewTypeComponentState'; import { lastRecordBoardQueryIdentifierComponentState } from '@/object-record/record-board/states/lastRecordBoardQueryIdentifierComponentState'; import { recordBoardCurrentGroupByQueryOffsetComponentState } from '@/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState'; import { recordBoardIsFetchingMoreComponentState } from '@/object-record/record-board/states/recordBoardIsFetchingMoreComponentState'; @@ -22,6 +23,11 @@ export const RecordBoardQueryEffect = () => { const [lastRecordBoardQueryIdentifier, setLastRecordBoardQueryIdentifier] = useRecoilComponentState(lastRecordBoardQueryIdentifierComponentState); + const [ + isSwitchingToKanbanViewTypeCallbackState, + setIsSwitchingToKanbanViewTypeComponentState, + ] = useRecoilComponentState(isSwitchingToKanbanViewTypeComponentState); + const [recordIndexRecordGroupsAreInInitialLoading] = useRecoilComponentState( recordIndexRecordGroupsAreInInitialLoadingComponentState, ); @@ -60,9 +66,10 @@ export const RecordBoardQueryEffect = () => { useEffect(() => { if ( !recordIndexRecordGroupsAreInInitialLoading && - queryIdentifierHasChanged + (queryIdentifierHasChanged || isSwitchingToKanbanViewTypeCallbackState) ) { triggerRecordBoardInitialQuery(); + setIsSwitchingToKanbanViewTypeComponentState(false); } else if ( !recordIndexRecordGroupsAreInInitialLoading && shouldFetchMore && @@ -82,6 +89,8 @@ export const RecordBoardQueryEffect = () => { shouldFetchMore, recordBoardIsFetchingMore, triggerRecordBoardFetchMore, + isSwitchingToKanbanViewTypeCallbackState, + setIsSwitchingToKanbanViewTypeComponentState, ]); return null; diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isSwitchingToKanbanViewTypeComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isSwitchingToKanbanViewTypeComponentState.ts new file mode 100644 index 0000000000..74bdeb91f1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isSwitchingToKanbanViewTypeComponentState.ts @@ -0,0 +1,9 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const isSwitchingToKanbanViewTypeComponentState = + createComponentState({ + key: 'isSwitchingToKanbanViewTypeComponentState', + componentInstanceContext: ContextStoreComponentInstanceContext, + defaultValue: false, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts index 47a99f92d0..58a3543b5d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts @@ -5,8 +5,8 @@ import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRec import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { usePersistView } from '@/views/hooks/internal/usePersistView'; -import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup'; import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState'; +import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; import { type ViewGroup } from '@/views/types/ViewGroup'; import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; import { useRecoilCallback } from 'recoil'; @@ -16,8 +16,6 @@ import { type CoreView } from '~/generated/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useHandleRecordGroupField = () => { - const { deleteViewGroups } = usePersistViewGroupRecords(); - const currentViewIdCallbackState = useRecoilComponentCallbackState( contextStoreCurrentViewIdComponentState, ); @@ -30,6 +28,8 @@ export const useHandleRecordGroupField = () => { const { updateView } = usePersistView(); const { loadRecordIndexStates } = useLoadRecordIndexStates(); + const { refreshCoreViewsByObjectMetadataId } = + useRefreshCoreViewsByObjectMetadataId(); const handleRecordGroupFieldChange = useRecoilCallback( ({ snapshot }) => @@ -127,6 +127,8 @@ export const useHandleRecordGroupField = () => { viewGroups: newViewGroupsList, objectMetadataItem, }); + + await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id); }, [ currentViewIdCallbackState, @@ -134,6 +136,7 @@ export const useHandleRecordGroupField = () => { updateView, setRecordGroupsFromViewGroups, objectMetadataItem, + refreshCoreViewsByObjectMetadataId, loadRecordIndexStates, ], ); @@ -159,32 +162,14 @@ export const useHandleRecordGroupField = () => { return; } - await deleteViewGroups( - view.viewGroups.map((group) => ({ - input: { - id: group.id, - }, - })), - ); - - if (!isDefined(view.mainGroupByFieldMetadataId)) { - throw new Error('mainGroupByFieldMetadataId is required'); - } - - setRecordGroupsFromViewGroups({ - viewId: view.id, - mainGroupByFieldMetadataId: view.mainGroupByFieldMetadataId, - viewGroups: [], - objectMetadataItem, + await updateView({ + id: view.id, + input: { + mainGroupByFieldMetadataId: null, + }, }); }, - [ - deleteViewGroups, - currentViewIdCallbackState, - getViewFromPrefetchState, - setRecordGroupsFromViewGroups, - objectMetadataItem, - ], + [currentViewIdCallbackState, getViewFromPrefetchState, updateView], ); return { handleRecordGroupFieldChange, resetRecordGroupField }; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts index 4ab299bf4f..f2c4c6f328 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts @@ -3,8 +3,11 @@ import { useCallback } from 'react'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useViewsSideEffectsOnViewGroups } from '@/views/hooks/useViewsSideEffectsOnViewGroups'; import { ApolloError } from '@apollo/client'; import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; import { type CreateCoreViewMutationVariables, type DeleteCoreViewMutationVariables, @@ -12,12 +15,15 @@ import { useCreateCoreViewMutation, useDeleteCoreViewMutation, useUpdateCoreViewMutation, + ViewType, } from '~/generated/graphql'; export const usePersistView = () => { const [createCoreViewMutation] = useCreateCoreViewMutation(); const [updateCoreViewMutation] = useUpdateCoreViewMutation(); const [deleteCoreViewMutation] = useDeleteCoreViewMutation(); + const { triggerViewGroupOptimisticEffectAtViewCreation } = + useViewsSideEffectsOnViewGroups(); const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); @@ -25,14 +31,39 @@ export const usePersistView = () => { const createView = useCallback( async ( variables: CreateCoreViewMutationVariables, + objectMetadataItemId: string, ): Promise< MetadataRequestResult>> > => { try { + const newViewId = variables.input.id ?? v4(); + if (variables.input.type === ViewType.KANBAN) { + triggerViewGroupOptimisticEffectAtViewCreation({ + newViewId, + objectMetadataItemId: objectMetadataItemId, + mainGroupByFieldMetadataId: + variables.input.mainGroupByFieldMetadataId, + }); + } + const result = await createCoreViewMutation({ - variables, + variables: { + input: { + ...variables.input, + id: newViewId, + }, + }, }); + const newView = result.data?.createCoreView; + + if (!isDefined(newView)) { + return { + status: 'failed', + error: new Error('Failed to create view'), + }; + } + return { status: 'successful', response: result, @@ -52,7 +83,12 @@ export const usePersistView = () => { }; } }, - [createCoreViewMutation, handleMetadataError, enqueueErrorSnackBar], + [ + createCoreViewMutation, + triggerViewGroupOptimisticEffectAtViewCreation, + handleMetadataError, + enqueueErrorSnackBar, + ], ); const updateView = useCallback( diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts index bd728989e2..eca0fff275 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts @@ -8,13 +8,7 @@ import { ApolloError } from '@apollo/client'; import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; import { - type CreateManyCoreViewGroupsMutationVariables, - type DeleteCoreViewGroupMutationVariables, - type DestroyCoreViewGroupMutationVariables, type UpdateCoreViewGroupMutationVariables, - useCreateManyCoreViewGroupsMutation, - useDeleteCoreViewGroupMutation, - useDestroyCoreViewGroupMutation, useUpdateCoreViewGroupMutation, } from '~/generated/graphql'; @@ -22,77 +16,11 @@ export const usePersistViewGroupRecords = () => { const { triggerViewGroupOptimisticEffect } = useTriggerViewGroupOptimisticEffect(); - const [createManyCoreViewGroupsMutation] = - useCreateManyCoreViewGroupsMutation(); const [updateCoreViewGroupMutation] = useUpdateCoreViewGroupMutation(); - const [deleteCoreViewGroupMutation] = useDeleteCoreViewGroupMutation(); - const [destroyCoreViewGroupMutation] = useDestroyCoreViewGroupMutation(); const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); - const createViewGroups = useCallback( - async ({ - createCoreViewGroupInputs, - }: { - createCoreViewGroupInputs: CreateManyCoreViewGroupsMutationVariables; - }): Promise< - MetadataRequestResult - > | null> - > => { - if ( - !Array.isArray(createCoreViewGroupInputs.inputs) || - createCoreViewGroupInputs.inputs.length === 0 - ) { - return { - status: 'successful', - response: null, - }; - } - - try { - const result = await createManyCoreViewGroupsMutation({ - variables: createCoreViewGroupInputs, - update: (_cache, { data }) => { - const createdViewGroups = data?.createManyCoreViewGroups; - if (!isDefined(createdViewGroups)) { - return; - } - - triggerViewGroupOptimisticEffect({ - createdViewGroups, - }); - }, - }); - - return { - status: 'successful', - response: result, - }; - } catch (error) { - if (error instanceof ApolloError) { - handleMetadataError(error, { - primaryMetadataName: 'viewGroup', - }); - } else { - enqueueErrorSnackBar({ message: t`An error occurred.` }); - } - - return { - status: 'failed', - error, - }; - } - }, - [ - triggerViewGroupOptimisticEffect, - createManyCoreViewGroupsMutation, - handleMetadataError, - enqueueErrorSnackBar, - ], - ); - const updateViewGroups = useCallback( async ( updateCoreViewGroupInputs: UpdateCoreViewGroupMutationVariables[], @@ -154,117 +82,7 @@ export const usePersistViewGroupRecords = () => { ], ); - const deleteViewGroups = useCallback( - async ( - deleteCoreViewGroupInputs: DeleteCoreViewGroupMutationVariables[], - ): Promise< - MetadataRequestResult< - Awaited>[] - > - > => { - if (deleteCoreViewGroupInputs.length === 0) { - return { - status: 'successful', - response: [], - }; - } - - try { - const results = await Promise.all( - deleteCoreViewGroupInputs.map((variables) => - deleteCoreViewGroupMutation({ - variables, - update: (_cache, { data }) => { - const deletedViewGroup = data?.deleteCoreViewGroup; - if (!isDefined(deletedViewGroup)) { - return; - } - - triggerViewGroupOptimisticEffect({ - deletedViewGroups: [deletedViewGroup], - }); - }, - }), - ), - ); - - return { - status: 'successful', - response: results, - }; - } catch (error) { - if (error instanceof ApolloError) { - handleMetadataError(error, { - primaryMetadataName: 'viewGroup', - }); - } else { - enqueueErrorSnackBar({ message: t`An error occurred.` }); - } - - return { - status: 'failed', - error, - }; - } - }, - [ - triggerViewGroupOptimisticEffect, - deleteCoreViewGroupMutation, - handleMetadataError, - enqueueErrorSnackBar, - ], - ); - - const destroyViewGroups = useCallback( - async ( - destroyCoreViewGroupInputs: DestroyCoreViewGroupMutationVariables[], - ): Promise< - MetadataRequestResult< - Awaited>[] - > - > => { - if (destroyCoreViewGroupInputs.length === 0) { - return { - status: 'successful', - response: [], - }; - } - - try { - const results = await Promise.all( - destroyCoreViewGroupInputs.map((variables) => - destroyCoreViewGroupMutation({ - variables, - }), - ), - ); - - return { - status: 'successful', - response: results, - }; - } catch (error) { - if (error instanceof ApolloError) { - handleMetadataError(error, { - primaryMetadataName: 'viewGroup', - }); - } else { - enqueueErrorSnackBar({ message: t`An error occurred.` }); - } - - return { - status: 'failed', - error, - }; - } - }, - [destroyCoreViewGroupMutation, handleMetadataError, enqueueErrorSnackBar], - ); - return { - createViewGroups, updateViewGroups, - deleteViewGroups, - destroyViewGroups, }; }; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index 64d7e2c568..b17abae067 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -13,11 +13,8 @@ import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistVi import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroup'; import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSort'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; -import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect'; -import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState'; import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector'; import { type GraphQLView } from '@/views/types/GraphQLView'; -import { type ViewGroup } from '@/views/types/ViewGroup'; import { ViewType } from '@/views/types/ViewType'; import { convertViewOpenRecordInToCore } from '@/views/utils/convertViewOpenRecordInToCore'; import { convertViewTypeToCore } from '@/views/utils/convertViewTypeToCore'; @@ -28,10 +25,7 @@ import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; -import { - type CoreViewGroup, - ViewCalendarLayout, -} from '~/generated-metadata/graphql'; +import { ViewCalendarLayout } from '~/generated-metadata/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { @@ -50,9 +44,6 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { const { createViewSorts } = usePersistViewSortRecords(); - const { triggerViewGroupOptimisticEffect } = - useTriggerViewGroupOptimisticEffect(); - const { createViewFilters } = usePersistViewFilterRecords(); const { createViewFilterGroups } = usePersistViewFilterGroupRecords(); @@ -75,7 +66,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { ); const createViewFromCurrentView = useRecoilCallback( - ({ snapshot, set }) => + ({ snapshot }) => async ( { id, @@ -120,53 +111,52 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { return undefined; } - set(isPersistingViewFieldsState, true); - const viewType = type ?? sourceView.type; - const result = await createView({ - input: { - id: id ?? v4(), - name: name ?? sourceView.name, - icon: icon ?? sourceView.icon, - key: null, - kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate - ? sourceView.kanbanAggregateOperation - : undefined, - kanbanAggregateOperationFieldMetadataId: - shouldCopyFiltersAndSortsAndAggregate - ? sourceView.kanbanAggregateOperationFieldMetadataId + const result = await createView( + { + input: { + id: id ?? v4(), + name: name ?? sourceView.name, + icon: icon ?? sourceView.icon, + key: null, + kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate + ? sourceView.kanbanAggregateOperation : undefined, - mainGroupByFieldMetadataId: shouldCopyFiltersAndSortsAndAggregate - ? sourceView.mainGroupByFieldMetadataId - : mainGroupByFieldMetadataId, - type: convertViewTypeToCore(viewType), - objectMetadataId: sourceView.objectMetadataId, - openRecordIn: convertViewOpenRecordInToCore( - sourceView.openRecordIn, - ), - anyFieldFilterValue: anyFieldFilterValue, - calendarLayout: - viewType === ViewType.Calendar - ? ViewCalendarLayout.MONTH - : undefined, - calendarFieldMetadataId: - viewType === ViewType.Calendar - ? calendarFieldMetadataId - : undefined, - visibility, + kanbanAggregateOperationFieldMetadataId: + shouldCopyFiltersAndSortsAndAggregate + ? sourceView.kanbanAggregateOperationFieldMetadataId + : undefined, + mainGroupByFieldMetadataId: shouldCopyFiltersAndSortsAndAggregate + ? sourceView.mainGroupByFieldMetadataId + : mainGroupByFieldMetadataId, + type: convertViewTypeToCore(viewType), + objectMetadataId: sourceView.objectMetadataId, + openRecordIn: convertViewOpenRecordInToCore( + sourceView.openRecordIn, + ), + anyFieldFilterValue: anyFieldFilterValue, + calendarLayout: + viewType === ViewType.Calendar + ? ViewCalendarLayout.MONTH + : undefined, + calendarFieldMetadataId: + viewType === ViewType.Calendar + ? calendarFieldMetadataId + : undefined, + visibility, + }, }, - }); + objectMetadataItem.id, + ); if (result.status === 'failed') { - set(isPersistingViewFieldsState, false); return undefined; } const newViewId = result.response.data?.createCoreView.id; if (isUndefinedOrNull(newViewId)) { - set(isPersistingViewFieldsState, false); throw new Error('Failed to create view'); } @@ -181,60 +171,9 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { }); if (fieldResult.status === 'failed') { - set(isPersistingViewFieldsState, false); return undefined; } - if (type === ViewType.Kanban) { - if ( - !isDefined(mainGroupByFieldMetadataId) || - mainGroupByFieldMetadataId === '' - ) { - throw new Error('Kanban view must have a kanban field'); - } - - const viewGroupsToCreate = - objectMetadataItem.fields - ?.find((field) => field.id === mainGroupByFieldMetadataId) - ?.options?.map( - (option, index) => - ({ - id: v4(), - __typename: 'ViewGroup', - fieldValue: option.value, - isVisible: true, - position: index, - }) satisfies ViewGroup, - ) ?? []; - - if ( - objectMetadataItem.fields.find( - (field) => field.id === mainGroupByFieldMetadataId, - )?.isNullable === true - ) { - viewGroupsToCreate.push({ - __typename: 'ViewGroup', - id: v4(), - fieldValue: '', - position: viewGroupsToCreate.length, - isVisible: true, - } satisfies ViewGroup); - } - - triggerViewGroupOptimisticEffect({ - createdViewGroups: viewGroupsToCreate.map( - ({ __typename, ...viewGroup }) => - ({ - ...viewGroup, - viewId: newViewId, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - deletedAt: null, - }) as Omit, - ), - }); - } - if (shouldCopyFiltersAndSortsAndAggregate === true) { const viewFilterGroupsToCopy = currentRecordFilterGroups.map( (recordFilterGroup) => @@ -285,7 +224,6 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { const filterResult = await createViewFilters(createViewFilterInputs); if (filterResult.status === 'failed') { - set(isPersistingViewFieldsState, false); return undefined; } @@ -294,24 +232,21 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id); - set(isPersistingViewFieldsState, false); return newViewId; }, [ currentViewIdCallbackState, - createViewFields, createView, anyFieldFilterValue, - objectMetadataItem.fields, - objectMetadataItem.id, + objectMetadataItem, + createViewFields, + refreshCoreViewsByObjectMetadataId, currentRecordFilterGroups, currentRecordFilters, currentRecordSorts, createViewFilterGroups, createViewFilters, createViewSorts, - refreshCoreViewsByObjectMetadataId, - triggerViewGroupOptimisticEffect, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts index 521685e23e..261b7c3be6 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts @@ -5,7 +5,6 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component- import { usePersistViewField } from '@/views/hooks/internal/usePersistViewField'; import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges'; import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState'; -import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState'; import { type ViewField } from '@/views/types/ViewField'; import { type CreateViewFieldInput, @@ -25,7 +24,7 @@ export const useSaveCurrentViewFields = () => { ); const saveViewFields = useRecoilCallback( - ({ set, snapshot }) => + ({ snapshot }) => async (viewFieldsToSave: Omit[]) => { if (!canPersistChanges) { return; @@ -39,8 +38,6 @@ export const useSaveCurrentViewFields = () => { return; } - set(isPersistingViewFieldsState, true); - const view = getViewFromPrefetchState(currentViewId); if (isUndefinedOrNull(view)) { @@ -127,8 +124,6 @@ export const useSaveCurrentViewFields = () => { createViewFields({ inputs: viewFieldsToCreate }), updateViewFields(viewFieldsToUpdate), ]); - - set(isPersistingViewFieldsState, false); }, [ canPersistChanges, diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts index fbba95e40a..c7ee2ef48d 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts @@ -12,7 +12,7 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useSaveCurrentViewGroups = () => { const { canPersistChanges } = useCanPersistViewChanges(); - const { createViewGroups, updateViewGroups } = usePersistViewGroupRecords(); + const { updateViewGroups } = usePersistViewGroupRecords(); const { getViewFromPrefetchState } = useGetViewFromPrefetchState(); @@ -150,33 +150,14 @@ export const useSaveCurrentViewGroups = () => { }) .filter(isDefined); - const viewGroupsToCreate = viewGroupsToSave.filter( - (viewFieldToSave) => - !currentViewGroups.some( - (currentViewGroup) => - currentViewGroup.fieldValue === viewFieldToSave.fieldValue, - ), - ); - if (!isDefined(view.mainGroupByFieldMetadataId)) { throw new Error('mainGroupByFieldMetadataId is required'); } - await Promise.all([ - createViewGroups({ - createCoreViewGroupInputs: { - inputs: viewGroupsToCreate.map((viewGroupToCreate) => ({ - ...viewGroupToCreate, - viewId: view.id, - })), - }, - }), - updateViewGroups(viewGroupsToUpdate), - ]); + await updateViewGroups(viewGroupsToUpdate); }, [ canPersistChanges, - createViewGroups, currentViewIdCallbackState, getViewFromPrefetchState, updateViewGroups, diff --git a/packages/twenty-front/src/modules/views/hooks/useUpdateCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useUpdateCurrentView.ts index 3f9fe9ead6..2384114ff5 100644 --- a/packages/twenty-front/src/modules/views/hooks/useUpdateCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useUpdateCurrentView.ts @@ -1,13 +1,21 @@ -import { useRecoilCallback } from 'recoil'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; +import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates'; +import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector'; import { type GraphQLView } from '@/views/types/GraphQLView'; +import { type View } from '@/views/types/View'; +import { type ViewGroup } from '@/views/types/ViewGroup'; +import { type ViewType } from '@/views/types/ViewType'; import { convertUpdateViewInputToCore } from '@/views/utils/convertUpdateViewInputToCore'; +import { useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; import { useUpdateCoreViewMutation } from '~/generated-metadata/graphql'; export const useUpdateCurrentView = () => { @@ -15,15 +23,70 @@ export const useUpdateCurrentView = () => { const currentViewIdCallbackState = useRecoilComponentCallbackState( contextStoreCurrentViewIdComponentState, ); + const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); + const { loadRecordIndexStates } = useLoadRecordIndexStates(); + const setRecordIndexViewType = useSetRecoilState(recordIndexViewTypeState); + const [updateOneCoreView] = useUpdateCoreViewMutation(); const { refreshCoreViewsByObjectMetadataId } = useRefreshCoreViewsByObjectMetadataId(); - const [updateOneCoreView] = useUpdateCoreViewMutation(); + const getViewGroupsToCreateAtViewUpdate = useMemo(() => { + return ({ + existingView, + newMainGroupByFieldMetadataId, + }: { + existingView: View; + newMainGroupByFieldMetadataId?: string | null; + }) => { + if (newMainGroupByFieldMetadataId === undefined) { + return {}; + } + + let viewGroupsToCreate: ViewGroup[] = []; + + if ( + newMainGroupByFieldMetadataId !== + existingView.mainGroupByFieldMetadataId + ) { + if (newMainGroupByFieldMetadataId !== null) { + viewGroupsToCreate = + objectMetadataItem.fields + ?.find((field) => field.id === newMainGroupByFieldMetadataId) + ?.options?.map( + (option, index) => + ({ + id: v4(), + __typename: 'ViewGroup', + fieldValue: option.value, + isVisible: true, + position: index, + }) satisfies ViewGroup, + ) ?? []; + + if ( + objectMetadataItem.fields.find( + (field) => field.id === newMainGroupByFieldMetadataId, + )?.isNullable === true + ) { + viewGroupsToCreate.push({ + __typename: 'ViewGroup', + id: v4(), + fieldValue: '', + position: viewGroupsToCreate.length, + isVisible: true, + } satisfies ViewGroup); + } + } + } + + return { viewGroupsToCreate }; + }; + }, [objectMetadataItem.fields]); const updateCurrentView = useRecoilCallback( ({ snapshot }) => - async (view: Partial) => { + async (view: Partial & { type?: ViewType }) => { if (!canPersistChanges) { return; } @@ -53,6 +116,31 @@ export const useUpdateCurrentView = () => { input, }, }); + + if ( + input.mainGroupByFieldMetadataId !== undefined && + currentView.mainGroupByFieldMetadataId !== + input.mainGroupByFieldMetadataId + ) { + const { viewGroupsToCreate } = getViewGroupsToCreateAtViewUpdate({ + existingView: currentView, + newMainGroupByFieldMetadataId: input.mainGroupByFieldMetadataId, + }); + + loadRecordIndexStates( + { + ...currentView, + mainGroupByFieldMetadataId: input.mainGroupByFieldMetadataId, + viewGroups: viewGroupsToCreate ?? [], + }, + objectMetadataItem, + ); + } + + if (isDefined(view.type)) { + setRecordIndexViewType(view.type); + } + await refreshCoreViewsByObjectMetadataId( currentView.objectMetadataId, ); @@ -61,7 +149,11 @@ export const useUpdateCurrentView = () => { [ canPersistChanges, currentViewIdCallbackState, + getViewGroupsToCreateAtViewUpdate, + loadRecordIndexStates, + objectMetadataItem, refreshCoreViewsByObjectMetadataId, + setRecordIndexViewType, updateOneCoreView, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useViewsSideEffectsOnViewGroups.ts b/packages/twenty-front/src/modules/views/hooks/useViewsSideEffectsOnViewGroups.ts new file mode 100644 index 0000000000..db0ae3b00b --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useViewsSideEffectsOnViewGroups.ts @@ -0,0 +1,87 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect'; +import { type ViewGroup } from '@/views/types/ViewGroup'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; +import { type CoreViewGroup } from '~/generated/graphql'; + +const useViewsSideEffectsOnViewGroups = () => { + const { triggerViewGroupOptimisticEffect } = + useTriggerViewGroupOptimisticEffect(); + + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const triggerViewGroupOptimisticEffectAtViewCreation = ({ + newViewId, + mainGroupByFieldMetadataId, + objectMetadataItemId, + }: { + newViewId: string; + mainGroupByFieldMetadataId?: string | null; + objectMetadataItemId: string; + }) => { + if (!isDefined(mainGroupByFieldMetadataId)) { + return {}; + } + + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.id === objectMetadataItemId, + ); + + if (!isDefined(objectMetadataItem)) { + throw new Error('Object metadata item not found'); + } + + let viewGroupsToCreate: ViewGroup[] = []; + + viewGroupsToCreate = + objectMetadataItem.fields + ?.find((field) => field.id === mainGroupByFieldMetadataId) + ?.options?.map( + (option, index) => + ({ + id: v4(), + __typename: 'ViewGroup', + fieldValue: option.value, + isVisible: true, + position: index, + }) satisfies ViewGroup, + ) ?? []; + + if ( + objectMetadataItem.fields.find( + (field) => field.id === mainGroupByFieldMetadataId, + )?.isNullable === true + ) { + viewGroupsToCreate.push({ + __typename: 'ViewGroup', + id: v4(), + fieldValue: '', + position: viewGroupsToCreate.length, + isVisible: true, + } satisfies ViewGroup); + } + + triggerViewGroupOptimisticEffect({ + createdViewGroups: viewGroupsToCreate.map( + ({ __typename, ...viewGroup }) => + ({ + ...viewGroup, + viewId: newViewId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + deletedAt: null, + }) as Omit, + ), + }); + + return { viewGroupsToCreate }; + }; + + return { + triggerViewGroupOptimisticEffectAtViewCreation, + }; +}; + +export { useViewsSideEffectsOnViewGroups }; diff --git a/packages/twenty-front/src/modules/views/states/isPersistingViewFieldsState.ts b/packages/twenty-front/src/modules/views/states/isPersistingViewFieldsState.ts deleted file mode 100644 index 5d411b6675..0000000000 --- a/packages/twenty-front/src/modules/views/states/isPersistingViewFieldsState.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; -export const isPersistingViewFieldsState = createState({ - key: 'isPersistingViewFieldsState', - defaultValue: false, -}); diff --git a/packages/twenty-front/src/modules/views/utils/convertUpdateViewInputToCore.ts b/packages/twenty-front/src/modules/views/utils/convertUpdateViewInputToCore.ts index 87dc96b497..4bac580f7f 100644 --- a/packages/twenty-front/src/modules/views/utils/convertUpdateViewInputToCore.ts +++ b/packages/twenty-front/src/modules/views/utils/convertUpdateViewInputToCore.ts @@ -46,5 +46,8 @@ export const convertUpdateViewInputToCore = ( calendarFieldMetadataId: view.calendarFieldMetadataId, }), ...(isDefined(view.visibility) && { visibility: view.visibility }), + ...(view.mainGroupByFieldMetadataId !== undefined && { + mainGroupByFieldMetadataId: view.mainGroupByFieldMetadataId, + }), }; };