refactor: metadata store cleanup, SSE unification, mock metadata loading & login redirect fix (#18651)
## Summary - **SSE unification**: Replaced 11 individual SSE effect components with a single generic `MetadataStoreSSEEffect` - **Metadata store cleanup**: Merged `metadataCollectionHashesState` into `metadataStoreState` (currentCollectionHash / draftCollectionHash per entity), moved `objectMetadataItemsSelector` to `object-metadata` domain, converted `navigationMenuItemsState` to a derived selector - **Naming clarity**: Renamed `isAppMetadataReadyState` → `isMinimalMetadataReadyState`, `MetadataGater` → `MinimalMetadataGater`, `useIsLogged` → `useHasAccessTokenPair`, `patchMetadataStoreFromSSEEvent` now takes named object params - **Mock metadata loading**: Added `generate-navigation-menu-items.ts` script, rewrote `useLoadMockedMinimalMetadata` to load full objects/fields/indexes/views/navItems from generated mock data, enabling proper sign-in background rendering (table columns, view picker, navigation) - **Login/logout transitions**: `MinimalMetadataLoadEffect` manages mocked↔real metadata transitions based on auth state, `MainContextStoreProvider` computes context on auth pages for view picker support - **Login redirect fix**: `handleLoadWorkspaceAfterAuthentication` now re-enables `isAppEffectRedirectEnabled` after `loadCurrentUser()` completes, fixing the blocked post-login navigation - **Dead code removal**: Deleted `useRefreshPageLayouts`, `useApplyPageLayouts`, `useStaleMetadataEntities`, `metadataCollectionHashesState`, and all individual SSE effects ## Test plan - [x] Login from welcome page redirects to companies page - [x] Logout transitions cleanly to mocked metadata on welcome page - [x] Sign-in background shows table columns, view picker, and navigation items - [x] SSE events still update metadata store entries correctly - [x] Navigation menu items persist across page refreshes - [ ] CI: lint, typecheck, tests pass
This commit is contained in:
+7
-10
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
|
||||
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -11,10 +9,8 @@ import { AppPath } from 'twenty-shared/types';
|
||||
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const MetadataGater = ({ children }: React.PropsWithChildren) => {
|
||||
const isAppMetadataReady = useAtomStateValue(isAppMetadataReadyState);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const isLoggedIn = useIsLogged();
|
||||
export const MinimalMetadataGater = ({ children }: React.PropsWithChildren) => {
|
||||
const isMinimalMetadataReady = useAtomStateValue(isMinimalMetadataReadyState);
|
||||
const location = useLocation();
|
||||
|
||||
const { dateFormat, timeFormat, timeZone } = useDateTimeFormat();
|
||||
@@ -22,11 +18,12 @@ export const MetadataGater = ({ children }: React.PropsWithChildren) => {
|
||||
const isOnExcludedPath =
|
||||
isMatchingLocation(location, AppPath.Verify) ||
|
||||
isMatchingLocation(location, AppPath.VerifyEmail) ||
|
||||
isMatchingLocation(location, AppPath.SignInUp) ||
|
||||
isMatchingLocation(location, AppPath.Invite) ||
|
||||
isMatchingLocation(location, AppPath.ResetPassword) ||
|
||||
isMatchingLocation(location, AppPath.CreateWorkspace);
|
||||
|
||||
const shouldShowLoader =
|
||||
(!isAppMetadataReady && isLoggedIn && !isOnExcludedPath) ||
|
||||
objectMetadataItems.length === 0;
|
||||
const shouldShowLoader = !isMinimalMetadataReady && !isOnExcludedPath;
|
||||
|
||||
if (shouldShowLoader) {
|
||||
return <UserOrMetadataLoader />;
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const FieldMetadataSSEEffect = () => {
|
||||
const queryId = 'field-metadata-sse-effect';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.fieldMetadata,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.fieldMetadata,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'fieldMetadataItems',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
+18
-13
@@ -1,7 +1,7 @@
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -10,8 +10,8 @@ import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
|
||||
export const IsAppMetadataReadyEffect = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
export const IsMinimalMetadataReadyEffect = () => {
|
||||
const hasAccessTokenPair = useHasAccessTokenPair();
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const metadataStore = useAtomFamilyStateValue(
|
||||
@@ -23,7 +23,9 @@ export const IsAppMetadataReadyEffect = () => {
|
||||
metadataStoreState,
|
||||
'views',
|
||||
);
|
||||
const setIsAppMetadataReady = useSetAtomState(isAppMetadataReadyState);
|
||||
const setIsMinimalMetadataReady = useSetAtomState(
|
||||
isMinimalMetadataReadyState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const hasActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
|
||||
@@ -31,23 +33,26 @@ export const IsAppMetadataReadyEffect = () => {
|
||||
const areObjectsLoaded = metadataStore.status === 'up-to-date';
|
||||
const areViewsLoaded = metadataStoreViews.status === 'up-to-date';
|
||||
|
||||
const isReady = !areObjectsLoaded
|
||||
? false
|
||||
: !hasAccessTokenPair ||
|
||||
(isDefined(currentUser) && (!hasActiveWorkspace || areViewsLoaded));
|
||||
|
||||
if (!areObjectsLoaded) {
|
||||
setIsAppMetadataReady(false);
|
||||
setIsMinimalMetadataReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isReady =
|
||||
!isLoggedIn ||
|
||||
(isDefined(currentUser) && (!hasActiveWorkspace || areViewsLoaded));
|
||||
|
||||
setIsAppMetadataReady(isReady);
|
||||
if (isReady) {
|
||||
setIsMinimalMetadataReady(true);
|
||||
}
|
||||
}, [
|
||||
isLoggedIn,
|
||||
hasAccessTokenPair,
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
metadataStore.status,
|
||||
metadataStoreViews.status,
|
||||
setIsAppMetadataReady,
|
||||
setIsMinimalMetadataReady,
|
||||
]);
|
||||
|
||||
return null;
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { ObjectMetadataProviderInitialEffect } from '@/metadata-store/effect-components/ObjectMetadataProviderInitialEffect';
|
||||
import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect';
|
||||
import { ViewMetadataProviderInitialEffect } from '@/metadata-store/effect-components/ViewMetadataProviderInitialEffect';
|
||||
|
||||
export const MetadataProviderInitialEffects = () => (
|
||||
<>
|
||||
<UserMetadataProviderInitialEffect />
|
||||
<ObjectMetadataProviderInitialEffect />
|
||||
<ViewMetadataProviderInitialEffect />
|
||||
</>
|
||||
);
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { mapAllMetadataNameToEntityKey } from '@/metadata-store/utils/mapAllMetadataNameToEntityKey';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const MetadataStoreSSEEffect = () => {
|
||||
const store = useStore();
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
const entityKey = mapAllMetadataNameToEntityKey(eventDetail.metadataName);
|
||||
|
||||
if (!isDefined(entityKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
patchMetadataStoreFromSSEEvent({
|
||||
store,
|
||||
entityKey,
|
||||
operation: eventDetail.operation,
|
||||
updatedCollectionHash: eventDetail.updatedCollectionHash,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
+45
-34
@@ -1,62 +1,73 @@
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useLoadMinimalMetadata } from '@/metadata-store/hooks/useLoadMinimalMetadata';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { useLoadMockedMinimalMetadata } from '@/metadata-store/hooks/useLoadMockedMinimalMetadata';
|
||||
import { useLoadStaleMetadataEntities } from '@/metadata-store/hooks/useLoadStaleMetadataEntities';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
|
||||
type LoadedState = 'none' | 'mocked' | 'real';
|
||||
|
||||
const computeDesiredLoadState = (
|
||||
hasAccessTokenPair: boolean,
|
||||
isActiveWorkspace: boolean,
|
||||
): LoadedState => {
|
||||
if (hasAccessTokenPair && isActiveWorkspace) {
|
||||
return 'real';
|
||||
}
|
||||
|
||||
return 'mocked';
|
||||
};
|
||||
|
||||
export const MinimalMetadataLoadEffect = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
const hasAccessTokenPair = useHasAccessTokenPair();
|
||||
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const store = useStore();
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const [loadedState, setLoadedState] = useState<LoadedState>('none');
|
||||
|
||||
const { loadMinimalMetadata } = useLoadMinimalMetadata();
|
||||
const { loadMockedMinimalMetadata } = useLoadMockedMinimalMetadata();
|
||||
const { loadStaleMetadataEntities } = useLoadStaleMetadataEntities();
|
||||
|
||||
const isActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
|
||||
|
||||
const desiredLoadState = computeDesiredLoadState(
|
||||
hasAccessTokenPair,
|
||||
isActiveWorkspace,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLoaded) {
|
||||
if (!isCurrentUserLoaded || loadedState === desiredLoadState) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCurrentUserLoaded || !isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
setLoadedState(desiredLoadState);
|
||||
|
||||
if (!isWorkspaceActiveOrSuspended(currentWorkspace)) {
|
||||
return;
|
||||
}
|
||||
const performLoad = async () => {
|
||||
if (desiredLoadState === 'mocked') {
|
||||
await loadMockedMinimalMetadata();
|
||||
return;
|
||||
}
|
||||
|
||||
const objectsEntry = store.get(
|
||||
metadataStoreState.atomFamily('objectMetadataItems'),
|
||||
);
|
||||
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
|
||||
const result = await loadMinimalMetadata();
|
||||
|
||||
const hasObjectsFromStorage = objectsEntry.status === 'up-to-date';
|
||||
const hasViewsFromStorage = viewsEntry.status === 'up-to-date';
|
||||
|
||||
if (hasObjectsFromStorage && hasViewsFromStorage) {
|
||||
setHasLoaded(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
await loadMinimalMetadata();
|
||||
setHasLoaded(true);
|
||||
if (result?.staleEntityKeys && result.staleEntityKeys.length > 0) {
|
||||
await loadStaleMetadataEntities(result.staleEntityKeys);
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
performLoad();
|
||||
}, [
|
||||
hasLoaded,
|
||||
isCurrentUserLoaded,
|
||||
isLoggedIn,
|
||||
currentWorkspace,
|
||||
store,
|
||||
hasAccessTokenPair,
|
||||
isActiveWorkspace,
|
||||
desiredLoadState,
|
||||
loadedState,
|
||||
loadMinimalMetadata,
|
||||
loadMockedMinimalMetadata,
|
||||
loadStaleMetadataEntities,
|
||||
]);
|
||||
|
||||
return null;
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import {
|
||||
AllMetadataName,
|
||||
FindManyNavigationMenuItemsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const NavigationMenuItemSSEEffect = () => {
|
||||
const queryId = 'navigation-menu-item-sse-effect';
|
||||
|
||||
const store = useStore();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
const client = useApolloClient();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.navigationMenuItem,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.navigationMenuItem,
|
||||
onMetadataOperationBrowserEvent: async (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'navigationMenuItems',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
const result = await client.query({
|
||||
query: FindManyNavigationMenuItemsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (!isDefined(result.data?.navigationMenuItems)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingNavigationMenuItems = store.get(
|
||||
navigationMenuItemsState.atom,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
existingNavigationMenuItems,
|
||||
result.data.navigationMenuItems,
|
||||
)
|
||||
) {
|
||||
store.set(
|
||||
navigationMenuItemsState.atom,
|
||||
result.data.navigationMenuItems,
|
||||
);
|
||||
|
||||
updateDraft('navigationMenuItems', result.data.navigationMenuItems);
|
||||
applyChanges();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import {
|
||||
AllMetadataName,
|
||||
FindManyNavigationMenuItemsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const ObjectMetadataItemSSEEffect = () => {
|
||||
const queryId = 'object-metadata-sse-effect';
|
||||
|
||||
const store = useStore();
|
||||
const client = useApolloClient();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.objectMetadata,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.objectMetadata,
|
||||
onMetadataOperationBrowserEvent: async (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'objectMetadataItems',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
const navigationMenuItemsResult = await client.query({
|
||||
query: FindManyNavigationMenuItemsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const existingNavigationMenuItems = store.get(
|
||||
navigationMenuItemsState.atom,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
existingNavigationMenuItems,
|
||||
navigationMenuItemsResult.data?.navigationMenuItems,
|
||||
)
|
||||
) {
|
||||
store.set(
|
||||
navigationMenuItemsState.atom,
|
||||
navigationMenuItemsResult.data?.navigationMenuItems ?? [],
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
|
||||
export const ObjectMetadataProviderInitialEffect = () => {
|
||||
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
if (!isCurrentUserLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldLoadReal = isWorkspaceActiveOrSuspended(currentWorkspace);
|
||||
|
||||
const loadObjectMetadata = async () => {
|
||||
if (shouldLoadReal) {
|
||||
await refreshObjectMetadataItems();
|
||||
} else {
|
||||
await loadMockedObjectMetadataItems();
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
loadObjectMetadata();
|
||||
}, [
|
||||
isInitialized,
|
||||
isCurrentUserLoaded,
|
||||
currentWorkspace,
|
||||
refreshObjectMetadataItems,
|
||||
loadMockedObjectMetadataItems,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useRefreshPageLayouts } from '@/page-layout/hooks/useRefreshPageLayouts';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const PageLayoutSSEEffect = () => {
|
||||
const queryId = 'page-layout-metadata-sse-effect';
|
||||
|
||||
const store = useStore();
|
||||
const { refreshPageLayouts } = useRefreshPageLayouts();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.pageLayout,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.pageLayout,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'pageLayouts',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
refreshPageLayouts();
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useRefreshPageLayouts } from '@/page-layout/hooks/useRefreshPageLayouts';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const PageLayoutTabSSEEffect = () => {
|
||||
const queryId = 'page-layout-tab-sse-effect';
|
||||
|
||||
const store = useStore();
|
||||
const { refreshPageLayouts } = useRefreshPageLayouts();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.pageLayoutTab,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.pageLayoutTab,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'pageLayoutTabs',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
refreshPageLayouts();
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useRefreshPageLayouts } from '@/page-layout/hooks/useRefreshPageLayouts';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const PageLayoutWidgetSSEEffect = () => {
|
||||
const queryId = 'page-layout-widget-sse-effect';
|
||||
|
||||
const store = useStore();
|
||||
const { refreshPageLayouts } = useRefreshPageLayouts();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.pageLayoutWidget,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.pageLayoutWidget,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'pageLayoutWidgets',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
refreshPageLayouts();
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
@@ -27,7 +27,7 @@ import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
|
||||
export const UserMetadataProviderInitialEffect = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
const hasAccessTokenPair = useHasAccessTokenPair();
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const store = useStore();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
@@ -65,7 +65,7 @@ export const UserMetadataProviderInitialEffect = () => {
|
||||
[store],
|
||||
);
|
||||
|
||||
const shouldSkipUserQuery = !isLoggedIn || isDefined(currentUser);
|
||||
const shouldSkipUserQuery = !hasAccessTokenPair || isDefined(currentUser);
|
||||
|
||||
const { data: userQueryData, loading: userQueryLoading } = useQuery(
|
||||
GetCurrentUserDocument,
|
||||
@@ -79,7 +79,7 @@ export const UserMetadataProviderInitialEffect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
if (!hasAccessTokenPair) {
|
||||
setIsCurrentUserLoaded(true);
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
@@ -165,7 +165,7 @@ export const UserMetadataProviderInitialEffect = () => {
|
||||
setIsInitialized(true);
|
||||
}, [
|
||||
isInitialized,
|
||||
isLoggedIn,
|
||||
hasAccessTokenPair,
|
||||
userQueryLoading,
|
||||
userQueryData?.currentUser,
|
||||
setCurrentUser,
|
||||
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
AllMetadataName,
|
||||
type CoreViewField,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const ViewFieldSSEEffect = () => {
|
||||
const store = useStore();
|
||||
|
||||
const queryId = 'view-field-metadata-sse-effect';
|
||||
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewField,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewField,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'viewFields',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
const coreViews = store.get(coreViewsState.atom);
|
||||
|
||||
let viewId: string | undefined;
|
||||
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
const createdViewField = eventDetail.operation
|
||||
.createdRecord as unknown as CoreViewField;
|
||||
viewId = createdViewField.viewId;
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
const updatedViewField = eventDetail.operation
|
||||
.updatedRecord as unknown as CoreViewField;
|
||||
viewId = updatedViewField.viewId;
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedViewFieldId = eventDetail.operation
|
||||
.deletedRecordId as string;
|
||||
|
||||
const view = coreViews.find((coreView) =>
|
||||
coreView.viewFields.some(
|
||||
(viewField) => viewField.id === deletedViewFieldId,
|
||||
),
|
||||
);
|
||||
|
||||
viewId = view?.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = coreViews.find((coreView) => coreView.id === viewId);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ViewFilterGroupSSEEffect = () => {
|
||||
const store = useStore();
|
||||
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-filter-groups-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewFilterGroup,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewFilterGroup,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'viewFilterGroups',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
const coreViews = store.get(coreViewsState.atom);
|
||||
|
||||
let viewId: string | undefined;
|
||||
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
viewId = (eventDetail.operation.createdRecord as { viewId?: string })
|
||||
.viewId;
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
viewId = (eventDetail.operation.updatedRecord as { viewId?: string })
|
||||
.viewId;
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedId = eventDetail.operation.deletedRecordId as string;
|
||||
viewId = coreViews.find((view) =>
|
||||
view.viewFilterGroups?.some(
|
||||
(viewFilterGroup) => viewFilterGroup.id === deletedId,
|
||||
),
|
||||
)?.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = coreViews.find((coreView) => coreView.id === viewId);
|
||||
|
||||
if (isDefined(view)) {
|
||||
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ViewFilterSSEEffect = () => {
|
||||
const store = useStore();
|
||||
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-filters-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewFilter,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewFilter,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'viewFilters',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
const coreViews = store.get(coreViewsState.atom);
|
||||
|
||||
let viewId: string | undefined;
|
||||
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
viewId = (eventDetail.operation.createdRecord as { viewId?: string })
|
||||
.viewId;
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
viewId = (eventDetail.operation.updatedRecord as { viewId?: string })
|
||||
.viewId;
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedId = eventDetail.operation.deletedRecordId as string;
|
||||
viewId = coreViews.find((view) =>
|
||||
view.viewFilters.some((viewFilter) => viewFilter.id === deletedId),
|
||||
)?.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = coreViews.find((coreView) => coreView.id === viewId);
|
||||
|
||||
if (isDefined(view)) {
|
||||
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useFetchAndLoadIndexViews } from '@/metadata-store/hooks/useFetchAndLoadIndexViews';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
|
||||
export const ViewMetadataProviderInitialEffect = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const { fetchAndLoadIndexViews } = useFetchAndLoadIndexViews();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
if (!isCurrentUserLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLoggedIn || !isWorkspaceActiveOrSuspended(currentWorkspace)) {
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const initialize = async () => {
|
||||
await fetchAndLoadIndexViews();
|
||||
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
initialize();
|
||||
}, [
|
||||
isInitialized,
|
||||
isCurrentUserLoaded,
|
||||
isLoggedIn,
|
||||
currentWorkspace,
|
||||
fetchAndLoadIndexViews,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { useGetObjectMetadataItemIdFromViewRelationEventDetail } from '@/metadata-store/hooks/useGetObjectMetadataItemIdFromViewRelationEventDetail';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { type ViewSort } from '@/views/types/ViewSort';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ViewRelationsSSEEffect = () => {
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
const { getObjectMetadataItemIdFromViewRelationEventDetail } =
|
||||
useGetObjectMetadataItemIdFromViewRelationEventDetail();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-filters-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewFilter,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-sorts-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewSort,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-fields-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewField,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-filter-groups-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewFilterGroup,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: replace this with detailed SSE optimistic effects
|
||||
const debouncedRefreshCoreViewsByObjectMetadataId = useDebouncedCallback(
|
||||
(objectMetadataId: string) => {
|
||||
refreshCoreViewsByObjectMetadataId(objectMetadataId);
|
||||
},
|
||||
500,
|
||||
{
|
||||
leading: false,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewFilter,
|
||||
onMetadataOperationBrowserEvent: (
|
||||
detail: MetadataOperationBrowserEventDetail<ViewFilter>,
|
||||
) => {
|
||||
const objectMetadataItemId =
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
|
||||
|
||||
if (!isDefined(objectMetadataItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewSort,
|
||||
onMetadataOperationBrowserEvent: (
|
||||
detail: MetadataOperationBrowserEventDetail<ViewSort>,
|
||||
) => {
|
||||
const objectMetadataItemId =
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
|
||||
|
||||
if (!isDefined(objectMetadataItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewFilterGroup,
|
||||
onMetadataOperationBrowserEvent: (
|
||||
detail: MetadataOperationBrowserEventDetail<ViewFilterGroup>,
|
||||
) => {
|
||||
const objectMetadataItemId =
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
|
||||
|
||||
if (!isDefined(objectMetadataItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewField,
|
||||
onMetadataOperationBrowserEvent: (
|
||||
detail: MetadataOperationBrowserEventDetail<ViewField>,
|
||||
) => {
|
||||
const objectMetadataItemId =
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
|
||||
|
||||
if (!isDefined(objectMetadataItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type CoreViewWithoutRelations } from '@/views/types/CoreViewWithoutRelations';
|
||||
import { useStore } from 'jotai';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
export const ViewSSEEffect = () => {
|
||||
const queryId = 'views-sse-effect';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.view,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.view,
|
||||
onMetadataOperationBrowserEvent: async (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'views',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
const createdView = eventDetail.operation
|
||||
.createdRecord as unknown as CoreViewWithoutRelations;
|
||||
|
||||
store.set(coreViewsState.atom, (prevCoreViews) => [
|
||||
...prevCoreViews,
|
||||
{
|
||||
...createdView,
|
||||
viewFields: [],
|
||||
viewFieldGroups: [],
|
||||
viewFilterGroups: [],
|
||||
viewSorts: [],
|
||||
viewFilters: [],
|
||||
viewGroups: [],
|
||||
},
|
||||
]);
|
||||
|
||||
await sleep(50);
|
||||
|
||||
refreshCoreViewsByObjectMetadataId(createdView.objectMetadataId);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
const updatedView = eventDetail.operation
|
||||
.updatedRecord as unknown as CoreViewWithoutRelations;
|
||||
|
||||
store.set(coreViewsState.atom, (prevCoreViews) =>
|
||||
prevCoreViews.map((coreView) =>
|
||||
coreView.id === updatedView.id
|
||||
? {
|
||||
...coreView,
|
||||
...updatedView,
|
||||
}
|
||||
: coreView,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedViewId = eventDetail.operation.deletedRecordId as string;
|
||||
|
||||
store.set(coreViewsState.atom, (prevCoreViews) =>
|
||||
prevCoreViews.filter((coreView) => coreView.id !== deletedViewId),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ViewSortSSEEffect = () => {
|
||||
const store = useStore();
|
||||
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-sorts-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewSort,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewSort,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
patchMetadataStoreFromSSEEvent(
|
||||
store,
|
||||
'viewSorts',
|
||||
eventDetail.operation,
|
||||
eventDetail.updatedCollectionHash,
|
||||
);
|
||||
|
||||
const coreViews = store.get(coreViewsState.atom);
|
||||
|
||||
let viewId: string | undefined;
|
||||
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
viewId = (eventDetail.operation.createdRecord as { viewId?: string })
|
||||
.viewId;
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
viewId = (eventDetail.operation.updatedRecord as { viewId?: string })
|
||||
.viewId;
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedId = eventDetail.operation.deletedRecordId as string;
|
||||
viewId = coreViews.find((view) =>
|
||||
view.viewSorts.some((viewSort) => viewSort.id === deletedId),
|
||||
)?.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = coreViews.find((coreView) => coreView.id === viewId);
|
||||
|
||||
if (isDefined(view)) {
|
||||
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
+4
-1
@@ -21,7 +21,10 @@ export const FIND_MINIMAL_METADATA = gql`
|
||||
key
|
||||
objectMetadataId
|
||||
}
|
||||
collectionHashes
|
||||
collectionHashes {
|
||||
collectionName
|
||||
hash
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useSetIndexViews } from '@/metadata-store/hooks/useSetIndexViews';
|
||||
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import {
|
||||
ViewType,
|
||||
FindAllCoreViewsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const INDEX_VIEW_TYPES = [ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR];
|
||||
|
||||
export const useFetchAndLoadIndexViews = () => {
|
||||
const client = useApolloClient();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
const { setIndexViews } = useSetIndexViews();
|
||||
|
||||
const fetchAndLoadIndexViews = useCallback(async () => {
|
||||
const result = await client.query({
|
||||
query: FindAllCoreViewsDocument,
|
||||
variables: { viewTypes: INDEX_VIEW_TYPES },
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (isDefined(result.data?.getCoreViews)) {
|
||||
setIndexViews(result.data.getCoreViews);
|
||||
|
||||
const {
|
||||
flatViews,
|
||||
flatViewFields,
|
||||
flatViewFilters,
|
||||
flatViewSorts,
|
||||
flatViewGroups,
|
||||
flatViewFilterGroups,
|
||||
flatViewFieldGroups,
|
||||
} = splitViewWithRelated(result.data.getCoreViews);
|
||||
|
||||
updateDraft('views', flatViews);
|
||||
updateDraft('viewFields', flatViewFields);
|
||||
updateDraft('viewFilters', flatViewFilters);
|
||||
updateDraft('viewSorts', flatViewSorts);
|
||||
updateDraft('viewGroups', flatViewGroups);
|
||||
updateDraft('viewFilterGroups', flatViewFilterGroups);
|
||||
updateDraft('viewFieldGroups', flatViewFieldGroups);
|
||||
applyChanges();
|
||||
}
|
||||
}, [client, setIndexViews, updateDraft, applyChanges]);
|
||||
|
||||
return { fetchAndLoadIndexViews };
|
||||
};
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { useGetViewIdFromViewSSERelationEventDetail } from '@/metadata-store/hooks/useGetViewIdFromViewSSERelationEventDetail';
|
||||
import { useGetViewById } from '@/views/hooks/useGetViewById';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { type ViewSort } from '@/views/types/ViewSort';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useGetObjectMetadataItemIdFromViewRelationEventDetail = () => {
|
||||
const { getViewIdFromViewSSERelationEventDetail } =
|
||||
useGetViewIdFromViewSSERelationEventDetail();
|
||||
|
||||
const { getViewById } = useGetViewById();
|
||||
|
||||
const getObjectMetadataItemIdFromViewRelationEventDetail = <
|
||||
T extends ViewFilter | ViewFilterGroup | ViewField | ViewSort,
|
||||
>(
|
||||
eventDetail: MetadataOperationBrowserEventDetail<T>,
|
||||
) => {
|
||||
const viewId = getViewIdFromViewSSERelationEventDetail(eventDetail);
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { view } = getViewById(viewId);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return view.objectMetadataId;
|
||||
};
|
||||
|
||||
return {
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail,
|
||||
};
|
||||
};
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { type ViewSort } from '@/views/types/ViewSort';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetViewIdFromViewSSERelationEventDetail = () => {
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
|
||||
const getViewIdFromViewSSERelationEventDetail = <
|
||||
T extends ViewFilter | ViewFilterGroup | ViewField | ViewSort,
|
||||
>(
|
||||
eventDetail: MetadataOperationBrowserEventDetail<T>,
|
||||
) => {
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
return eventDetail.operation.createdRecord?.viewId as Nullable<string>;
|
||||
}
|
||||
case 'update': {
|
||||
return eventDetail.operation.updatedRecord?.viewId as Nullable<string>;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedRecordId = eventDetail.operation.deletedRecordId;
|
||||
|
||||
switch (eventDetail.metadataName) {
|
||||
case AllMetadataName.viewFilter:
|
||||
return coreViews.find((view) =>
|
||||
view.viewFilters.some(
|
||||
(viewFilter) => viewFilter.id === deletedRecordId,
|
||||
),
|
||||
)?.id;
|
||||
case AllMetadataName.viewField:
|
||||
return coreViews.find((view) =>
|
||||
view.viewFields.some(
|
||||
(viewField) => viewField.id === deletedRecordId,
|
||||
),
|
||||
)?.id;
|
||||
case AllMetadataName.viewFilterGroup:
|
||||
return coreViews.find((view) =>
|
||||
view.viewFilterGroups?.some(
|
||||
(viewFilterGroup) => viewFilterGroup.id === deletedRecordId,
|
||||
),
|
||||
)?.id;
|
||||
case AllMetadataName.viewSort:
|
||||
return coreViews.find((view) =>
|
||||
view.viewSorts.some(
|
||||
(viewSort) => viewSort.id === deletedRecordId,
|
||||
),
|
||||
)?.id;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getViewIdFromViewSSERelationEventDetail,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FIND_MINIMAL_METADATA } from '@/metadata-store/graphql/queries/findMinimalMetadata';
|
||||
import { metadataCollectionHashesState } from '@/metadata-store/states/metadataCollectionHashesState';
|
||||
import {
|
||||
metadataStoreState,
|
||||
type MetadataEntityKey,
|
||||
@@ -30,51 +29,56 @@ export const useLoadMinimalMetadata = () => {
|
||||
const { objectMetadataItems, views, collectionHashes } =
|
||||
result.data.minimalMetadata;
|
||||
|
||||
const localHashes = store.get(metadataCollectionHashesState.atom);
|
||||
|
||||
const serverHashes: Partial<Record<MetadataEntityKey, string>> = {};
|
||||
const staleEntityKeys: MetadataEntityKey[] = [];
|
||||
|
||||
if (isDefined(collectionHashes)) {
|
||||
for (const [metadataName, hash] of Object.entries(
|
||||
collectionHashes as Record<string, string>,
|
||||
)) {
|
||||
const entityKey = mapAllMetadataNameToEntityKey(metadataName);
|
||||
for (const { collectionName, hash } of collectionHashes) {
|
||||
const entityKey = mapAllMetadataNameToEntityKey(collectionName);
|
||||
|
||||
if (isDefined(entityKey)) {
|
||||
serverHashes[entityKey] = hash;
|
||||
if (!isDefined(entityKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = store.get(metadataStoreState.atomFamily(entityKey));
|
||||
|
||||
if (entry.currentCollectionHash !== hash) {
|
||||
staleEntityKeys.push(entityKey);
|
||||
}
|
||||
|
||||
store.set(metadataStoreState.atomFamily(entityKey), (prev) => ({
|
||||
...prev,
|
||||
draftCollectionHash: hash,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
store.set(metadataCollectionHashesState.atom, serverHashes);
|
||||
|
||||
const staleEntityKeys: MetadataEntityKey[] = Object.entries(serverHashes)
|
||||
.filter(
|
||||
([entityKey, hash]) =>
|
||||
localHashes[entityKey as MetadataEntityKey] !== hash,
|
||||
)
|
||||
.map(([entityKey]) => entityKey as MetadataEntityKey);
|
||||
|
||||
const objectsEntry = store.get(
|
||||
metadataStoreState.atomFamily('objectMetadataItems'),
|
||||
);
|
||||
|
||||
if (objectsEntry.status === 'empty') {
|
||||
store.set(metadataStoreState.atomFamily('objectMetadataItems'), {
|
||||
current: objectMetadataItems as unknown as FlatObjectMetadataItem[],
|
||||
draft: [],
|
||||
status: 'up-to-date',
|
||||
});
|
||||
store.set(
|
||||
metadataStoreState.atomFamily('objectMetadataItems'),
|
||||
(prev) => ({
|
||||
...prev,
|
||||
current: objectMetadataItems as unknown as FlatObjectMetadataItem[],
|
||||
status: 'up-to-date',
|
||||
currentCollectionHash: prev.draftCollectionHash,
|
||||
draftCollectionHash: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
|
||||
|
||||
if (viewsEntry.status === 'empty') {
|
||||
store.set(metadataStoreState.atomFamily('views'), {
|
||||
store.set(metadataStoreState.atomFamily('views'), (prev) => ({
|
||||
...prev,
|
||||
current: views as unknown as FlatView[],
|
||||
draft: [],
|
||||
status: 'up-to-date',
|
||||
});
|
||||
currentCollectionHash: prev.draftCollectionHash,
|
||||
draftCollectionHash: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
return { staleEntityKeys };
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
|
||||
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const MOCKED_COLLECTION_HASH = 'mocked';
|
||||
|
||||
export const useLoadMockedMinimalMetadata = () => {
|
||||
const { updateDraft, applyChanges, resetMetadataStore } = useMetadataStore();
|
||||
|
||||
const loadMockedMinimalMetadata = useCallback(async () => {
|
||||
resetMetadataStore();
|
||||
const [
|
||||
{ generatedMockObjectMetadataItems },
|
||||
{ mockedCoreViews },
|
||||
{ mockedNavigationMenuItems },
|
||||
] = await Promise.all([
|
||||
import('~/testing/utils/generatedMockObjectMetadataItems'),
|
||||
import('~/testing/mock-data/generated/metadata/views/mock-views-data'),
|
||||
import(
|
||||
'~/testing/mock-data/generated/metadata/navigation-menu-items/mock-navigation-menu-items-data'
|
||||
),
|
||||
]);
|
||||
|
||||
const { flatObjects, flatFields, flatIndexes } =
|
||||
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
|
||||
|
||||
updateDraft('objectMetadataItems', flatObjects, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('fieldMetadataItems', flatFields, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('indexMetadataItems', flatIndexes, MOCKED_COLLECTION_HASH);
|
||||
|
||||
const {
|
||||
flatViews,
|
||||
flatViewFields,
|
||||
flatViewFilters,
|
||||
flatViewSorts,
|
||||
flatViewGroups,
|
||||
flatViewFilterGroups,
|
||||
flatViewFieldGroups,
|
||||
} = splitViewWithRelated(mockedCoreViews);
|
||||
|
||||
updateDraft('views', flatViews, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('viewFields', flatViewFields, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('viewFilters', flatViewFilters, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('viewSorts', flatViewSorts, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('viewGroups', flatViewGroups, MOCKED_COLLECTION_HASH);
|
||||
updateDraft(
|
||||
'viewFilterGroups',
|
||||
flatViewFilterGroups,
|
||||
MOCKED_COLLECTION_HASH,
|
||||
);
|
||||
updateDraft('viewFieldGroups', flatViewFieldGroups, MOCKED_COLLECTION_HASH);
|
||||
|
||||
updateDraft(
|
||||
'navigationMenuItems',
|
||||
mockedNavigationMenuItems,
|
||||
MOCKED_COLLECTION_HASH,
|
||||
);
|
||||
|
||||
applyChanges();
|
||||
}, [updateDraft, applyChanges, resetMetadataStore]);
|
||||
|
||||
return { loadMockedMinimalMetadata };
|
||||
};
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { type MetadataEntityKey } from '@/metadata-store/states/metadataStoreState';
|
||||
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
|
||||
import { splitPageLayoutWithRelated } from '@/metadata-store/utils/splitPageLayoutWithRelated';
|
||||
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
|
||||
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
|
||||
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
|
||||
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
|
||||
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FindAllCoreViewsDocument,
|
||||
FindAllRecordPageLayoutsDocument,
|
||||
FindFieldsWidgetCoreViewsDocument,
|
||||
FindManyLogicFunctionsDocument,
|
||||
FindManyNavigationMenuItemsDocument,
|
||||
type ObjectMetadataItemsQuery,
|
||||
ViewType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const OBJECTS_GROUP_KEYS: MetadataEntityKey[] = [
|
||||
'objectMetadataItems',
|
||||
'fieldMetadataItems',
|
||||
'indexMetadataItems',
|
||||
];
|
||||
|
||||
const VIEWS_GROUP_KEYS: MetadataEntityKey[] = [
|
||||
'views',
|
||||
'viewFields',
|
||||
'viewFilters',
|
||||
'viewSorts',
|
||||
'viewGroups',
|
||||
'viewFilterGroups',
|
||||
'viewFieldGroups',
|
||||
];
|
||||
|
||||
const PAGE_LAYOUTS_GROUP_KEYS: MetadataEntityKey[] = [
|
||||
'pageLayouts',
|
||||
'pageLayoutTabs',
|
||||
'pageLayoutWidgets',
|
||||
];
|
||||
|
||||
const INDEX_VIEW_TYPES = [ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR];
|
||||
const FIELDS_WIDGET_VIEW_TYPES = [ViewType.FIELDS_WIDGET];
|
||||
|
||||
const hasOverlap = (
|
||||
staleKeys: MetadataEntityKey[],
|
||||
groupKeys: MetadataEntityKey[],
|
||||
): boolean => staleKeys.some((key) => groupKeys.includes(key));
|
||||
|
||||
export const useLoadStaleMetadataEntities = () => {
|
||||
const client = useApolloClient();
|
||||
const store = useStore();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const loadStaleMetadataEntities = useCallback(
|
||||
async (staleEntityKeys: MetadataEntityKey[]) => {
|
||||
if (staleEntityKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchPromises: Promise<void>[] = [];
|
||||
|
||||
if (hasOverlap(staleEntityKeys, OBJECTS_GROUP_KEYS)) {
|
||||
fetchPromises.push(
|
||||
client
|
||||
.query<ObjectMetadataItemsQuery>({
|
||||
query: FIND_MANY_OBJECT_METADATA_ITEMS,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
const compositeObjects =
|
||||
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
|
||||
pagedObjectMetadataItems: result.data,
|
||||
});
|
||||
|
||||
const { flatObjects, flatFields, flatIndexes } =
|
||||
splitObjectMetadataItemWithRelated(compositeObjects);
|
||||
|
||||
updateDraft('objectMetadataItems', flatObjects);
|
||||
updateDraft('fieldMetadataItems', flatFields);
|
||||
updateDraft('indexMetadataItems', flatIndexes);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (hasOverlap(staleEntityKeys, VIEWS_GROUP_KEYS)) {
|
||||
fetchPromises.push(
|
||||
Promise.all([
|
||||
client.query({
|
||||
query: FindAllCoreViewsDocument,
|
||||
variables: { viewTypes: INDEX_VIEW_TYPES },
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
client.query({
|
||||
query: FindFieldsWidgetCoreViewsDocument,
|
||||
variables: { viewTypes: FIELDS_WIDGET_VIEW_TYPES },
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
]).then(([indexViewsResult, fieldsWidgetViewsResult]) => {
|
||||
const allViews = [
|
||||
...(indexViewsResult.data?.getCoreViews ?? []),
|
||||
...(fieldsWidgetViewsResult.data?.getCoreViews ?? []),
|
||||
];
|
||||
|
||||
const {
|
||||
flatViews,
|
||||
flatViewFields,
|
||||
flatViewFilters,
|
||||
flatViewSorts,
|
||||
flatViewGroups,
|
||||
flatViewFilterGroups,
|
||||
flatViewFieldGroups,
|
||||
} = splitViewWithRelated(allViews);
|
||||
|
||||
updateDraft('views', flatViews);
|
||||
updateDraft('viewFields', flatViewFields);
|
||||
updateDraft('viewFilters', flatViewFilters);
|
||||
updateDraft('viewSorts', flatViewSorts);
|
||||
updateDraft('viewGroups', flatViewGroups);
|
||||
updateDraft('viewFilterGroups', flatViewFilterGroups);
|
||||
updateDraft('viewFieldGroups', flatViewFieldGroups);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (hasOverlap(staleEntityKeys, PAGE_LAYOUTS_GROUP_KEYS)) {
|
||||
fetchPromises.push(
|
||||
client
|
||||
.query({
|
||||
query: FindAllRecordPageLayoutsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
if (!isDefined(result.data?.getPageLayouts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformed =
|
||||
result.data.getPageLayouts.map(transformPageLayout);
|
||||
|
||||
const {
|
||||
flatPageLayouts,
|
||||
flatPageLayoutTabs,
|
||||
flatPageLayoutWidgets,
|
||||
} = splitPageLayoutWithRelated(transformed);
|
||||
|
||||
updateDraft('pageLayouts', flatPageLayouts);
|
||||
updateDraft('pageLayoutTabs', flatPageLayoutTabs);
|
||||
updateDraft('pageLayoutWidgets', flatPageLayoutWidgets);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (staleEntityKeys.includes('logicFunctions')) {
|
||||
fetchPromises.push(
|
||||
client
|
||||
.query({
|
||||
query: FindManyLogicFunctionsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
if (!isDefined(result.data?.findManyLogicFunctions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(
|
||||
logicFunctionsState.atom,
|
||||
result.data.findManyLogicFunctions,
|
||||
);
|
||||
updateDraft('logicFunctions', result.data.findManyLogicFunctions);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (staleEntityKeys.includes('navigationMenuItems')) {
|
||||
fetchPromises.push(
|
||||
client
|
||||
.query({
|
||||
query: FindManyNavigationMenuItemsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
if (!isDefined(result.data?.navigationMenuItems)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateDraft(
|
||||
'navigationMenuItems',
|
||||
result.data.navigationMenuItems,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(fetchPromises);
|
||||
applyChanges();
|
||||
},
|
||||
[client, store, updateDraft, applyChanges],
|
||||
);
|
||||
|
||||
return { loadStaleMetadataEntities };
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { metadataCollectionHashesState } from '@/metadata-store/states/metadataCollectionHashesState';
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
|
||||
import {
|
||||
ALL_METADATA_ENTITY_KEYS,
|
||||
metadataStoreState,
|
||||
@@ -37,8 +36,7 @@ export const resetMetadataStore = (store: JotaiStore) => {
|
||||
store.set(metadataStoreState.atomFamily(key), EMPTY_ENTRY);
|
||||
}
|
||||
|
||||
store.set(metadataCollectionHashesState.atom, {});
|
||||
store.set(isAppMetadataReadyState.atom, false);
|
||||
store.set(isMinimalMetadataReadyState.atom, false);
|
||||
};
|
||||
|
||||
const changeMetadataEntityAsUpToDate = (
|
||||
@@ -51,6 +49,9 @@ const changeMetadataEntityAsUpToDate = (
|
||||
current: entry.draft,
|
||||
draft: [],
|
||||
status: 'up-to-date',
|
||||
currentCollectionHash:
|
||||
entry.draftCollectionHash ?? entry.currentCollectionHash,
|
||||
draftCollectionHash: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -58,7 +59,11 @@ export const useMetadataStore = () => {
|
||||
const store = useStore();
|
||||
|
||||
const updateDraft = useCallback(
|
||||
<K extends MetadataEntityKey>(key: K, data: MetadataEntityTypeMap[K][]) => {
|
||||
<K extends MetadataEntityKey>(
|
||||
key: K,
|
||||
data: MetadataEntityTypeMap[K][],
|
||||
collectionHash?: string,
|
||||
) => {
|
||||
const currentEntry = store.get(metadataStoreState.atomFamily(key));
|
||||
|
||||
if (
|
||||
@@ -72,6 +77,9 @@ export const useMetadataStore = () => {
|
||||
...prev,
|
||||
draft: data,
|
||||
status: 'draft-pending' as const,
|
||||
...(collectionHash !== undefined
|
||||
? { draftCollectionHash: collectionHash }
|
||||
: {}),
|
||||
}));
|
||||
},
|
||||
[store],
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
|
||||
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useReloadWorkspaceMetadata = () => {
|
||||
const store = useStore();
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
|
||||
const { updateDraft, applyChanges, resetMetadataStore } = useMetadataStore();
|
||||
|
||||
const reloadWorkspaceMetadata = useCallback(async () => {
|
||||
resetMetadataStore();
|
||||
|
||||
await refreshObjectMetadataItems();
|
||||
|
||||
const loadedViews = store.get(coreViewsState.atom);
|
||||
const {
|
||||
flatViews,
|
||||
flatViewFields,
|
||||
flatViewFilters,
|
||||
flatViewSorts,
|
||||
flatViewGroups,
|
||||
flatViewFilterGroups,
|
||||
flatViewFieldGroups,
|
||||
} = splitViewWithRelated(loadedViews);
|
||||
|
||||
updateDraft('views', flatViews);
|
||||
updateDraft('viewFields', flatViewFields);
|
||||
updateDraft('viewFilters', flatViewFilters);
|
||||
updateDraft('viewSorts', flatViewSorts);
|
||||
updateDraft('viewGroups', flatViewGroups);
|
||||
updateDraft('viewFilterGroups', flatViewFilterGroups);
|
||||
updateDraft('viewFieldGroups', flatViewFieldGroups);
|
||||
applyChanges();
|
||||
}, [
|
||||
resetMetadataStore,
|
||||
refreshObjectMetadataItems,
|
||||
store,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
|
||||
const resetToMockedMetadata = useCallback(async () => {
|
||||
resetMetadataStore();
|
||||
|
||||
await loadMockedObjectMetadataItems();
|
||||
}, [resetMetadataStore, loadMockedObjectMetadataItems]);
|
||||
|
||||
return { reloadWorkspaceMetadata, resetToMockedMetadata };
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { ViewType } from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useSetIndexViews = () => {
|
||||
const store = useStore();
|
||||
|
||||
const setIndexViews = useCallback(
|
||||
(indexViews: CoreViewWithRelations[]) => {
|
||||
const existingCoreViews = store.get(coreViewsState.atom);
|
||||
const existingFieldsWidgetViews = existingCoreViews.filter(
|
||||
(view) => view.type === ViewType.FIELDS_WIDGET,
|
||||
);
|
||||
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
|
||||
|
||||
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
|
||||
store.set(coreViewsState.atom, mergedViews);
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return {
|
||||
setIndexViews,
|
||||
};
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { metadataCollectionHashesState } from '@/metadata-store/states/metadataCollectionHashesState';
|
||||
import {
|
||||
ALL_METADATA_ENTITY_KEYS,
|
||||
type MetadataEntityKey,
|
||||
} from '@/metadata-store/states/metadataStoreState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
// Returns the list of MetadataEntityKey whose local collection hash
|
||||
// is missing or differs from the last known server collection hash.
|
||||
export const useStaleMetadataEntities = (): MetadataEntityKey[] => {
|
||||
const metadataCollectionHashes = useAtomStateValue(
|
||||
metadataCollectionHashesState,
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const staleKeys: MetadataEntityKey[] = [];
|
||||
|
||||
for (const entityKey of ALL_METADATA_ENTITY_KEYS) {
|
||||
if (metadataCollectionHashes[entityKey] === undefined) {
|
||||
staleKeys.push(entityKey);
|
||||
}
|
||||
}
|
||||
|
||||
return staleKeys;
|
||||
}, [metadataCollectionHashes]);
|
||||
};
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const isAppMetadataReadyState = createAtomState<boolean>({
|
||||
key: 'isAppMetadataReadyState',
|
||||
export const isMinimalMetadataReadyState = createAtomState<boolean>({
|
||||
key: 'isMinimalMetadataReadyState',
|
||||
defaultValue: false,
|
||||
});
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { type MetadataEntityKey } from '@/metadata-store/states/metadataStoreState';
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const metadataCollectionHashesState = createAtomState<
|
||||
Partial<Record<MetadataEntityKey, string>>
|
||||
>({
|
||||
key: 'metadataCollectionHashesState',
|
||||
defaultValue: {},
|
||||
useLocalStorage: true,
|
||||
localStorageOptions: { getOnInit: true },
|
||||
});
|
||||
@@ -38,6 +38,8 @@ export type MetadataStoreItem = {
|
||||
current: object[];
|
||||
draft: object[];
|
||||
status: MetadataEntityStoreStatus;
|
||||
currentCollectionHash?: string;
|
||||
draftCollectionHash?: string;
|
||||
};
|
||||
|
||||
const METADATA_STORE_ITEM_INITIAL_VALUE: MetadataStoreItem = {
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
|
||||
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
|
||||
|
||||
export const objectMetadataItemsSelector = createAtomSelector<
|
||||
FlatObjectMetadataItem[]
|
||||
>({
|
||||
key: 'objectMetadataItemsSelector',
|
||||
get: ({ get }) => {
|
||||
const storeItem = get(metadataStoreState, 'objectMetadataItems');
|
||||
|
||||
return storeItem.current as FlatObjectMetadataItem[];
|
||||
},
|
||||
});
|
||||
+18
-16
@@ -1,4 +1,3 @@
|
||||
import { metadataCollectionHashesState } from '@/metadata-store/states/metadataCollectionHashesState';
|
||||
import {
|
||||
metadataStoreState,
|
||||
type MetadataEntityKey,
|
||||
@@ -22,19 +21,29 @@ type SSEEventOperation =
|
||||
deletedRecordId: string;
|
||||
};
|
||||
|
||||
export const patchMetadataStoreFromSSEEvent = (
|
||||
store: JotaiStore,
|
||||
entityKey: MetadataEntityKey,
|
||||
operation: SSEEventOperation,
|
||||
updatedCollectionHash?: string,
|
||||
) => {
|
||||
export const patchMetadataStoreFromSSEEvent = ({
|
||||
store,
|
||||
entityKey,
|
||||
operation,
|
||||
updatedCollectionHash,
|
||||
}: {
|
||||
store: JotaiStore;
|
||||
entityKey: MetadataEntityKey;
|
||||
operation: SSEEventOperation;
|
||||
updatedCollectionHash?: string;
|
||||
}) => {
|
||||
const entry = store.get(metadataStoreState.atomFamily(entityKey));
|
||||
const currentItems = entry.current as Array<{ id: string }>;
|
||||
|
||||
const hashUpdate = isDefined(updatedCollectionHash)
|
||||
? { currentCollectionHash: updatedCollectionHash }
|
||||
: {};
|
||||
|
||||
switch (operation.type) {
|
||||
case 'create': {
|
||||
store.set(metadataStoreState.atomFamily(entityKey), {
|
||||
...entry,
|
||||
...hashUpdate,
|
||||
current: [...currentItems, operation.createdRecord],
|
||||
});
|
||||
break;
|
||||
@@ -42,6 +51,7 @@ export const patchMetadataStoreFromSSEEvent = (
|
||||
case 'update': {
|
||||
store.set(metadataStoreState.atomFamily(entityKey), {
|
||||
...entry,
|
||||
...hashUpdate,
|
||||
current: currentItems.map((item) =>
|
||||
item.id === (operation.updatedRecord as { id: string }).id
|
||||
? { ...item, ...operation.updatedRecord }
|
||||
@@ -53,6 +63,7 @@ export const patchMetadataStoreFromSSEEvent = (
|
||||
case 'delete': {
|
||||
store.set(metadataStoreState.atomFamily(entityKey), {
|
||||
...entry,
|
||||
...hashUpdate,
|
||||
current: currentItems.filter(
|
||||
(item) => item.id !== operation.deletedRecordId,
|
||||
),
|
||||
@@ -60,13 +71,4 @@ export const patchMetadataStoreFromSSEEvent = (
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(updatedCollectionHash)) {
|
||||
const currentHashes = store.get(metadataCollectionHashesState.atom);
|
||||
|
||||
store.set(metadataCollectionHashesState.atom, {
|
||||
...currentHashes,
|
||||
[entityKey]: updatedCollectionHash,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user