From 8212606043d0dd17520ddbd93bda8f341a328130 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 24 Nov 2023 16:32:59 +0100 Subject: [PATCH] Fix views (#2701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Fix viewsé --- .../hooks/useObjectMetadataItem.ts | 12 +- .../hooks/useFindManyObjectRecords.ts | 2 +- .../hooks/useUpdateOneObjectRecord.ts | 15 +- .../utils/useGenerateCacheFragment.ts | 29 --- .../utils/useGetRecordFromCache.ts | 43 ++++ .../utils/useModifyRecordFromCache.ts | 31 +++ .../modules/people/components/PeopleCard.tsx | 2 +- .../ui/object/field/hooks/usePersistField.ts | 1 - .../meta-types/hooks/useCurrencyField.ts | 5 + .../AddObjectFilterFromDetailsButton.tsx | 4 + .../components/MultipleFiltersButton.tsx | 16 +- ...ObjectFilterDropdownEntitySearchSelect.tsx | 2 +- .../object-filter-dropdown/hooks/useFilter.ts | 2 + .../TableOptionsDropdownContent.tsx | 1 - .../views/components/ViewBarEffect.tsx | 178 ++--------------- .../views/components/ViewsDropdownButton.tsx | 7 +- .../views/hooks/internal/useViewFields.ts | 61 ++++-- .../views/hooks/internal/useViewFilters.ts | 42 +++- .../views/hooks/internal/useViewSorts.ts | 28 ++- .../modules/views/hooks/internal/useViews.ts | 12 +- front/src/modules/views/hooks/useView.ts | 188 ++++++++++++++++-- .../selectors/currentViewScopedSelector.ts | 30 ++- .../selectors/viewsByIdScopedSelector.ts | 11 +- .../modules/views/states/viewsScopedState.ts | 5 +- 24 files changed, 428 insertions(+), 299 deletions(-) delete mode 100644 front/src/modules/object-record/utils/useGenerateCacheFragment.ts create mode 100644 front/src/modules/object-record/utils/useGetRecordFromCache.ts create mode 100644 front/src/modules/object-record/utils/useModifyRecordFromCache.ts diff --git a/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index d75a13bfb..8466d4b6f 100644 --- a/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -3,11 +3,12 @@ import { useRecoilValue } from 'recoil'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { useGenerateCreateOneObjectMutation } from '@/object-record/utils/generateCreateOneObjectMutation'; -import { useGenerateCacheFragment } from '@/object-record/utils/useGenerateCacheFragment'; import { useGenerateDeleteOneObjectMutation } from '@/object-record/utils/useGenerateDeleteOneObjectMutation'; import { useGenerateFindManyCustomObjectsQuery } from '@/object-record/utils/useGenerateFindManyCustomObjectsQuery'; import { useGenerateFindOneCustomObjectQuery } from '@/object-record/utils/useGenerateFindOneCustomObjectQuery'; import { useGenerateUpdateOneObjectMutation } from '@/object-record/utils/useGenerateUpdateOneObjectMutation'; +import { useGetRecordFromCache } from '@/object-record/utils/useGetRecordFromCache'; +import { useModifyRecordFromCache } from '@/object-record/utils/useModifyRecordFromCache'; import { isDefined } from '~/utils/isDefined'; import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier'; @@ -37,7 +38,11 @@ export const useObjectMetadataItem = ( const objectNotFoundInMetadata = !isDefined(objectMetadataItem); - const cacheFragment = useGenerateCacheFragment({ + const getRecordFromCache = useGetRecordFromCache({ + objectMetadataItem, + }); + + const modifyRecordFromCache = useModifyRecordFromCache({ objectMetadataItem, }); @@ -74,7 +79,8 @@ export const useObjectMetadataItem = ( basePathToShowPage, objectMetadataItem, objectNotFoundInMetadata, - cacheFragment, + getRecordFromCache, + modifyRecordFromCache, findManyQuery, findOneQuery, createOneMutation, diff --git a/front/src/modules/object-record/hooks/useFindManyObjectRecords.ts b/front/src/modules/object-record/hooks/useFindManyObjectRecords.ts index 2b9f495be..19836e7ea 100644 --- a/front/src/modules/object-record/hooks/useFindManyObjectRecords.ts +++ b/front/src/modules/object-record/hooks/useFindManyObjectRecords.ts @@ -41,7 +41,7 @@ export const useFindManyObjectRecords = < skip?: boolean; }) => { const findManyQueryStateIdentifier = - objectNamePlural + JSON.stringify(filter); + objectNamePlural + JSON.stringify(filter) + JSON.stringify(orderBy) + limit; const [lastCursor, setLastCursor] = useRecoilState( cursorFamilyState(findManyQueryStateIdentifier), diff --git a/front/src/modules/object-record/hooks/useUpdateOneObjectRecord.ts b/front/src/modules/object-record/hooks/useUpdateOneObjectRecord.ts index 8d39d1e44..d3fb5dbd0 100644 --- a/front/src/modules/object-record/hooks/useUpdateOneObjectRecord.ts +++ b/front/src/modules/object-record/hooks/useUpdateOneObjectRecord.ts @@ -1,4 +1,4 @@ -import { useApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -12,14 +12,12 @@ export const useUpdateOneObjectRecord = ({ objectMetadataItem: foundObjectMetadataItem, objectNotFoundInMetadata, updateOneMutation, - cacheFragment, + getRecordFromCache, findManyQuery, } = useObjectMetadataItem({ objectNameSingular, }); - const { cache } = useApolloClient(); - // TODO: type this with a minimal type at least with Record const [mutate] = useMutation(updateOneMutation); @@ -36,14 +34,7 @@ export const useUpdateOneObjectRecord = ({ return null; } - const cachedRecordId = cache.identify({ - __typename: capitalize(foundObjectMetadataItem?.nameSingular ?? ''), - id: idToUpdate, - }); - const cachedRecord = cache.readFragment({ - id: cachedRecordId, - fragment: cacheFragment, - }); + const cachedRecord = getRecordFromCache(idToUpdate); const updatedObject = await mutate({ variables: { diff --git a/front/src/modules/object-record/utils/useGenerateCacheFragment.ts b/front/src/modules/object-record/utils/useGenerateCacheFragment.ts deleted file mode 100644 index 3f88fe78c..000000000 --- a/front/src/modules/object-record/utils/useGenerateCacheFragment.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { gql } from '@apollo/client'; - -import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery'; -import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { capitalize } from '~/utils/string/capitalize'; - -export const useGenerateCacheFragment = ({ - objectMetadataItem, -}: { - objectMetadataItem: ObjectMetadataItem | undefined | null; -}) => { - const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery(); - - if (!objectMetadataItem) { - return EMPTY_MUTATION; - } - - const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular); - - return gql` - fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} { - id - ${objectMetadataItem.fields - .map((field) => mapFieldMetadataToGraphQLQuery(field)) - .join('\n')} - } -`; -}; diff --git a/front/src/modules/object-record/utils/useGetRecordFromCache.ts b/front/src/modules/object-record/utils/useGetRecordFromCache.ts new file mode 100644 index 000000000..80283c681 --- /dev/null +++ b/front/src/modules/object-record/utils/useGetRecordFromCache.ts @@ -0,0 +1,43 @@ +import { gql, useApolloClient } from '@apollo/client'; + +import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery'; +import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { capitalize } from '~/utils/string/capitalize'; + +export const useGetRecordFromCache = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem | undefined | null; +}) => { + const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery(); + const apolloClient = useApolloClient(); + + return (recordId: string) => { + if (!objectMetadataItem) { + return EMPTY_MUTATION; + } + + const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular); + + const cacheReadFragment = gql` + fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} { + id + ${objectMetadataItem.fields + .map((field) => mapFieldMetadataToGraphQLQuery(field)) + .join('\n')} + } + `; + + const cache = apolloClient.cache; + const cachedRecordId = cache.identify({ + __typename: capitalize(objectMetadataItem?.nameSingular ?? ''), + id: recordId, + }); + + return cache.readFragment({ + id: cachedRecordId, + fragment: cacheReadFragment, + }); + }; +}; diff --git a/front/src/modules/object-record/utils/useModifyRecordFromCache.ts b/front/src/modules/object-record/utils/useModifyRecordFromCache.ts new file mode 100644 index 000000000..3ac695e4f --- /dev/null +++ b/front/src/modules/object-record/utils/useModifyRecordFromCache.ts @@ -0,0 +1,31 @@ +import { useApolloClient } from '@apollo/client'; +import { Modifiers } from '@apollo/client/cache'; + +import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { capitalize } from '~/utils/string/capitalize'; + +export const useModifyRecordFromCache = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem | undefined | null; +}) => { + const apolloClient = useApolloClient(); + + return (recordId: string, fieldModifiers: Modifiers) => { + if (!objectMetadataItem) { + return EMPTY_MUTATION; + } + + const cache = apolloClient.cache; + const cachedRecordId = cache.identify({ + __typename: capitalize(objectMetadataItem?.nameSingular ?? ''), + id: recordId, + }); + + cache.modify({ + id: cachedRecordId, + fields: fieldModifiers, + }); + }; +}; diff --git a/front/src/modules/people/components/PeopleCard.tsx b/front/src/modules/people/components/PeopleCard.tsx index b1efb6d95..3615eefc2 100644 --- a/front/src/modules/people/components/PeopleCard.tsx +++ b/front/src/modules/people/components/PeopleCard.tsx @@ -142,7 +142,7 @@ export const PeopleCard = ({ isHovered={isHovered} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} - onClick={() => navigate(`/person/${person.id}`)} + onClick={() => navigate(`/object/person/${person.id}`)} hasBottomBorder={hasBottomBorder} > { fieldIsFullName ) { const fieldName = fieldDefinition.metadata.fieldName; - set( entityFieldsFamilySelector({ entityId, fieldName }), valueToPersist, diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useCurrencyField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useCurrencyField.ts index 4cef066fe..dee999ffd 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useCurrencyField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useCurrencyField.ts @@ -30,6 +30,11 @@ const initializeValue = ( currencyCode: 'USD', }; } + + if (!fieldValue) { + return { amount: null, currencyCode: 'USD' }; + } + return { amount: convertCurrencyMicrosToCurrency(fieldValue.amountMicros), currencyCode: fieldValue.currencyCode, diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx index 6c9542741..0bc9d68a1 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx @@ -1,6 +1,7 @@ import { IconPlus } from '@/ui/display/icon'; import { LightButton } from '@/ui/input/button/components/LightButton'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useFilter } from '@/ui/object/object-filter-dropdown/hooks/useFilter'; import { ObjectFilterDropdownId } from '../constants/ObjectFilterDropdownId'; @@ -9,7 +10,10 @@ export const AddObjectFilterFromDetailsButton = () => { dropdownScopeId: ObjectFilterDropdownId, }); + const { resetFilter } = useFilter(); + const handleClick = () => { + resetFilter(); toggleDropdown(); }; diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersButton.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersButton.tsx index c5927d4e3..ea3942a33 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersButton.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersButton.tsx @@ -5,27 +5,15 @@ import { ObjectFilterDropdownId } from '../constants/ObjectFilterDropdownId'; import { useFilter } from '../hooks/useFilter'; export const MultipleFiltersButton = () => { - const { - setFilterDefinitionUsedInDropdown, - setIsObjectFilterDropdownOperandSelectUnfolded, - setObjectFilterDropdownSearchInput, - setSelectedOperandInDropdown, - } = useFilter(); + const { resetFilter } = useFilter(); const { isDropdownOpen, toggleDropdown } = useDropdown({ dropdownScopeId: ObjectFilterDropdownId, }); - const resetState = () => { - setIsObjectFilterDropdownOperandSelectUnfolded(false); - setFilterDefinitionUsedInDropdown(null); - setSelectedOperandInDropdown(null); - setObjectFilterDropdownSearchInput(''); - }; - const handleClick = () => { toggleDropdown(); - resetState(); + resetFilter(); }; return ( diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx index d93bb0d4d..d493be823 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx @@ -42,7 +42,6 @@ export const ObjectFilterDropdownEntitySearchSelect = ({ } setObjectFilterDropdownSelectedEntityId(selectedEntity.id); - closeDropdown(); selectFilter?.({ displayValue: selectedEntity.name, @@ -52,6 +51,7 @@ export const ObjectFilterDropdownEntitySearchSelect = ({ displayAvatarUrl: selectedEntity.avatarUrl, definition: filterDefinitionUsedInDropdown, }); + closeDropdown(); }; const isAllEntitySelectShown = diff --git a/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilter.ts b/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilter.ts index 7ba1d7f8c..c920e61d4 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilter.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilter.ts @@ -52,8 +52,10 @@ export const useFilter = (props?: UseFilterProps) => { setObjectFilterDropdownSearchInput(''); setObjectFilterDropdownSelectedEntityId(null); setSelectedFilter(undefined); + setFilterDefinitionUsedInDropdown(null); setSelectedOperandInDropdown(null); }, [ + setFilterDefinitionUsedInDropdown, setObjectFilterDropdownSearchInput, setObjectFilterDropdownSelectedEntityId, setSelectedFilter, diff --git a/front/src/modules/ui/object/record-table/options/components/TableOptionsDropdownContent.tsx b/front/src/modules/ui/object/record-table/options/components/TableOptionsDropdownContent.tsx index d9c560227..fdadf2c79 100644 --- a/front/src/modules/ui/object/record-table/options/components/TableOptionsDropdownContent.tsx +++ b/front/src/modules/ui/object/record-table/options/components/TableOptionsDropdownContent.tsx @@ -31,7 +31,6 @@ export const TableOptionsDropdownContent = ({ const viewEditMode = useRecoilValue(viewEditModeState); const currentView = useRecoilValue(currentViewSelector); - const { closeDropdown } = useDropdown(); const [currentMenu, setCurrentMenu] = useState( diff --git a/front/src/modules/views/components/ViewBarEffect.tsx b/front/src/modules/views/components/ViewBarEffect.tsx index fdf94eea3..da0ba0fa4 100644 --- a/front/src/modules/views/components/ViewBarEffect.tsx +++ b/front/src/modules/views/components/ViewBarEffect.tsx @@ -6,19 +6,21 @@ import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjec import { PaginatedObjectTypeResults } from '@/object-record/types/PaginatedObjectTypeResults'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { GraphQLView } from '@/views/types/GraphQLView'; -import { assertNotNull } from '~/utils/assert'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { useViewScopedStates } from '../hooks/internal/useViewScopedStates'; import { useView } from '../hooks/useView'; -import { ViewField } from '../types/ViewField'; -import { ViewFilter } from '../types/ViewFilter'; -import { ViewSort } from '../types/ViewSort'; import { getViewScopedStatesFromSnapshot } from '../utils/getViewScopedStatesFromSnapshot'; -import { getViewScopedStateValuesFromSnapshot } from '../utils/getViewScopedStateValuesFromSnapshot'; export const ViewBarEffect = () => { - const { scopeId: viewScopeId, loadView, changeViewInUrl } = useView(); + const { + scopeId: viewScopeId, + loadView, + changeViewInUrl, + loadViewFields, + loadViewFilters, + loadViewSorts, + } = useView(); const [searchParams] = useSearchParams(); const currentViewIdFromUrl = searchParams.get('view'); @@ -38,11 +40,7 @@ export const ViewBarEffect = () => { onCompleted: useRecoilCallback( ({ snapshot, set }) => async (data: PaginatedObjectTypeResults) => { - const nextViews = data.edges.map((view) => ({ - id: view.node.id, - name: view.node.name, - objectMetadataId: view.node.objectMetadataId, - })); + const nextViews = data.edges.map(({ node }) => node); const { viewsState, currentViewIdState } = getViewScopedStatesFromSnapshot({ @@ -52,7 +50,9 @@ export const ViewBarEffect = () => { const views = getSnapshotValue(snapshot, viewsState); - if (!isDeeplyEqual(views, nextViews)) set(viewsState, nextViews); + if (!isDeeplyEqual(views, nextViews)) { + set(viewsState, nextViews); + } const currentView = data.edges @@ -66,9 +66,9 @@ export const ViewBarEffect = () => { set(currentViewIdState, currentView.id); if (currentView?.viewFields) { - updateViewFields(currentView.viewFields, currentView.id); - updateViewFilters(currentView.viewFilters, currentView.id); - updateViewSorts(currentView.viewSorts, currentView.id); + loadViewFields(currentView.viewFields, currentView.id); + loadViewFilters(currentView.viewFilters, currentView.id); + loadViewSorts(currentView.viewSorts, currentView.id); } if (!nextViews.length) return; @@ -77,154 +77,6 @@ export const ViewBarEffect = () => { ), }); - const updateViewFields = useRecoilCallback( - ({ snapshot, set }) => - async ( - data: PaginatedObjectTypeResults, - currentViewId: string, - ) => { - const { - availableFieldDefinitions, - onViewFieldsChange, - savedViewFields, - isPersistingView, - } = getViewScopedStateValuesFromSnapshot({ - snapshot, - viewScopeId, - viewId: currentViewId, - }); - - const { savedViewFieldsState, currentViewFieldsState } = - getViewScopedStatesFromSnapshot({ - snapshot, - viewScopeId, - viewId: currentViewId, - }); - - if (!availableFieldDefinitions) { - return; - } - - const queriedViewFields = data.edges - .map((viewField) => viewField.node) - .filter(assertNotNull); - - if (isPersistingView) { - return; - } - - if (!isDeeplyEqual(savedViewFields, queriedViewFields)) { - set(currentViewFieldsState, queriedViewFields); - set(savedViewFieldsState, queriedViewFields); - onViewFieldsChange?.(queriedViewFields); - } - }, - [viewScopeId], - ); - - const updateViewFilters = useRecoilCallback( - ({ snapshot, set }) => - async ( - data: PaginatedObjectTypeResults>, - currentViewId: string, - ) => { - const { - availableFilterDefinitions, - savedViewFilters, - onViewFiltersChange, - } = getViewScopedStateValuesFromSnapshot({ - snapshot, - viewScopeId, - viewId: currentViewId, - }); - - const { savedViewFiltersState, currentViewFiltersState } = - getViewScopedStatesFromSnapshot({ - snapshot, - viewScopeId, - viewId: currentViewId, - }); - - if (!availableFilterDefinitions) { - return; - } - - const queriedViewFilters = data.edges - .map(({ node }) => { - const availableFilterDefinition = availableFilterDefinitions.find( - (filterDefinition) => - filterDefinition.fieldMetadataId === node.fieldMetadataId, - ); - - if (!availableFilterDefinition) return null; - - return { - ...node, - displayValue: node.displayValue ?? node.value, - definition: availableFilterDefinition, - }; - }) - .filter(assertNotNull); - - if (!isDeeplyEqual(savedViewFilters, queriedViewFilters)) { - set(savedViewFiltersState, queriedViewFilters); - set(currentViewFiltersState, queriedViewFilters); - onViewFiltersChange?.(queriedViewFilters); - } - }, - [viewScopeId], - ); - - const updateViewSorts = useRecoilCallback( - ({ snapshot, set }) => - async ( - data: PaginatedObjectTypeResults>, - currentViewId: string, - ) => { - const { availableSortDefinitions, savedViewSorts, onViewSortsChange } = - getViewScopedStateValuesFromSnapshot({ - snapshot, - viewScopeId, - viewId: currentViewId, - }); - - const { savedViewSortsState, currentViewSortsState } = - getViewScopedStatesFromSnapshot({ - snapshot, - viewScopeId, - viewId: currentViewId, - }); - - if (!availableSortDefinitions || !currentViewId) { - return; - } - - const queriedViewSorts = data.edges - .map(({ node }) => { - const availableSortDefinition = availableSortDefinitions.find( - (sort) => sort.fieldMetadataId === node.fieldMetadataId, - ); - - if (!availableSortDefinition) return null; - - return { - id: node.id, - fieldMetadataId: node.fieldMetadataId, - direction: node.direction, - definition: availableSortDefinition, - }; - }) - .filter(assertNotNull); - - if (!isDeeplyEqual(savedViewSorts, queriedViewSorts)) { - set(savedViewSortsState, queriedViewSorts); - set(currentViewSortsState, queriedViewSorts); - onViewSortsChange?.(queriedViewSorts); - } - }, - [viewScopeId], - ); - useEffect(() => { if (!currentViewIdFromUrl) return; diff --git a/front/src/modules/views/components/ViewsDropdownButton.tsx b/front/src/modules/views/components/ViewsDropdownButton.tsx index 72b223cdd..36a80548d 100644 --- a/front/src/modules/views/components/ViewsDropdownButton.tsx +++ b/front/src/modules/views/components/ViewsDropdownButton.tsx @@ -79,7 +79,7 @@ export const ViewsDropdownButton = ({ entityCountInCurrentViewState, ); - const { setViewEditMode } = useView(); + const { setViewEditMode, setCurrentViewId, loadView } = useView(); const { isDropdownOpen: isViewsDropdownOpen, @@ -95,10 +95,10 @@ export const ViewsDropdownButton = ({ const handleViewSelect = useRecoilCallback( () => async (viewId: string) => { changeViewInUrl(viewId); - + loadView(viewId); closeViewsDropdown(); }, - [changeViewInUrl, closeViewsDropdown], + [changeViewInUrl, closeViewsDropdown, loadView], ); const handleAddViewButtonClick = () => { @@ -114,6 +114,7 @@ export const ViewsDropdownButton = ({ ) => { event.stopPropagation(); changeViewInUrl(viewId); + setCurrentViewId(viewId); setViewEditMode('edit'); onViewEditModeChange?.(); closeViewsDropdown(); diff --git a/front/src/modules/views/hooks/internal/useViewFields.ts b/front/src/modules/views/hooks/internal/useViewFields.ts index e96771477..3a4264f53 100644 --- a/front/src/modules/views/hooks/internal/useViewFields.ts +++ b/front/src/modules/views/hooks/internal/useViewFields.ts @@ -1,5 +1,4 @@ import { useApolloClient } from '@apollo/client'; -import { getOperationName } from '@apollo/client/utilities'; import { useRecoilCallback } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -12,7 +11,7 @@ export const useViewFields = (viewScopeId: string) => { objectNameSingular: 'viewField', }); - const { findManyQuery: findManyViewsQuery } = useObjectMetadataItem({ + const { modifyRecordFromCache } = useObjectMetadataItem({ objectNameSingular: 'view', }); @@ -21,14 +20,23 @@ export const useViewFields = (viewScopeId: string) => { const persistViewFields = useRecoilCallback( ({ snapshot, set }) => async (viewFieldsToPersist: ViewField[], viewId?: string) => { - const { viewObjectMetadataId, currentViewId, savedViewFieldsByKey } = - getViewScopedStateValuesFromSnapshot({ - snapshot, - viewScopeId, - viewId, - }); + const { + viewObjectMetadataId, + currentViewId, + savedViewFieldsByKey, + onViewFieldsChange, + views, + } = getViewScopedStateValuesFromSnapshot({ + snapshot, + viewScopeId, + viewId, + }); - const { isPersistingViewState } = getViewScopedStatesFromSnapshot({ + const { + isPersistingViewState, + currentViewFieldsState, + savedViewFieldsState, + } = getViewScopedStatesFromSnapshot({ snapshot, viewScopeId, viewId, @@ -58,9 +66,6 @@ export const useViewFields = (viewScopeId: string) => { position: viewField.position, }, }, - // TODO: implement optimistic response - refetchQueries: [getOperationName(findManyViewsQuery) ?? ''], - awaitRefetchQueries: true, }), ), ); @@ -83,9 +88,6 @@ export const useViewFields = (viewScopeId: string) => { position: viewField.position, }, }, - // TODO: implement optimistic response - refetchQueries: [getOperationName(findManyViewsQuery) ?? ''], - awaitRefetchQueries: true, }), ), ); @@ -113,13 +115,38 @@ export const useViewFields = (viewScopeId: string) => { await _updateViewFields(viewFieldsToUpdate); set(isPersistingViewState, false); + set(currentViewFieldsState, viewFieldsToPersist); + set(savedViewFieldsState, viewFieldsToPersist); + + const existingView = views.find((view) => view.id === viewIdToPersist); + + if (!existingView) { + return; + } + + modifyRecordFromCache(viewIdToPersist ?? '', { + viewFields: () => ({ + edges: viewFieldsToPersist.map((viewField) => ({ + node: viewField, + cursor: '', + })), + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', + }, + }), + }); + + onViewFieldsChange?.(viewFieldsToPersist); }, [ + viewScopeId, + modifyRecordFromCache, apolloClient, createOneMutation, updateOneMutation, - viewScopeId, - findManyViewsQuery, ], ); diff --git a/front/src/modules/views/hooks/internal/useViewFilters.ts b/front/src/modules/views/hooks/internal/useViewFilters.ts index 9dc152ca7..c564154bb 100644 --- a/front/src/modules/views/hooks/internal/useViewFilters.ts +++ b/front/src/modules/views/hooks/internal/useViewFilters.ts @@ -15,6 +15,11 @@ export const useViewFilters = (viewScopeId: string) => { useObjectMetadataItem({ objectNameSingular: 'viewFilter', }); + + const { modifyRecordFromCache } = useObjectMetadataItem({ + objectNameSingular: 'view', + }); + const apolloClient = useApolloClient(); const { currentViewFiltersState } = useViewScopedStates({ @@ -24,11 +29,15 @@ export const useViewFilters = (viewScopeId: string) => { const persistViewFilters = useRecoilCallback( ({ snapshot, set }) => async (viewId?: string) => { - const { currentViewId, currentViewFilters, savedViewFiltersByKey } = - getViewScopedStateValuesFromSnapshot({ - snapshot, - viewScopeId, - }); + const { + currentViewId, + currentViewFilters, + savedViewFiltersByKey, + views, + } = getViewScopedStateValuesFromSnapshot({ + snapshot, + viewScopeId, + }); if (!currentViewId) { return; @@ -129,11 +138,34 @@ export const useViewFilters = (viewScopeId: string) => { }), currentViewFilters, ); + + const existingViewId = viewId ?? currentViewId; + const existingView = views.find((view) => view.id === existingViewId); + + if (!existingView) { + return; + } + + modifyRecordFromCache(existingViewId, { + viewFilters: () => ({ + edges: currentViewFilters.map((viewFilter) => ({ + node: viewFilter, + cursor: '', + })), + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', + }, + }), + }); }, [ apolloClient, createOneMutation, deleteOneMutation, + modifyRecordFromCache, updateOneMutation, viewScopeId, ], diff --git a/front/src/modules/views/hooks/internal/useViewSorts.ts b/front/src/modules/views/hooks/internal/useViewSorts.ts index f7e7f1fe9..624d56c36 100644 --- a/front/src/modules/views/hooks/internal/useViewSorts.ts +++ b/front/src/modules/views/hooks/internal/useViewSorts.ts @@ -15,6 +15,10 @@ export const useViewSorts = (viewScopeId: string) => { useObjectMetadataItem({ objectNameSingular: 'viewSort', }); + + const { modifyRecordFromCache } = useObjectMetadataItem({ + objectNameSingular: 'view', + }); const apolloClient = useApolloClient(); const { currentViewSortsState } = useViewScopedStates({ @@ -24,7 +28,7 @@ export const useViewSorts = (viewScopeId: string) => { const persistViewSorts = useRecoilCallback( ({ snapshot, set }) => async (viewId?: string) => { - const { currentViewId, currentViewSorts, savedViewSortsByKey } = + const { currentViewId, currentViewSorts, savedViewSortsByKey, views } = getViewScopedStateValuesFromSnapshot({ snapshot, viewScopeId, @@ -122,11 +126,33 @@ export const useViewSorts = (viewScopeId: string) => { }), currentViewSorts, ); + const existingViewId = viewId ?? currentViewId; + const existingView = views.find((view) => view.id === existingViewId); + + if (!existingView) { + return; + } + + modifyRecordFromCache(existingViewId, { + viewSorts: () => ({ + edges: currentViewSorts.map((viewSort) => ({ + node: viewSort, + cursor: '', + })), + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', + }, + }), + }); }, [ apolloClient, createOneMutation, deleteOneMutation, + modifyRecordFromCache, updateOneMutation, viewScopeId, ], diff --git a/front/src/modules/views/hooks/internal/useViews.ts b/front/src/modules/views/hooks/internal/useViews.ts index 8100c5c40..24c1e46c6 100644 --- a/front/src/modules/views/hooks/internal/useViews.ts +++ b/front/src/modules/views/hooks/internal/useViews.ts @@ -2,7 +2,7 @@ import { useApolloClient } from '@apollo/client'; import { useRecoilCallback } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { View } from '@/views/types/View'; +import { GraphQLView } from '@/views/types/GraphQLView'; import { getViewScopedStateValuesFromSnapshot } from '@/views/utils/getViewScopedStateValuesFromSnapshot'; export const useViews = (scopeId: string) => { @@ -18,7 +18,7 @@ export const useViews = (scopeId: string) => { const createView = useRecoilCallback( ({ snapshot }) => - async (view: Pick) => { + async (view: Pick) => { const { viewObjectMetadataId, viewType } = getViewScopedStateValuesFromSnapshot({ snapshot, @@ -32,7 +32,8 @@ export const useViews = (scopeId: string) => { mutation: createOneMutation, variables: { input: { - ...view, + id: view.id, + name: view.name, objectMetadataId: viewObjectMetadataId, type: viewType, }, @@ -43,13 +44,14 @@ export const useViews = (scopeId: string) => { [scopeId, apolloClient, createOneMutation, findManyQuery], ); - const updateView = async (view: View) => { + const updateView = async (view: GraphQLView) => { await apolloClient.mutate({ mutation: updateOneMutation, variables: { idToUpdate: view.id, input: { - ...view, + id: view.id, + name: view.name, }, }, refetchQueries: [findManyQuery], diff --git a/front/src/modules/views/hooks/useView.ts b/front/src/modules/views/hooks/useView.ts index 62447d0d2..5b0b5c27a 100644 --- a/front/src/modules/views/hooks/useView.ts +++ b/front/src/modules/views/hooks/useView.ts @@ -3,7 +3,13 @@ import { useSearchParams } from 'react-router-dom'; import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; import { v4 } from 'uuid'; +import { PaginatedObjectTypeResults } from '@/object-record/types/PaginatedObjectTypeResults'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; +import { ViewField } from '@/views/types/ViewField'; +import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewSort } from '@/views/types/ViewSort'; +import { assertNotNull } from '~/utils/assert'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { ViewScopeInternalContext } from '../scopes/scope-internal-context/ViewScopeInternalContext'; import { currentViewFieldsScopedFamilyState } from '../states/currentViewFieldsScopedFamilyState'; @@ -85,29 +91,174 @@ export const useView = (props?: UseViewProps) => { [setSearchParams], ); + const loadViewFields = useRecoilCallback( + ({ snapshot, set }) => + async ( + data: PaginatedObjectTypeResults, + currentViewId: string, + ) => { + const { + availableFieldDefinitions, + onViewFieldsChange, + savedViewFields, + isPersistingView, + } = getViewScopedStateValuesFromSnapshot({ + snapshot, + viewScopeId: scopeId, + viewId: currentViewId, + }); + + const { savedViewFieldsState, currentViewFieldsState } = + getViewScopedStatesFromSnapshot({ + snapshot, + viewScopeId: scopeId, + viewId: currentViewId, + }); + + if (!availableFieldDefinitions) { + return; + } + + const queriedViewFields = data.edges + .map((viewField) => viewField.node) + .filter(assertNotNull); + + if (isPersistingView) { + return; + } + + if (!isDeeplyEqual(savedViewFields, queriedViewFields)) { + set(currentViewFieldsState, queriedViewFields); + set(savedViewFieldsState, queriedViewFields); + onViewFieldsChange?.(queriedViewFields); + } + }, + [scopeId], + ); + + const loadViewFilters = useRecoilCallback( + ({ snapshot, set }) => + async ( + data: PaginatedObjectTypeResults>, + currentViewId: string, + ) => { + const { + availableFilterDefinitions, + savedViewFilters, + onViewFiltersChange, + } = getViewScopedStateValuesFromSnapshot({ + snapshot, + viewScopeId: scopeId, + viewId: currentViewId, + }); + + const { savedViewFiltersState, currentViewFiltersState } = + getViewScopedStatesFromSnapshot({ + snapshot, + viewScopeId: scopeId, + viewId: currentViewId, + }); + + if (!availableFilterDefinitions) { + return; + } + + const queriedViewFilters = data.edges + .map(({ node }) => { + const availableFilterDefinition = availableFilterDefinitions.find( + (filterDefinition) => + filterDefinition.fieldMetadataId === node.fieldMetadataId, + ); + + if (!availableFilterDefinition) return null; + + return { + ...node, + displayValue: node.displayValue ?? node.value, + definition: availableFilterDefinition, + }; + }) + .filter(assertNotNull); + + if (!isDeeplyEqual(savedViewFilters, queriedViewFilters)) { + set(savedViewFiltersState, queriedViewFilters); + set(currentViewFiltersState, queriedViewFilters); + } + onViewFiltersChange?.(queriedViewFilters); + }, + [scopeId], + ); + + const loadViewSorts = useRecoilCallback( + ({ snapshot, set }) => + async ( + data: PaginatedObjectTypeResults>, + currentViewId: string, + ) => { + const { availableSortDefinitions, savedViewSorts, onViewSortsChange } = + getViewScopedStateValuesFromSnapshot({ + snapshot, + viewScopeId: scopeId, + viewId: currentViewId, + }); + + const { savedViewSortsState, currentViewSortsState } = + getViewScopedStatesFromSnapshot({ + snapshot, + viewScopeId: scopeId, + viewId: currentViewId, + }); + + if (!availableSortDefinitions || !currentViewId) { + return; + } + + const queriedViewSorts = data.edges + .map(({ node }) => { + const availableSortDefinition = availableSortDefinitions.find( + (sort) => sort.fieldMetadataId === node.fieldMetadataId, + ); + + if (!availableSortDefinition) return null; + + return { + id: node.id, + fieldMetadataId: node.fieldMetadataId, + direction: node.direction, + definition: availableSortDefinition, + }; + }) + .filter(assertNotNull); + + if (!isDeeplyEqual(savedViewSorts, queriedViewSorts)) { + set(savedViewSortsState, queriedViewSorts); + set(currentViewSortsState, queriedViewSorts); + } + onViewSortsChange?.(queriedViewSorts); + }, + [scopeId], + ); + const loadView = useRecoilCallback( ({ snapshot }) => (viewId: string) => { setCurrentViewId?.(viewId); - const { - currentViewFields, - onViewFieldsChange, - currentViewFilters, - onViewFiltersChange, - currentViewSorts, - onViewSortsChange, - } = getViewScopedStateValuesFromSnapshot({ + const { currentView } = getViewScopedStateValuesFromSnapshot({ snapshot, viewScopeId: scopeId, viewId, }); - onViewFieldsChange?.(currentViewFields); - onViewFiltersChange?.(currentViewFilters); - onViewSortsChange?.(currentViewSorts); + if (!currentView) { + return; + } + + loadViewFields(currentView.viewFields, viewId); + loadViewFilters(currentView.viewFilters, viewId); + loadViewSorts(currentView.viewSorts, viewId); }, - [setCurrentViewId, scopeId], + [setCurrentViewId, scopeId, loadViewFields, loadViewFilters, loadViewSorts], ); const resetViewBar = useRecoilCallback( @@ -246,12 +397,12 @@ export const useView = (props?: UseViewProps) => { if (viewEditMode === 'create' && name) { await createView(name); - } else { - await internalUpdateView({ - ...currentView, - name, - }); } + + await internalUpdateView({ + ...currentView, + name, + }); }, [createView, internalUpdateView, scopeId], ); @@ -284,5 +435,8 @@ export const useView = (props?: UseViewProps) => { persistViewFields, changeViewInUrl, loadView, + loadViewFields, + loadViewFilters, + loadViewSorts, }; }; diff --git a/front/src/modules/views/states/selectors/currentViewScopedSelector.ts b/front/src/modules/views/states/selectors/currentViewScopedSelector.ts index e0862ffc2..fbecda597 100644 --- a/front/src/modules/views/states/selectors/currentViewScopedSelector.ts +++ b/front/src/modules/views/states/selectors/currentViewScopedSelector.ts @@ -1,23 +1,21 @@ import { createScopedSelector } from '@/ui/utilities/recoil-scope/utils/createScopedSelector'; -import { View } from '@/views/types/View'; +import { GraphQLView } from '@/views/types/GraphQLView'; import { currentViewIdScopedState } from '../currentViewIdScopedState'; import { viewsByIdScopedSelector } from './viewsByIdScopedSelector'; -export const currentViewScopedSelector = createScopedSelector( - { - key: 'currentViewScopedSelector', - get: - ({ scopeId }: { scopeId: string }) => - ({ get }) => { - const currentViewId = get( - currentViewIdScopedState({ scopeId: scopeId }), - ); +export const currentViewScopedSelector = createScopedSelector< + GraphQLView | undefined +>({ + key: 'currentViewScopedSelector', + get: + ({ scopeId }: { scopeId: string }) => + ({ get }) => { + const currentViewId = get(currentViewIdScopedState({ scopeId: scopeId })); - return currentViewId - ? get(viewsByIdScopedSelector(scopeId))[currentViewId] - : undefined; - }, - }, -); + return currentViewId + ? get(viewsByIdScopedSelector(scopeId))[currentViewId] + : undefined; + }, +}); diff --git a/front/src/modules/views/states/selectors/viewsByIdScopedSelector.ts b/front/src/modules/views/states/selectors/viewsByIdScopedSelector.ts index cd15cb8dd..0ec9d8919 100644 --- a/front/src/modules/views/states/selectors/viewsByIdScopedSelector.ts +++ b/front/src/modules/views/states/selectors/viewsByIdScopedSelector.ts @@ -1,19 +1,18 @@ import { selectorFamily } from 'recoil'; -import { View } from '@/views/types/View'; +import { GraphQLView } from '@/views/types/GraphQLView'; import { viewsScopedState } from '../viewsScopedState'; export const viewsByIdScopedSelector = selectorFamily< - Record, + Record, string >({ key: 'viewsByIdScopedSelector', get: (scopeId) => ({ get }) => - get(viewsScopedState({ scopeId: scopeId })).reduce>( - (result, view) => ({ ...result, [view.id]: view }), - {}, - ), + get(viewsScopedState({ scopeId: scopeId })).reduce< + Record + >((result, view) => ({ ...result, [view.id]: view }), {}), }); diff --git a/front/src/modules/views/states/viewsScopedState.ts b/front/src/modules/views/states/viewsScopedState.ts index 6ba48eb42..3f141877b 100644 --- a/front/src/modules/views/states/viewsScopedState.ts +++ b/front/src/modules/views/states/viewsScopedState.ts @@ -1,8 +1,7 @@ import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState'; +import { GraphQLView } from '@/views/types/GraphQLView'; -import { View } from '../types/View'; - -export const viewsScopedState = createScopedState({ +export const viewsScopedState = createScopedState({ key: 'viewsScopedState', defaultValue: [], });