Various SDK improvements (#18115)
## Summary - **Refactor frontend metadata loading architecture**: Split the monolithic `EagerMetadataLoadEffect` into focused provider effects (`UserMetadataProviderEffect`, `ObjectMetadataProviderEffect`, `ViewMetadataProviderEffect`) orchestrated by `MetadataProviderEffects`. Replaced `UserProvider` + `ObjectMetadataItemsProvider` with a single `MetadataGater` that gates rendering on `isAppMetadataReadyState`. The metadata store now validates view-object consistency before promoting views, and `updateDraft` skips no-op updates via deep equality checks. - **SDK CLI improvements**: Added `app:typecheck` command, improved error handling in API sync (extracts GraphQL error messages), added `serializeError` utility for human-readable error output, added `error` file status to dev mode orchestrator with UI support, and fixed ClickHouse migration/seed commands to use `transpile-only`.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
|
||||
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
||||
import { MetadataGater } from '@/metadata-store/components/MetadataGater';
|
||||
import { IsAppMetadataReadyEffect } from '@/metadata-store/effect-components/IsAppMetadataReadyEffect';
|
||||
import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider';
|
||||
import { MetadataProviderInitialEffects } from '@/metadata-store/effect-components/MetadataProviderInitialEffects';
|
||||
import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
|
||||
import { AuthProvider } from '@/auth/components/AuthProvider';
|
||||
import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
|
||||
@@ -13,7 +16,7 @@ import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffec
|
||||
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
|
||||
import { HeadlessFrontComponentMountRoot } from '@/front-components/components/HeadlessFrontComponentMountRoot';
|
||||
import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider';
|
||||
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
|
||||
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
||||
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
|
||||
import { SSEProvider } from '@/sse-db-event/components/SSEProvider';
|
||||
import { SupportChatEffect } from '@/support/components/SupportChatEffect';
|
||||
@@ -25,10 +28,7 @@ import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
|
||||
import { UserThemeProviderEffect } from '@/ui/theme/components/UserThemeProviderEffect';
|
||||
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||
import { EagerMetadataLoadEffect } from '@/users/components/EagerMetadataLoadEffect';
|
||||
import { LazyMetadataLoadEffect } from '@/users/components/LazyMetadataLoadEffect';
|
||||
import { MetadataProviderEffect } from '@/users/components/MetadataProviderEffect';
|
||||
import { UserProvider } from '@/users/components/UserProvider';
|
||||
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
|
||||
import { StrictMode } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
@@ -42,19 +42,19 @@ export const AppRouterProviders = () => {
|
||||
<ApolloProvider>
|
||||
<BaseThemeProvider>
|
||||
<ClientConfigProviderEffect />
|
||||
<MetadataProviderEffect />
|
||||
<EagerMetadataLoadEffect />
|
||||
<MetadataProviderInitialEffects />
|
||||
<LazyMetadataLoadEffect />
|
||||
<IsAppMetadataReadyEffect />
|
||||
<WorkspaceProviderEffect />
|
||||
<ClientConfigProvider>
|
||||
<CaptchaProvider>
|
||||
<ChromeExtensionSidecarEffect />
|
||||
<ChromeExtensionSidecarProvider>
|
||||
<UserProvider>
|
||||
<MetadataGater>
|
||||
<AuthProvider>
|
||||
<ApolloCoreProvider>
|
||||
<SSEProvider>
|
||||
<ObjectMetadataItemsProvider>
|
||||
<PreComputedChipGeneratorsProvider>
|
||||
<PrefetchDataProvider>
|
||||
<UserThemeProviderEffect />
|
||||
<SnackBarProvider>
|
||||
@@ -81,11 +81,11 @@ export const AppRouterProviders = () => {
|
||||
<SupportChatEffect />
|
||||
</PrefetchDataProvider>
|
||||
<PageChangeEffect />
|
||||
</ObjectMetadataItemsProvider>
|
||||
</PreComputedChipGeneratorsProvider>
|
||||
</SSEProvider>
|
||||
</ApolloCoreProvider>
|
||||
</AuthProvider>
|
||||
</UserProvider>
|
||||
</MetadataGater>
|
||||
</ChromeExtensionSidecarProvider>
|
||||
</CaptchaProvider>
|
||||
</ClientConfigProvider>
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import {
|
||||
metadataStoreState,
|
||||
type MetadataKey,
|
||||
type MetadataLoadEntry,
|
||||
} from '@/app/states/metadataStoreState';
|
||||
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const EMPTY_ENTRY: MetadataLoadEntry = {
|
||||
current: [],
|
||||
draft: [],
|
||||
status: 'empty',
|
||||
};
|
||||
|
||||
// Views reference objects via objectMetadataId — both drafts must agree on IDs.
|
||||
const areDraftsConsistent = (
|
||||
objectsDraft: object[],
|
||||
viewsDraft: object[],
|
||||
): boolean => {
|
||||
const objectIds = new Set(
|
||||
objectsDraft.map((item) => (item as { id: string }).id),
|
||||
);
|
||||
|
||||
return viewsDraft.every((view) =>
|
||||
objectIds.has((view as { objectMetadataId: string }).objectMetadataId),
|
||||
);
|
||||
};
|
||||
|
||||
export const useMetadataStore = () => {
|
||||
const store = useStore();
|
||||
|
||||
const updateDraft = useCallback(
|
||||
(key: MetadataKey, data: object[]) => {
|
||||
store.set(metadataStoreState.atomFamily(key), (prev) => ({
|
||||
...prev,
|
||||
draft: data,
|
||||
status: 'draft_pending' as const,
|
||||
}));
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
// Validates consistency then atomically promotes all 'draft_pending' drafts
|
||||
// to current. If objects and views are inconsistent (e.g. mocked objects with
|
||||
// real views), resets the stale entries back to 'empty' so effects re-fetch.
|
||||
const applyChanges = useCallback(() => {
|
||||
const objectsEntry = store.get(metadataStoreState.atomFamily('objects'));
|
||||
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
|
||||
|
||||
if (
|
||||
objectsEntry.status === 'draft_pending' &&
|
||||
viewsEntry.status === 'draft_pending' &&
|
||||
!areDraftsConsistent(objectsEntry.draft, viewsEntry.draft)
|
||||
) {
|
||||
store.set(metadataStoreState.atomFamily('objects'), EMPTY_ENTRY);
|
||||
store.set(metadataStoreState.atomFamily('views'), EMPTY_ENTRY);
|
||||
return;
|
||||
}
|
||||
|
||||
const allKeys: MetadataKey[] = [
|
||||
'objects',
|
||||
'views',
|
||||
'pageLayouts',
|
||||
'logicFunctions',
|
||||
];
|
||||
|
||||
for (const key of allKeys) {
|
||||
const entry = store.get(metadataStoreState.atomFamily(key));
|
||||
|
||||
if (entry.status === 'draft_pending') {
|
||||
store.set(metadataStoreState.atomFamily(key), {
|
||||
current: entry.draft,
|
||||
draft: [],
|
||||
status: 'loaded',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
store.set(shouldAppBeLoadingState.atom, false);
|
||||
}, [store]);
|
||||
|
||||
return { updateDraft, applyChanges };
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import { metadataStoreState } from '@/app/states/metadataStoreState';
|
||||
import { createSelectorV2 } from '@/ui/utilities/state/jotai/utils/createSelectorV2';
|
||||
|
||||
export const isAppLoadingState = createSelectorV2<boolean>({
|
||||
key: 'isAppLoadingState',
|
||||
get: ({ get }) => {
|
||||
const objectsEntry = get(metadataStoreState, 'objects');
|
||||
const viewsEntry = get(metadataStoreState, 'views');
|
||||
|
||||
return objectsEntry.status !== 'loaded' || viewsEntry.status !== 'loaded';
|
||||
},
|
||||
});
|
||||
@@ -35,7 +35,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
|
||||
import { metadataStoreState } from '@/app/states/metadataStoreState';
|
||||
import { useReloadWorkspaceMetadata } from '@/metadata-store/hooks/useReloadWorkspaceMetadata';
|
||||
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
|
||||
import { loginTokenState } from '@/auth/states/loginTokenState';
|
||||
import {
|
||||
@@ -58,8 +58,6 @@ import { useOrigin } from '@/domain-manager/hooks/useOrigin';
|
||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { sseClientState } from '@/sse-db-event/states/sseClientState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
|
||||
@@ -89,7 +87,8 @@ export const useAuth = () => {
|
||||
);
|
||||
const { loadCurrentUser } = useLoadCurrentUser();
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { reloadWorkspaceMetadata, resetToMockedMetadata } =
|
||||
useReloadWorkspaceMetadata();
|
||||
const { createWorkspace } = useSignUpInNewWorkspace();
|
||||
|
||||
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
|
||||
@@ -123,7 +122,6 @@ export const useAuth = () => {
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
|
||||
|
||||
const clearSession = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
@@ -161,26 +159,6 @@ export const useAuth = () => {
|
||||
|
||||
jotaiStore.set(workspaceAuthProvidersState.atom, authProvidersValue);
|
||||
jotaiStore.set(workspacePublicDataState.atom, workspacePublicDataValue);
|
||||
jotaiStore.set(metadataStoreState.atomFamily('objects'), {
|
||||
current: [],
|
||||
draft: [],
|
||||
status: 'empty',
|
||||
});
|
||||
jotaiStore.set(metadataStoreState.atomFamily('views'), {
|
||||
current: [],
|
||||
draft: [],
|
||||
status: 'empty',
|
||||
});
|
||||
jotaiStore.set(metadataStoreState.atomFamily('pageLayouts'), {
|
||||
current: [],
|
||||
draft: [],
|
||||
status: 'empty',
|
||||
});
|
||||
jotaiStore.set(metadataStoreState.atomFamily('logicFunctions'), {
|
||||
current: [],
|
||||
draft: [],
|
||||
status: 'empty',
|
||||
});
|
||||
jotaiStore.set(domainConfigurationState.atom, domainConfigurationValue);
|
||||
jotaiStore.set(
|
||||
isCaptchaScriptLoadedState.atom,
|
||||
@@ -209,14 +187,14 @@ export const useAuth = () => {
|
||||
|
||||
await client.clearStore();
|
||||
setLastAuthenticateWorkspaceDomain(null);
|
||||
await loadMockedObjectMetadataItems();
|
||||
await resetToMockedMetadata();
|
||||
navigate(AppPath.SignInUp);
|
||||
},
|
||||
[
|
||||
goToRecoilSnapshot,
|
||||
client,
|
||||
setLastAuthenticateWorkspaceDomain,
|
||||
loadMockedObjectMetadataItems,
|
||||
resetToMockedMetadata,
|
||||
navigate,
|
||||
],
|
||||
);
|
||||
@@ -344,19 +322,15 @@ export const useAuth = () => {
|
||||
const handleLoadWorkspaceAfterAuthentication = useCallback(
|
||||
async (authTokens: AuthTokenPair) => {
|
||||
handleSetAuthTokens(authTokens);
|
||||
|
||||
setIsAppEffectRedirectEnabled(false);
|
||||
|
||||
// TODO: We can't parallelize this yet because when loadCurrentUSer is loaded
|
||||
// then UserProvider updates its children and PrefetchDataProvider is then triggered
|
||||
// which requires the correct metadata to be loaded (not the mocks)
|
||||
await loadCurrentUser();
|
||||
await refreshObjectMetadataItems();
|
||||
await reloadWorkspaceMetadata();
|
||||
},
|
||||
[
|
||||
loadCurrentUser,
|
||||
handleSetAuthTokens,
|
||||
refreshObjectMetadataItems,
|
||||
reloadWorkspaceMetadata,
|
||||
setIsAppEffectRedirectEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { metadataStoreState } from '@/app/states/metadataStoreState';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { MainContextStoreProviderEffect } from '@/context-store/components/MainContextStoreProviderEffect';
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
|
||||
|
||||
+18
-12
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { isAppLoadingState } from '@/app/states/isAppLoadingState';
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -10,23 +11,28 @@ import { AppPath } from 'twenty-shared/types';
|
||||
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const UserProvider = ({ children }: React.PropsWithChildren) => {
|
||||
const isAppLoading = useRecoilValueV2(isAppLoadingState);
|
||||
export const MetadataGater = ({ children }: React.PropsWithChildren) => {
|
||||
const isAppMetadataReady = useRecoilValueV2(isAppMetadataReadyState);
|
||||
const objectMetadataItems = useRecoilValueV2(objectMetadataItemsState);
|
||||
const isLoggedIn = useIsLogged();
|
||||
const location = useLocation();
|
||||
|
||||
const { dateFormat, timeFormat, timeZone } = useDateTimeFormat();
|
||||
|
||||
const shouldShowLoader =
|
||||
isAppLoading &&
|
||||
isLoggedIn &&
|
||||
!isMatchingLocation(location, AppPath.Verify) &&
|
||||
!isMatchingLocation(location, AppPath.VerifyEmail) &&
|
||||
!isMatchingLocation(location, AppPath.CreateWorkspace);
|
||||
const isOnExcludedPath =
|
||||
isMatchingLocation(location, AppPath.Verify) ||
|
||||
isMatchingLocation(location, AppPath.VerifyEmail) ||
|
||||
isMatchingLocation(location, AppPath.CreateWorkspace);
|
||||
|
||||
return shouldShowLoader ? (
|
||||
<UserOrMetadataLoader />
|
||||
) : (
|
||||
const shouldShowLoader =
|
||||
(!isAppMetadataReady && isLoggedIn && !isOnExcludedPath) ||
|
||||
objectMetadataItems.length === 0;
|
||||
|
||||
if (shouldShowLoader) {
|
||||
return <UserOrMetadataLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
dateFormat,
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
|
||||
export const IsAppMetadataReadyEffect = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
const currentUser = useRecoilValueV2(currentUserState);
|
||||
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
|
||||
const objectsEntry = useFamilyRecoilValueV2(metadataStoreState, 'objects');
|
||||
const viewsEntry = useFamilyRecoilValueV2(metadataStoreState, 'views');
|
||||
const setIsAppMetadataReady = useSetRecoilStateV2(isAppMetadataReadyState);
|
||||
|
||||
console.log('objectsEntry', objectsEntry);
|
||||
console.log('viewsEntry', viewsEntry);
|
||||
console.log('isLoggedIn', isLoggedIn);
|
||||
console.log('currentUser', currentUser);
|
||||
console.log('currentWorkspace', currentWorkspace);
|
||||
console.log('setIsAppMetadataReady', setIsAppMetadataReady);
|
||||
|
||||
useEffect(() => {
|
||||
const hasActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
|
||||
|
||||
const areObjectsLoaded = objectsEntry.status === 'loaded';
|
||||
const areViewsLoaded = viewsEntry.status === 'loaded';
|
||||
|
||||
if (!areObjectsLoaded) {
|
||||
setIsAppMetadataReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isReady =
|
||||
!isLoggedIn ||
|
||||
(isDefined(currentUser) && (!hasActiveWorkspace || areViewsLoaded));
|
||||
|
||||
setIsAppMetadataReady(isReady);
|
||||
}, [
|
||||
isLoggedIn,
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
objectsEntry.status,
|
||||
viewsEntry.status,
|
||||
setIsAppMetadataReady,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
+29
-20
@@ -1,45 +1,54 @@
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { useStore } from 'jotai';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const ObjectMetadataItemsLoadEffect = () => {
|
||||
const currentUser = useRecoilValueV2(currentUserState);
|
||||
export const ObjectMetadataProviderInitialEffect = () => {
|
||||
const isCurrentUserLoaded = useRecoilValueV2(isCurrentUserLoadedState);
|
||||
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
|
||||
const store = useStore();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
if (isInitialized) return;
|
||||
if (!isCurrentUserLoaded) return;
|
||||
|
||||
const shouldLoadReal = isWorkspaceActiveOrSuspended(currentWorkspace);
|
||||
|
||||
const loadObjectMetadata = async () => {
|
||||
if (
|
||||
isUndefinedOrNull(currentUser) ||
|
||||
!isWorkspaceActiveOrSuspended(currentWorkspace)
|
||||
) {
|
||||
await loadMockedObjectMetadataItems();
|
||||
} else {
|
||||
if (shouldLoadReal) {
|
||||
await refreshObjectMetadataItems();
|
||||
} else {
|
||||
await loadMockedObjectMetadataItems();
|
||||
}
|
||||
|
||||
const loadedItems = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objects', loadedItems);
|
||||
applyChanges();
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
loadObjectMetadata();
|
||||
}, [
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
loadMockedObjectMetadataItems,
|
||||
refreshObjectMetadataItems,
|
||||
isInitialized,
|
||||
isCurrentUserLoaded,
|
||||
currentWorkspace,
|
||||
refreshObjectMetadataItems,
|
||||
loadMockedObjectMetadataItems,
|
||||
store,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
return null;
|
||||
};
|
||||
+39
-24
@@ -1,5 +1,3 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
@@ -8,6 +6,7 @@ import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorksp
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useInitializeFormatPreferences } from '@/localization/hooks/useInitializeFormatPreferences';
|
||||
import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
@@ -15,7 +14,8 @@ import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetReco
|
||||
import { type ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import { useStore } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { AppPath, type ObjectPermissions } from 'twenty-shared/types';
|
||||
@@ -29,10 +29,12 @@ import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const MetadataProviderEffect = () => {
|
||||
export const UserMetadataProviderInitialEffect = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isLoggedIn = useIsLogged();
|
||||
const currentUser = useRecoilValueV2(currentUserState);
|
||||
const store = useStore();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const setCurrentUser = useSetRecoilStateV2(currentUserState);
|
||||
const setCurrentWorkspace = useSetRecoilStateV2(currentWorkspaceState);
|
||||
@@ -40,10 +42,22 @@ export const MetadataProviderEffect = () => {
|
||||
currentUserWorkspaceState,
|
||||
);
|
||||
const setAvailableWorkspaces = useSetRecoilStateV2(availableWorkspacesState);
|
||||
const { initializeFormatPreferences } = useInitializeFormatPreferences();
|
||||
const isLoggedIn = useIsLogged();
|
||||
const setCurrentWorkspaceMember = useSetRecoilStateV2(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setCurrentWorkspaceMembers = useSetRecoilStateV2(
|
||||
currentWorkspaceMembersState,
|
||||
);
|
||||
const setCurrentWorkspaceMembersWithDeleted = useSetRecoilStateV2(
|
||||
currentWorkspaceDeletedMembersState,
|
||||
);
|
||||
const setIsCurrentUserLoaded = useSetRecoilStateV2(isCurrentUserLoadedState);
|
||||
|
||||
const store = useStore();
|
||||
const { initializeFormatPreferences } = useInitializeFormatPreferences();
|
||||
|
||||
const isOnAuthPath =
|
||||
isMatchingLocation(location, AppPath.Verify) ||
|
||||
isMatchingLocation(location, AppPath.VerifyEmail);
|
||||
|
||||
const updateLocaleCatalog = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
@@ -63,28 +77,23 @@ export const MetadataProviderEffect = () => {
|
||||
[store],
|
||||
);
|
||||
|
||||
const setCurrentWorkspaceMember = useSetRecoilStateV2(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setCurrentWorkspaceMembers = useSetRecoilStateV2(
|
||||
currentWorkspaceMembersState,
|
||||
);
|
||||
const setCurrentWorkspaceMembersWithDeleted = useSetRecoilStateV2(
|
||||
currentWorkspaceDeletedMembersState,
|
||||
);
|
||||
|
||||
const shouldSkip =
|
||||
!isLoggedIn ||
|
||||
isDefined(currentUser) ||
|
||||
isMatchingLocation(location, AppPath.Verify) ||
|
||||
isMatchingLocation(location, AppPath.VerifyEmail);
|
||||
const shouldSkipUserQuery =
|
||||
!isLoggedIn || isDefined(currentUser) || isOnAuthPath;
|
||||
|
||||
const { data: userQueryData, loading: userQueryLoading } =
|
||||
useGetCurrentUserQuery({
|
||||
skip: shouldSkip,
|
||||
skip: shouldSkipUserQuery,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized) return;
|
||||
|
||||
if (!isLoggedIn) {
|
||||
setIsCurrentUserLoaded(true);
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userQueryLoading || !isDefined(userQueryData?.currentUser)) {
|
||||
return;
|
||||
}
|
||||
@@ -160,7 +169,12 @@ export const MetadataProviderEffect = () => {
|
||||
if (isDefined(availableWorkspaces)) {
|
||||
setAvailableWorkspaces(availableWorkspaces);
|
||||
}
|
||||
|
||||
setIsCurrentUserLoaded(true);
|
||||
setIsInitialized(true);
|
||||
}, [
|
||||
isInitialized,
|
||||
isLoggedIn,
|
||||
userQueryLoading,
|
||||
userQueryData?.currentUser,
|
||||
setCurrentUser,
|
||||
@@ -172,6 +186,7 @@ export const MetadataProviderEffect = () => {
|
||||
initializeFormatPreferences,
|
||||
setCurrentWorkspaceMembersWithDeleted,
|
||||
updateLocaleCatalog,
|
||||
setIsCurrentUserLoaded,
|
||||
]);
|
||||
|
||||
return null;
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
import {
|
||||
ViewType as CoreViewType,
|
||||
useFindAllCoreViewsLazyQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
const INDEX_VIEW_TYPES = [
|
||||
CoreViewType.TABLE,
|
||||
CoreViewType.KANBAN,
|
||||
CoreViewType.CALENDAR,
|
||||
];
|
||||
|
||||
export const ViewMetadataProviderInitialEffect = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
const isCurrentUserLoaded = useRecoilValueV2(isCurrentUserLoadedState);
|
||||
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
|
||||
const store = useStore();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const [findAllCoreViews] = useFindAllCoreViewsLazyQuery();
|
||||
|
||||
const setIndexCoreViews = useCallback(
|
||||
(indexViews: CoreViewWithRelations[]) => {
|
||||
const existingCoreViews = store.get(coreViewsState.atom);
|
||||
const existingFieldsWidgetViews = existingCoreViews.filter(
|
||||
(view) => view.type === CoreViewType.FIELDS_WIDGET,
|
||||
);
|
||||
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
|
||||
|
||||
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
|
||||
store.set(coreViewsState.atom, mergedViews);
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized) return;
|
||||
if (!isCurrentUserLoaded) return;
|
||||
|
||||
if (!isLoggedIn || !isWorkspaceActiveOrSuspended(currentWorkspace)) {
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadViews = async () => {
|
||||
const result = await findAllCoreViews({
|
||||
variables: { viewTypes: INDEX_VIEW_TYPES },
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (isDefined(result.data?.getCoreViews)) {
|
||||
setIndexCoreViews(result.data.getCoreViews);
|
||||
updateDraft('views', result.data.getCoreViews);
|
||||
applyChanges();
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
loadViews();
|
||||
}, [
|
||||
isInitialized,
|
||||
isCurrentUserLoaded,
|
||||
isLoggedIn,
|
||||
currentWorkspace,
|
||||
findAllCoreViews,
|
||||
setIndexCoreViews,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
ALL_METADATA_KEYS,
|
||||
metadataStoreState,
|
||||
type MetadataKey,
|
||||
type MetadataLoadEntry,
|
||||
} from '@/metadata-store/states/metadataStoreState';
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { type createStore, useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type JotaiStore = ReturnType<typeof createStore>;
|
||||
|
||||
const EMPTY_ENTRY: MetadataLoadEntry = {
|
||||
current: [],
|
||||
draft: [],
|
||||
status: 'empty',
|
||||
};
|
||||
|
||||
const areViewsConsistentWithObjects = (
|
||||
viewsDraft: object[],
|
||||
objectsCurrent: object[],
|
||||
): boolean => {
|
||||
const objectIds = new Set(
|
||||
objectsCurrent.map((item) => (item as { id: string }).id),
|
||||
);
|
||||
|
||||
return viewsDraft.every((view) =>
|
||||
objectIds.has((view as { objectMetadataId: string }).objectMetadataId),
|
||||
);
|
||||
};
|
||||
|
||||
export const resetMetadataStore = (store: JotaiStore) => {
|
||||
for (const key of ALL_METADATA_KEYS) {
|
||||
store.set(metadataStoreState.atomFamily(key), EMPTY_ENTRY);
|
||||
}
|
||||
|
||||
store.set(isAppMetadataReadyState.atom, false);
|
||||
};
|
||||
|
||||
const promoteEntry = (store: JotaiStore, key: MetadataKey) => {
|
||||
const entry = store.get(metadataStoreState.atomFamily(key));
|
||||
|
||||
store.set(metadataStoreState.atomFamily(key), {
|
||||
current: entry.draft,
|
||||
draft: [],
|
||||
status: 'loaded',
|
||||
});
|
||||
};
|
||||
|
||||
export const useMetadataStore = () => {
|
||||
const store = useStore();
|
||||
|
||||
const updateDraft = useCallback(
|
||||
(key: MetadataKey, data: object[]) => {
|
||||
const currentEntry = store.get(metadataStoreState.atomFamily(key));
|
||||
|
||||
if (
|
||||
currentEntry.status === 'loaded' &&
|
||||
isDeeplyEqual(currentEntry.current, data)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(metadataStoreState.atomFamily(key), (prev) => ({
|
||||
...prev,
|
||||
draft: data,
|
||||
status: 'draft_pending' as const,
|
||||
}));
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
const applyChanges = useCallback((): boolean => {
|
||||
let promoted = false;
|
||||
|
||||
for (const key of ALL_METADATA_KEYS) {
|
||||
if (key === 'views') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = store.get(metadataStoreState.atomFamily(key));
|
||||
|
||||
if (entry.status === 'draft_pending') {
|
||||
promoteEntry(store, key);
|
||||
promoted = true;
|
||||
}
|
||||
}
|
||||
|
||||
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
|
||||
|
||||
if (viewsEntry.status === 'draft_pending') {
|
||||
const objectsEntry = store.get(metadataStoreState.atomFamily('objects'));
|
||||
|
||||
if (
|
||||
areViewsConsistentWithObjects(viewsEntry.draft, objectsEntry.current)
|
||||
) {
|
||||
promoteEntry(store, 'views');
|
||||
promoted = true;
|
||||
}
|
||||
}
|
||||
|
||||
return promoted;
|
||||
}, [store]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
resetMetadataStore(store);
|
||||
}, [store]);
|
||||
|
||||
return { updateDraft, applyChanges, resetMetadataStore: reset };
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
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 loadedObjects = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objects', loadedObjects);
|
||||
applyChanges();
|
||||
|
||||
const loadedViews = store.get(coreViewsState.atom);
|
||||
updateDraft('views', loadedViews);
|
||||
applyChanges();
|
||||
}, [
|
||||
resetMetadataStore,
|
||||
refreshObjectMetadataItems,
|
||||
store,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
|
||||
const resetToMockedMetadata = useCallback(async () => {
|
||||
resetMetadataStore();
|
||||
|
||||
await loadMockedObjectMetadataItems();
|
||||
const loadedObjects = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objects', loadedObjects);
|
||||
applyChanges();
|
||||
}, [
|
||||
resetMetadataStore,
|
||||
loadMockedObjectMetadataItems,
|
||||
store,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
|
||||
return { reloadWorkspaceMetadata, resetToMockedMetadata };
|
||||
};
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
|
||||
|
||||
export const shouldAppBeLoadingState = createStateV2<boolean>({
|
||||
key: 'shouldAppBeLoadingState',
|
||||
export const isAppMetadataReadyState = createStateV2<boolean>({
|
||||
key: 'isAppMetadataReadyState',
|
||||
defaultValue: false,
|
||||
});
|
||||
+7
@@ -8,6 +8,13 @@ export type MetadataKey =
|
||||
| 'pageLayouts'
|
||||
| 'logicFunctions';
|
||||
|
||||
export const ALL_METADATA_KEYS: MetadataKey[] = [
|
||||
'objects',
|
||||
'views',
|
||||
'pageLayouts',
|
||||
'logicFunctions',
|
||||
];
|
||||
|
||||
export type MetadataLoadEntry = {
|
||||
current: object[];
|
||||
draft: object[];
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
||||
|
||||
export const ObjectMetadataItemsProvider = ({
|
||||
children,
|
||||
}: React.PropsWithChildren) => {
|
||||
const objectMetadataItems = useRecoilValueV2(objectMetadataItemsState);
|
||||
|
||||
const shouldAppBeLoading = useRecoilValueV2(shouldAppBeLoadingState);
|
||||
|
||||
const shouldDisplayChildren =
|
||||
!shouldAppBeLoading && objectMetadataItems.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldDisplayChildren ? (
|
||||
<PreComputedChipGeneratorsProvider>
|
||||
{children}
|
||||
</PreComputedChipGeneratorsProvider>
|
||||
) : (
|
||||
<UserOrMetadataLoader />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+16
-11
@@ -8,18 +8,11 @@ import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-met
|
||||
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
|
||||
import { useFamilySelectorValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorValueV2';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useColumnDefinitionsFromObjectMetadata = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
) => {
|
||||
const activeFieldMetadataItems = objectMetadataItem.readableFields.filter(
|
||||
(field) =>
|
||||
field.isActive &&
|
||||
// Allow label identifier field (e.g. id for junction tables) even if it's a hidden system field
|
||||
(!isHiddenSystemField(field) ||
|
||||
field.id === objectMetadataItem.labelIdentifierFieldMetadataId),
|
||||
);
|
||||
|
||||
const filterableFieldMetadataItems = useFamilySelectorValueV2(
|
||||
availableFieldMetadataItemsForFilterFamilySelector,
|
||||
{
|
||||
@@ -34,10 +27,16 @@ export const useColumnDefinitionsFromObjectMetadata = (
|
||||
},
|
||||
);
|
||||
|
||||
const restrictedFieldMetadataIds: string[] = [];
|
||||
const columnDefinitions: ColumnDefinition<FieldMetadata>[] = useMemo(() => {
|
||||
const activeFieldMetadataItems =
|
||||
objectMetadataItem.readableFields.filter(
|
||||
(field) =>
|
||||
field.isActive &&
|
||||
(!isHiddenSystemField(field) ||
|
||||
field.id === objectMetadataItem.labelIdentifierFieldMetadataId),
|
||||
);
|
||||
|
||||
const columnDefinitions: ColumnDefinition<FieldMetadata>[] =
|
||||
activeFieldMetadataItems
|
||||
return activeFieldMetadataItems
|
||||
.map((field, index) =>
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
position: index,
|
||||
@@ -47,6 +46,7 @@ export const useColumnDefinitionsFromObjectMetadata = (
|
||||
)
|
||||
.filter(filterAvailableTableColumns)
|
||||
.filter((column) => {
|
||||
const restrictedFieldMetadataIds: string[] = [];
|
||||
return !restrictedFieldMetadataIds.includes(column.fieldMetadataId);
|
||||
})
|
||||
.map((column) => {
|
||||
@@ -65,6 +65,11 @@ export const useColumnDefinitionsFromObjectMetadata = (
|
||||
isSortable: existsInSortDefinitions,
|
||||
};
|
||||
});
|
||||
}, [
|
||||
filterableFieldMetadataItems,
|
||||
sortableFieldMetadataItems,
|
||||
objectMetadataItem,
|
||||
]);
|
||||
|
||||
return {
|
||||
columnDefinitions,
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
|
||||
import {
|
||||
@@ -39,7 +39,7 @@ const meta: Meta<typeof DashboardWidgetPlaceholder> = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
snapshot.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
|
||||
+15
-15
@@ -13,7 +13,7 @@ import { CatalogDecorator, type CatalogStory } from 'twenty-ui/testing';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
@@ -284,7 +284,7 @@ export const WithNumberChart: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(widget);
|
||||
snapshot.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
@@ -372,7 +372,7 @@ export const WithGaugeChart: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(widget);
|
||||
snapshot.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
@@ -464,7 +464,7 @@ export const WithBarChart: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(widget);
|
||||
snapshot.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
@@ -559,7 +559,7 @@ export const SmallWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(widget);
|
||||
snapshot.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
@@ -658,7 +658,7 @@ export const MediumWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(widget);
|
||||
snapshot.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
@@ -757,7 +757,7 @@ export const LargeWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(widget);
|
||||
snapshot.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
@@ -852,7 +852,7 @@ export const WideWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(widget);
|
||||
snapshot.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
@@ -951,7 +951,7 @@ export const TallWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(widget);
|
||||
snapshot.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
@@ -1046,7 +1046,7 @@ export const WithManyToOneRelationFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
PageLayoutType.RECORD_PAGE,
|
||||
@@ -1163,7 +1163,7 @@ export const WithOneToManyRelationFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
PageLayoutType.RECORD_PAGE,
|
||||
@@ -1272,7 +1272,7 @@ export const OneToManyRelationFieldWidgetWithSeeAllButton: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
PageLayoutType.RECORD_PAGE,
|
||||
@@ -1408,7 +1408,7 @@ export const OnMobile: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
PageLayoutType.RECORD_PAGE,
|
||||
@@ -1507,7 +1507,7 @@ export const InSidePanel: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
PageLayoutType.RECORD_PAGE,
|
||||
@@ -1667,7 +1667,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
|
||||
if (state === 'hover') {
|
||||
snapshot.set(
|
||||
|
||||
+18
-18
@@ -10,7 +10,7 @@ import { expect, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
|
||||
@@ -369,7 +369,7 @@ export const TextFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -467,7 +467,7 @@ export const AddressFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -568,7 +568,7 @@ export const NumberFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -666,7 +666,7 @@ export const LinkFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -764,7 +764,7 @@ export const ManyToOneRelationFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -873,7 +873,7 @@ export const OneToManyRelationFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -973,7 +973,7 @@ export const BooleanFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -1070,7 +1070,7 @@ export const CurrencyFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -1167,7 +1167,7 @@ export const EmailsFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
personObjectMetadataItem.id,
|
||||
@@ -1265,7 +1265,7 @@ export const PhonesFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
personObjectMetadataItem.id,
|
||||
@@ -1363,7 +1363,7 @@ export const SelectFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
opportunityObjectMetadataItem.id,
|
||||
@@ -1466,7 +1466,7 @@ export const MultiSelectFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -1568,7 +1568,7 @@ export const TimelineActivityRelationFieldWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
timelineActivityObjectMetadataItem.id,
|
||||
@@ -1676,7 +1676,7 @@ export const ManyToOneRelationCardWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -1793,7 +1793,7 @@ export const OneToManyRelationCardWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
@@ -1892,7 +1892,7 @@ export const TimelineActivityRelationCardWidget: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
timelineActivityObjectMetadataItem.id,
|
||||
@@ -2062,7 +2062,7 @@ export const OneToManyRelationCardWidgetWithProgressiveLoading: Story = {
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(shouldAppBeLoadingState.atom, false);
|
||||
jotaiStore.set(isAppMetadataReadyState.atom, true);
|
||||
const pageLayoutData = createPageLayoutWithWidget(
|
||||
widget,
|
||||
companyObjectMetadataItem.id,
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
|
||||
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
|
||||
|
||||
export const useImpersonationAuth = () => {
|
||||
const { getAuthTokensFromLoginToken } = useAuth();
|
||||
const setShouldAppBeLoading = useSetRecoilStateV2(shouldAppBeLoadingState);
|
||||
const setIsAppEffectRedirectEnabled = useSetRecoilStateV2(
|
||||
isAppEffectRedirectEnabledState,
|
||||
);
|
||||
|
||||
const executeImpersonationAuth = async (loginToken: string) => {
|
||||
setShouldAppBeLoading(true);
|
||||
setIsAppEffectRedirectEnabled(false);
|
||||
await getAuthTokensFromLoginToken(loginToken);
|
||||
setShouldAppBeLoading(false);
|
||||
setIsAppEffectRedirectEnabled(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -207,7 +207,6 @@ Core metadata states used by record-table, record-board, views, etc.
|
||||
```
|
||||
object-metadata/states/lastFieldMetadataItemUpdateState.ts
|
||||
object-metadata/states/objectMetadataItemsState.ts
|
||||
object-metadata/states/shouldAppBeLoadingState.ts
|
||||
```
|
||||
|
||||
### Selectors (12 files)
|
||||
|
||||
@@ -45,6 +45,7 @@ export const createStateV2 = <ValueType>({
|
||||
useCookieStorage.cookieKey,
|
||||
defaultValue,
|
||||
storage,
|
||||
{ getOnInit: true },
|
||||
) as StateAtom<ValueType>;
|
||||
} else if (useLocalStorage) {
|
||||
baseAtom = atomWithStorage<ValueType>(
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { useMetadataStore } from '@/app/hooks/useMetadataStore';
|
||||
import { metadataStoreState } from '@/app/states/metadataStoreState';
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
import {
|
||||
ViewType as CoreViewType,
|
||||
useFindAllCoreViewsQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
const INDEX_VIEW_TYPES = [
|
||||
CoreViewType.TABLE,
|
||||
CoreViewType.KANBAN,
|
||||
CoreViewType.CALENDAR,
|
||||
];
|
||||
|
||||
export const EagerMetadataLoadEffect = () => {
|
||||
const location = useLocation();
|
||||
const isLoggedIn = useIsLogged();
|
||||
const currentUser = useRecoilValueV2(currentUserState);
|
||||
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
|
||||
const store = useStore();
|
||||
|
||||
const objectsEntry = useFamilyRecoilValueV2(metadataStoreState, 'objects');
|
||||
const viewsEntry = useFamilyRecoilValueV2(metadataStoreState, 'views');
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const isOnAuthPath =
|
||||
isMatchingLocation(location, AppPath.Verify) ||
|
||||
isMatchingLocation(location, AppPath.VerifyEmail);
|
||||
|
||||
const { data: queryDataCoreViews } = useFindAllCoreViewsQuery({
|
||||
skip: !isLoggedIn || viewsEntry.status !== 'empty' || isOnAuthPath,
|
||||
variables: { viewTypes: INDEX_VIEW_TYPES },
|
||||
});
|
||||
|
||||
const setIndexCoreViews = useCallback(
|
||||
(indexViews: CoreViewWithRelations[]) => {
|
||||
const existingCoreViews = store.get(coreViewsState.atom);
|
||||
const existingFieldsWidgetViews = existingCoreViews.filter(
|
||||
(view) => view.type === CoreViewType.FIELDS_WIDGET,
|
||||
);
|
||||
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
|
||||
|
||||
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
|
||||
store.set(coreViewsState.atom, mergedViews);
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (objectsEntry.status !== 'empty') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoggedIn && !isDefined(currentUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadObjectMetadata = async () => {
|
||||
if (!isLoggedIn || !isWorkspaceActiveOrSuspended(currentWorkspace)) {
|
||||
await loadMockedObjectMetadataItems();
|
||||
} else {
|
||||
await refreshObjectMetadataItems();
|
||||
}
|
||||
|
||||
const loadedItems = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objects', loadedItems);
|
||||
};
|
||||
|
||||
loadObjectMetadata();
|
||||
}, [
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
isLoggedIn,
|
||||
loadMockedObjectMetadataItems,
|
||||
objectsEntry.status,
|
||||
refreshObjectMetadataItems,
|
||||
store,
|
||||
updateDraft,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(queryDataCoreViews?.getCoreViews)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIndexCoreViews(queryDataCoreViews.getCoreViews);
|
||||
updateDraft('views', queryDataCoreViews.getCoreViews);
|
||||
}, [queryDataCoreViews?.getCoreViews, setIndexCoreViews, updateDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
objectsEntry.status === 'draft_pending' &&
|
||||
viewsEntry.status === 'draft_pending'
|
||||
) {
|
||||
applyChanges();
|
||||
}
|
||||
}, [objectsEntry.status, viewsEntry.status, applyChanges]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { metadataStoreState } from '@/app/states/metadataStoreState';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { recordPageLayoutsState } from '@/page-layout/states/recordPageLayoutsState';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
@@ -8,8 +8,8 @@ import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -29,6 +29,7 @@ export const LazyMetadataLoadEffect = () => {
|
||||
const store = useStore();
|
||||
|
||||
const setLogicFunctions = useSetRecoilState(logicFunctionsState);
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const isOnAuthPath =
|
||||
isMatchingLocation(location, AppPath.Verify) ||
|
||||
@@ -75,14 +76,8 @@ export const LazyMetadataLoadEffect = () => {
|
||||
if (!isDeeplyEqual(existingRecordPageLayouts, recordPageLayouts)) {
|
||||
set(recordPageLayoutsState, recordPageLayouts);
|
||||
}
|
||||
|
||||
store.set(metadataStoreState.atomFamily('pageLayouts'), {
|
||||
current: recordPageLayouts,
|
||||
draft: [],
|
||||
status: 'loaded',
|
||||
});
|
||||
},
|
||||
[store],
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -102,7 +97,14 @@ export const LazyMetadataLoadEffect = () => {
|
||||
queryDataRecordPageLayouts.getPageLayouts.map(transformPageLayout);
|
||||
|
||||
setRecordPageLayouts(transformedPageLayouts);
|
||||
}, [queryDataRecordPageLayouts?.getPageLayouts, setRecordPageLayouts]);
|
||||
updateDraft('pageLayouts', transformedPageLayouts);
|
||||
applyChanges();
|
||||
}, [
|
||||
queryDataRecordPageLayouts?.getPageLayouts,
|
||||
setRecordPageLayouts,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(logicFunctionsData?.findManyLogicFunctions)) {
|
||||
@@ -110,13 +112,14 @@ export const LazyMetadataLoadEffect = () => {
|
||||
}
|
||||
|
||||
setLogicFunctions(logicFunctionsData.findManyLogicFunctions);
|
||||
|
||||
store.set(metadataStoreState.atomFamily('logicFunctions'), {
|
||||
current: logicFunctionsData.findManyLogicFunctions,
|
||||
draft: [],
|
||||
status: 'loaded',
|
||||
});
|
||||
}, [logicFunctionsData?.findManyLogicFunctions, setLogicFunctions, store]);
|
||||
updateDraft('logicFunctions', logicFunctionsData.findManyLogicFunctions);
|
||||
applyChanges();
|
||||
}, [
|
||||
logicFunctionsData?.findManyLogicFunctions,
|
||||
setLogicFunctions,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -16,16 +16,16 @@ import { ClientConfigProviderEffect } from '@/client-config/components/ClientCon
|
||||
import { ApolloCoreClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloCoreClientMockedProvider';
|
||||
|
||||
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
|
||||
import { MetadataGater } from '@/metadata-store/components/MetadataGater';
|
||||
import { MetadataProviderInitialEffects } from '@/metadata-store/effect-components/MetadataProviderInitialEffects';
|
||||
import { IsAppMetadataReadyEffect } from '@/metadata-store/effect-components/IsAppMetadataReadyEffect';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { MetadataProviderEffect } from '@/users/components/MetadataProviderEffect';
|
||||
import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider';
|
||||
import { UserProvider } from '~/modules/users/components/UserProvider';
|
||||
import { mockedApolloClient } from '~/testing/mockedApolloClient';
|
||||
|
||||
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
|
||||
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
|
||||
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
||||
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
|
||||
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
||||
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
|
||||
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
|
||||
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
|
||||
@@ -88,12 +88,12 @@ const Providers = () => {
|
||||
<ApolloStorybookDevLogEffect />
|
||||
<ClientConfigProviderEffect />
|
||||
<ClientConfigProvider>
|
||||
<MetadataProviderEffect />
|
||||
<MetadataProviderInitialEffects />
|
||||
<IsAppMetadataReadyEffect />
|
||||
<WorkspaceProviderEffect />
|
||||
<UserProvider>
|
||||
<MetadataGater>
|
||||
<ApolloCoreClientMockedProvider>
|
||||
<ObjectMetadataItemsLoadEffect />
|
||||
<ObjectMetadataItemsProvider>
|
||||
<PreComputedChipGeneratorsProvider>
|
||||
<FullHeightStorybookLayout>
|
||||
<HelmetProvider>
|
||||
<IconsProvider>
|
||||
@@ -105,10 +105,10 @@ const Providers = () => {
|
||||
</IconsProvider>
|
||||
</HelmetProvider>
|
||||
</FullHeightStorybookLayout>
|
||||
</ObjectMetadataItemsProvider>
|
||||
</PreComputedChipGeneratorsProvider>
|
||||
<MainContextStoreProvider />
|
||||
</ApolloCoreClientMockedProvider>
|
||||
</UserProvider>
|
||||
</MetadataGater>
|
||||
</ClientConfigProvider>
|
||||
</I18nProvider>
|
||||
</ApolloProvider>
|
||||
|
||||
@@ -52,6 +52,7 @@ Commands:
|
||||
auth:switch Switch the default workspace
|
||||
auth:list List all configured workspaces
|
||||
app:dev Watch and sync local application changes
|
||||
app:typecheck Run TypeScript type checking on the application
|
||||
app:uninstall Uninstall application from Twenty
|
||||
entity:add Add a new entity to your application
|
||||
function:logs Watch application function logs
|
||||
@@ -124,6 +125,8 @@ Application development commands.
|
||||
- `twenty app:dev [appPath]` — Start development mode: watch and sync local application changes.
|
||||
- Behavior: Builds your application (functions and front components), computes the manifest, syncs everything to your workspace, then watches the directory for changes and re-syncs automatically. Displays an interactive UI showing build and sync status in real time. Press Ctrl+C to stop.
|
||||
|
||||
- `twenty app:typecheck [appPath]` — Run TypeScript type checking on the application (runs `tsc --noEmit`). Exits with code 1 if type errors are found.
|
||||
|
||||
- `twenty app:uninstall [appPath]` — Uninstall the application from the current workspace.
|
||||
|
||||
### Entity
|
||||
@@ -166,6 +169,9 @@ twenty app:dev
|
||||
# Start dev mode with a custom workspace profile
|
||||
twenty app:dev --workspace my-custom-workspace
|
||||
|
||||
# Type check the application
|
||||
twenty app:typecheck
|
||||
|
||||
# Add a new entity interactively
|
||||
twenty entity:add
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { formatPath } from '@/cli/utilities/file/file-path';
|
||||
import chalk from 'chalk';
|
||||
import type { Command } from 'commander';
|
||||
import { AppDevCommand } from './app/app-dev';
|
||||
import { AppTypecheckCommand } from './app/app-typecheck';
|
||||
import { AppUninstallCommand } from './app/app-uninstall';
|
||||
import { AuthListCommand } from './auth/auth-list';
|
||||
import { AuthLoginCommand } from './auth/auth-login';
|
||||
@@ -60,6 +61,7 @@ export const registerCommands = (program: Command): void => {
|
||||
|
||||
// App commands
|
||||
const devCommand = new AppDevCommand();
|
||||
const typecheckCommand = new AppTypecheckCommand();
|
||||
const uninstallCommand = new AppUninstallCommand();
|
||||
const addCommand = new EntityAddCommand();
|
||||
const logsCommand = new LogicFunctionLogsCommand();
|
||||
@@ -74,6 +76,15 @@ export const registerCommands = (program: Command): void => {
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:typecheck [appPath]')
|
||||
.description('Run TypeScript type checking on the application')
|
||||
.action(async (appPath) => {
|
||||
await typecheckCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:uninstall [appPath]')
|
||||
.description('Uninstall application from Twenty')
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import {
|
||||
runTypecheck,
|
||||
type TypecheckError,
|
||||
} from '@/cli/utilities/build/common/typecheck-plugin';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type AppTypecheckOptions = {
|
||||
appPath?: string;
|
||||
};
|
||||
|
||||
const formatTypecheckError = (error: TypecheckError): string => {
|
||||
return `${chalk.cyan(error.file)}:${chalk.yellow(String(error.line))}:${chalk.yellow(String(error.column + 1))} - ${chalk.red('error')} ${error.text}`;
|
||||
};
|
||||
|
||||
export class AppTypecheckCommand {
|
||||
async execute(options: AppTypecheckOptions): Promise<void> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
console.log(chalk.blue('Running type check...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const errors = await runTypecheck(appPath);
|
||||
|
||||
if (errors.length === 0) {
|
||||
console.log(chalk.green('✓ No type errors found'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const error of errors) {
|
||||
console.log(formatTypecheckError(error));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(
|
||||
chalk.red(
|
||||
`✗ Found ${errors.length} type error${errors.length === 1 ? '' : 's'}`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -356,9 +356,27 @@ export class ApiService {
|
||||
message: `Successfully synced application: ${manifest.application.displayName}`,
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const graphqlErrors = error.response.data?.errors;
|
||||
|
||||
if (Array.isArray(graphqlErrors) && graphqlErrors.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: graphqlErrors[0]?.message || error.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error.response.data?.message ||
|
||||
`HTTP ${error.response.status}: ${error.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
error: error instanceof Error ? error.message : error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -13,7 +13,7 @@ export const getDefaultObjectFields = (
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
defaultValue: 'uuid',
|
||||
type: FieldMetadataType.UUID,
|
||||
type: FieldMetadataType.UUID as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'id',
|
||||
@@ -27,7 +27,7 @@ export const getDefaultObjectFields = (
|
||||
icon: 'IconAbc',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TEXT,
|
||||
type: FieldMetadataType.TEXT as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'name',
|
||||
@@ -41,7 +41,7 @@ export const getDefaultObjectFields = (
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
type: FieldMetadataType.DATE_TIME as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'createdAt',
|
||||
@@ -55,7 +55,7 @@ export const getDefaultObjectFields = (
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
type: FieldMetadataType.DATE_TIME as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'updatedAt',
|
||||
@@ -69,7 +69,7 @@ export const getDefaultObjectFields = (
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
type: FieldMetadataType.DATE_TIME as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'deletedAt',
|
||||
@@ -83,7 +83,7 @@ export const getDefaultObjectFields = (
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR,
|
||||
type: FieldMetadataType.ACTOR as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'createdBy',
|
||||
@@ -97,7 +97,7 @@ export const getDefaultObjectFields = (
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR,
|
||||
type: FieldMetadataType.ACTOR as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'updatedBy',
|
||||
|
||||
+7
-5
@@ -40,7 +40,8 @@ export type OrchestratorStateFileStatus =
|
||||
| 'pending'
|
||||
| 'building'
|
||||
| 'uploading'
|
||||
| 'success';
|
||||
| 'success'
|
||||
| 'error';
|
||||
|
||||
export type OrchestratorStateEntityInfo = {
|
||||
name: string;
|
||||
@@ -81,10 +82,11 @@ const FILE_STATUS_TRANSITION_MATRIX: Record<
|
||||
OrchestratorStateFileStatus,
|
||||
OrchestratorStateFileStatus[]
|
||||
> = {
|
||||
pending: ['building', 'uploading', 'success'],
|
||||
building: ['pending', 'uploading', 'success'],
|
||||
uploading: ['pending', 'success'],
|
||||
success: ['pending', 'building', 'uploading'],
|
||||
pending: ['building', 'uploading', 'success', 'error'],
|
||||
building: ['pending', 'uploading', 'success', 'error'],
|
||||
uploading: ['pending', 'success', 'error'],
|
||||
success: ['pending', 'building', 'uploading', 'error'],
|
||||
error: ['pending', 'building', 'uploading', 'success'],
|
||||
};
|
||||
|
||||
export class OrchestratorState {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step';
|
||||
import { SyncApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step';
|
||||
import { UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step';
|
||||
import { serializeError } from '@/cli/utilities/error/serialize-error';
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { OUTPUT_DIR, type Manifest } from 'twenty-shared/application';
|
||||
@@ -143,10 +144,11 @@ export class DevModeOrchestrator {
|
||||
await this.runSyncPipeline();
|
||||
} catch (error) {
|
||||
this.state.addEvent({
|
||||
message: `Sync failed with error ${JSON.stringify(error, null, 2)}`,
|
||||
message: `Sync failed with error: ${serializeError(error)}`,
|
||||
status: 'error',
|
||||
});
|
||||
this.state.updatePipeline({ status: 'error' });
|
||||
this.state.updateAllEntitiesStatus('error');
|
||||
} finally {
|
||||
this.state.updatePipeline({ isSyncing: false });
|
||||
}
|
||||
|
||||
+3
-1
@@ -7,6 +7,7 @@ import {
|
||||
type OrchestratorStateStepEvent,
|
||||
type OrchestratorStateSyncStatus,
|
||||
} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
import { serializeError } from '@/cli/utilities/error/serialize-error';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
export type SyncApplicationOrchestratorStepOutput = {
|
||||
@@ -73,12 +74,13 @@ export class SyncApplicationOrchestratorStep {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage = `Sync failed with error ${JSON.stringify(syncResult.error, null, 2)}`;
|
||||
const errorMessage = `Sync failed with error: ${serializeError(syncResult.error)}`;
|
||||
|
||||
events.push({ message: errorMessage, status: 'error' });
|
||||
step.output = { syncStatus: 'error', error: errorMessage };
|
||||
step.status = 'error';
|
||||
this.state.updatePipeline({ status: 'error', error: errorMessage });
|
||||
this.state.updateAllEntitiesStatus('error');
|
||||
this.state.applyStepEvents(events);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,11 @@ export const DevUiEntityLegend = (): React.ReactElement => {
|
||||
<Text color={DEV_UI_STATUS_CONFIG.done.color}>
|
||||
{DEV_UI_STATUS_CONFIG.done.icon}
|
||||
</Text>{' '}
|
||||
success
|
||||
success{' '}
|
||||
<Text color={DEV_UI_STATUS_CONFIG.error.color}>
|
||||
{DEV_UI_STATUS_CONFIG.error.icon}
|
||||
</Text>{' '}
|
||||
error
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -49,6 +49,7 @@ export const mapFileStatusToDevUiStatus = (
|
||||
building: 'in_progress',
|
||||
uploading: 'uploading',
|
||||
success: 'done',
|
||||
error: 'error',
|
||||
};
|
||||
|
||||
return mapping[status];
|
||||
@@ -182,6 +183,7 @@ export const getPipelineRows = (
|
||||
): DevUiPipelineRow[] => {
|
||||
const entities = [...state.entities.values()];
|
||||
|
||||
const hasError = entities.some((entity) => entity.status === 'error');
|
||||
const isBuilding = entities.some((entity) => entity.status === 'building');
|
||||
const allUploaded =
|
||||
entities.length > 0 &&
|
||||
@@ -189,11 +191,13 @@ export const getPipelineRows = (
|
||||
(entity) => entity.status === 'uploading' || entity.status === 'success',
|
||||
);
|
||||
|
||||
const resourcesBuildStatus: OrchestratorStateStepStatus = isBuilding
|
||||
? 'in_progress'
|
||||
: allUploaded
|
||||
? 'done'
|
||||
: 'idle';
|
||||
const resourcesBuildStatus: OrchestratorStateStepStatus = hasError
|
||||
? 'error'
|
||||
: isBuilding
|
||||
? 'in_progress'
|
||||
: allUploaded
|
||||
? 'done'
|
||||
: 'idle';
|
||||
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const serializeError = (error: unknown): string => {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const parts: string[] = [];
|
||||
const status = error.response?.status;
|
||||
const statusText = error.response?.statusText;
|
||||
|
||||
if (status) {
|
||||
parts.push(`HTTP ${status}${statusText ? ` ${statusText}` : ''}`);
|
||||
}
|
||||
|
||||
const graphqlErrors = error.response?.data?.errors;
|
||||
|
||||
if (Array.isArray(graphqlErrors) && graphqlErrors.length > 0) {
|
||||
const messages = graphqlErrors
|
||||
.map(
|
||||
(graphqlError: { message?: string }) =>
|
||||
graphqlError.message ?? 'Unknown GraphQL error',
|
||||
)
|
||||
.join('; ');
|
||||
|
||||
parts.push(messages);
|
||||
} else if (error.response?.data?.message) {
|
||||
parts.push(error.response.data.message);
|
||||
} else if (error.message) {
|
||||
parts.push(error.message);
|
||||
}
|
||||
|
||||
if (error.code) {
|
||||
parts.push(`(${error.code})`);
|
||||
}
|
||||
|
||||
return parts.join(' - ') || 'Unknown Axios error';
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message || error.toString();
|
||||
}
|
||||
|
||||
const stringified = JSON.stringify(error, null, 2);
|
||||
|
||||
if (stringified === '{}' || stringified === undefined) {
|
||||
return String(error);
|
||||
}
|
||||
|
||||
return stringified;
|
||||
};
|
||||
@@ -248,14 +248,14 @@
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "packages/twenty-server",
|
||||
"command": "nx ts-node-no-deps -- src/database/clickHouse/migrations/run-migrations.ts"
|
||||
"command": "nx ts-node-no-deps-transpile-only -- src/database/clickHouse/migrations/run-migrations.ts"
|
||||
}
|
||||
},
|
||||
"clickhouse:seed": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "packages/twenty-server",
|
||||
"command": "nx ts-node-no-deps -- src/database/clickHouse/seeds/run-seeds.ts"
|
||||
"command": "nx ts-node-no-deps-transpile-only -- src/database/clickHouse/seeds/run-seeds.ts"
|
||||
}
|
||||
},
|
||||
"lingui:extract": {
|
||||
|
||||
Reference in New Issue
Block a user