Workflow statuses update on record table - use cache instead of web sockets (#16391)

Updated the 3 hooks - activate, deactivate and discard draft
(deleteVersion) - so those updates the cache.
Removed the update listeners.

Also wrapped a few actions to unsure workflowId is not undefined when
received by useWorkflowWithCurrentVersion hook. Otherwise, useFindRecord
with by performed with skip true, disconnecting tmp from the cache and
making it miss a statuses update.



https://github.com/user-attachments/assets/5fc355e8-cf18-4881-855b-744eb253b79e
This commit is contained in:
Thomas Trompette
2025-12-08 17:17:13 +01:00
committed by GitHub
parent ac89b5aff6
commit 1c14567bf1
10 changed files with 348 additions and 89 deletions
@@ -5,13 +5,16 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { useRecoilValue } from 'recoil';
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const SeeRunsWorkflowVersionSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
workflowVersion?.workflow.id,
);
const SeeRunsWorkflowVersionSingleRecordActionContent = ({
workflowId,
recordId,
}: {
workflowId: string;
recordId: string;
}) => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
return (
<ActionLink
@@ -34,3 +37,21 @@ export const SeeRunsWorkflowVersionSingleRecordAction = () => {
/>
);
};
export const SeeRunsWorkflowVersionSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
const workflowId = workflowVersion?.workflow?.id;
if (!isDefined(workflowId)) {
return null;
}
return (
<SeeRunsWorkflowVersionSingleRecordActionContent
workflowId={workflowId}
recordId={recordId}
/>
);
};
@@ -5,13 +5,14 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { useRecoilValue } from 'recoil';
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const SeeVersionsWorkflowVersionSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
workflowVersion?.workflowId,
);
const SeeVersionsWorkflowVersionSingleRecordActionContent = ({
workflowId,
}: {
workflowId: string;
}) => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
return (
<ActionLink
@@ -29,3 +30,18 @@ export const SeeVersionsWorkflowVersionSingleRecordAction = () => {
/>
);
};
export const SeeVersionsWorkflowVersionSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
if (!isDefined(workflowVersion) || !isDefined(workflowVersion.workflowId)) {
return null;
}
return (
<SeeVersionsWorkflowVersionSingleRecordActionContent
workflowId={workflowVersion.workflowId}
/>
);
};
@@ -12,14 +12,15 @@ import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const UseAsDraftWorkflowVersionSingleRecordAction = () => {
const UseAsDraftWorkflowVersionSingleRecordActionContent = ({
workflowId,
workflowVersionId,
}: {
workflowId: string;
workflowVersionId: string;
}) => {
const { openModal } = useModal();
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useWorkflowVersion(recordId);
const workflow = useWorkflowWithCurrentVersion(
workflowVersion?.workflow?.id ?? '',
);
const workflow = useWorkflowWithCurrentVersion(workflowId);
const { createDraftFromWorkflowVersion } =
useCreateDraftFromWorkflowVersion();
const navigate = useNavigateApp();
@@ -29,7 +30,7 @@ export const UseAsDraftWorkflowVersionSingleRecordAction = () => {
workflow?.versions.some((version) => version.status === 'DRAFT') || false;
const handleClick = () => {
if (!isDefined(workflowVersion) || !isDefined(workflow) || hasNavigated) {
if (!isDefined(workflow) || hasNavigated) {
return;
}
@@ -38,13 +39,13 @@ export const UseAsDraftWorkflowVersionSingleRecordAction = () => {
} else {
const executeActionWithoutWaiting = async () => {
await createDraftFromWorkflowVersion({
workflowId: workflowVersion.workflow.id,
workflowVersionIdToCopy: workflowVersion.id,
workflowId,
workflowVersionIdToCopy: workflowVersionId,
});
navigate(AppPath.RecordShowPage, {
objectNameSingular: CoreObjectNameSingular.Workflow,
objectRecordId: workflowVersion.workflow.id,
objectRecordId: workflowId,
});
setHasNavigated(true);
@@ -54,13 +55,29 @@ export const UseAsDraftWorkflowVersionSingleRecordAction = () => {
}
};
return isDefined(workflowVersion) ? (
return (
<>
<Action onClick={handleClick} />
<OverrideWorkflowDraftConfirmationModal
workflowId={workflowVersion.workflow.id}
workflowVersionIdToCopy={workflowVersion.id}
workflowId={workflowId}
workflowVersionIdToCopy={workflowVersionId}
/>
</>
) : null;
);
};
export const UseAsDraftWorkflowVersionSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();
const workflowVersion = useWorkflowVersion(recordId);
if (!isDefined(workflowVersion?.workflow?.id)) {
return null;
}
return (
<UseAsDraftWorkflowVersionSingleRecordActionContent
workflowId={workflowVersion.workflow.id}
workflowVersionId={workflowVersion.id}
/>
);
};
@@ -5,29 +5,39 @@ import {
} from '@/action-menu/contexts/ActionMenuContext';
import { useRegisteredActions } from '@/action-menu/hooks/useRegisteredActions';
import { useShouldActionBeRegisteredParams } from '@/action-menu/hooks/useShouldActionBeRegisteredParams';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { type WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const ActionMenuContextProviderWorkflowObjects = ({
objectMetadataItem,
isInRightDrawer,
displayType,
actionMenuType,
children,
}: {
type ActionMenuContextProviderWorkflowObjectsProps = {
objectMetadataItem: ObjectMetadataItem;
isInRightDrawer: ActionMenuContextType['isInRightDrawer'];
displayType: ActionMenuContextType['displayType'];
actionMenuType: ActionMenuContextType['actionMenuType'];
children: React.ReactNode;
};
const ActionMenuContextProviderWorkflowObjectsContent = ({
objectMetadataItem,
isInRightDrawer,
displayType,
actionMenuType,
children,
selectedRecordId,
}: ActionMenuContextProviderWorkflowObjectsProps & {
selectedRecordId: string;
}) => {
const params = useShouldActionBeRegisteredParams({
objectMetadataItem,
});
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
params.selectedRecord?.id,
);
const workflowWithCurrentVersion =
useWorkflowWithCurrentVersion(selectedRecordId);
const shouldBeRegisteredParams = {
...params,
@@ -52,3 +62,87 @@ export const ActionMenuContextProviderWorkflowObjects = ({
</ActionMenuContext.Provider>
);
};
const ActionMenuContextProviderWorkflowObjectsWithoutWorkflow = ({
objectMetadataItem,
isInRightDrawer,
displayType,
actionMenuType,
children,
}: ActionMenuContextProviderWorkflowObjectsProps & {
workflowWithCurrentVersion: WorkflowWithCurrentVersion | undefined;
}) => {
const params = useShouldActionBeRegisteredParams({
objectMetadataItem,
});
const shouldBeRegisteredParams = {
...params,
workflowWithCurrentVersion: undefined,
};
const actions = useRegisteredActions(shouldBeRegisteredParams);
const runWorkflowRecordAgnosticActions =
useRunWorkflowRecordAgnosticActions();
return (
<ActionMenuContext.Provider
value={{
isInRightDrawer,
displayType,
actionMenuType,
actions: [...actions, ...runWorkflowRecordAgnosticActions],
}}
>
{children}
</ActionMenuContext.Provider>
);
};
export const ActionMenuContextProviderWorkflowObjects = ({
objectMetadataItem,
isInRightDrawer,
displayType,
actionMenuType,
children,
}: ActionMenuContextProviderWorkflowObjectsProps) => {
const contextStoreTargetedRecordsRule = useRecoilComponentValue(
contextStoreTargetedRecordsRuleComponentState,
);
const recordId =
contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
: undefined;
const selectedRecord =
useRecoilValue(recordStoreFamilyState(recordId ?? '')) || undefined;
if (isDefined(selectedRecord?.id)) {
return (
<ActionMenuContextProviderWorkflowObjectsContent
objectMetadataItem={objectMetadataItem}
isInRightDrawer={isInRightDrawer}
displayType={displayType}
actionMenuType={actionMenuType}
selectedRecordId={selectedRecord.id}
>
{children}
</ActionMenuContextProviderWorkflowObjectsContent>
);
}
return (
<ActionMenuContextProviderWorkflowObjectsWithoutWorkflow
objectMetadataItem={objectMetadataItem}
isInRightDrawer={isInRightDrawer}
displayType={displayType}
actionMenuType={actionMenuType}
workflowWithCurrentVersion={undefined}
>
{children}
</ActionMenuContextProviderWorkflowObjectsWithoutWorkflow>
);
};
@@ -2,8 +2,8 @@ import { type ApolloCache, type Modifiers } from '@apollo/client/cache';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const modifyRecordFromCache = <
CachedObjectRecord extends ObjectRecord = ObjectRecord,
@@ -81,11 +81,6 @@ export const RecordTableRow = ({
<RecordTableFieldsCells />
<RecordTablePlusButtonCellPlaceholder />
<RecordTableLastEmptyCell />
<ListenRecordUpdatesEffect
objectNameSingular={objectNameSingular}
recordId={recordId}
listenedFields={listenedFields}
/>
</RecordTableDraggableTr>
);
};
@@ -1,5 +1,3 @@
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDragAndDrop';
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
@@ -13,8 +11,6 @@ import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/rec
import { RecordTableRowVirtualizedSkeleton } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedSkeleton';
import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState';
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenRecordUpdatesEffect';
import { getDefaultRecordFieldsToListen } from '@/subscription/utils/getDefaultRecordFieldsToListen.util';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { isDefined } from 'twenty-shared/utils';
@@ -27,11 +23,6 @@ type RecordTableRowVirtualizedFullDataProps = {
export const RecordTableRowVirtualizedFullData = ({
realIndex,
}: RecordTableRowVirtualizedFullDataProps) => {
const { objectNameSingular } = useRecordIndexContextOrThrow();
const listenedFields = getDefaultRecordFieldsToListen({
objectNameSingular,
});
const isFocused = useRecoilComponentFamilyValue(
isRecordTableRowFocusedComponentFamilyState,
realIndex ?? 0,
@@ -67,11 +58,6 @@ export const RecordTableRowVirtualizedFullData = ({
<RecordTableFieldsCells />
<RecordTablePlusButtonCellPlaceholder />
<RecordTableLastEmptyCell />
<ListenRecordUpdatesEffect
objectNameSingular={objectNameSingular}
recordId={recordId}
listenedFields={listenedFields}
/>
</RecordTableDraggableTr>
);
};
@@ -5,11 +5,16 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { ACTIVATE_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/activateWorkflowVersion';
import { type WorkflowVersion } from '@/workflow/types/Workflow';
import {
type Workflow,
type WorkflowStatus,
type WorkflowVersion,
} from '@/workflow/types/Workflow';
import { isDefined } from 'twenty-shared/utils';
import {
type ActivateWorkflowVersionMutation,
@@ -30,6 +35,10 @@ export const useActivateWorkflowVersion = () => {
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const { objectMetadataItem: objectMetadataItemWorkflow } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Workflow,
});
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -56,6 +65,7 @@ export const useActivateWorkflowVersion = () => {
});
const cacheSnapshot = apolloCoreClient.cache.extract();
const allWorkflowVersions: Array<WorkflowVersion> = Object.values(
cacheSnapshot,
).filter(
@@ -114,6 +124,37 @@ export const useActivateWorkflowVersion = () => {
upsertRecordsInStore,
});
}
const cachedWorkflow = getRecordFromCache<Workflow>({
objectMetadataItem: objectMetadataItemWorkflow,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
recordId: workflowId,
});
const newStatuses = new Set(
[...(cachedWorkflow?.statuses ?? []), 'ACTIVE'].filter(
(status) => status !== 'DEACTIVATED',
),
);
if (isDefined(cachedWorkflow)) {
modifyRecordFromCache({
cache: apolloCoreClient.cache,
recordId: workflowId,
objectMetadataItem: objectMetadataItemWorkflow,
fieldModifiers: {
statuses: () => Array.from(newStatuses),
},
});
upsertRecordsInStore([
{
...cachedWorkflow,
statuses: Array.from(newStatuses) as WorkflowStatus[],
},
]);
}
},
});
};
@@ -3,18 +3,23 @@ import { useMutation } from '@apollo/client';
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { DEACTIVATE_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/deactivateWorkflowVersion';
import { type WorkflowVersion } from '@/workflow/types/Workflow';
import {
type Workflow,
type WorkflowStatus,
type WorkflowVersion,
} from '@/workflow/types/Workflow';
import { isDefined } from 'twenty-shared/utils';
import {
type DeactivateWorkflowVersionMutation,
type DeactivateWorkflowVersionMutationVariables,
} from '~/generated-metadata/graphql';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
export const useDeactivateWorkflowVersion = () => {
const apolloCoreClient = useApolloCoreClient();
@@ -29,6 +34,10 @@ export const useDeactivateWorkflowVersion = () => {
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const { objectMetadataItem: objectMetadataItemWorkflow } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Workflow,
});
const { objectMetadataItem: objectMetadataItemWorkflowVersion } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
@@ -78,6 +87,37 @@ export const useDeactivateWorkflowVersion = () => {
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
});
const cachedWorkflow = getRecordFromCache<Workflow>({
objectMetadataItem: objectMetadataItemWorkflow,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
recordId: workflowVersion.workflowId,
});
const newStatuses = new Set(
[...(cachedWorkflow?.statuses ?? []), 'DEACTIVATED'].filter(
(status) => status !== 'ACTIVE',
),
);
if (isDefined(cachedWorkflow)) {
modifyRecordFromCache({
cache: apolloCoreClient.cache,
recordId: workflowVersion.workflowId,
objectMetadataItem: objectMetadataItemWorkflow,
fieldModifiers: {
statuses: () => Array.from(newStatuses),
},
});
upsertRecordsInStore([
{
...cachedWorkflow,
statuses: Array.from(newStatuses) as WorkflowStatus[],
},
]);
}
},
});
};
@@ -1,21 +1,95 @@
import { useCallback } from 'react';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type Workflow, type WorkflowVersion } from '@/workflow/types/Workflow';
import { isDefined } from 'twenty-shared/utils';
export const useDeleteOneWorkflowVersion = () => {
const apolloCoreClient = useApolloCoreClient();
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const getWorkflowVersionFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const getWorkflowFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.Workflow,
});
const { objectMetadataItem: objectMetadataItemWorkflow } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Workflow,
});
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const handleUpdate = useCallback(
(workflowVersionId: string) => {
if (!workflowVersionId) {
return;
}
const cache = apolloCoreClient.cache;
const cachedWorkflowVersion =
getWorkflowVersionFromCache<WorkflowVersion>(workflowVersionId);
if (!isDefined(cachedWorkflowVersion)) {
return;
}
const cachedWorkflow = getWorkflowFromCache<Workflow>(
cachedWorkflowVersion.workflowId,
);
if (!isDefined(cachedWorkflow)) {
return;
}
modifyRecordFromCache({
objectMetadataItem: objectMetadataItemWorkflow,
cache,
recordId: cachedWorkflow.id,
fieldModifiers: {
versions: () => {
return cachedWorkflow.versions.filter(
(version) => version.id !== workflowVersionId,
);
},
statuses: () => {
return (
cachedWorkflow.statuses?.filter((status) => status !== 'DRAFT') ??
[]
);
},
},
});
upsertRecordsInStore([
{
...cachedWorkflow,
statuses:
cachedWorkflow.statuses?.filter((status) => status !== 'DRAFT') ??
[],
versions: cachedWorkflow.versions.filter(
(version) => version.id !== workflowVersionId,
),
},
]);
},
[
apolloCoreClient.cache,
getWorkflowFromCache,
getWorkflowVersionFromCache,
objectMetadataItemWorkflow,
upsertRecordsInStore,
],
);
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
const deleteOneWorkflowVersion = async ({
workflowVersionId,
@@ -23,32 +97,7 @@ export const useDeleteOneWorkflowVersion = () => {
workflowVersionId: string;
}) => {
await deleteOneRecord(workflowVersionId);
const cachedWorkflowVersion =
getWorkflowVersionFromCache<WorkflowVersion>(workflowVersionId);
if (!isDefined(cachedWorkflowVersion)) {
return;
}
const cachedWorkflow = getWorkflowFromCache<Workflow>(
cachedWorkflowVersion.workflowId,
);
if (!isDefined(cachedWorkflow)) {
return;
}
apolloCoreClient.cache.modify({
id: apolloCoreClient.cache.identify(cachedWorkflow),
fields: {
versions: () => {
return cachedWorkflow.versions.filter(
(version) => version.id !== workflowVersionId,
);
},
},
});
handleUpdate(workflowVersionId);
};
return { deleteOneWorkflowVersion };