Migrate update step changes to diff (#14663)
This PR should fix optimistic rendering issues on step updates: - compute a diff for trigger and steps on mutations - build an util that applies that diff (built a more robust version from https://github.com/AsyncBanana/micropatch) - apply diff in cache
This commit is contained in:
+12
-1
@@ -6,7 +6,15 @@ import { useCreateStep } from '../useCreateStep';
|
||||
const mockGetUpdatableWorkflowVersion = jest.fn();
|
||||
const mockCreateWorkflowVersionStep = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
createWorkflowVersionStep: { createdStep: { id: '1', type: 'CODE' } },
|
||||
createWorkflowVersionStep: {
|
||||
stepsDiff: [
|
||||
{
|
||||
type: 'CREATE',
|
||||
path: ['steps', 0],
|
||||
value: { id: 'step-id', type: 'CODE' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,6 +33,8 @@ jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('uuid', () => ({ v4: () => 'step-id' }));
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const workflowVisualizerComponentInstanceId =
|
||||
'workflow-visualizer-instance-id';
|
||||
@@ -65,6 +75,7 @@ describe('useCreateStep', () => {
|
||||
|
||||
expect(mockGetUpdatableWorkflowVersion).toHaveBeenCalled();
|
||||
expect(mockCreateWorkflowVersionStep).toHaveBeenCalledWith({
|
||||
id: 'step-id',
|
||||
workflowVersionId: mockWorkflowVersionId,
|
||||
stepType: 'CODE',
|
||||
parentStepId: 'parent-step-id',
|
||||
|
||||
+10
-3
@@ -41,11 +41,18 @@ describe('useDeleteStep', () => {
|
||||
it('should delete step and clean up dependencies', async () => {
|
||||
const mockWorkflowVersionId = 'version-123';
|
||||
const mockStepId = 'step-1';
|
||||
const mockDeletedStepIds = ['step-1', 'step-2'];
|
||||
|
||||
mockGetUpdatableWorkflowVersion.mockResolvedValue(mockWorkflowVersionId);
|
||||
mockDeleteWorkflowVersionStep.mockResolvedValue({
|
||||
deletedStepIds: mockDeletedStepIds,
|
||||
deletedStepIds: {
|
||||
stepsDiff: [
|
||||
{
|
||||
type: 'DELETE',
|
||||
path: ['steps', 0],
|
||||
value: mockStepId,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useDeleteStep());
|
||||
@@ -58,7 +65,7 @@ describe('useDeleteStep', () => {
|
||||
});
|
||||
expect(mockCloseCommandMenu).toHaveBeenCalled();
|
||||
expect(mockDeleteStepsOutputSchema).toHaveBeenCalledWith({
|
||||
stepIds: mockDeletedStepIds,
|
||||
stepIds: [mockStepId],
|
||||
workflowVersionId: mockWorkflowVersionId,
|
||||
});
|
||||
});
|
||||
|
||||
+9
-1
@@ -6,7 +6,15 @@ import { WorkflowVisualizerComponentInstanceContext } from '../../../workflow-di
|
||||
const mockGetUpdatableWorkflowVersion = jest.fn();
|
||||
const mockDuplicateWorkflowVersionStep = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
duplicateWorkflowVersionStep: { createdStep: { id: '2', type: 'CODE' } },
|
||||
duplicateWorkflowVersionStep: {
|
||||
stepsDiff: [
|
||||
{
|
||||
type: 'CREATE',
|
||||
path: ['steps', 0],
|
||||
value: { id: '2', type: 'CODE' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,15 @@ import { type WorkflowStepType } from '@/workflow/types/Workflow';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { type WorkflowStepConnectionOptions } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowStepConnectionOptions';
|
||||
import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep';
|
||||
import {
|
||||
type Difference,
|
||||
type DifferenceChange,
|
||||
type DifferenceCreate,
|
||||
} from 'microdiff';
|
||||
import { useState } from 'react';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useCreateStep = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -42,9 +49,11 @@ export const useCreateStep = () => {
|
||||
|
||||
try {
|
||||
const workflowVersionId = await getUpdatableWorkflowVersion();
|
||||
const id = v4();
|
||||
|
||||
const workflowVersionStepChanges = (
|
||||
await createWorkflowVersionStep({
|
||||
id,
|
||||
workflowVersionId,
|
||||
stepType: newStepType,
|
||||
parentStepId,
|
||||
@@ -54,16 +63,26 @@ export const useCreateStep = () => {
|
||||
})
|
||||
)?.data?.createWorkflowVersionStep;
|
||||
|
||||
const createdStep = workflowVersionStepChanges?.createdStep;
|
||||
const stepsDiff = workflowVersionStepChanges?.stepsDiff as Difference[];
|
||||
|
||||
if (!isDefined(createdStep)) {
|
||||
const addedStepDiff = stepsDiff?.find(
|
||||
(diff) => diff.type === 'CREATE' && diff.value.id === id,
|
||||
) as Nullable<DifferenceCreate>;
|
||||
|
||||
const createdFirstStepDiff = stepsDiff?.find(
|
||||
(diff) => diff.type === 'CHANGE' && diff.value?.[0]?.id === id,
|
||||
) as Nullable<DifferenceChange>;
|
||||
|
||||
if (!isDefined(createdFirstStepDiff) && !isDefined(addedStepDiff)) {
|
||||
throw new Error("Couldn't create step");
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(createdStep.id);
|
||||
setWorkflowLastCreatedStepId(createdStep.id);
|
||||
setWorkflowSelectedNode(id);
|
||||
setWorkflowLastCreatedStepId(id);
|
||||
|
||||
return createdStep;
|
||||
return isDefined(createdFirstStepDiff)
|
||||
? createdFirstStepDiff.value[0]
|
||||
: addedStepDiff?.value;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const useDeleteStep = () => {
|
||||
const deleteStep = async (stepId: string) => {
|
||||
const workflowVersionId = await getUpdatableWorkflowVersion();
|
||||
|
||||
const workflowVersionStepChanges = await deleteWorkflowVersionStep({
|
||||
await deleteWorkflowVersionStep({
|
||||
workflowVersionId,
|
||||
stepId,
|
||||
});
|
||||
@@ -22,7 +22,7 @@ export const useDeleteStep = () => {
|
||||
closeCommandMenu();
|
||||
|
||||
deleteStepsOutputSchema({
|
||||
stepIds: workflowVersionStepChanges?.deletedStepIds ?? [],
|
||||
stepIds: [stepId],
|
||||
workflowVersionId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUp
|
||||
import { workflowLastCreatedStepIdComponentState } from '@/workflow/states/workflowLastCreatedStepIdComponentState';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { useDuplicateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useDuplicateWorkflowVersionStep';
|
||||
import { type Difference } from 'microdiff';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -36,16 +37,17 @@ export const useDuplicateStep = () => {
|
||||
})
|
||||
)?.data?.duplicateWorkflowVersionStep;
|
||||
|
||||
const createdStep = workflowVersionStepChanges?.createdStep;
|
||||
const stepsDiff = workflowVersionStepChanges?.stepsDiff as Difference[];
|
||||
const createdStepDiff = stepsDiff?.find((diff) => diff.type === 'CREATE');
|
||||
|
||||
if (!isDefined(createdStep)) {
|
||||
if (!isDefined(createdStepDiff)) {
|
||||
throw new Error("Couldn't duplicate step");
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(createdStep.id);
|
||||
setWorkflowLastCreatedStepId(createdStep.id);
|
||||
setWorkflowSelectedNode(createdStepDiff.value.id);
|
||||
setWorkflowLastCreatedStepId(createdStepDiff.value.id);
|
||||
|
||||
return createdStep;
|
||||
return createdStepDiff.value;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
+7
-43
@@ -5,11 +5,8 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
type WorkflowVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { applyDiff, isDefined } from 'twenty-shared/utils';
|
||||
import { type WorkflowVersionStepChanges } from '~/generated/graphql';
|
||||
|
||||
export const useUpdateWorkflowVersionCache = () => {
|
||||
@@ -44,47 +41,14 @@ export const useUpdateWorkflowVersionCache = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
triggerNextStepIds,
|
||||
stepsNextStepIds,
|
||||
createdStep,
|
||||
deletedStepIds,
|
||||
} = workflowVersionStepChanges;
|
||||
const { triggerDiff, stepsDiff } = workflowVersionStepChanges;
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
trigger: isDefined(cachedRecord.trigger)
|
||||
? {
|
||||
...cachedRecord.trigger,
|
||||
nextStepIds: triggerNextStepIds,
|
||||
}
|
||||
: cachedRecord.trigger,
|
||||
steps: (cachedRecord.steps || []).map((step: WorkflowAction) => ({
|
||||
...step,
|
||||
nextStepIds: stepsNextStepIds[step.id] ?? step.nextStepIds,
|
||||
})),
|
||||
} satisfies WorkflowVersion;
|
||||
|
||||
if (isDefined(createdStep)) {
|
||||
const formattedCreatedStep = {
|
||||
...(createdStep as WorkflowAction),
|
||||
nextStepIds: createdStep.nextStepIds || [],
|
||||
};
|
||||
|
||||
newCachedRecord.steps.push(formattedCreatedStep);
|
||||
}
|
||||
|
||||
if (isDefined(deletedStepIds) && deletedStepIds.length > 0) {
|
||||
newCachedRecord.steps = newCachedRecord.steps.filter(
|
||||
(step: WorkflowAction) => !deletedStepIds.includes(step.id),
|
||||
);
|
||||
|
||||
const hasDeletedTrigger: boolean = deletedStepIds.includes('trigger');
|
||||
|
||||
if (hasDeletedTrigger) {
|
||||
newCachedRecord.trigger = null;
|
||||
}
|
||||
}
|
||||
steps: applyDiff({ steps: cachedRecord.steps }, stepsDiff).steps,
|
||||
trigger: applyDiff({ trigger: cachedRecord.trigger }, triggerDiff)
|
||||
.trigger,
|
||||
} as WorkflowVersion;
|
||||
|
||||
const recordGqlFields = {
|
||||
steps: true,
|
||||
|
||||
Reference in New Issue
Block a user