Add SSE for metadata and plug front components (#17998)
Create the necessary tooling to listen to metadata events and plug it to the front components. Now we have a hot reload like experience when we edit a component in an app. ## Backend - Split `EventWithQueryIds` into `ObjectRecordEventWithQueryIds` and `MetadataEventWithQueryIds` - Publish metadata event batches to active SSE streams in `MetadataEventsToDbListener` ## Frontend - Create a metadata event dispatching pipeline: SSE metadata events are grouped by metadata name, transformed into `MetadataOperationBrowserEventDetail` objects, and dispatched as browser `CustomEvents` - Add `useListenToMetadataOperationBrowserEvent` hook for consuming metadata operation events filtered by metadata name and operation type - Rename `useListenToObjectRecordEventsForQuery` to `useListenToEventsForQuery`, now accepting both `RecordGqlOperationSignature` and `MetadataGqlOperationSignature` - Implement `useOnFrontComponentUpdated` which subscribes to front component metadata events and updates the Apollo cache when the component is modified - Add `builtComponentChecksum` to the front component query and appends it to the component URL for browser cache invalidation
This commit is contained in:
+11
-3
@@ -1,5 +1,6 @@
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext';
|
||||
import { useOnFrontComponentUpdated } from '@/front-components/hooks/useOnFrontComponentUpdated';
|
||||
import { getFrontComponentUrl } from '@/front-components/utils/getFrontComponentUrl';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -21,8 +22,6 @@ export const FrontComponentRenderer = ({
|
||||
const { executionContext, frontComponentHostCommunicationApi } =
|
||||
useFrontComponentExecutionContext();
|
||||
|
||||
const componentUrl = `${REST_API_BASE_URL}/front-components/${frontComponentId}`;
|
||||
|
||||
const handleError = useCallback(
|
||||
(error?: Error) => {
|
||||
if (!isDefined(error)) {
|
||||
@@ -43,6 +42,15 @@ export const FrontComponentRenderer = ({
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
useOnFrontComponentUpdated({
|
||||
frontComponentId,
|
||||
});
|
||||
|
||||
const componentUrl = getFrontComponentUrl({
|
||||
frontComponentId,
|
||||
checksum: data?.frontComponent?.builtComponentChecksum,
|
||||
});
|
||||
|
||||
if (
|
||||
loading ||
|
||||
!isDefined(data?.frontComponent) ||
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ export const FIND_ONE_FRONT_COMPONENT = gql`
|
||||
id
|
||||
name
|
||||
applicationId
|
||||
builtComponentChecksum
|
||||
applicationTokenPair {
|
||||
applicationAccessToken {
|
||||
token
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { useUpdateFrontComponentApolloCache } from '@/front-components/hooks/useUpdateFrontComponentApolloCache';
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import {
|
||||
AllMetadataName,
|
||||
type FrontComponent,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseOnFrontComponentUpdatedArgs = {
|
||||
frontComponentId: string;
|
||||
};
|
||||
|
||||
export const useOnFrontComponentUpdated = ({
|
||||
frontComponentId,
|
||||
}: UseOnFrontComponentUpdatedArgs) => {
|
||||
const queryId = `front-component-updated-${frontComponentId}`;
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
metadataName: AllMetadataName.frontComponent,
|
||||
variables: {
|
||||
filter: { id: { eq: frontComponentId } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { updateFrontComponentApolloCache } =
|
||||
useUpdateFrontComponentApolloCache({
|
||||
frontComponentId,
|
||||
});
|
||||
|
||||
useListenToMetadataOperationBrowserEvent<FrontComponent>({
|
||||
metadataName: AllMetadataName.frontComponent,
|
||||
onMetadataOperationBrowserEvent: updateFrontComponentApolloCache,
|
||||
});
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FindOneFrontComponentDocument,
|
||||
type FindOneFrontComponentQuery,
|
||||
type FrontComponent,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseUpdateFrontComponentApolloCacheArgs = {
|
||||
frontComponentId: string;
|
||||
};
|
||||
|
||||
export const useUpdateFrontComponentApolloCache = ({
|
||||
frontComponentId,
|
||||
}: UseUpdateFrontComponentApolloCacheArgs) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const updateFrontComponentApolloCache = (
|
||||
detail: MetadataOperationBrowserEventDetail<FrontComponent>,
|
||||
) => {
|
||||
if (detail.operation.type !== 'update') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { updatedRecord } = detail.operation;
|
||||
|
||||
if (!isDefined(updatedRecord) || updatedRecord.id !== frontComponentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
apolloClient.cache.updateQuery<FindOneFrontComponentQuery>(
|
||||
{
|
||||
query: FindOneFrontComponentDocument,
|
||||
variables: { id: frontComponentId },
|
||||
},
|
||||
(existingData) => {
|
||||
if (!isDefined(existingData?.frontComponent)) {
|
||||
return existingData;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingData,
|
||||
frontComponent: {
|
||||
...existingData.frontComponent,
|
||||
...updatedRecord,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return { updateFrontComponentApolloCache };
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getFrontComponentUrl = ({
|
||||
frontComponentId,
|
||||
checksum,
|
||||
}: {
|
||||
frontComponentId: string;
|
||||
checksum?: string;
|
||||
}): string => {
|
||||
return isDefined(checksum)
|
||||
? `${REST_API_BASE_URL}/front-components/${frontComponentId}?checksum=${checksum}`
|
||||
: `${REST_API_BASE_URL}/front-components/${frontComponentId}`;
|
||||
};
|
||||
Reference in New Issue
Block a user