diff --git a/packages/twenty-front/jest.config.mjs b/packages/twenty-front/jest.config.mjs index ded5c4f426..e068787970 100644 --- a/packages/twenty-front/jest.config.mjs +++ b/packages/twenty-front/jest.config.mjs @@ -62,7 +62,7 @@ const jestConfig = { coverageThreshold: { global: { statements: 48.8, - lines: 47.5, + lines: 47.0, functions: 39.5, }, }, diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index fe059659b8..be0483f4d5 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -2419,6 +2419,35 @@ export type MetadataEventWithQueryIds = { queryIds: Array; }; +export type MinimalMetadata = { + __typename?: 'MinimalMetadata'; + metadataVersion: Scalars['Int']; + objectMetadataItems: Array; + views: Array; +}; + +export type MinimalObjectMetadata = { + __typename?: 'MinimalObjectMetadata'; + icon?: Maybe; + id: Scalars['UUID']; + isActive: Scalars['Boolean']; + isCustom: Scalars['Boolean']; + isRemote: Scalars['Boolean']; + isSystem: Scalars['Boolean']; + labelPlural: Scalars['String']; + labelSingular: Scalars['String']; + namePlural: Scalars['String']; + nameSingular: Scalars['String']; +}; + +export type MinimalView = { + __typename?: 'MinimalView'; + id: Scalars['UUID']; + key?: Maybe; + objectMetadataId: Scalars['UUID']; + type: ViewType; +}; + export enum ModelFamily { ANTHROPIC = 'ANTHROPIC', GOOGLE = 'GOOGLE', @@ -4108,6 +4137,7 @@ export type Query = { indexMetadatas: IndexConnection; lineChartData: LineChartData; listPlans: Array; + minimalMetadata: MinimalMetadata; navigationMenuItem?: Maybe; navigationMenuItems: Array; object: Object; diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx index 3b7da864ab..b03a42041f 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx @@ -3,7 +3,9 @@ import { CommandMenuConfirmationModalManager } from '@/command-menu-item/confirm 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 { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect'; import { MetadataProviderInitialEffects } from '@/metadata-store/effect-components/MetadataProviderInitialEffects'; import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect'; import { AuthProvider } from '@/auth/components/AuthProvider'; @@ -43,6 +45,7 @@ export const AppRouterProviders = () => { + diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/FieldMetadataSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/FieldMetadataSSEEffect.tsx index 2b9b06b1ea..f44521b224 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/FieldMetadataSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/FieldMetadataSSEEffect.tsx @@ -1,6 +1,5 @@ import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent'; -import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; -import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem'; +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'; @@ -21,51 +20,11 @@ export const FieldMetadataSSEEffect = () => { useListenToMetadataOperationBrowserEvent({ metadataName: AllMetadataName.fieldMetadata, onMetadataOperationBrowserEvent: (eventDetail) => { - const entry = store.get( - metadataStoreState.atomFamily('fieldMetadataItems'), + patchMetadataStoreFromSSEEvent( + store, + 'fieldMetadataItems', + eventDetail.operation, ); - const currentFields = entry.current as FlatFieldMetadataItem[]; - - switch (eventDetail.operation.type) { - case 'create': { - const createdField = eventDetail.operation - .createdRecord as unknown as FlatFieldMetadataItem; - - store.set(metadataStoreState.atomFamily('fieldMetadataItems'), { - ...entry, - current: [...currentFields, createdField], - }); - break; - } - case 'update': { - const updatedField = eventDetail.operation - .updatedRecord as unknown as FlatFieldMetadataItem; - - store.set(metadataStoreState.atomFamily('fieldMetadataItems'), { - ...entry, - current: currentFields.map((field) => - field.id === updatedField.id - ? { ...field, ...updatedField } - : field, - ), - }); - break; - } - case 'delete': { - const deletedFieldId = eventDetail.operation - .deletedRecordId as string; - - store.set(metadataStoreState.atomFamily('fieldMetadataItems'), { - ...entry, - current: currentFields.filter( - (field) => field.id !== deletedFieldId, - ), - }); - break; - } - default: - return; - } }, }); diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx new file mode 100644 index 0000000000..7878587ae2 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx @@ -0,0 +1,63 @@ +import { useIsLogged } from '@/auth/hooks/useIsLogged'; +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 { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useStore } from 'jotai'; +import { useEffect, useState } from 'react'; +import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace'; + +export const MinimalMetadataLoadEffect = () => { + const isLoggedIn = useIsLogged(); + const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState); + const currentWorkspace = useAtomStateValue(currentWorkspaceState); + const store = useStore(); + const [hasLoaded, setHasLoaded] = useState(false); + + const { loadMinimalMetadata } = useLoadMinimalMetadata(); + + useEffect(() => { + if (hasLoaded) { + return; + } + + if (!isCurrentUserLoaded || !isLoggedIn) { + return; + } + + if (!isWorkspaceActiveOrSuspended(currentWorkspace)) { + return; + } + + const objectsEntry = store.get( + metadataStoreState.atomFamily('objectMetadataItems'), + ); + const viewsEntry = store.get(metadataStoreState.atomFamily('views')); + + 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); + }; + + load(); + }, [ + hasLoaded, + isCurrentUserLoaded, + isLoggedIn, + currentWorkspace, + store, + loadMinimalMetadata, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/NavigationMenuItemSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/NavigationMenuItemSSEEffect.tsx index 824e612da0..ac84c01155 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/NavigationMenuItemSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/NavigationMenuItemSSEEffect.tsx @@ -1,5 +1,6 @@ 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'; @@ -28,7 +29,13 @@ export const NavigationMenuItemSSEEffect = () => { useListenToMetadataOperationBrowserEvent({ metadataName: AllMetadataName.navigationMenuItem, - onMetadataOperationBrowserEvent: async () => { + onMetadataOperationBrowserEvent: async (eventDetail) => { + patchMetadataStoreFromSSEEvent( + store, + 'navigationMenuItems', + eventDetail.operation, + ); + const result = await client.query({ query: FindManyNavigationMenuItemsDocument, fetchPolicy: 'network-only', diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/ObjectMetadataItemSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/ObjectMetadataItemSSEEffect.tsx index 3cbe602bf5..ef0ce959d8 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/ObjectMetadataItemSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/ObjectMetadataItemSSEEffect.tsx @@ -1,6 +1,5 @@ import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent'; -import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; -import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem'; +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'; @@ -28,51 +27,11 @@ export const ObjectMetadataItemSSEEffect = () => { useListenToMetadataOperationBrowserEvent({ metadataName: AllMetadataName.objectMetadata, onMetadataOperationBrowserEvent: async (eventDetail) => { - const entry = store.get( - metadataStoreState.atomFamily('objectMetadataItems'), + patchMetadataStoreFromSSEEvent( + store, + 'objectMetadataItems', + eventDetail.operation, ); - const currentObjects = entry.current as FlatObjectMetadataItem[]; - - switch (eventDetail.operation.type) { - case 'create': { - const createdObject = eventDetail.operation - .createdRecord as unknown as FlatObjectMetadataItem; - - store.set(metadataStoreState.atomFamily('objectMetadataItems'), { - ...entry, - current: [...currentObjects, createdObject], - }); - break; - } - case 'update': { - const updatedObject = eventDetail.operation - .updatedRecord as unknown as FlatObjectMetadataItem; - - store.set(metadataStoreState.atomFamily('objectMetadataItems'), { - ...entry, - current: currentObjects.map((object) => - object.id === updatedObject.id - ? { ...object, ...updatedObject } - : object, - ), - }); - break; - } - case 'delete': { - const deletedObjectId = eventDetail.operation - .deletedRecordId as string; - - store.set(metadataStoreState.atomFamily('objectMetadataItems'), { - ...entry, - current: currentObjects.filter( - (object) => object.id !== deletedObjectId, - ), - }); - break; - } - default: - return; - } const navigationMenuItemsResult = await client.query({ query: FindManyNavigationMenuItemsDocument, diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutSSEEffect.tsx index 3ffa51a362..fbe8418686 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutSSEEffect.tsx @@ -1,11 +1,14 @@ 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({ @@ -18,7 +21,13 @@ export const PageLayoutSSEEffect = () => { useListenToMetadataOperationBrowserEvent({ metadataName: AllMetadataName.pageLayout, - onMetadataOperationBrowserEvent: () => { + onMetadataOperationBrowserEvent: (eventDetail) => { + patchMetadataStoreFromSSEEvent( + store, + 'pageLayouts', + eventDetail.operation, + ); + refreshPageLayouts(); }, }); diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutTabSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutTabSSEEffect.tsx index 55093348cd..4efa02f700 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutTabSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutTabSSEEffect.tsx @@ -1,11 +1,14 @@ 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-sse-effect'; + const queryId = 'page-layout-tab-sse-effect'; + const store = useStore(); const { refreshPageLayouts } = useRefreshPageLayouts(); useListenToEventsForQuery({ @@ -18,7 +21,13 @@ export const PageLayoutTabSSEEffect = () => { useListenToMetadataOperationBrowserEvent({ metadataName: AllMetadataName.pageLayoutTab, - onMetadataOperationBrowserEvent: () => { + onMetadataOperationBrowserEvent: (eventDetail) => { + patchMetadataStoreFromSSEEvent( + store, + 'pageLayoutTabs', + eventDetail.operation, + ); + refreshPageLayouts(); }, }); diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutWidgetSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutWidgetSSEEffect.tsx index 2b67f6f7e6..74dcb85624 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutWidgetSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutWidgetSSEEffect.tsx @@ -1,11 +1,14 @@ 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({ @@ -18,7 +21,13 @@ export const PageLayoutWidgetSSEEffect = () => { useListenToMetadataOperationBrowserEvent({ metadataName: AllMetadataName.pageLayoutWidget, - onMetadataOperationBrowserEvent: () => { + onMetadataOperationBrowserEvent: (eventDetail) => { + patchMetadataStoreFromSSEEvent( + store, + 'pageLayoutWidgets', + eventDetail.operation, + ); + refreshPageLayouts(); }, }); diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFieldSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFieldSSEEffect.tsx index d9014683b0..6b706fd251 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFieldSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFieldSSEEffect.tsx @@ -1,4 +1,5 @@ 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'; @@ -28,6 +29,12 @@ export const ViewFieldSSEEffect = () => { useListenToMetadataOperationBrowserEvent({ metadataName: AllMetadataName.viewField, onMetadataOperationBrowserEvent: (eventDetail) => { + patchMetadataStoreFromSSEEvent( + store, + 'viewFields', + eventDetail.operation, + ); + const coreViews = store.get(coreViewsState.atom); let viewId: string | undefined; diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFilterGroupSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFilterGroupSSEEffect.tsx new file mode 100644 index 0000000000..cf25b07098 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFilterGroupSSEEffect.tsx @@ -0,0 +1,72 @@ +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, + ); + + 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; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFilterSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFilterSSEEffect.tsx new file mode 100644 index 0000000000..0978da19a0 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewFilterSSEEffect.tsx @@ -0,0 +1,70 @@ +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, + ); + + 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; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSSEEffect.tsx index b4e7024ffb..6cdf8c836b 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSSEEffect.tsx @@ -1,4 +1,5 @@ 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'; @@ -26,6 +27,8 @@ export const ViewSSEEffect = () => { useListenToMetadataOperationBrowserEvent({ metadataName: AllMetadataName.view, onMetadataOperationBrowserEvent: async (eventDetail) => { + patchMetadataStoreFromSSEEvent(store, 'views', eventDetail.operation); + switch (eventDetail.operation.type) { case 'create': { const createdView = eventDetail.operation diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSortSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSortSSEEffect.tsx new file mode 100644 index 0000000000..44b7bc6ce3 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSortSSEEffect.tsx @@ -0,0 +1,66 @@ +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); + + 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; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/graphql/queries/findMinimalMetadata.ts b/packages/twenty-front/src/modules/metadata-store/graphql/queries/findMinimalMetadata.ts new file mode 100644 index 0000000000..117e9f1c7d --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/graphql/queries/findMinimalMetadata.ts @@ -0,0 +1,27 @@ +import { gql } from '@apollo/client'; + +export const FIND_MINIMAL_METADATA = gql` + query FindMinimalMetadata { + minimalMetadata { + objectMetadataItems { + id + nameSingular + namePlural + labelSingular + labelPlural + icon + isCustom + isActive + isSystem + isRemote + } + views { + id + type + key + objectMetadataId + } + metadataVersion + } + } +`; diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useFetchAndLoadIndexViews.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useFetchAndLoadIndexViews.ts index 7f65c805c6..959fbd0bbe 100644 --- a/packages/twenty-front/src/modules/metadata-store/hooks/useFetchAndLoadIndexViews.ts +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useFetchAndLoadIndexViews.ts @@ -1,6 +1,6 @@ import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; import { useSetIndexViews } from '@/metadata-store/hooks/useSetIndexViews'; -import { type View } from '@/views/types/View'; +import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated'; import { useCallback } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { useApolloClient } from '@apollo/client/react'; @@ -25,8 +25,24 @@ export const useFetchAndLoadIndexViews = () => { if (isDefined(result.data?.getCoreViews)) { setIndexViews(result.data.getCoreViews); - // TODO: align generated ViewType with app ViewType to remove this cast - updateDraft('views', result.data.getCoreViews as unknown as View[]); + + 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]); diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMinimalMetadata.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMinimalMetadata.ts new file mode 100644 index 0000000000..e4945c9543 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMinimalMetadata.ts @@ -0,0 +1,70 @@ +import { FIND_MINIMAL_METADATA } from '@/metadata-store/graphql/queries/findMinimalMetadata'; +import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; +import { metadataVersionState } from '@/metadata-store/states/metadataVersionState'; +import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem'; +import { type FlatView } from '@/metadata-store/types/FlatView'; +import { type FindMinimalMetadataQuery } from '@/metadata-store/types/MinimalMetadata'; +import { useApolloClient } from '@apollo/client/react'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +export const useLoadMinimalMetadata = () => { + const client = useApolloClient(); + const store = useStore(); + + const loadMinimalMetadata = useCallback(async () => { + const result = await client.query({ + query: FIND_MINIMAL_METADATA, + fetchPolicy: 'network-only', + }); + + if (!isDefined(result.data?.minimalMetadata)) { + return null; + } + + const { objectMetadataItems, views, metadataVersion } = + result.data.minimalMetadata; + + const localMetadataVersion = store.get(metadataVersionState.atom); + + // Skip hydration if data already loaded with same or newer version + if ( + isDefined(localMetadataVersion) && + localMetadataVersion >= metadataVersion + ) { + return { metadataVersion, isStale: false }; + } + + 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', + }); + } + + const viewsEntry = store.get(metadataStoreState.atomFamily('views')); + + if (viewsEntry.status === 'empty') { + store.set(metadataStoreState.atomFamily('views'), { + current: views as unknown as FlatView[], + draft: [], + status: 'up-to-date', + }); + } + + store.set(metadataVersionState.atom, metadataVersion); + + const isStale = + isDefined(localMetadataVersion) && localMetadataVersion < metadataVersion; + + return { metadataVersion, isStale }; + }, [client, store]); + + return { loadMinimalMetadata }; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useReloadWorkspaceMetadata.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useReloadWorkspaceMetadata.ts index 2b6746f463..9f5c6f3d95 100644 --- a/packages/twenty-front/src/modules/metadata-store/hooks/useReloadWorkspaceMetadata.ts +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useReloadWorkspaceMetadata.ts @@ -1,7 +1,7 @@ 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 { type View } from '@/views/types/View'; import { coreViewsState } from '@/views/states/coreViewState'; import { useStore } from 'jotai'; import { useCallback } from 'react'; @@ -17,9 +17,24 @@ export const useReloadWorkspaceMetadata = () => { await refreshObjectMetadataItems(); - // TODO: align generated ViewType with app ViewType to remove this cast - const loadedViews = store.get(coreViewsState.atom) as unknown as View[]; - updateDraft('views', loadedViews); + 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, diff --git a/packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts b/packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts index cce7ff57c2..dcea5b2f96 100644 --- a/packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts +++ b/packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts @@ -13,9 +13,23 @@ export const ALL_METADATA_ENTITY_KEYS = [ 'viewFields', 'viewFilters', 'viewSorts', + 'viewGroups', + 'viewFilterGroups', + 'viewFieldGroups', 'pageLayouts', + 'pageLayoutTabs', + 'pageLayoutWidgets', 'logicFunctions', 'navigationMenuItems', + 'commandMenuItems', + 'frontComponents', + 'webhooks', + 'roles', + 'roleTargets', + 'agents', + 'skills', + 'rowLevelPermissionPredicates', + 'rowLevelPermissionPredicateGroups', ] as const; export type MetadataEntityKey = (typeof ALL_METADATA_ENTITY_KEYS)[number]; @@ -38,4 +52,6 @@ export const metadataStoreState = createAtomFamilyState< >({ key: 'metadataStoreState', defaultValue: METADATA_STORE_ITEM_INITIAL_VALUE, + useLocalStorage: true, + localStorageOptions: { getOnInit: true }, }); diff --git a/packages/twenty-front/src/modules/metadata-store/states/metadataVersionState.ts b/packages/twenty-front/src/modules/metadata-store/states/metadataVersionState.ts new file mode 100644 index 0000000000..b78166033f --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/states/metadataVersionState.ts @@ -0,0 +1,8 @@ +import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; + +export const metadataVersionState = createAtomState({ + key: 'metadataVersionState', + defaultValue: null, + useLocalStorage: true, + localStorageOptions: { getOnInit: true }, +}); diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatAgent.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatAgent.ts new file mode 100644 index 0000000000..692934965b --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatAgent.ts @@ -0,0 +1,3 @@ +import { type Agent } from '~/generated-metadata/graphql'; + +export type FlatAgent = Agent; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatCommandMenuItem.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatCommandMenuItem.ts new file mode 100644 index 0000000000..72040d239d --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatCommandMenuItem.ts @@ -0,0 +1,3 @@ +import { type CommandMenuItem } from '~/generated-metadata/graphql'; + +export type FlatCommandMenuItem = Omit; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatFrontComponent.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatFrontComponent.ts new file mode 100644 index 0000000000..46f74307be --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatFrontComponent.ts @@ -0,0 +1,3 @@ +import { type FrontComponent } from '~/generated-metadata/graphql'; + +export type FlatFrontComponent = Omit; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatLogicFunction.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatLogicFunction.ts new file mode 100644 index 0000000000..981daa6a23 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatLogicFunction.ts @@ -0,0 +1,3 @@ +import { type LogicFunction } from '@/settings/logic-functions/states/logicFunctionsState'; + +export type FlatLogicFunction = LogicFunction; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatNavigationMenuItem.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatNavigationMenuItem.ts new file mode 100644 index 0000000000..71b06569bc --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatNavigationMenuItem.ts @@ -0,0 +1,3 @@ +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +export type FlatNavigationMenuItem = NavigationMenuItem; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatPageLayout.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatPageLayout.ts new file mode 100644 index 0000000000..f0980890af --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatPageLayout.ts @@ -0,0 +1,3 @@ +import { type PageLayout } from '@/page-layout/types/PageLayout'; + +export type FlatPageLayout = Omit; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatPageLayoutTab.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatPageLayoutTab.ts new file mode 100644 index 0000000000..76e7ed97ac --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatPageLayoutTab.ts @@ -0,0 +1,5 @@ +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; + +export type FlatPageLayoutTab = Omit & { + pageLayoutId: string; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatPageLayoutWidget.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatPageLayoutWidget.ts new file mode 100644 index 0000000000..ca3b654f3a --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatPageLayoutWidget.ts @@ -0,0 +1,5 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; + +export type FlatPageLayoutWidget = PageLayoutWidget & { + pageLayoutTabId: string; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatRole.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatRole.ts new file mode 100644 index 0000000000..212aa10f30 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatRole.ts @@ -0,0 +1,13 @@ +import { type Role } from '~/generated-metadata/graphql'; + +export type FlatRole = Omit< + Role, + | 'agents' + | 'apiKeys' + | 'workspaceMembers' + | 'fieldPermissions' + | 'objectPermissions' + | 'permissionFlags' + | 'rowLevelPermissionPredicateGroups' + | 'rowLevelPermissionPredicates' +>; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatRoleTarget.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatRoleTarget.ts new file mode 100644 index 0000000000..8fa76c212d --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatRoleTarget.ts @@ -0,0 +1,7 @@ +export type FlatRoleTarget = { + id: string; + roleId: string; + userWorkspaceId: string | null; + agentId: string | null; + apiKeyId: string | null; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatRowLevelPermissionPredicate.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatRowLevelPermissionPredicate.ts new file mode 100644 index 0000000000..deff729c7a --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatRowLevelPermissionPredicate.ts @@ -0,0 +1,3 @@ +import { type RowLevelPermissionPredicate } from '~/generated-metadata/graphql'; + +export type FlatRowLevelPermissionPredicate = RowLevelPermissionPredicate; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatRowLevelPermissionPredicateGroup.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatRowLevelPermissionPredicateGroup.ts new file mode 100644 index 0000000000..fe9c43fdce --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatRowLevelPermissionPredicateGroup.ts @@ -0,0 +1,4 @@ +import { type RowLevelPermissionPredicateGroup } from '~/generated-metadata/graphql'; + +export type FlatRowLevelPermissionPredicateGroup = + RowLevelPermissionPredicateGroup; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatSkill.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatSkill.ts new file mode 100644 index 0000000000..dad67ec795 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatSkill.ts @@ -0,0 +1,3 @@ +import { type Skill } from '~/generated-metadata/graphql'; + +export type FlatSkill = Skill; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatView.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatView.ts new file mode 100644 index 0000000000..3e45d2d321 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatView.ts @@ -0,0 +1,11 @@ +import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; + +export type FlatView = Omit< + CoreViewWithRelations, + | 'viewFields' + | 'viewFieldGroups' + | 'viewGroups' + | 'viewFilters' + | 'viewFilterGroups' + | 'viewSorts' +>; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatViewField.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatViewField.ts new file mode 100644 index 0000000000..29751a07d8 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatViewField.ts @@ -0,0 +1,5 @@ +import { type CoreViewFieldEssential } from '@/views/types/CoreViewWithRelations'; + +export type FlatViewField = CoreViewFieldEssential & { + viewId: string; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatViewFieldGroup.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatViewFieldGroup.ts new file mode 100644 index 0000000000..a8d703c99a --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatViewFieldGroup.ts @@ -0,0 +1,6 @@ +import { type CoreViewFieldGroupEssential } from '@/views/types/CoreViewWithRelations'; + +export type FlatViewFieldGroup = Omit< + CoreViewFieldGroupEssential, + 'viewFields' +>; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatViewFilter.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatViewFilter.ts new file mode 100644 index 0000000000..22b21c13c2 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatViewFilter.ts @@ -0,0 +1,5 @@ +import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; + +export type FlatViewFilter = CoreViewWithRelations['viewFilters'][number] & { + viewId: string; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatViewFilterGroup.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatViewFilterGroup.ts new file mode 100644 index 0000000000..e798f2452d --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatViewFilterGroup.ts @@ -0,0 +1,7 @@ +import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; + +type ViewFilterGroupRelation = NonNullable< + CoreViewWithRelations['viewFilterGroups'] +>[number]; + +export type FlatViewFilterGroup = ViewFilterGroupRelation; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatViewGroup.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatViewGroup.ts new file mode 100644 index 0000000000..0b45cce1e1 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatViewGroup.ts @@ -0,0 +1,5 @@ +import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; + +export type FlatViewGroup = CoreViewWithRelations['viewGroups'][number] & { + viewId: string; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatViewSort.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatViewSort.ts new file mode 100644 index 0000000000..4eb30fd504 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatViewSort.ts @@ -0,0 +1,5 @@ +import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; + +export type FlatViewSort = CoreViewWithRelations['viewSorts'][number] & { + viewId: string; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/types/FlatWebhook.ts b/packages/twenty-front/src/modules/metadata-store/types/FlatWebhook.ts new file mode 100644 index 0000000000..f37648ede4 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/FlatWebhook.ts @@ -0,0 +1,3 @@ +import { type Webhook } from '~/generated-metadata/graphql'; + +export type FlatWebhook = Webhook; diff --git a/packages/twenty-front/src/modules/metadata-store/types/MetadataEntityTypeMap.ts b/packages/twenty-front/src/modules/metadata-store/types/MetadataEntityTypeMap.ts index 2e9fe2ddfa..72b52e56b5 100644 --- a/packages/twenty-front/src/modules/metadata-store/types/MetadataEntityTypeMap.ts +++ b/packages/twenty-front/src/modules/metadata-store/types/MetadataEntityTypeMap.ts @@ -1,23 +1,51 @@ +import { type FlatAgent } from '@/metadata-store/types/FlatAgent'; +import { type FlatCommandMenuItem } from '@/metadata-store/types/FlatCommandMenuItem'; import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem'; +import { type FlatFrontComponent } from '@/metadata-store/types/FlatFrontComponent'; import { type FlatIndexMetadataItem } from '@/metadata-store/types/FlatIndexMetadataItem'; +import { type FlatLogicFunction } from '@/metadata-store/types/FlatLogicFunction'; +import { type FlatNavigationMenuItem } from '@/metadata-store/types/FlatNavigationMenuItem'; import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem'; -import { type PageLayout } from '@/page-layout/types/PageLayout'; -import { type LogicFunction } from '@/settings/logic-functions/states/logicFunctionsState'; -import { type View } from '@/views/types/View'; -import { type ViewField } from '@/views/types/ViewField'; -import { type ViewFilter } from '@/views/types/ViewFilter'; -import { type ViewSort } from '@/views/types/ViewSort'; -import { type NavigationMenuItem } from '~/generated-metadata/graphql'; +import { type FlatPageLayout } from '@/metadata-store/types/FlatPageLayout'; +import { type FlatPageLayoutTab } from '@/metadata-store/types/FlatPageLayoutTab'; +import { type FlatPageLayoutWidget } from '@/metadata-store/types/FlatPageLayoutWidget'; +import { type FlatRole } from '@/metadata-store/types/FlatRole'; +import { type FlatRoleTarget } from '@/metadata-store/types/FlatRoleTarget'; +import { type FlatRowLevelPermissionPredicate } from '@/metadata-store/types/FlatRowLevelPermissionPredicate'; +import { type FlatRowLevelPermissionPredicateGroup } from '@/metadata-store/types/FlatRowLevelPermissionPredicateGroup'; +import { type FlatSkill } from '@/metadata-store/types/FlatSkill'; +import { type FlatView } from '@/metadata-store/types/FlatView'; +import { type FlatViewField } from '@/metadata-store/types/FlatViewField'; +import { type FlatViewFieldGroup } from '@/metadata-store/types/FlatViewFieldGroup'; +import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter'; +import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup'; +import { type FlatViewGroup } from '@/metadata-store/types/FlatViewGroup'; +import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort'; +import { type FlatWebhook } from '@/metadata-store/types/FlatWebhook'; export type MetadataEntityTypeMap = { objectMetadataItems: FlatObjectMetadataItem; fieldMetadataItems: FlatFieldMetadataItem; indexMetadataItems: FlatIndexMetadataItem; - views: View; - viewFields: ViewField; - viewFilters: ViewFilter; - viewSorts: ViewSort; - pageLayouts: PageLayout; - logicFunctions: LogicFunction; - navigationMenuItems: NavigationMenuItem; + views: FlatView; + viewFields: FlatViewField; + viewFilters: FlatViewFilter; + viewSorts: FlatViewSort; + viewGroups: FlatViewGroup; + viewFilterGroups: FlatViewFilterGroup; + viewFieldGroups: FlatViewFieldGroup; + pageLayouts: FlatPageLayout; + pageLayoutTabs: FlatPageLayoutTab; + pageLayoutWidgets: FlatPageLayoutWidget; + logicFunctions: FlatLogicFunction; + navigationMenuItems: FlatNavigationMenuItem; + commandMenuItems: FlatCommandMenuItem; + frontComponents: FlatFrontComponent; + webhooks: FlatWebhook; + roles: FlatRole; + roleTargets: FlatRoleTarget; + agents: FlatAgent; + skills: FlatSkill; + rowLevelPermissionPredicates: FlatRowLevelPermissionPredicate; + rowLevelPermissionPredicateGroups: FlatRowLevelPermissionPredicateGroup; }; diff --git a/packages/twenty-front/src/modules/metadata-store/types/MinimalMetadata.ts b/packages/twenty-front/src/modules/metadata-store/types/MinimalMetadata.ts new file mode 100644 index 0000000000..03214f7028 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/types/MinimalMetadata.ts @@ -0,0 +1,15 @@ +import { + type MinimalMetadata as GeneratedMinimalMetadata, + type MinimalObjectMetadata as GeneratedMinimalObjectMetadata, + type MinimalView as GeneratedMinimalView, +} from '~/generated-metadata/graphql'; + +export type MinimalObjectMetadata = GeneratedMinimalObjectMetadata; + +export type MinimalView = GeneratedMinimalView; + +export type MinimalMetadata = GeneratedMinimalMetadata; + +export type FindMinimalMetadataQuery = { + minimalMetadata: MinimalMetadata; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/utils/patchMetadataStoreFromSSEEvent.ts b/packages/twenty-front/src/modules/metadata-store/utils/patchMetadataStoreFromSSEEvent.ts new file mode 100644 index 0000000000..23787e396a --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/utils/patchMetadataStoreFromSSEEvent.ts @@ -0,0 +1,60 @@ +import { + metadataStoreState, + type MetadataEntityKey, +} from '@/metadata-store/states/metadataStoreState'; +import { type createStore } from 'jotai'; + +type JotaiStore = ReturnType; + +type SSEEventOperation = + | { + type: 'create'; + createdRecord: Record; + } + | { + type: 'update'; + updatedRecord: Record; + } + | { + type: 'delete'; + deletedRecordId: string; + }; + +export const patchMetadataStoreFromSSEEvent = ( + store: JotaiStore, + entityKey: MetadataEntityKey, + operation: SSEEventOperation, +) => { + const entry = store.get(metadataStoreState.atomFamily(entityKey)); + const currentItems = entry.current as Array<{ id: string }>; + + switch (operation.type) { + case 'create': { + store.set(metadataStoreState.atomFamily(entityKey), { + ...entry, + current: [...currentItems, operation.createdRecord], + }); + break; + } + case 'update': { + store.set(metadataStoreState.atomFamily(entityKey), { + ...entry, + current: currentItems.map((item) => + item.id === (operation.updatedRecord as { id: string }).id + ? { ...item, ...operation.updatedRecord } + : item, + ), + }); + break; + } + case 'delete': { + store.set(metadataStoreState.atomFamily(entityKey), { + ...entry, + current: currentItems.filter( + (item) => item.id !== operation.deletedRecordId, + ), + }); + break; + } + } +}; diff --git a/packages/twenty-front/src/modules/metadata-store/utils/splitPageLayoutWithRelated.ts b/packages/twenty-front/src/modules/metadata-store/utils/splitPageLayoutWithRelated.ts new file mode 100644 index 0000000000..9d27131202 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/utils/splitPageLayoutWithRelated.ts @@ -0,0 +1,46 @@ +import { type FlatPageLayout } from '@/metadata-store/types/FlatPageLayout'; +import { type FlatPageLayoutTab } from '@/metadata-store/types/FlatPageLayoutTab'; +import { type FlatPageLayoutWidget } from '@/metadata-store/types/FlatPageLayoutWidget'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; + +type SplitResult = { + flatPageLayouts: FlatPageLayout[]; + flatPageLayoutTabs: FlatPageLayoutTab[]; + flatPageLayoutWidgets: FlatPageLayoutWidget[]; +}; + +export const splitPageLayoutWithRelated = ( + pageLayouts: PageLayout[], +): SplitResult => { + const flatPageLayouts: FlatPageLayout[] = []; + const flatPageLayoutTabs: FlatPageLayoutTab[] = []; + const flatPageLayoutWidgets: FlatPageLayoutWidget[] = []; + + for (const pageLayout of pageLayouts) { + const { tabs = [], ...pageLayoutProperties } = pageLayout; + + flatPageLayouts.push(pageLayoutProperties); + + for (const tab of tabs) { + const { widgets = [], ...tabProperties } = tab; + + flatPageLayoutTabs.push({ + ...tabProperties, + pageLayoutId: pageLayout.id, + }); + + for (const widget of widgets) { + flatPageLayoutWidgets.push({ + ...widget, + pageLayoutTabId: tab.id, + }); + } + } + } + + return { + flatPageLayouts, + flatPageLayoutTabs, + flatPageLayoutWidgets, + }; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/utils/splitViewWithRelated.ts b/packages/twenty-front/src/modules/metadata-store/utils/splitViewWithRelated.ts new file mode 100644 index 0000000000..83542e42a3 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/utils/splitViewWithRelated.ts @@ -0,0 +1,93 @@ +import { type FlatView } from '@/metadata-store/types/FlatView'; +import { type FlatViewField } from '@/metadata-store/types/FlatViewField'; +import { type FlatViewFieldGroup } from '@/metadata-store/types/FlatViewFieldGroup'; +import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter'; +import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup'; +import { type FlatViewGroup } from '@/metadata-store/types/FlatViewGroup'; +import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort'; +import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; + +type SplitResult = { + flatViews: FlatView[]; + flatViewFields: FlatViewField[]; + flatViewFilters: FlatViewFilter[]; + flatViewSorts: FlatViewSort[]; + flatViewGroups: FlatViewGroup[]; + flatViewFilterGroups: FlatViewFilterGroup[]; + flatViewFieldGroups: FlatViewFieldGroup[]; +}; + +export const splitViewWithRelated = ( + viewsWithRelated: CoreViewWithRelations[], +): SplitResult => { + const flatViews: FlatView[] = []; + const flatViewFields: FlatViewField[] = []; + const flatViewFilters: FlatViewFilter[] = []; + const flatViewSorts: FlatViewSort[] = []; + const flatViewGroups: FlatViewGroup[] = []; + const flatViewFilterGroups: FlatViewFilterGroup[] = []; + const flatViewFieldGroups: FlatViewFieldGroup[] = []; + + for (const viewWithRelated of viewsWithRelated) { + const { + viewFields = [], + viewFilters = [], + viewSorts = [], + viewGroups = [], + viewFilterGroups = [], + viewFieldGroups = [], + ...viewProperties + } = viewWithRelated; + + flatViews.push(viewProperties); + + for (const viewField of viewFields) { + flatViewFields.push({ + ...viewField, + viewId: viewWithRelated.id, + }); + } + + for (const viewFilter of viewFilters) { + flatViewFilters.push({ + ...viewFilter, + viewId: viewWithRelated.id, + }); + } + + for (const viewSort of viewSorts) { + flatViewSorts.push({ + ...viewSort, + viewId: viewWithRelated.id, + }); + } + + for (const viewGroup of viewGroups) { + flatViewGroups.push({ + ...viewGroup, + viewId: viewWithRelated.id, + }); + } + + for (const viewFilterGroup of viewFilterGroups) { + flatViewFilterGroups.push(viewFilterGroup); + } + + for (const viewFieldGroup of viewFieldGroups) { + const { viewFields: _viewFields, ...viewFieldGroupProperties } = + viewFieldGroup; + + flatViewFieldGroups.push(viewFieldGroupProperties); + } + } + + return { + flatViews, + flatViewFields, + flatViewFilters, + flatViewSorts, + flatViewGroups, + flatViewFilterGroups, + flatViewFieldGroups, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useApplyPageLayouts.ts b/packages/twenty-front/src/modules/page-layout/hooks/useApplyPageLayouts.ts index ab20cf2704..37c730eda7 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useApplyPageLayouts.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useApplyPageLayouts.ts @@ -1,4 +1,5 @@ import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { splitPageLayoutWithRelated } from '@/metadata-store/utils/splitPageLayoutWithRelated'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; import { recordPageLayoutsState } from '@/page-layout/states/recordPageLayoutsState'; @@ -32,7 +33,13 @@ export const useApplyPageLayouts = () => { } store.set(recordPageLayoutsState.atom, transformedPageLayouts); - updateDraft('pageLayouts', transformedPageLayouts); + + const { flatPageLayouts, flatPageLayoutTabs, flatPageLayoutWidgets } = + splitPageLayoutWithRelated(transformedPageLayouts); + + updateDraft('pageLayouts', flatPageLayouts); + updateDraft('pageLayoutTabs', flatPageLayoutTabs); + updateDraft('pageLayoutWidgets', flatPageLayoutWidgets); applyChanges(); }, [store, updateDraft, applyChanges], diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutsWithRelationsSelector.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutsWithRelationsSelector.ts new file mode 100644 index 0000000000..755040a923 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutsWithRelationsSelector.ts @@ -0,0 +1,52 @@ +import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; +import { type FlatPageLayout } from '@/metadata-store/types/FlatPageLayout'; +import { type FlatPageLayoutTab } from '@/metadata-store/types/FlatPageLayoutTab'; +import { type FlatPageLayoutWidget } from '@/metadata-store/types/FlatPageLayoutWidget'; +import { type PageLayout } from '@/page-layout/types/PageLayout'; +import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector'; +import { isDefined } from 'twenty-shared/utils'; + +export const pageLayoutsWithRelationsSelector = createAtomSelector< + PageLayout[] +>({ + key: 'pageLayoutsWithRelationsSelector', + get: ({ get }) => { + const flatPageLayouts = get(metadataStoreState, 'pageLayouts') + .current as FlatPageLayout[]; + const allFlatTabs = get(metadataStoreState, 'pageLayoutTabs') + .current as FlatPageLayoutTab[]; + const allFlatWidgets = get(metadataStoreState, 'pageLayoutWidgets') + .current as FlatPageLayoutWidget[]; + + const tabsByPageLayoutId = new Map(); + const widgetsByTabId = new Map(); + + for (const tab of allFlatTabs) { + const existing = tabsByPageLayoutId.get(tab.pageLayoutId); + + if (isDefined(existing)) { + existing.push(tab); + } else { + tabsByPageLayoutId.set(tab.pageLayoutId, [tab]); + } + } + + for (const widget of allFlatWidgets) { + const existing = widgetsByTabId.get(widget.pageLayoutTabId); + + if (isDefined(existing)) { + existing.push(widget); + } else { + widgetsByTabId.set(widget.pageLayoutTabId, [widget]); + } + } + + return flatPageLayouts.map((flatPageLayout) => ({ + ...flatPageLayout, + tabs: (tabsByPageLayoutId.get(flatPageLayout.id) ?? []).map((tab) => ({ + ...tab, + widgets: widgetsByTabId.get(tab.id) ?? [], + })), + })); + }, +}); diff --git a/packages/twenty-front/src/modules/sse-db-event/components/SSEProvider.tsx b/packages/twenty-front/src/modules/sse-db-event/components/SSEProvider.tsx index 8e841ce3e0..086945a7b6 100644 --- a/packages/twenty-front/src/modules/sse-db-event/components/SSEProvider.tsx +++ b/packages/twenty-front/src/modules/sse-db-event/components/SSEProvider.tsx @@ -4,7 +4,10 @@ import { ObjectMetadataItemSSEEffect } from '@/metadata-store/effect-components/ import { PageLayoutSSEEffect } from '@/metadata-store/effect-components/PageLayoutSSEEffect'; import { PageLayoutTabSSEEffect } from '@/metadata-store/effect-components/PageLayoutTabSSEEffect'; import { PageLayoutWidgetSSEEffect } from '@/metadata-store/effect-components/PageLayoutWidgetSSEEffect'; -import { ViewRelationsSSEEffect } from '@/metadata-store/effect-components/ViewRelationsSSEEffect'; +import { ViewFieldSSEEffect } from '@/metadata-store/effect-components/ViewFieldSSEEffect'; +import { ViewFilterGroupSSEEffect } from '@/metadata-store/effect-components/ViewFilterGroupSSEEffect'; +import { ViewFilterSSEEffect } from '@/metadata-store/effect-components/ViewFilterSSEEffect'; +import { ViewSortSSEEffect } from '@/metadata-store/effect-components/ViewSortSSEEffect'; import { ViewSSEEffect } from '@/metadata-store/effect-components/ViewSSEEffect'; import { SSEClientEffect } from '@/sse-db-event/components/SSEClientEffect'; import { SSEEventStreamEffect } from '@/sse-db-event/components/SSEEventStreamEffect'; @@ -27,8 +30,11 @@ export const SSEProvider = ({ children }: SSEProviderProps) => { - + + + + {children} ); diff --git a/packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts b/packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts index 0e67b8747b..c47d1afc66 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts @@ -3,6 +3,13 @@ import { createStore } from 'jotai'; export let jotaiStore = createStore(); export const resetJotaiStore = () => { + try { + localStorage.clear(); + } catch { + // localStorage may be unavailable in some environments + } + jotaiStore = createStore(); + return jotaiStore; }; diff --git a/packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomFamilyState.ts b/packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomFamilyState.ts index 31a8f740d1..cecd0c76b1 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomFamilyState.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomFamilyState.ts @@ -7,10 +7,12 @@ export const createAtomFamilyState = ({ key, defaultValue, useLocalStorage = false, + localStorageOptions, }: { key: string; defaultValue: ValueType; useLocalStorage?: boolean; + localStorageOptions?: { getOnInit?: boolean }; }): FamilyState => { const atomCache = new Map< string, @@ -31,7 +33,12 @@ export const createAtomFamilyState = ({ const atomKey = `${key}__${cacheKey}`; const baseAtom = useLocalStorage - ? atomWithStorage(atomKey, defaultValue) + ? atomWithStorage( + atomKey, + defaultValue, + undefined, + localStorageOptions ?? undefined, + ) : atom(defaultValue); baseAtom.debugLabel = atomKey; atomCache.set(cacheKey, baseAtom); diff --git a/packages/twenty-front/src/modules/users/components/LazyMetadataLoadEffect.tsx b/packages/twenty-front/src/modules/users/components/LazyMetadataLoadEffect.tsx index 6fe8bc2377..e4cc205eec 100644 --- a/packages/twenty-front/src/modules/users/components/LazyMetadataLoadEffect.tsx +++ b/packages/twenty-front/src/modules/users/components/LazyMetadataLoadEffect.tsx @@ -1,5 +1,6 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { splitPageLayoutWithRelated } from '@/metadata-store/utils/splitPageLayoutWithRelated'; import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState'; import { recordPageLayoutsState } from '@/page-layout/states/recordPageLayoutsState'; import { type PageLayout } from '@/page-layout/types/PageLayout'; @@ -109,7 +110,13 @@ export const LazyMetadataLoadEffect = () => { queryDataRecordPageLayouts.getPageLayouts.map(transformPageLayout); setRecordPageLayouts(transformedPageLayouts); - updateDraft('pageLayouts', transformedPageLayouts); + + const { flatPageLayouts, flatPageLayoutTabs, flatPageLayoutWidgets } = + splitPageLayoutWithRelated(transformedPageLayouts); + + updateDraft('pageLayouts', flatPageLayouts); + updateDraft('pageLayoutTabs', flatPageLayoutTabs); + updateDraft('pageLayoutWidgets', flatPageLayoutWidgets); applyChanges(); }, [ queryDataRecordPageLayouts?.getPageLayouts, diff --git a/packages/twenty-front/src/modules/views/states/viewsWithRelationsSelector.ts b/packages/twenty-front/src/modules/views/states/viewsWithRelationsSelector.ts new file mode 100644 index 0000000000..fc5b8fdbb1 --- /dev/null +++ b/packages/twenty-front/src/modules/views/states/viewsWithRelationsSelector.ts @@ -0,0 +1,130 @@ +import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; +import { type FlatView } from '@/metadata-store/types/FlatView'; +import { type FlatViewField } from '@/metadata-store/types/FlatViewField'; +import { type FlatViewFieldGroup } from '@/metadata-store/types/FlatViewFieldGroup'; +import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter'; +import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup'; +import { type FlatViewGroup } from '@/metadata-store/types/FlatViewGroup'; +import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort'; +import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector'; +import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; +import { isDefined } from 'twenty-shared/utils'; + +export const viewsWithRelationsSelector = createAtomSelector< + CoreViewWithRelations[] +>({ + key: 'viewsWithRelationsSelector', + get: ({ get }) => { + const flatViews = get(metadataStoreState, 'views').current as FlatView[]; + const allFlatViewFields = get(metadataStoreState, 'viewFields') + .current as FlatViewField[]; + const allFlatViewFilters = get(metadataStoreState, 'viewFilters') + .current as FlatViewFilter[]; + const allFlatViewSorts = get(metadataStoreState, 'viewSorts') + .current as FlatViewSort[]; + const allFlatViewGroups = get(metadataStoreState, 'viewGroups') + .current as FlatViewGroup[]; + const allFlatViewFilterGroups = get(metadataStoreState, 'viewFilterGroups') + .current as FlatViewFilterGroup[]; + const allFlatViewFieldGroups = get(metadataStoreState, 'viewFieldGroups') + .current as FlatViewFieldGroup[]; + + const viewFieldsByViewId = new Map(); + const viewFiltersByViewId = new Map(); + const viewSortsByViewId = new Map(); + const viewGroupsByViewId = new Map(); + const viewFilterGroupsByViewId = new Map(); + const viewFieldGroupsByViewId = new Map(); + + for (const viewField of allFlatViewFields) { + const existing = viewFieldsByViewId.get(viewField.viewId); + + if (isDefined(existing)) { + existing.push(viewField); + } else { + viewFieldsByViewId.set(viewField.viewId, [viewField]); + } + } + + for (const viewFilter of allFlatViewFilters) { + const existing = viewFiltersByViewId.get(viewFilter.viewId); + + if (isDefined(existing)) { + existing.push(viewFilter); + } else { + viewFiltersByViewId.set(viewFilter.viewId, [viewFilter]); + } + } + + for (const viewSort of allFlatViewSorts) { + const existing = viewSortsByViewId.get(viewSort.viewId); + + if (isDefined(existing)) { + existing.push(viewSort); + } else { + viewSortsByViewId.set(viewSort.viewId, [viewSort]); + } + } + + for (const viewGroup of allFlatViewGroups) { + const existing = viewGroupsByViewId.get(viewGroup.viewId); + + if (isDefined(existing)) { + existing.push(viewGroup); + } else { + viewGroupsByViewId.set(viewGroup.viewId, [viewGroup]); + } + } + + for (const viewFilterGroup of allFlatViewFilterGroups) { + const viewId = (viewFilterGroup as { viewId?: string }).viewId; + + if (!isDefined(viewId)) { + continue; + } + + const existing = viewFilterGroupsByViewId.get(viewId); + + if (isDefined(existing)) { + existing.push(viewFilterGroup); + } else { + viewFilterGroupsByViewId.set(viewId, [viewFilterGroup]); + } + } + + for (const viewFieldGroup of allFlatViewFieldGroups) { + const viewId = (viewFieldGroup as { viewId?: string }).viewId; + + if (!isDefined(viewId)) { + continue; + } + + const existing = viewFieldGroupsByViewId.get(viewId); + + if (isDefined(existing)) { + existing.push(viewFieldGroup); + } else { + viewFieldGroupsByViewId.set(viewId, [viewFieldGroup]); + } + } + + return flatViews.map((flatView) => ({ + ...flatView, + viewFields: viewFieldsByViewId.get(flatView.id) ?? [], + viewFilters: viewFiltersByViewId.get(flatView.id) ?? [], + viewSorts: viewSortsByViewId.get(flatView.id) ?? [], + viewGroups: viewGroupsByViewId.get(flatView.id) ?? [], + viewFilterGroups: viewFilterGroupsByViewId.get(flatView.id) ?? [], + viewFieldGroups: (viewFieldGroupsByViewId.get(flatView.id) ?? []).map( + (group) => ({ + ...group, + viewFields: (viewFieldsByViewId.get(flatView.id) ?? []).filter( + (field) => + (field as unknown as { viewFieldGroupId?: string }) + .viewFieldGroupId === group.id, + ), + }), + ), + })); + }, +}); diff --git a/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx b/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx index 9c351e924a..68933d7526 100644 --- a/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx +++ b/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx @@ -12,7 +12,7 @@ export const JestObjectMetadataItemSetter = ({ children: ReactNode; objectMetadataItems?: ObjectMetadataItem[]; }) => { - const { updateDraft, applyChanges } = useMetadataStore(); + const { updateDraft, applyChanges, resetMetadataStore } = useMetadataStore(); const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { @@ -20,12 +20,17 @@ export const JestObjectMetadataItemSetter = ({ const { flatObjects, flatFields, flatIndexes } = splitObjectMetadataItemWithRelated(items); + // Reset first so updateDraft always proceeds and writes to localStorage. + // Without this, atomWithStorage's onMount re-reads from empty localStorage + // and overwrites in-memory values when the store is reused across tests. + resetMetadataStore(); + updateDraft('objectMetadataItems', flatObjects); updateDraft('fieldMetadataItems', flatFields); updateDraft('indexMetadataItems', flatIndexes); applyChanges(); setIsLoaded(true); - }, [objectMetadataItems, updateDraft, applyChanges]); + }, [objectMetadataItems, updateDraft, applyChanges, resetMetadataStore]); return isLoaded ? <>{children} : null; }; diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql index faeed8cf29..1d55db2050 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql @@ -2204,6 +2204,32 @@ type AgentTurn { createdAt: DateTime! } +type MinimalObjectMetadata { + id: UUID! + nameSingular: String! + namePlural: String! + labelSingular: String! + labelPlural: String! + icon: String + isCustom: Boolean! + isActive: Boolean! + isSystem: Boolean! + isRemote: Boolean! +} + +type MinimalView { + id: UUID! + type: ViewType! + key: ViewKey + objectMetadataId: UUID! +} + +type MinimalMetadata { + objectMetadataItems: [MinimalObjectMetadata!]! + views: [MinimalView!]! + metadataVersion: Int! +} + type Webhook { id: UUID! targetUrl: String! @@ -2955,6 +2981,7 @@ type Query { getSSOIdentityProviders: [FindAvailableSSOIDP!]! webhooks: [Webhook!]! webhook(id: UUID!): Webhook + minimalMetadata: MinimalMetadata! chatThread(id: UUID!): AgentChatThread! chatMessages(threadId: UUID!): [AgentMessage!]! getAISystemPromptPreview: AISystemPromptPreview! diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts index 0729796ffe..9cbeccff29 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts @@ -1885,6 +1885,35 @@ export interface AgentTurn { __typename: 'AgentTurn' } +export interface MinimalObjectMetadata { + id: Scalars['UUID'] + nameSingular: Scalars['String'] + namePlural: Scalars['String'] + labelSingular: Scalars['String'] + labelPlural: Scalars['String'] + icon?: Scalars['String'] + isCustom: Scalars['Boolean'] + isActive: Scalars['Boolean'] + isSystem: Scalars['Boolean'] + isRemote: Scalars['Boolean'] + __typename: 'MinimalObjectMetadata' +} + +export interface MinimalView { + id: Scalars['UUID'] + type: ViewType + key?: ViewKey + objectMetadataId: Scalars['UUID'] + __typename: 'MinimalView' +} + +export interface MinimalMetadata { + objectMetadataItems: MinimalObjectMetadata[] + views: MinimalView[] + metadataVersion: Scalars['Int'] + __typename: 'MinimalMetadata' +} + export interface Webhook { id: Scalars['UUID'] targetUrl: Scalars['String'] @@ -2595,6 +2624,7 @@ export interface Query { getSSOIdentityProviders: FindAvailableSSOIDP[] webhooks: Webhook[] webhook?: Webhook + minimalMetadata: MinimalMetadata chatThread: AgentChatThread chatMessages: AgentMessage[] getAISystemPromptPreview: AISystemPromptPreview @@ -4848,6 +4878,38 @@ export interface AgentTurnGenqlSelection{ __scalar?: boolean | number } +export interface MinimalObjectMetadataGenqlSelection{ + id?: boolean | number + nameSingular?: boolean | number + namePlural?: boolean | number + labelSingular?: boolean | number + labelPlural?: boolean | number + icon?: boolean | number + isCustom?: boolean | number + isActive?: boolean | number + isSystem?: boolean | number + isRemote?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface MinimalViewGenqlSelection{ + id?: boolean | number + type?: boolean | number + key?: boolean | number + objectMetadataId?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface MinimalMetadataGenqlSelection{ + objectMetadataItems?: MinimalObjectMetadataGenqlSelection + views?: MinimalViewGenqlSelection + metadataVersion?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + export interface WebhookGenqlSelection{ id?: boolean | number targetUrl?: boolean | number @@ -5618,6 +5680,7 @@ export interface QueryGenqlSelection{ getSSOIdentityProviders?: FindAvailableSSOIDPGenqlSelection webhooks?: WebhookGenqlSelection webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) + minimalMetadata?: MinimalMetadataGenqlSelection chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} }) chatMessages?: (AgentMessageGenqlSelection & { __args: {threadId: Scalars['UUID']} }) getAISystemPromptPreview?: AISystemPromptPreviewGenqlSelection @@ -7611,6 +7674,30 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null + const MinimalObjectMetadata_possibleTypes: string[] = ['MinimalObjectMetadata'] + export const isMinimalObjectMetadata = (obj?: { __typename?: any } | null): obj is MinimalObjectMetadata => { + if (!obj?.__typename) throw new Error('__typename is missing in "isMinimalObjectMetadata"') + return MinimalObjectMetadata_possibleTypes.includes(obj.__typename) + } + + + + const MinimalView_possibleTypes: string[] = ['MinimalView'] + export const isMinimalView = (obj?: { __typename?: any } | null): obj is MinimalView => { + if (!obj?.__typename) throw new Error('__typename is missing in "isMinimalView"') + return MinimalView_possibleTypes.includes(obj.__typename) + } + + + + const MinimalMetadata_possibleTypes: string[] = ['MinimalMetadata'] + export const isMinimalMetadata = (obj?: { __typename?: any } | null): obj is MinimalMetadata => { + if (!obj?.__typename) throw new Error('__typename is missing in "isMinimalMetadata"') + return MinimalMetadata_possibleTypes.includes(obj.__typename) + } + + + const Webhook_possibleTypes: string[] = ['Webhook'] export const isWebhook = (obj?: { __typename?: any } | null): obj is Webhook => { if (!obj?.__typename) throw new Error('__typename is missing in "isWebhook"') diff --git a/packages/twenty-sdk/src/clients/generated/metadata/types.ts b/packages/twenty-sdk/src/clients/generated/metadata/types.ts index d749e8df8e..4ac4e27d1d 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/types.ts +++ b/packages/twenty-sdk/src/clients/generated/metadata/types.ts @@ -53,29 +53,29 @@ export default { 218, 226, 227, - 240, - 241, - 246, + 243, + 244, 249, - 255, - 256, + 252, 258, - 263, - 268, - 269, - 281, - 297, - 298, - 321, - 328, - 329, - 330, + 259, + 261, + 266, + 271, + 272, + 284, + 300, + 301, + 324, + 331, 332, - 341, - 388, - 451, - 452, - 453 + 333, + 335, + 344, + 391, + 454, + 455, + 456 ], "types": { "BillingProductDTO": { @@ -4323,6 +4323,72 @@ export default { 1 ] }, + "MinimalObjectMetadata": { + "id": [ + 3 + ], + "nameSingular": [ + 1 + ], + "namePlural": [ + 1 + ], + "labelSingular": [ + 1 + ], + "labelPlural": [ + 1 + ], + "icon": [ + 1 + ], + "isCustom": [ + 6 + ], + "isActive": [ + 6 + ], + "isSystem": [ + 6 + ], + "isRemote": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "MinimalView": { + "id": [ + 3 + ], + "type": [ + 61 + ], + "key": [ + 62 + ], + "objectMetadataId": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "MinimalMetadata": { + "objectMetadataItems": [ + 236 + ], + "views": [ + 237 + ], + "metadataVersion": [ + 21 + ], + "__typename": [ + 1 + ] + }, "Webhook": { "id": [ 3 @@ -4385,10 +4451,10 @@ export default { 1 ], "modelFamily": [ - 240 + 243 ], "inferenceProvider": [ - 241 + 244 ], "inputCostPerMillionTokensInCredits": [ 11 @@ -4397,7 +4463,7 @@ export default { 11 ], "nativeCapabilities": [ - 238 + 241 ], "deprecated": [ 6 @@ -4419,10 +4485,10 @@ export default { 1 ], "modelFamily": [ - 240 + 243 ], "inferenceProvider": [ - 241 + 244 ], "isAvailable": [ 6 @@ -4445,7 +4511,7 @@ export default { 6 ], "models": [ - 242 + 245 ], "__typename": [ 1 @@ -4459,7 +4525,7 @@ export default { 1 ], "trialPeriods": [ - 237 + 240 ], "__typename": [ 1 @@ -4467,7 +4533,7 @@ export default { }, "Support": { "supportDriver": [ - 246 + 249 ], "supportFrontChatId": [ 1 @@ -4493,7 +4559,7 @@ export default { }, "Captcha": { "provider": [ - 249 + 252 ], "siteKey": [ 1 @@ -4530,7 +4596,7 @@ export default { 164 ], "metadata": [ - 251 + 254 ], "__typename": [ 1 @@ -4544,10 +4610,10 @@ export default { 166 ], "billing": [ - 244 + 247 ], "aiModels": [ - 239 + 242 ], "signInPrefilled": [ 6 @@ -4568,28 +4634,28 @@ export default { 6 ], "support": [ - 245 + 248 ], "isAttachmentPreviewEnabled": [ 6 ], "sentry": [ - 247 + 250 ], "captcha": [ - 248 + 251 ], "chromeExtensionId": [ 1 ], "api": [ - 250 + 253 ], "canManageFeatureFlags": [ 6 ], "publicFeatureFlags": [ - 252 + 255 ], "isMicrosoftMessagingEnabled": [ 6 @@ -4639,13 +4705,13 @@ export default { 6 ], "source": [ - 255 + 258 ], "isEnvOnly": [ 6 ], "type": [ - 256 + 259 ], "options": [ 15 @@ -4658,10 +4724,10 @@ export default { "ConfigVariableType": {}, "ConfigVariablesGroupData": { "variables": [ - 254 + 257 ], "name": [ - 258 + 261 ], "description": [ 1 @@ -4676,7 +4742,7 @@ export default { "ConfigVariablesGroup": {}, "ConfigVariables": { "groups": [ - 257 + 260 ], "__typename": [ 1 @@ -4701,7 +4767,7 @@ export default { 21 ], "results": [ - 260 + 263 ], "__typename": [ 1 @@ -4718,7 +4784,7 @@ export default { 15 ], "state": [ - 263 + 266 ], "timestamp": [ 11 @@ -4768,7 +4834,7 @@ export default { }, "QueueJobsResponse": { "jobs": [ - 262 + 265 ], "count": [ 11 @@ -4780,7 +4846,7 @@ export default { 6 ], "retentionConfig": [ - 264 + 267 ], "__typename": [ 1 @@ -4791,7 +4857,7 @@ export default { 21 ], "results": [ - 260 + 263 ], "__typename": [ 1 @@ -4799,13 +4865,13 @@ export default { }, "SystemHealthService": { "id": [ - 268 + 271 ], "label": [ 1 ], "status": [ - 269 + 272 ], "__typename": [ 1 @@ -4815,7 +4881,7 @@ export default { "AdminPanelHealthServiceStatus": {}, "SystemHealth": { "services": [ - 267 + 270 ], "__typename": [ 1 @@ -4858,7 +4924,7 @@ export default { 152 ], "users": [ - 271 + 274 ], "featureFlags": [ 163 @@ -4869,10 +4935,10 @@ export default { }, "UserLookup": { "user": [ - 271 + 274 ], "workspaces": [ - 272 + 275 ], "__typename": [ 1 @@ -4897,7 +4963,7 @@ export default { 1 ], "status": [ - 269 + 272 ], "__typename": [ 1 @@ -4905,7 +4971,7 @@ export default { }, "AdminPanelHealthServiceData": { "id": [ - 268 + 271 ], "label": [ 1 @@ -4914,7 +4980,7 @@ export default { 1 ], "status": [ - 269 + 272 ], "errorMessage": [ 1 @@ -4923,7 +4989,7 @@ export default { 1 ], "queues": [ - 275 + 278 ], "__typename": [ 1 @@ -4945,7 +5011,7 @@ export default { 1 ], "data": [ - 277 + 280 ], "__typename": [ 1 @@ -4988,13 +5054,13 @@ export default { 11 ], "timeRange": [ - 281 + 284 ], "details": [ - 279 + 282 ], "data": [ - 278 + 281 ], "__typename": [ 1 @@ -5100,7 +5166,7 @@ export default { 1 ], "fields": [ - 286 + 289 ], "__typename": [ 1 @@ -5197,10 +5263,10 @@ export default { 6 ], "objectPermissions": [ - 290 + 293 ], "fieldPermissions": [ - 291 + 294 ], "permissionFlags": [ 1 @@ -5250,20 +5316,20 @@ export default { 1 ], "objects": [ - 287 + 290 ], "fields": [ - 286 - ], - "logicFunctions": [ - 288 - ], - "frontComponents": [ 289 ], - "defaultRole": [ + "logicFunctions": [ + 291 + ], + "frontComponents": [ 292 ], + "defaultRole": [ + 295 + ], "sourcePackage": [ 1 ], @@ -5322,13 +5388,13 @@ export default { 1 ], "driver": [ - 297 + 300 ], "status": [ - 298 + 301 ], "verificationRecords": [ - 295 + 298 ], "verifiedAt": [ 4 @@ -5375,7 +5441,7 @@ export default { 1 ], "location": [ - 300 + 303 ], "__typename": [ 1 @@ -5403,13 +5469,13 @@ export default { }, "ImapSmtpCaldavConnectionParameters": { "IMAP": [ - 302 + 305 ], "SMTP": [ - 302 + 305 ], "CALDAV": [ - 302 + 305 ], "__typename": [ 1 @@ -5429,7 +5495,7 @@ export default { 3 ], "connectionParameters": [ - 303 + 306 ], "__typename": [ 1 @@ -5493,7 +5559,7 @@ export default { 1 ], "series": [ - 308 + 311 ], "xAxisLabel": [ 1 @@ -5542,7 +5608,7 @@ export default { 1 ], "data": [ - 310 + 313 ], "__typename": [ 1 @@ -5550,7 +5616,7 @@ export default { }, "LineChartData": { "series": [ - 311 + 314 ], "xAxisLabel": [ 1 @@ -5587,7 +5653,7 @@ export default { }, "PieChartData": { "data": [ - 313 + 316 ], "showLegend": [ 6 @@ -5670,13 +5736,13 @@ export default { }, "EventLogQueryResult": { "records": [ - 316 + 319 ], "totalCount": [ 21 ], "pageInfo": [ - 317 + 320 ], "__typename": [ 1 @@ -5743,7 +5809,7 @@ export default { 32, { "input": [ - 320, + 323, "LogicFunctionIdInput!" ] } @@ -5755,7 +5821,7 @@ export default { 15, { "input": [ - 320, + 323, "LogicFunctionIdInput!" ] } @@ -5764,7 +5830,7 @@ export default { 1, { "input": [ - 320, + 323, "LogicFunctionIdInput!" ] } @@ -5921,7 +5987,7 @@ export default { 25, { "input": [ - 322, + 325, "AgentIdInput!" ] } @@ -5978,7 +6044,7 @@ export default { 2, { "input": [ - 323, + 326, "GetApiKeyInput!" ] } @@ -6191,10 +6257,10 @@ export default { 193 ], "webhooks": [ - 236 + 239 ], "webhook": [ - 236, + 239, { "id": [ 3, @@ -6202,6 +6268,9 @@ export default { ] } ], + "minimalMetadata": [ + 238 + ], "chatThread": [ 228, { @@ -6243,11 +6312,11 @@ export default { "CursorPaging!" ], "filter": [ - 324, + 327, "AgentChatThreadFilter!" ], "sorting": [ - 327, + 330, "[AgentChatThreadSort!]!" ] } @@ -6262,43 +6331,43 @@ export default { } ], "eventLogs": [ - 318, + 321, { "input": [ - 331, + 334, "EventLogQueryInput!" ] } ], "pieChartData": [ - 314, + 317, { "input": [ - 335, + 338, "PieChartDataInput!" ] } ], "lineChartData": [ - 312, + 315, { "input": [ - 336, + 339, "LineChartDataInput!" ] } ], "barChartData": [ - 309, + 312, { "input": [ - 337, + 340, "BarChartDataInput!" ] } ], "getConnectedImapSmtpCaldavAccount": [ - 304, + 307, { "id": [ 3, @@ -6307,7 +6376,7 @@ export default { } ], "getAutoCompleteAddress": [ - 299, + 302, { "address": [ 1, @@ -6326,7 +6395,7 @@ export default { } ], "getAddressDetails": [ - 301, + 304, { "placeId": [ 1, @@ -6339,40 +6408,40 @@ export default { } ], "getConfigVariablesGrouped": [ - 259 + 262 ], "getSystemHealthStatus": [ - 270 + 273 ], "getIndicatorHealthStatus": [ - 276, + 279, { "indicatorId": [ - 268, + 271, "HealthIndicatorId!" ] } ], "getQueueMetrics": [ - 280, + 283, { "queueName": [ 1, "String!" ], "timeRange": [ - 281 + 284 ] } ], "versionInfo": [ - 274 + 277 ], "getAdminAiModels": [ - 243 + 246 ], "getDatabaseConfigVariable": [ - 254, + 257, { "key": [ 1, @@ -6381,14 +6450,14 @@ export default { } ], "getQueueJobs": [ - 265, + 268, { "queueName": [ 1, "String!" ], "state": [ - 263, + 266, "JobState!" ], "limit": [ @@ -6403,19 +6472,19 @@ export default { 7 ], "getPostgresCredentials": [ - 306 + 309 ], "findManyPublicDomains": [ - 294 + 297 ], "getEmailingDomains": [ - 296 + 299 ], "findManyMarketplaceApps": [ - 293 + 296 ], "findOneMarketplaceApp": [ - 293, + 296, { "universalIdentifier": [ 1, @@ -6443,7 +6512,7 @@ export default { }, "LogicFunctionIdInput": { "id": [ - 321 + 324 ], "__typename": [ 1 @@ -6468,16 +6537,16 @@ export default { }, "AgentChatThreadFilter": { "and": [ - 324 + 327 ], "or": [ - 324 + 327 ], "id": [ 42 ], "updatedAt": [ - 325 + 328 ], "__typename": [ 1 @@ -6515,10 +6584,10 @@ export default { 4 ], "between": [ - 326 + 329 ], "notBetween": [ - 326 + 329 ], "__typename": [ 1 @@ -6537,13 +6606,13 @@ export default { }, "AgentChatThreadSort": { "field": [ - 328 + 331 ], "direction": [ - 329 + 332 ], "nulls": [ - 330 + 333 ], "__typename": [ 1 @@ -6554,10 +6623,10 @@ export default { "SortNulls": {}, "EventLogQueryInput": { "table": [ - 332 + 335 ], "filters": [ - 333 + 336 ], "first": [ 21 @@ -6578,7 +6647,7 @@ export default { 1 ], "dateRange": [ - 334 + 337 ], "recordId": [ 1 @@ -6639,7 +6708,7 @@ export default { 6, { "input": [ - 339, + 342, "AddQuerySubscriptionInput!" ] } @@ -6648,7 +6717,7 @@ export default { 6, { "input": [ - 340, + 343, "RemoveQueryFromEventStreamInput!" ] } @@ -6677,7 +6746,7 @@ export default { 122, { "type": [ - 341, + 344, "AnalyticsType!" ], "name": [ @@ -6695,7 +6764,7 @@ export default { 75, { "input": [ - 342, + 345, "CreatePageLayoutWidgetInput!" ] } @@ -6708,7 +6777,7 @@ export default { "String!" ], "input": [ - 344, + 347, "UpdatePageLayoutWidgetInput!" ] } @@ -6726,7 +6795,7 @@ export default { 110, { "input": [ - 345, + 348, "CreatePageLayoutTabInput!" ] } @@ -6739,7 +6808,7 @@ export default { "String!" ], "input": [ - 346, + 349, "UpdatePageLayoutTabInput!" ] } @@ -6757,7 +6826,7 @@ export default { 111, { "input": [ - 347, + 350, "CreatePageLayoutInput!" ] } @@ -6770,7 +6839,7 @@ export default { "String!" ], "input": [ - 348, + 351, "UpdatePageLayoutInput!" ] } @@ -6792,7 +6861,7 @@ export default { "String!" ], "input": [ - 349, + 352, "UpdatePageLayoutWithTabsInput!" ] } @@ -6801,7 +6870,7 @@ export default { 32, { "input": [ - 320, + 323, "LogicFunctionIdInput!" ] } @@ -6810,7 +6879,7 @@ export default { 32, { "input": [ - 352, + 355, "CreateLogicFunctionFromSourceInput!" ] } @@ -6819,7 +6888,7 @@ export default { 217, { "input": [ - 353, + 356, "ExecuteOneLogicFunctionInput!" ] } @@ -6828,7 +6897,7 @@ export default { 6, { "input": [ - 354, + 357, "UpdateLogicFunctionFromSourceInput!" ] } @@ -6837,7 +6906,7 @@ export default { 46, { "input": [ - 356, + 359, "CreateOneObjectInput!" ] } @@ -6846,7 +6915,7 @@ export default { 46, { "input": [ - 358, + 361, "DeleteOneObjectInput!" ] } @@ -6855,7 +6924,7 @@ export default { 46, { "input": [ - 359, + 362, "UpdateOneObjectInput!" ] } @@ -6864,7 +6933,7 @@ export default { 50, { "input": [ - 361, + 364, "UpdateViewFieldInput!" ] } @@ -6873,7 +6942,7 @@ export default { 50, { "input": [ - 363, + 366, "CreateViewFieldInput!" ] } @@ -6882,7 +6951,7 @@ export default { 50, { "inputs": [ - 363, + 366, "[CreateViewFieldInput!]!" ] } @@ -6891,7 +6960,7 @@ export default { 50, { "input": [ - 364, + 367, "DeleteViewFieldInput!" ] } @@ -6900,7 +6969,7 @@ export default { 50, { "input": [ - 365, + 368, "DestroyViewFieldInput!" ] } @@ -6909,7 +6978,7 @@ export default { 60, { "input": [ - 366, + 369, "CreateViewInput!" ] } @@ -6922,7 +6991,7 @@ export default { "String!" ], "input": [ - 367, + 370, "UpdateViewInput!" ] } @@ -6949,7 +7018,7 @@ export default { 57, { "input": [ - 368, + 371, "CreateViewSortInput!" ] } @@ -6958,7 +7027,7 @@ export default { 57, { "input": [ - 369, + 372, "UpdateViewSortInput!" ] } @@ -6967,7 +7036,7 @@ export default { 6, { "input": [ - 371, + 374, "DeleteViewSortInput!" ] } @@ -6976,7 +7045,7 @@ export default { 6, { "input": [ - 372, + 375, "DestroyViewSortInput!" ] } @@ -6985,7 +7054,7 @@ export default { 59, { "input": [ - 373, + 376, "UpdateViewFieldGroupInput!" ] } @@ -6994,7 +7063,7 @@ export default { 59, { "input": [ - 375, + 378, "CreateViewFieldGroupInput!" ] } @@ -7003,7 +7072,7 @@ export default { 59, { "inputs": [ - 375, + 378, "[CreateViewFieldGroupInput!]!" ] } @@ -7012,7 +7081,7 @@ export default { 59, { "input": [ - 376, + 379, "DeleteViewFieldGroupInput!" ] } @@ -7021,7 +7090,7 @@ export default { 59, { "input": [ - 377, + 380, "DestroyViewFieldGroupInput!" ] } @@ -7030,7 +7099,7 @@ export default { 60, { "input": [ - 378, + 381, "UpsertFieldsWidgetInput!" ] } @@ -7039,7 +7108,7 @@ export default { 225, { "input": [ - 381, + 384, "CreateCommandMenuItemInput!" ] } @@ -7048,7 +7117,7 @@ export default { 225, { "input": [ - 382, + 385, "UpdateCommandMenuItemInput!" ] } @@ -7066,7 +7135,7 @@ export default { 224, { "input": [ - 383, + 386, "CreateFrontComponentInput!" ] } @@ -7075,7 +7144,7 @@ export default { 224, { "input": [ - 384, + 387, "UpdateFrontComponentInput!" ] } @@ -7093,7 +7162,7 @@ export default { 25, { "input": [ - 386, + 389, "CreateAgentInput!" ] } @@ -7102,7 +7171,7 @@ export default { 25, { "input": [ - 387, + 390, "UpdateAgentInput!" ] } @@ -7111,7 +7180,7 @@ export default { 25, { "input": [ - 322, + 325, "AgentIdInput!" ] } @@ -7120,7 +7189,7 @@ export default { 148, { "file": [ - 388, + 391, "Upload!" ] } @@ -7129,7 +7198,7 @@ export default { 148, { "file": [ - 388, + 391, "Upload!" ] } @@ -7138,7 +7207,7 @@ export default { 148, { "file": [ - 388, + 391, "Upload!" ] } @@ -7147,7 +7216,7 @@ export default { 148, { "file": [ - 388, + 391, "Upload!" ] } @@ -7156,7 +7225,7 @@ export default { 148, { "file": [ - 388, + 391, "Upload!" ], "fieldMetadataId": [ @@ -7169,7 +7238,7 @@ export default { 148, { "file": [ - 388, + 391, "Upload!" ], "fieldMetadataUniversalIdentifier": [ @@ -7241,7 +7310,7 @@ export default { 216, { "input": [ - 389, + 392, "CreateNavigationMenuItemInput!" ] } @@ -7250,7 +7319,7 @@ export default { 216, { "input": [ - 390, + 393, "UpdateOneNavigationMenuItemInput!" ] } @@ -7268,7 +7337,7 @@ export default { 2, { "input": [ - 392, + 395, "CreateApiKeyInput!" ] } @@ -7277,7 +7346,7 @@ export default { 2, { "input": [ - 393, + 396, "UpdateApiKeyInput!" ] } @@ -7286,7 +7355,7 @@ export default { 2, { "input": [ - 394, + 397, "RevokeApiKeyInput!" ] } @@ -7321,7 +7390,7 @@ export default { 29, { "createRoleInput": [ - 395, + 398, "CreateRoleInput!" ] } @@ -7330,7 +7399,7 @@ export default { 29, { "updateRoleInput": [ - 396, + 399, "UpdateRoleInput!" ] } @@ -7348,7 +7417,7 @@ export default { 16, { "upsertObjectPermissionsInput": [ - 398, + 401, "UpsertObjectPermissionsInput!" ] } @@ -7357,7 +7426,7 @@ export default { 27, { "upsertPermissionFlagsInput": [ - 400, + 403, "UpsertPermissionFlagsInput!" ] } @@ -7366,7 +7435,7 @@ export default { 26, { "upsertFieldPermissionsInput": [ - 401, + 404, "UpsertFieldPermissionsInput!" ] } @@ -7375,7 +7444,7 @@ export default { 181, { "input": [ - 403, + 406, "UpsertRowLevelPermissionPredicatesInput!" ] } @@ -7442,7 +7511,7 @@ export default { 147, { "input": [ - 406, + 409, "CreateApprovedAccessDomainInput!" ] } @@ -7451,7 +7520,7 @@ export default { 6, { "input": [ - 407, + 410, "DeleteApprovedAccessDomainInput!" ] } @@ -7460,7 +7529,7 @@ export default { 147, { "input": [ - 408, + 411, "ValidateApprovedAccessDomainInput!" ] } @@ -7469,7 +7538,7 @@ export default { 34, { "input": [ - 409, + 412, "CreateOneFieldMetadataInput!" ] } @@ -7478,7 +7547,7 @@ export default { 34, { "input": [ - 411, + 414, "UpdateOneFieldMetadataInput!" ] } @@ -7487,7 +7556,7 @@ export default { 34, { "input": [ - 413, + 416, "DeleteOneFieldInput!" ] } @@ -7496,7 +7565,7 @@ export default { 56, { "input": [ - 414, + 417, "CreateViewGroupInput!" ] } @@ -7505,7 +7574,7 @@ export default { 56, { "inputs": [ - 414, + 417, "[CreateViewGroupInput!]!" ] } @@ -7514,7 +7583,7 @@ export default { 56, { "input": [ - 415, + 418, "UpdateViewGroupInput!" ] } @@ -7523,7 +7592,7 @@ export default { 56, { "input": [ - 417, + 420, "DeleteViewGroupInput!" ] } @@ -7532,7 +7601,7 @@ export default { 56, { "input": [ - 418, + 421, "DestroyViewGroupInput!" ] } @@ -7541,7 +7610,7 @@ export default { 54, { "input": [ - 419, + 422, "CreateViewFilterInput!" ] } @@ -7550,7 +7619,7 @@ export default { 54, { "input": [ - 420, + 423, "UpdateViewFilterInput!" ] } @@ -7559,7 +7628,7 @@ export default { 54, { "input": [ - 422, + 425, "DeleteViewFilterInput!" ] } @@ -7568,7 +7637,7 @@ export default { 54, { "input": [ - 423, + 426, "DestroyViewFilterInput!" ] } @@ -7577,7 +7646,7 @@ export default { 52, { "input": [ - 424, + 427, "CreateViewFilterGroupInput!" ] } @@ -7590,7 +7659,7 @@ export default { "String!" ], "input": [ - 425, + 428, "UpdateViewFilterGroupInput!" ] } @@ -7654,7 +7723,7 @@ export default { 66, { "data": [ - 426, + 429, "ActivateWorkspaceInput!" ] } @@ -7663,7 +7732,7 @@ export default { 66, { "data": [ - 427, + 430, "UpdateWorkspaceInput!" ] } @@ -7678,7 +7747,7 @@ export default { 203, { "input": [ - 428, + 431, "GetAuthorizationUrlForSSOInput!" ] } @@ -7932,7 +8001,7 @@ export default { 187, { "input": [ - 429, + 432, "CreateApplicationRegistrationInput!" ] } @@ -7941,7 +8010,7 @@ export default { 7, { "input": [ - 430, + 433, "UpdateApplicationRegistrationInput!" ] } @@ -7968,7 +8037,7 @@ export default { 5, { "input": [ - 432, + 435, "CreateApplicationRegistrationVariableInput!" ] } @@ -7977,7 +8046,7 @@ export default { 5, { "input": [ - 433, + 436, "UpdateApplicationRegistrationVariableInput!" ] } @@ -7995,7 +8064,7 @@ export default { 7, { "file": [ - 388, + 391, "Upload!" ], "universalIdentifier": [ @@ -8054,7 +8123,7 @@ export default { 194, { "input": [ - 435, + 438, "SetupOIDCSsoInput!" ] } @@ -8063,7 +8132,7 @@ export default { 194, { "input": [ - 436, + 439, "SetupSAMLSsoInput!" ] } @@ -8072,7 +8141,7 @@ export default { 190, { "input": [ - 437, + 440, "DeleteSsoInput!" ] } @@ -8081,31 +8150,31 @@ export default { 191, { "input": [ - 438, + 441, "EditSsoInput!" ] } ], "createWebhook": [ - 236, + 239, { "input": [ - 439, + 442, "CreateWebhookInput!" ] } ], "updateWebhook": [ - 236, + 239, { "input": [ - 440, + 443, "UpdateWebhookInput!" ] } ], "deleteWebhook": [ - 236, + 239, { "id": [ 3, @@ -8120,7 +8189,7 @@ export default { 222, { "input": [ - 442, + 445, "CreateSkillInput!" ] } @@ -8129,7 +8198,7 @@ export default { 222, { "input": [ - 443, + 446, "UpdateSkillInput!" ] } @@ -8184,7 +8253,7 @@ export default { } ], "duplicateDashboard": [ - 315, + 318, { "id": [ 3, @@ -8193,7 +8262,7 @@ export default { } ], "impersonate": [ - 282, + 285, { "userId": [ 3, @@ -8206,7 +8275,7 @@ export default { } ], "startChannelSync": [ - 307, + 310, { "connectedAccountId": [ 3, @@ -8215,7 +8284,7 @@ export default { } ], "saveImapSmtpCaldavAccount": [ - 305, + 308, { "accountOwnerId": [ 3, @@ -8226,7 +8295,7 @@ export default { "String!" ], "connectionParameters": [ - 444, + 447, "EmailAccountConnectionParameters!" ], "id": [ @@ -8238,13 +8307,13 @@ export default { 163, { "input": [ - 446, + 449, "UpdateLabPublicFeatureFlagInput!" ] } ], "userLookupAdminPanel": [ - 273, + 276, { "userIdentifier": [ 1, @@ -8318,7 +8387,7 @@ export default { } ], "retryJobs": [ - 266, + 269, { "queueName": [ 1, @@ -8331,7 +8400,7 @@ export default { } ], "deleteJobs": [ - 261, + 264, { "queueName": [ 1, @@ -8344,13 +8413,13 @@ export default { } ], "enablePostgresProxy": [ - 306 + 309 ], "disablePostgresProxy": [ - 306 + 309 ], "createPublicDomain": [ - 294, + 297, { "domain": [ 1, @@ -8377,14 +8446,14 @@ export default { } ], "createEmailingDomain": [ - 296, + 299, { "domain": [ 1, "String!" ], "driver": [ - 297, + 300, "EmailingDomainDriver!" ] } @@ -8399,7 +8468,7 @@ export default { } ], "verifyEmailingDomain": [ - 296, + 299, { "id": [ 1, @@ -8411,7 +8480,7 @@ export default { 68, { "input": [ - 447, + 450, "CreateOneAppTokenInput!" ] } @@ -8444,7 +8513,7 @@ export default { 6, { "workspaceMigration": [ - 449, + 452, "WorkspaceMigrationInput!" ] } @@ -8476,7 +8545,7 @@ export default { } ], "createDevelopmentApplication": [ - 283, + 286, { "universalIdentifier": [ 1, @@ -8498,7 +8567,7 @@ export default { } ], "syncApplication": [ - 284, + 287, { "manifest": [ 15, @@ -8507,10 +8576,10 @@ export default { } ], "uploadApplicationFile": [ - 285, + 288, { "file": [ - 388, + 391, "Upload!" ], "applicationUniversalIdentifier": [ @@ -8518,7 +8587,7 @@ export default { "String!" ], "fileFolder": [ - 453, + 456, "FileFolder!" ], "filePath": [ @@ -8593,7 +8662,7 @@ export default { 3 ], "gridPosition": [ - 343 + 346 ], "position": [ 15 @@ -8633,7 +8702,7 @@ export default { 3 ], "gridPosition": [ - 343 + 346 ], "position": [ 15 @@ -8715,7 +8784,7 @@ export default { 3 ], "tabs": [ - 350 + 353 ], "__typename": [ 1 @@ -8735,7 +8804,7 @@ export default { 1 ], "widgets": [ - 351 + 354 ], "__typename": [ 1 @@ -8758,7 +8827,7 @@ export default { 3 ], "gridPosition": [ - 343 + 346 ], "position": [ 15 @@ -8827,7 +8896,7 @@ export default { 3 ], "update": [ - 355 + 358 ], "__typename": [ 1 @@ -8873,7 +8942,7 @@ export default { }, "CreateOneObjectInput": { "object": [ - 357 + 360 ], "__typename": [ 1 @@ -8930,7 +8999,7 @@ export default { }, "UpdateOneObjectInput": { "update": [ - 360 + 363 ], "id": [ 3 @@ -8982,7 +9051,7 @@ export default { 3 ], "update": [ - 362 + 365 ], "__typename": [ 1 @@ -9181,7 +9250,7 @@ export default { 3 ], "update": [ - 370 + 373 ], "__typename": [ 1 @@ -9216,7 +9285,7 @@ export default { 3 ], "update": [ - 374 + 377 ], "__typename": [ 1 @@ -9280,10 +9349,10 @@ export default { 3 ], "groups": [ - 379 + 382 ], "fields": [ - 380 + 383 ], "__typename": [ 1 @@ -9303,7 +9372,7 @@ export default { 6 ], "fields": [ - 380 + 383 ], "__typename": [ 1 @@ -9424,7 +9493,7 @@ export default { 3 ], "update": [ - 385 + 388 ], "__typename": [ 1 @@ -9555,7 +9624,7 @@ export default { 3 ], "update": [ - 391 + 394 ], "__typename": [ 1 @@ -9672,7 +9741,7 @@ export default { }, "UpdateRoleInput": { "update": [ - 397 + 400 ], "id": [ 3 @@ -9727,7 +9796,7 @@ export default { 3 ], "objectPermissions": [ - 399 + 402 ], "__typename": [ 1 @@ -9769,7 +9838,7 @@ export default { 3 ], "fieldPermissions": [ - 402 + 405 ], "__typename": [ 1 @@ -9800,10 +9869,10 @@ export default { 3 ], "predicates": [ - 404 + 407 ], "predicateGroups": [ - 405 + 408 ], "__typename": [ 1 @@ -9893,7 +9962,7 @@ export default { }, "CreateOneFieldMetadataInput": { "field": [ - 410 + 413 ], "__typename": [ 1 @@ -9966,7 +10035,7 @@ export default { 3 ], "update": [ - 412 + 415 ], "__typename": [ 1 @@ -10055,7 +10124,7 @@ export default { 3 ], "update": [ - 416 + 419 ], "__typename": [ 1 @@ -10128,7 +10197,7 @@ export default { 3 ], "update": [ - 421 + 424 ], "__typename": [ 1 @@ -10349,7 +10418,7 @@ export default { 1 ], "update": [ - 431 + 434 ], "__typename": [ 1 @@ -10412,7 +10481,7 @@ export default { 1 ], "update": [ - 434 + 437 ], "__typename": [ 1 @@ -10513,7 +10582,7 @@ export default { 3 ], "update": [ - 441 + 444 ], "__typename": [ 1 @@ -10587,13 +10656,13 @@ export default { }, "EmailAccountConnectionParameters": { "IMAP": [ - 445 + 448 ], "SMTP": [ - 445 + 448 ], "CALDAV": [ - 445 + 448 ], "__typename": [ 1 @@ -10632,7 +10701,7 @@ export default { }, "CreateOneAppTokenInput": { "appToken": [ - 448 + 451 ], "__typename": [ 1 @@ -10648,7 +10717,7 @@ export default { }, "WorkspaceMigrationInput": { "actions": [ - 450 + 453 ], "__typename": [ 1 @@ -10656,10 +10725,10 @@ export default { }, "WorkspaceMigrationDeleteActionInput": { "type": [ - 451 + 454 ], "metadataName": [ - 452 + 455 ], "universalIdentifier": [ 1 @@ -10676,7 +10745,7 @@ export default { 121, { "input": [ - 455, + 458, "OnDbEventInput!" ] } @@ -10694,7 +10763,7 @@ export default { 219, { "input": [ - 456, + 459, "LogicFunctionLogsInput!" ] } diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts index 9eb3ef6e89..ed51770b28 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts @@ -15,6 +15,7 @@ import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/ import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { MinimalMetadataModule } from 'src/engine/metadata-modules/minimal-metadata/minimal-metadata.module'; import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; import { RouteTriggerModule } from 'src/engine/metadata-modules/route-trigger/route-trigger.module'; import { SearchFieldMetadataModule } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.module'; @@ -39,6 +40,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work AiAgentMonitorModule, AiChatModule, AiGenerateTextModule, + MinimalMetadataModule, ViewModule, WorkspaceMetadataVersionModule, RoleModule, @@ -64,6 +66,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work NavigationMenuItemModule, AiAgentModule, AiChatModule, + MinimalMetadataModule, ViewModule, RoleModule, PermissionsModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/dtos/minimal-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/dtos/minimal-metadata.dto.ts new file mode 100644 index 0000000000..5626488899 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/dtos/minimal-metadata.dto.ts @@ -0,0 +1,16 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql'; + +import { MinimalObjectMetadataDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/minimal-object-metadata.dto'; +import { MinimalViewDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/minimal-view.dto'; + +@ObjectType('MinimalMetadata') +export class MinimalMetadataDTO { + @Field(() => [MinimalObjectMetadataDTO]) + objectMetadataItems: MinimalObjectMetadataDTO[]; + + @Field(() => [MinimalViewDTO]) + views: MinimalViewDTO[]; + + @Field(() => Int) + metadataVersion: number; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/dtos/minimal-object-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/dtos/minimal-object-metadata.dto.ts new file mode 100644 index 0000000000..1233a92ad7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/dtos/minimal-object-metadata.dto.ts @@ -0,0 +1,38 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('MinimalObjectMetadata') +export class MinimalObjectMetadataDTO { + @IDField(() => UUIDScalarType) + id: string; + + @Field() + nameSingular: string; + + @Field() + namePlural: string; + + @Field() + labelSingular: string; + + @Field() + labelPlural: string; + + @Field({ nullable: true }) + icon?: string; + + @Field() + isCustom: boolean; + + @Field() + isActive: boolean; + + @Field() + isSystem: boolean; + + @Field() + isRemote: boolean; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/dtos/minimal-view.dto.ts b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/dtos/minimal-view.dto.ts new file mode 100644 index 0000000000..c1146205a1 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/dtos/minimal-view.dto.ts @@ -0,0 +1,21 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { ViewKey, ViewType } from 'twenty-shared/types'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('MinimalView') +export class MinimalViewDTO { + @IDField(() => UUIDScalarType) + id: string; + + @Field(() => ViewType) + type: ViewType; + + @Field(() => ViewKey, { nullable: true }) + key: ViewKey | null; + + @Field(() => UUIDScalarType) + objectMetadataId: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.module.ts new file mode 100644 index 0000000000..b3b5c630f3 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { MinimalMetadataResolver } from 'src/engine/metadata-modules/minimal-metadata/minimal-metadata.resolver'; +import { MinimalMetadataService } from 'src/engine/metadata-modules/minimal-metadata/minimal-metadata.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([WorkspaceEntity]), + WorkspaceManyOrAllFlatEntityMapsCacheModule, + ], + providers: [MinimalMetadataResolver, MinimalMetadataService], + exports: [MinimalMetadataService], +}) +export class MinimalMetadataModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.resolver.ts new file mode 100644 index 0000000000..0b8aab5a92 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.resolver.ts @@ -0,0 +1,30 @@ +import { UseGuards } from '@nestjs/common'; +import { Query } from '@nestjs/graphql'; + +import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { MinimalMetadataDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/minimal-metadata.dto'; +import { MinimalMetadataService } from 'src/engine/metadata-modules/minimal-metadata/minimal-metadata.service'; + +@MetadataResolver(() => MinimalMetadataDTO) +@UseGuards(WorkspaceAuthGuard, NoPermissionGuard) +export class MinimalMetadataResolver { + constructor( + private readonly minimalMetadataService: MinimalMetadataService, + ) {} + + @Query(() => MinimalMetadataDTO) + async minimalMetadata( + @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string | undefined, + ): Promise { + return this.minimalMetadataService.getMinimalMetadata( + workspace.id, + userWorkspaceId, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.service.ts new file mode 100644 index 0000000000..9f2f1f980d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.service.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { ViewVisibility } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { MinimalMetadataDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/minimal-metadata.dto'; +import { MinimalObjectMetadataDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/minimal-object-metadata.dto'; +import { MinimalViewDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/minimal-view.dto'; + +@Injectable() +export class MinimalMetadataService { + constructor( + @InjectRepository(WorkspaceEntity) + private readonly workspaceRepository: Repository, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + ) {} + + async getMinimalMetadata( + workspaceId: string, + userWorkspaceId?: string, + ): Promise { + const [workspace, { flatObjectMetadataMaps, flatViewMaps }] = + await Promise.all([ + this.workspaceRepository.findOneOrFail({ + where: { id: workspaceId }, + select: ['metadataVersion'], + }), + this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({ + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatViewMaps'], + }), + ]); + + const objectMetadataItems: MinimalObjectMetadataDTO[] = Object.values( + flatObjectMetadataMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((flatObjectMetadata) => flatObjectMetadata.isActive === true) + .map((flatObjectMetadata) => ({ + id: flatObjectMetadata.id, + nameSingular: flatObjectMetadata.nameSingular, + namePlural: flatObjectMetadata.namePlural, + labelSingular: flatObjectMetadata.labelSingular, + labelPlural: flatObjectMetadata.labelPlural, + icon: flatObjectMetadata.icon ?? undefined, + isCustom: flatObjectMetadata.isCustom, + isActive: flatObjectMetadata.isActive, + isSystem: flatObjectMetadata.isSystem, + isRemote: flatObjectMetadata.isRemote, + })); + + const views: MinimalViewDTO[] = Object.values( + flatViewMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((flatView) => flatView.workspaceId === workspaceId) + .filter((flatView) => flatView.deletedAt === null) + .filter( + (flatView) => + flatView.visibility === ViewVisibility.WORKSPACE || + (flatView.visibility === ViewVisibility.UNLISTED && + isDefined(userWorkspaceId) && + flatView.createdByUserWorkspaceId === userWorkspaceId), + ) + .map((flatView) => ({ + id: flatView.id, + type: flatView.type, + key: flatView.key, + objectMetadataId: flatView.objectMetadataId, + })); + + return { + objectMetadataItems, + views, + metadataVersion: workspace.metadataVersion, + }; + } +}