Fix concurrent page layout edition bug (#15740)

This PR fixes a bug which could happen if two people were editing a page
layout at the same time.
If one person deletes a widget or a tab and saves first, and the second
person saves after but doesn't delete this widget or tab, an error is
raised.

This is because, in the backend, a diff is computed to know which tab or
widget to create, update or delete. But the logic was maid without
considering soft deletion. So, when the second person saves, the diff
tries to create the widget or tab which had been soft deleted by the
first use. Since they have the same id, a duplicate primary key error is
raised.

Now we always consider that the last user to save has the truth. So we
restore the tab/widget and update it.
This commit is contained in:
Raphaël Bosi
2025-11-13 12:01:11 +01:00
committed by GitHub
parent a453d53656
commit cb759aa22e
7 changed files with 131 additions and 23 deletions
@@ -4,6 +4,7 @@ import deepEqual from 'deep-equal';
type Diff<T extends { id: string }> = {
toCreate: T[];
toUpdate: T[];
toRestoreAndUpdate: T[];
idsToDelete: string[];
};
@@ -29,7 +30,7 @@ type ComputeDiffBetweenObjectsParams<
};
export const computeDiffBetweenObjects = <
T extends { id: string },
T extends { id: string; deletedAt: Date | null },
K extends { id: string },
>({
existingObjects,
@@ -38,6 +39,7 @@ export const computeDiffBetweenObjects = <
}: ComputeDiffBetweenObjectsParams<T, K>): Diff<K> => {
const toCreate: K[] = [];
const toUpdate: K[] = [];
const toRestoreAndUpdate: K[] = [];
const existingEntitiesMap = new Map(
existingObjects.map((entity) => [entity.id, entity]),
@@ -50,18 +52,22 @@ export const computeDiffBetweenObjects = <
const existingEntity = existingEntitiesMap.get(receivedObject.id);
if (isDefined(existingEntity)) {
const comparableExistingEntity = extractProperties(
existingEntity,
propertiesToCompare,
);
if (isDefined(existingEntity.deletedAt)) {
toRestoreAndUpdate.push(receivedObject);
} else {
const comparableExistingEntity = extractProperties(
existingEntity,
propertiesToCompare,
);
const comparableReceivedEntity = extractProperties(
receivedObject,
propertiesToCompare,
);
const comparableReceivedEntity = extractProperties(
receivedObject,
propertiesToCompare,
);
if (!deepEqual(comparableExistingEntity, comparableReceivedEntity)) {
toUpdate.push(receivedObject);
if (!deepEqual(comparableExistingEntity, comparableReceivedEntity)) {
toUpdate.push(receivedObject);
}
}
} else {
toCreate.push(receivedObject);
@@ -69,12 +75,14 @@ export const computeDiffBetweenObjects = <
}
const idsToDelete = existingObjects
.filter((existingEntity) => !isDefined(existingEntity.deletedAt))
.filter((existingEntity) => !receivedEntitiesMap.has(existingEntity.id))
.map((entity) => entity.id);
return {
toCreate,
toUpdate,
toRestoreAndUpdate,
idsToDelete,
};
};