Added SSE effect for view relations objects (#18386)
This PR adds what is necessary for having SSE working for view relations : fields, filters, filter groups and sorts. This should allow to have AI working well while creating views with detailed filtering and sorting. ## Demo https://github.com/user-attachments/assets/026c7fb5-8e1a-4498-b7f4-d16993e5a7c4 ## Fixes Also fixed in this PR while working on the filter area : - Advanced filter does not update - Advanced filter sub field selection is broken (due to Jotai migration) - No view fields when creating a new view - Error on advanced filter deletion (cascade delete wasn't taken into account on the frontend) - Bug advanced filter creation --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEvent
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const PageLayoutTabSSEEffect = () => {
|
||||
const queryId = 'page-layout-tab-metadata-sse-effect';
|
||||
const queryId = 'page-layout-sse-effect';
|
||||
|
||||
const { refreshPageLayouts } = useRefreshPageLayouts();
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEvent
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const PageLayoutWidgetSSEEffect = () => {
|
||||
const queryId = 'page-layout-widget-metadata-sse-effect';
|
||||
const queryId = 'page-layout-widget-sse-effect';
|
||||
|
||||
const { refreshPageLayouts } = useRefreshPageLayouts();
|
||||
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { useGetObjectMetadataItemIdFromViewRelationEventDetail } from '@/metadata-store/hooks/useGetObjectMetadataItemIdFromViewRelationEventDetail';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { type ViewSort } from '@/views/types/ViewSort';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ViewRelationsSSEEffect = () => {
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
const { getObjectMetadataItemIdFromViewRelationEventDetail } =
|
||||
useGetObjectMetadataItemIdFromViewRelationEventDetail();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-filters-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewFilter,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-sorts-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewSort,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-fields-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewField,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId: 'view-filter-groups-sse-effect',
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.viewFilterGroup,
|
||||
variables: {},
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: replace this with detailed SSE optimistic effects
|
||||
const debouncedRefreshCoreViewsByObjectMetadataId = useDebouncedCallback(
|
||||
(objectMetadataId: string) => {
|
||||
refreshCoreViewsByObjectMetadataId(objectMetadataId);
|
||||
},
|
||||
500,
|
||||
{
|
||||
leading: false,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewFilter,
|
||||
onMetadataOperationBrowserEvent: (
|
||||
detail: MetadataOperationBrowserEventDetail<ViewFilter>,
|
||||
) => {
|
||||
const objectMetadataItemId =
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
|
||||
|
||||
if (!isDefined(objectMetadataItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewSort,
|
||||
onMetadataOperationBrowserEvent: (
|
||||
detail: MetadataOperationBrowserEventDetail<ViewSort>,
|
||||
) => {
|
||||
const objectMetadataItemId =
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
|
||||
|
||||
if (!isDefined(objectMetadataItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewFilterGroup,
|
||||
onMetadataOperationBrowserEvent: (
|
||||
detail: MetadataOperationBrowserEventDetail<ViewFilterGroup>,
|
||||
) => {
|
||||
const objectMetadataItemId =
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
|
||||
|
||||
if (!isDefined(objectMetadataItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
|
||||
},
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.viewField,
|
||||
onMetadataOperationBrowserEvent: (
|
||||
detail: MetadataOperationBrowserEventDetail<ViewField>,
|
||||
) => {
|
||||
const objectMetadataItemId =
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
|
||||
|
||||
if (!isDefined(objectMetadataItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
+12
-2
@@ -1,15 +1,20 @@
|
||||
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 { type CoreViewWithoutRelations } from '@/views/types/CoreViewWithoutRelations';
|
||||
import { useStore } from 'jotai';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
export const ViewSSEEffect = () => {
|
||||
const queryId = 'view-metadata-sse-effect';
|
||||
const queryId = 'views-sse-effect';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
@@ -20,7 +25,7 @@ export const ViewSSEEffect = () => {
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.view,
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
onMetadataOperationBrowserEvent: async (eventDetail) => {
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
const createdView = eventDetail.operation
|
||||
@@ -38,6 +43,11 @@ export const ViewSSEEffect = () => {
|
||||
viewGroups: [],
|
||||
},
|
||||
]);
|
||||
|
||||
await sleep(50);
|
||||
|
||||
refreshCoreViewsByObjectMetadataId(createdView.objectMetadataId);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { useGetViewIdFromViewSSERelationEventDetail } from '@/metadata-store/hooks/useGetViewIdFromViewSSERelationEventDetail';
|
||||
import { useGetViewById } from '@/views/hooks/useGetViewById';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { type ViewSort } from '@/views/types/ViewSort';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useGetObjectMetadataItemIdFromViewRelationEventDetail = () => {
|
||||
const { getViewIdFromViewSSERelationEventDetail } =
|
||||
useGetViewIdFromViewSSERelationEventDetail();
|
||||
|
||||
const { getViewById } = useGetViewById();
|
||||
|
||||
const getObjectMetadataItemIdFromViewRelationEventDetail = <
|
||||
T extends ViewFilter | ViewFilterGroup | ViewField | ViewSort,
|
||||
>(
|
||||
eventDetail: MetadataOperationBrowserEventDetail<T>,
|
||||
) => {
|
||||
const viewId = getViewIdFromViewSSERelationEventDetail(eventDetail);
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { view } = getViewById(viewId);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return view.objectMetadataId;
|
||||
};
|
||||
|
||||
return {
|
||||
getObjectMetadataItemIdFromViewRelationEventDetail,
|
||||
};
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { type ViewSort } from '@/views/types/ViewSort';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetViewIdFromViewSSERelationEventDetail = () => {
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
|
||||
const getViewIdFromViewSSERelationEventDetail = <
|
||||
T extends ViewFilter | ViewFilterGroup | ViewField | ViewSort,
|
||||
>(
|
||||
eventDetail: MetadataOperationBrowserEventDetail<T>,
|
||||
) => {
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
return eventDetail.operation.createdRecord?.viewId as Nullable<string>;
|
||||
}
|
||||
case 'update': {
|
||||
return eventDetail.operation.updatedRecord?.viewId as Nullable<string>;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedRecordId = eventDetail.operation.deletedRecordId;
|
||||
|
||||
switch (eventDetail.metadataName) {
|
||||
case AllMetadataName.viewFilter:
|
||||
return coreViews.find((view) =>
|
||||
view.viewFilters.some(
|
||||
(viewFilter) => viewFilter.id === deletedRecordId,
|
||||
),
|
||||
)?.id;
|
||||
case AllMetadataName.viewField:
|
||||
return coreViews.find((view) =>
|
||||
view.viewFields.some(
|
||||
(viewField) => viewField.id === deletedRecordId,
|
||||
),
|
||||
)?.id;
|
||||
case AllMetadataName.viewFilterGroup:
|
||||
return coreViews.find((view) =>
|
||||
view.viewFilterGroups?.some(
|
||||
(viewFilterGroup) => viewFilterGroup.id === deletedRecordId,
|
||||
),
|
||||
)?.id;
|
||||
case AllMetadataName.viewSort:
|
||||
return coreViews.find((view) =>
|
||||
view.viewSorts.some(
|
||||
(viewSort) => viewSort.id === deletedRecordId,
|
||||
),
|
||||
)?.id;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getViewIdFromViewSSERelationEventDetail,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user