Load views on user load and read in cache (#3552)

* WIP

* Poc

* Use cached root query + remove proloaded views state

* Fix storybook test + fix codegen

* Return default schema if token is absent, unauthenticated if token is invalid

* Use enum instead of bool

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas Trompette
2024-01-22 16:00:16 +01:00
committed by GitHub
parent 764374f6b8
commit f1b3d1537a
19 changed files with 324 additions and 113 deletions

View File

@ -0,0 +1,50 @@
import { useApolloClient } from '@apollo/client/react/hooks/useApolloClient';
import gql from 'graphql-tag';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { QueryMethodName } from '@/object-metadata/types/QueryMethodName';
export const useCachedRootQuery = ({
objectMetadataItem,
queryMethodName,
}: {
objectMetadataItem: ObjectMetadataItem | undefined;
queryMethodName: QueryMethodName;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
const apolloClient = useApolloClient();
if (!objectMetadataItem) {
return { cachedRootQuery: null };
}
const buildRecordFieldsFragment = () => {
return objectMetadataItem.fields
.filter((field) => field.type !== 'RELATION')
.map((field) => mapFieldMetadataToGraphQLQuery(field))
.join(' \n');
};
const cacheReadFragment = gql`
fragment RootQuery on Query {
${
QueryMethodName.FindMany === queryMethodName
? objectMetadataItem.namePlural
: objectMetadataItem.nameSingular
} {
${QueryMethodName.FindMany === queryMethodName ? 'edges { node { ' : ''}
${buildRecordFieldsFragment()}
${QueryMethodName.FindMany === queryMethodName ? '}}' : ''}
}
}
`;
const cachedRootQuery = apolloClient.readFragment({
id: 'ROOT_QUERY',
fragment: cacheReadFragment,
});
return { cachedRootQuery };
};

View File

@ -1,15 +1,38 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useCachedRootQuery } from '@/apollo/hooks/useCachedRootQuery';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { QueryMethodName } from '@/object-metadata/types/QueryMethodName';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
export const ObjectMetadataNavItems = () => {
const { activeObjectMetadataItems } = useObjectMetadataItemForSettings();
const { activeObjectMetadataItems, findObjectMetadataItemByNamePlural } =
useObjectMetadataItemForSettings();
const navigate = useNavigate();
const { getIcon } = useIcons();
const currentPath = useLocation().pathname;
const viewObjectMetadataItem = findObjectMetadataItemByNamePlural('views');
const { cachedRootQuery } = useCachedRootQuery({
objectMetadataItem: viewObjectMetadataItem,
queryMethodName: QueryMethodName.FindMany,
});
const { records } = useFindManyRecords({
skip: cachedRootQuery?.views,
objectNameSingular: CoreObjectNameSingular.View,
useRecordsWithoutConnection: true,
});
const views =
records.length > 0
? records
: cachedRootQuery?.views?.edges?.map((edge: any) => edge?.node);
return (
<>
{[
@ -39,18 +62,28 @@ export const ObjectMetadataNavItems = () => {
? 1
: -1;
}),
].map((objectMetadataItem) => (
<NavigationDrawerItem
key={objectMetadataItem.id}
label={objectMetadataItem.labelPlural}
to={`/objects/${objectMetadataItem.namePlural}`}
active={currentPath === `/objects/${objectMetadataItem.namePlural}`}
Icon={getIcon(objectMetadataItem.icon)}
onClick={() => {
navigate(`/objects/${objectMetadataItem.namePlural}`);
}}
/>
))}
].map((objectMetadataItem) => {
const viewId = views?.find(
(view: any) => view?.objectMetadataId === objectMetadataItem.id,
)?.id;
const navigationPath = `/objects/${objectMetadataItem.namePlural}${
viewId ? `?view=${viewId}` : ''
}`;
return (
<NavigationDrawerItem
key={objectMetadataItem.id}
label={objectMetadataItem.labelPlural}
to={navigationPath}
active={currentPath === navigationPath}
Icon={getIcon(objectMetadataItem.icon)}
onClick={() => {
navigate(navigationPath);
}}
/>
);
})}
</>
);
};

