Handle SSE event for View and ViewField (#18218)
This PR implements a first working version of SSE for View and ViewField. We could improve the logic but we re-use the same actual codepath for refreshing the views of an object. This does not handle filters and sorts for now. # Demo https://github.com/user-attachments/assets/73367f3a-a5fc-4b06-a2e2-fad3b99deec7
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { MainContextStoreProviderEffect } from '@/context-store/components/MainContextStoreProviderEffect';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
@@ -76,7 +76,7 @@ export const MainContextStoreProvider = () => {
|
||||
const shouldComputeContextStore =
|
||||
(isRecordIndexPage || isRecordShowPage || isSettingsPage) &&
|
||||
!showAuthModal &&
|
||||
viewsEntry.status === 'loaded';
|
||||
viewsEntry.status === 'up-to-date';
|
||||
|
||||
if (!shouldComputeContextStore) {
|
||||
return null;
|
||||
|
||||
+7
-4
@@ -14,15 +14,18 @@ export const IsAppMetadataReadyEffect = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const objectsEntry = useAtomFamilyStateValue(metadataStoreState, 'objects');
|
||||
const objectMetadataItemsEntry = useAtomFamilyStateValue(
|
||||
metadataStoreState,
|
||||
'objectMetadataItems',
|
||||
);
|
||||
const viewsEntry = useAtomFamilyStateValue(metadataStoreState, 'views');
|
||||
const setIsAppMetadataReady = useSetAtomState(isAppMetadataReadyState);
|
||||
|
||||
useEffect(() => {
|
||||
const hasActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
|
||||
|
||||
const areObjectsLoaded = objectsEntry.status === 'loaded';
|
||||
const areViewsLoaded = viewsEntry.status === 'loaded';
|
||||
const areObjectsLoaded = objectMetadataItemsEntry.status === 'up-to-date';
|
||||
const areViewsLoaded = viewsEntry.status === 'up-to-date';
|
||||
|
||||
if (!areObjectsLoaded) {
|
||||
setIsAppMetadataReady(false);
|
||||
@@ -38,7 +41,7 @@ export const IsAppMetadataReadyEffect = () => {
|
||||
isLoggedIn,
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
objectsEntry.status,
|
||||
objectMetadataItemsEntry.status,
|
||||
viewsEntry.status,
|
||||
setIsAppMetadataReady,
|
||||
]);
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
@@ -37,7 +37,7 @@ export const ObjectMetadataProviderInitialEffect = () => {
|
||||
}
|
||||
|
||||
const loadedItems = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objects', loadedItems);
|
||||
updateDraft('objectMetadataItems', loadedItems);
|
||||
applyChanges();
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
AllMetadataName,
|
||||
type CoreViewField,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const ViewFieldMetadataSSEEffect = () => {
|
||||
const store = useStore();
|
||||
|
||||
const queryId = 'view-field-metadata-sse-effect';
|
||||
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewField,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewField,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
const coreViews = store.get(coreViewsState.atom);
|
||||
|
||||
let viewId: string | undefined;
|
||||
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
const createdViewField = eventDetail.operation
|
||||
.createdRecord as unknown as CoreViewField;
|
||||
viewId = createdViewField.viewId;
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
const updatedViewField = eventDetail.operation
|
||||
.updatedRecord as unknown as CoreViewField;
|
||||
viewId = updatedViewField.viewId;
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedViewFieldId = eventDetail.operation
|
||||
.deletedRecordId as string;
|
||||
|
||||
const view = coreViews.find((coreView) =>
|
||||
coreView.viewFields.some(
|
||||
(viewField) => viewField.id === deletedViewFieldId,
|
||||
),
|
||||
);
|
||||
|
||||
viewId = view?.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = coreViews.find((coreView) => coreView.id === viewId);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
+9
-53
@@ -1,51 +1,19 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useFetchAndLoadIndexViews } from '@/metadata-store/hooks/useFetchAndLoadIndexViews';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
import {
|
||||
ViewType as CoreViewType,
|
||||
useFindAllCoreViewsLazyQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
const INDEX_VIEW_TYPES = [
|
||||
CoreViewType.TABLE,
|
||||
CoreViewType.KANBAN,
|
||||
CoreViewType.CALENDAR,
|
||||
];
|
||||
|
||||
export const ViewMetadataProviderInitialEffect = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const store = useStore();
|
||||
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const [findAllCoreViews] = useFindAllCoreViewsLazyQuery();
|
||||
|
||||
const setIndexCoreViews = useCallback(
|
||||
(indexViews: CoreViewWithRelations[]) => {
|
||||
const existingCoreViews = store.get(coreViewsState.atom);
|
||||
const existingFieldsWidgetViews = existingCoreViews.filter(
|
||||
(view) => view.type === CoreViewType.FIELDS_WIDGET,
|
||||
);
|
||||
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
|
||||
|
||||
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
|
||||
store.set(coreViewsState.atom, mergedViews);
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
const { fetchAndLoadIndexViews } = useFetchAndLoadIndexViews();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
@@ -60,31 +28,19 @@ export const ViewMetadataProviderInitialEffect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadViews = async () => {
|
||||
const result = await findAllCoreViews({
|
||||
variables: { viewTypes: INDEX_VIEW_TYPES },
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (isDefined(result.data?.getCoreViews)) {
|
||||
setIndexCoreViews(result.data.getCoreViews);
|
||||
updateDraft('views', result.data.getCoreViews);
|
||||
applyChanges();
|
||||
}
|
||||
const initialize = async () => {
|
||||
await fetchAndLoadIndexViews();
|
||||
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
loadViews();
|
||||
initialize();
|
||||
}, [
|
||||
isInitialized,
|
||||
isCurrentUserLoaded,
|
||||
isLoggedIn,
|
||||
currentWorkspace,
|
||||
findAllCoreViews,
|
||||
setIndexCoreViews,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
fetchAndLoadIndexViews,
|
||||
]);
|
||||
|
||||
return null;
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type CoreViewWithoutRelations } from '@/views/types/CoreViewWithoutRelations';
|
||||
import { useStore } from 'jotai';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ViewMetadataSSEEffect = () => {
|
||||
const queryId = 'view-metadata-sse-effect';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.view,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.view,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
const createdView = eventDetail.operation
|
||||
.createdRecord as unknown as CoreViewWithoutRelations;
|
||||
|
||||
store.set(coreViewsState.atom, (prevCoreViews) => [
|
||||
...prevCoreViews,
|
||||
{
|
||||
...createdView,
|
||||
viewFields: [],
|
||||
viewFieldGroups: [],
|
||||
viewFilterGroups: [],
|
||||
viewSorts: [],
|
||||
viewFilters: [],
|
||||
viewGroups: [],
|
||||
},
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
const updatedView = eventDetail.operation
|
||||
.updatedRecord as unknown as CoreViewWithoutRelations;
|
||||
|
||||
store.set(coreViewsState.atom, (prevCoreViews) =>
|
||||
prevCoreViews.map((coreView) =>
|
||||
coreView.id === updatedView.id
|
||||
? {
|
||||
...coreView,
|
||||
...updatedView,
|
||||
}
|
||||
: coreView,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedViewId = eventDetail.operation.deletedRecordId as string;
|
||||
|
||||
store.set(coreViewsState.atom, (prevCoreViews) =>
|
||||
prevCoreViews.filter((coreView) => coreView.id !== deletedViewId),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useSetIndexViews } from '@/metadata-store/hooks/useSetIndexViews';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
useFindAllCoreViewsLazyQuery,
|
||||
ViewType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const INDEX_VIEW_TYPES = [ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR];
|
||||
|
||||
export const useFetchAndLoadIndexViews = () => {
|
||||
const [findAllCoreViews] = useFindAllCoreViewsLazyQuery();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
const { setIndexViews } = useSetIndexViews();
|
||||
|
||||
const fetchAndLoadIndexViews = useCallback(async () => {
|
||||
const result = await findAllCoreViews({
|
||||
variables: { viewTypes: INDEX_VIEW_TYPES },
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (isDefined(result.data?.getCoreViews)) {
|
||||
setIndexViews(result.data.getCoreViews);
|
||||
updateDraft('views', result.data.getCoreViews);
|
||||
applyChanges();
|
||||
}
|
||||
}, [findAllCoreViews, setIndexViews, updateDraft, applyChanges]);
|
||||
|
||||
return { fetchAndLoadIndexViews };
|
||||
};
|
||||
@@ -1,17 +1,17 @@
|
||||
import {
|
||||
ALL_METADATA_KEYS,
|
||||
metadataStoreState,
|
||||
type MetadataKey,
|
||||
type MetadataLoadEntry,
|
||||
} from '@/metadata-store/states/metadataStoreState';
|
||||
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
|
||||
import { type createStore, useStore } from 'jotai';
|
||||
import {
|
||||
ALL_METADATA_ENTITY_KEYS,
|
||||
metadataStoreState,
|
||||
type MetadataEntityKey,
|
||||
type MetadataStoreItem,
|
||||
} from '@/metadata-store/states/metadataStoreState';
|
||||
import { useStore, type createStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type JotaiStore = ReturnType<typeof createStore>;
|
||||
|
||||
const EMPTY_ENTRY: MetadataLoadEntry = {
|
||||
const EMPTY_ENTRY: MetadataStoreItem = {
|
||||
current: [],
|
||||
draft: [],
|
||||
status: 'empty',
|
||||
@@ -31,20 +31,23 @@ const areViewsConsistentWithObjects = (
|
||||
};
|
||||
|
||||
export const resetMetadataStore = (store: JotaiStore) => {
|
||||
for (const key of ALL_METADATA_KEYS) {
|
||||
for (const key of ALL_METADATA_ENTITY_KEYS) {
|
||||
store.set(metadataStoreState.atomFamily(key), EMPTY_ENTRY);
|
||||
}
|
||||
|
||||
store.set(isAppMetadataReadyState.atom, false);
|
||||
};
|
||||
|
||||
const promoteEntry = (store: JotaiStore, key: MetadataKey) => {
|
||||
const entry = store.get(metadataStoreState.atomFamily(key));
|
||||
const changeMetadataEntityAsUpToDate = (
|
||||
store: JotaiStore,
|
||||
metadataEntityKey: MetadataEntityKey,
|
||||
) => {
|
||||
const entry = store.get(metadataStoreState.atomFamily(metadataEntityKey));
|
||||
|
||||
store.set(metadataStoreState.atomFamily(key), {
|
||||
store.set(metadataStoreState.atomFamily(metadataEntityKey), {
|
||||
current: entry.draft,
|
||||
draft: [],
|
||||
status: 'loaded',
|
||||
status: 'up-to-date',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -52,11 +55,11 @@ export const useMetadataStore = () => {
|
||||
const store = useStore();
|
||||
|
||||
const updateDraft = useCallback(
|
||||
(key: MetadataKey, data: object[]) => {
|
||||
(key: MetadataEntityKey, data: object[]) => {
|
||||
const currentEntry = store.get(metadataStoreState.atomFamily(key));
|
||||
|
||||
if (
|
||||
currentEntry.status === 'loaded' &&
|
||||
currentEntry.status === 'up-to-date' &&
|
||||
isDeeplyEqual(currentEntry.current, data)
|
||||
) {
|
||||
return;
|
||||
@@ -65,42 +68,48 @@ export const useMetadataStore = () => {
|
||||
store.set(metadataStoreState.atomFamily(key), (prev) => ({
|
||||
...prev,
|
||||
draft: data,
|
||||
status: 'draft_pending' as const,
|
||||
status: 'draft-pending' as const,
|
||||
}));
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
const applyChanges = useCallback((): boolean => {
|
||||
let promoted = false;
|
||||
const applyChanges = useCallback((): {
|
||||
hasPersistedAnyMetadataEntity: boolean;
|
||||
} => {
|
||||
let hasPersistedAnyMetadataEntity = false;
|
||||
|
||||
for (const key of ALL_METADATA_KEYS) {
|
||||
if (key === 'views') {
|
||||
for (const metadataEntityKey of ALL_METADATA_ENTITY_KEYS) {
|
||||
if (metadataEntityKey === 'views') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = store.get(metadataStoreState.atomFamily(key));
|
||||
const metadataStoreEntityEntry = store.get(
|
||||
metadataStoreState.atomFamily(metadataEntityKey),
|
||||
);
|
||||
|
||||
if (entry.status === 'draft_pending') {
|
||||
promoteEntry(store, key);
|
||||
promoted = true;
|
||||
if (metadataStoreEntityEntry.status === 'draft-pending') {
|
||||
changeMetadataEntityAsUpToDate(store, metadataEntityKey);
|
||||
hasPersistedAnyMetadataEntity = true;
|
||||
}
|
||||
}
|
||||
|
||||
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
|
||||
|
||||
if (viewsEntry.status === 'draft_pending') {
|
||||
const objectsEntry = store.get(metadataStoreState.atomFamily('objects'));
|
||||
if (viewsEntry.status === 'draft-pending') {
|
||||
const objectsEntry = store.get(
|
||||
metadataStoreState.atomFamily('objectMetadataItems'),
|
||||
);
|
||||
|
||||
if (
|
||||
areViewsConsistentWithObjects(viewsEntry.draft, objectsEntry.current)
|
||||
) {
|
||||
promoteEntry(store, 'views');
|
||||
promoted = true;
|
||||
changeMetadataEntityAsUpToDate(store, 'views');
|
||||
hasPersistedAnyMetadataEntity = true;
|
||||
}
|
||||
}
|
||||
|
||||
return promoted;
|
||||
return { hasPersistedAnyMetadataEntity };
|
||||
}, [store]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ export const useReloadWorkspaceMetadata = () => {
|
||||
|
||||
await refreshObjectMetadataItems();
|
||||
const loadedObjects = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objects', loadedObjects);
|
||||
updateDraft('objectMetadataItems', loadedObjects);
|
||||
applyChanges();
|
||||
|
||||
const loadedViews = store.get(coreViewsState.atom);
|
||||
@@ -36,7 +36,7 @@ export const useReloadWorkspaceMetadata = () => {
|
||||
|
||||
await loadMockedObjectMetadataItems();
|
||||
const loadedObjects = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objects', loadedObjects);
|
||||
updateDraft('objectMetadataItems', loadedObjects);
|
||||
applyChanges();
|
||||
}, [
|
||||
resetMetadataStore,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { ViewType } from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useSetIndexViews = () => {
|
||||
const store = useStore();
|
||||
|
||||
const setIndexViews = useCallback(
|
||||
(indexViews: CoreViewWithRelations[]) => {
|
||||
const existingCoreViews = store.get(coreViewsState.atom);
|
||||
const existingFieldsWidgetViews = existingCoreViews.filter(
|
||||
(view) => view.type === ViewType.FIELDS_WIDGET,
|
||||
);
|
||||
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
|
||||
|
||||
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
|
||||
store.set(coreViewsState.atom, mergedViews);
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return {
|
||||
setIndexViews,
|
||||
};
|
||||
};
|
||||
@@ -1,36 +1,39 @@
|
||||
import { createAtomFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomFamilyState';
|
||||
|
||||
export type MetadataLoadStatus = 'empty' | 'draft_pending' | 'loaded';
|
||||
export type MetadataEntityStoreStatus =
|
||||
| 'empty'
|
||||
| 'draft-pending'
|
||||
| 'up-to-date';
|
||||
|
||||
export type MetadataKey =
|
||||
| 'objects'
|
||||
| 'views'
|
||||
| 'pageLayouts'
|
||||
| 'logicFunctions';
|
||||
|
||||
export const ALL_METADATA_KEYS: MetadataKey[] = [
|
||||
'objects',
|
||||
export const ALL_METADATA_ENTITY_KEYS = [
|
||||
'objectMetadataItems',
|
||||
'fieldMetadataItems',
|
||||
'views',
|
||||
'viewFields',
|
||||
'viewFilters',
|
||||
'viewSorts',
|
||||
'pageLayouts',
|
||||
'logicFunctions',
|
||||
];
|
||||
] as const;
|
||||
|
||||
export type MetadataLoadEntry = {
|
||||
export type MetadataEntityKey = (typeof ALL_METADATA_ENTITY_KEYS)[number];
|
||||
|
||||
export type MetadataStoreItem = {
|
||||
current: object[];
|
||||
draft: object[];
|
||||
status: MetadataLoadStatus;
|
||||
status: MetadataEntityStoreStatus;
|
||||
};
|
||||
|
||||
const METADATA_LOAD_ENTRY_DEFAULT: MetadataLoadEntry = {
|
||||
const METADATA_STORE_ITEM_INITIAL_VALUE: MetadataStoreItem = {
|
||||
current: [],
|
||||
draft: [],
|
||||
status: 'empty',
|
||||
};
|
||||
|
||||
export const metadataStoreState = createAtomFamilyState<
|
||||
MetadataLoadEntry,
|
||||
MetadataKey
|
||||
MetadataStoreItem,
|
||||
MetadataEntityKey
|
||||
>({
|
||||
key: 'metadataStoreState',
|
||||
defaultValue: METADATA_LOAD_ENTRY_DEFAULT,
|
||||
defaultValue: METADATA_STORE_ITEM_INITIAL_VALUE,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ViewFieldMetadataSSEEffect } from '@/metadata-store/effect-components/ViewFieldMetadataSSEEffect';
|
||||
import { ViewMetadataSSEEffect } from '@/metadata-store/effect-components/ViewMetadataSSEEffect';
|
||||
import { SSEClientEffect } from '@/sse-db-event/components/SSEClientEffect';
|
||||
import { SSEEventStreamEffect } from '@/sse-db-event/components/SSEEventStreamEffect';
|
||||
import { SSEQuerySubscribeEffect } from '@/sse-db-event/components/SSEQuerySubscribeEffect';
|
||||
@@ -13,6 +15,8 @@ export const SSEProvider = ({ children }: SSEProviderProps) => {
|
||||
<SSEClientEffect />
|
||||
<SSEEventStreamEffect />
|
||||
<SSEQuerySubscribeEffect />
|
||||
<ViewFieldMetadataSSEEffect />
|
||||
<ViewMetadataSSEEffect />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { recordPageLayoutsState } from '@/page-layout/states/recordPageLayoutsState';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ import {
|
||||
useDestroyCoreViewFieldMutation,
|
||||
useUpdateCoreViewFieldMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const usePerformViewFieldAPIPersist = () => {
|
||||
const { triggerViewFieldOptimisticEffect } =
|
||||
useTriggerViewFieldOptimisticEffect();
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { recordIndexShouldHideEmptyRecordGroupsComponentState } from '@/object-record/record-index/states/recordIndexShouldHideEmptyRecordGroupsComponentState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/selectors/coreViewsByObjectMetadataIdFamilySelector';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { getFilterableFields } from '@/views/utils/getFilterableFields';
|
||||
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
import { type FindManyCoreViewsQuery } from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useApplyCoreViewsForObjectMetadataId = () => {
|
||||
const applyCoreViewsForObjectMetadataId = useCallback(
|
||||
(
|
||||
objectMetadataId: string,
|
||||
coreViewsFromResult: FindManyCoreViewsQuery['getCoreViews'],
|
||||
) => {
|
||||
const objectMetadataItems = jotaiStore.get(objectMetadataItemsState.atom);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coreViewsForObjectMetadataId = jotaiStore.get(
|
||||
coreViewsByObjectMetadataIdFamilySelector.selectorFamily(
|
||||
objectMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
if (isDeeplyEqual(coreViewsForObjectMetadataId, coreViewsFromResult)) {
|
||||
return;
|
||||
}
|
||||
|
||||
jotaiStore.set(
|
||||
coreViewsByObjectMetadataIdFamilySelector.selectorFamily(
|
||||
objectMetadataId,
|
||||
),
|
||||
coreViewsFromResult,
|
||||
);
|
||||
|
||||
for (const coreView of coreViewsFromResult) {
|
||||
const existingView = coreViewsForObjectMetadataId.find(
|
||||
(coreViewForObjectMetadata) =>
|
||||
coreViewForObjectMetadata.id === coreView.id,
|
||||
);
|
||||
|
||||
if (!isDefined(existingView)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
coreView.viewFields.map((viewField) =>
|
||||
removePropertiesFromRecord(viewField, ['updatedAt', 'createdAt']),
|
||||
),
|
||||
existingView.viewFields,
|
||||
)
|
||||
) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
jotaiStore.set(
|
||||
currentRecordFieldsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
view.viewFields
|
||||
.filter(isDefined)
|
||||
.map((viewField) => mapViewFieldToRecordField(viewField)),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
coreView.viewFilters.map((viewFilter) =>
|
||||
removePropertiesFromRecord(viewFilter, [
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
]),
|
||||
),
|
||||
existingView.viewFilters,
|
||||
)
|
||||
) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
jotaiStore.set(
|
||||
currentRecordFiltersComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
getFilterableFields(objectMetadataItem),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDeeplyEqual(coreView.viewSorts, existingView.viewSorts)) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
jotaiStore.set(
|
||||
currentRecordSortsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
view.viewSorts,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
coreView.shouldHideEmptyGroups !== existingView.shouldHideEmptyGroups
|
||||
) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
jotaiStore.set(
|
||||
recordIndexShouldHideEmptyRecordGroupsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
view.shouldHideEmptyGroups,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { applyCoreViewsForObjectMetadataId };
|
||||
};
|
||||
+8
-131
@@ -1,24 +1,12 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { recordIndexShouldHideEmptyRecordGroupsComponentState } from '@/object-record/record-index/states/recordIndexShouldHideEmptyRecordGroupsComponentState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/selectors/coreViewsByObjectMetadataIdFamilySelector';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { getFilterableFields } from '@/views/utils/getFilterableFields';
|
||||
|
||||
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { useApplyCoreViewsForObjectMetadataId } from '@/views/hooks/useApplyCoreViewsForObjectMetadataId';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useFindManyCoreViewsLazyQuery } from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useRefreshCoreViewsByObjectMetadataId = () => {
|
||||
const store = useStore();
|
||||
const [findManyCoreViewsLazy] = useFindManyCoreViewsLazyQuery();
|
||||
const { applyCoreViewsForObjectMetadataId } =
|
||||
useApplyCoreViewsForObjectMetadataId();
|
||||
|
||||
const refreshCoreViewsByObjectMetadataId = useCallback(
|
||||
async (objectMetadataId: string) => {
|
||||
@@ -33,123 +21,12 @@ export const useRefreshCoreViewsByObjectMetadataId = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.id === objectMetadataId,
|
||||
applyCoreViewsForObjectMetadataId(
|
||||
objectMetadataId,
|
||||
result.data.getCoreViews,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coreViewsForObjectMetadataId = store.get(
|
||||
coreViewsByObjectMetadataIdFamilySelector.selectorFamily(
|
||||
objectMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
const coreViewsFromResult = result.data.getCoreViews;
|
||||
|
||||
if (isDeeplyEqual(coreViewsForObjectMetadataId, coreViewsFromResult)) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(
|
||||
coreViewsByObjectMetadataIdFamilySelector.selectorFamily(
|
||||
objectMetadataId,
|
||||
),
|
||||
coreViewsFromResult,
|
||||
);
|
||||
|
||||
for (const coreView of coreViewsFromResult) {
|
||||
const existingView = coreViewsForObjectMetadataId.find(
|
||||
(coreViewForObjectMetadata) =>
|
||||
coreViewForObjectMetadata.id === coreView.id,
|
||||
);
|
||||
|
||||
if (!isDefined(existingView)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
coreView.viewFields.map((viewField) =>
|
||||
removePropertiesFromRecord(viewField, ['updatedAt', 'createdAt']),
|
||||
),
|
||||
existingView.viewFields,
|
||||
)
|
||||
) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
store.set(
|
||||
currentRecordFieldsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
view.viewFields
|
||||
.filter(isDefined)
|
||||
.map((viewField) => mapViewFieldToRecordField(viewField)),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
coreView.viewFilters.map((viewFilter) =>
|
||||
removePropertiesFromRecord(viewFilter, [
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
]),
|
||||
),
|
||||
existingView.viewFilters,
|
||||
)
|
||||
) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
store.set(
|
||||
currentRecordFiltersComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
getFilterableFields(objectMetadataItem),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDeeplyEqual(coreView.viewSorts, existingView.viewSorts)) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
store.set(
|
||||
currentRecordSortsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
view.viewSorts,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
coreView.shouldHideEmptyGroups !== existingView.shouldHideEmptyGroups
|
||||
) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
store.set(
|
||||
recordIndexShouldHideEmptyRecordGroupsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
view.shouldHideEmptyGroups,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[findManyCoreViewsLazy, store],
|
||||
[findManyCoreViewsLazy, applyCoreViewsForObjectMetadataId],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { type CoreView } from '~/generated-metadata/graphql';
|
||||
|
||||
export type CoreViewWithoutRelations = Exclude<
|
||||
CoreView,
|
||||
| 'viewFields'
|
||||
| 'viewFieldGroups'
|
||||
| 'viewGroups'
|
||||
| 'viewFilters'
|
||||
| 'viewFilterGroups'
|
||||
| 'viewSorts'
|
||||
>;
|
||||
Reference in New Issue
Block a user