View File

@ -0,0 +1,4 @@
export enum QueryMethodName {
FindOne = 'findOne',
FindMany = 'findMany',
}

View File

@ -20,7 +20,7 @@ export const useGenerateFindManyRecordsQuery = () => {
objectMetadataItem.nameSingular,
)}FilterInput, $orderBy: ${capitalize(
objectMetadataItem.nameSingular,
)}OrderByInput, $lastCursor: String, $limit: Float = 30) {
)}OrderByInput, $lastCursor: String, $limit: Float = 60) {
${
objectMetadataItem.namePlural
}(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){

View File

@ -1,11 +1,12 @@
import { useEffect, useState } from 'react';
import { useQuery } from '@apollo/client';
import { useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { useGetCurrentUserQuery } from '~/generated/graphql';
export const UserProvider = ({ children }: React.PropsWithChildren) => {
const [isLoading, setIsLoading] = useState(true);
@ -16,16 +17,18 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => {
currentWorkspaceMemberState,
);
const { data: userData, loading: userLoading } = useGetCurrentUserQuery({});
const { loading: queryLoading, data: queryData } = useQuery(
GET_CURRENT_USER_AND_VIEWS,
);
useEffect(() => {
if (!userLoading) {
if (!queryLoading) {
setIsLoading(false);
}
if (userData?.currentUser?.workspaceMember) {
setCurrentUser(userData.currentUser);
setCurrentWorkspace(userData.currentUser.defaultWorkspace);
const workspaceMember = userData.currentUser.workspaceMember;
if (queryData?.currentUser?.workspaceMember) {
setCurrentUser(queryData.currentUser);
setCurrentWorkspace(queryData.currentUser.defaultWorkspace);
const workspaceMember = queryData.currentUser.workspaceMember;
setCurrentWorkspaceMember({
...workspaceMember,
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light',
@ -34,10 +37,10 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => {
}, [
setCurrentUser,
isLoading,
userLoading,
queryLoading,
setCurrentWorkspace,
setCurrentWorkspaceMember,
userData?.currentUser,
queryData?.currentUser,
]);
return isLoading ? <></> : <>{children}</>;

View File

@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const GET_CURRENT_USER = gql`
query GetCurrentUser {
currentUser {
...UserQueryFragment
}
}
`;

View File

@ -0,0 +1,103 @@
// This query cannot be put in the graphQL folder because it cannot be generated by the graphQL codegen.
import { gql } from '@apollo/client';
export const GET_CURRENT_USER_AND_VIEWS = gql`
query GetCurrentUserAndViews {
currentUser {
id
firstName
lastName
email
canImpersonate
supportUserHash
workspaceMember {
id
name {
firstName
lastName
}
colorScheme
avatarUrl
locale
}
defaultWorkspace {
id
displayName
logo
domainName
inviteHash
allowImpersonation
subscriptionStatus
featureFlags {
id
key
value
workspaceId
}
}
}
views {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
id
createdAt
updatedAt
name
objectMetadataId
type
deletedAt
viewFilters {
edges {
cursor
node {
id
createdAt
updatedAt
fieldMetadataId
operand
value
displayValue
deletedAt
}
}
}
viewSorts {
edges {
cursor
node {
id
createdAt
updatedAt
fieldMetadataId
direction
deletedAt
}
}
}
viewFields {
edges {
cursor
node {
id
createdAt
updatedAt
fieldMetadataId
isVisible
size
position
deletedAt
}
}
}
}
}
}
}
`;

View File

@ -7,6 +7,7 @@ import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/c
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { TopBar } from '@/ui/layout/top-bar/TopBar';
import { FilterQueryParamsEffect } from '@/views/components/FilterQueryParamsEffect';
import { ViewBarEffect } from '@/views/components/ViewBarEffect';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect';
import { useViewBar } from '@/views/hooks/useViewBar';
@ -19,7 +20,6 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
import { ViewBarEffect } from './ViewBarEffect';
import { ViewsDropdownButton } from './ViewsDropdownButton';
export type ViewBarProps = {