Skip to content
Snippets Groups Projects
Commit 2f857fbc authored by utkarsh_33's avatar utkarsh_33 Committed by Jesse Baker
Browse files

Issue #3472507 by utkarsh_33, balintbrews, wim leers: `Duplicate` action does...

Issue #3472507 by utkarsh_33, balintbrews, wim leers: `Duplicate` action does not generate new UUIDs for components in slots, resulting in duplicate UUIDs
parent 8d181e97
No related branches found
No related tags found
1 merge request!266#3472507: `Duplicate` action does not generate new UUIDs for components in slots, resulting in duplicate UUIDs
Pipeline #285405 canceled
......@@ -54,4 +54,22 @@ describe('Contextual menu functionality', {testIsolation: false}, () => {
cy.getIframeBody().findByText('Two Column').should('not.exist');
});
it('should duplicate the element on clicking the "Duplicate" button', () => {
cy.loadURLandWaitForXBLoaded();
cy.getIframeBody().find('[data-component-id="experience_builder:two_column"]').should('have.length', 1);
// Right-click on the element that should trigger the context menu
cy.get('.primaryMenuContent').findByText('Two Column').trigger('contextmenu');
cy.get('[data-radix-scroll-area-viewport]')
.should('exist')
.and('be.visible');
cy.get('[data-radix-scroll-area-viewport]')
.within(() => {
// Click on the "Duplicate" button
cy.findByText('Duplicate').click();
});
cy.get('.primaryMenuContent').findAllByText('Two Column').should('have.length', 2);
});
});
......@@ -4,7 +4,7 @@ import {
moveNode,
layoutModelSlice,
setLayoutModel,
initialState,
initialState, duplicateNode,
} from '../../../../../ui/src/features/layout/layoutModelSlice';
import { makeStore } from '../../../../../ui/src/app/store';
import { ActionCreators } from 'redux-undo';
......@@ -135,3 +135,76 @@ describe('Undo/redo', () => {
cy.wrap(state.future).should('have.length', 0);
});
});
describe('Duplicate node', () => {
it('Should duplicate a node correctly with a new UUID and duplicate its children nodes', () => {
// Initialize state with a layout
const initialStateWithLayout = layoutModelSlice.reducer(
initialState,
setLayoutModel({
layout: {
uuid: 'root',
nodeType: 'root',
name: 'root',
children: [
{
uuid: 'original-node',
nodeType: 'component',
name: 'Original Node',
children: [
{
uuid: 'child-1',
nodeType: 'component',
name: 'Child 1',
children: [],
},
{
uuid: 'child-2',
nodeType: 'component',
name: 'Child 2',
children: [],
},
],
},
],
},
model: {},
initialized: true,
}),
);
const nodeToDuplicateUUID = 'original-node';
const stateAfterDuplication = layoutModelSlice.reducer(
initialStateWithLayout,
duplicateNode({ uuid: nodeToDuplicateUUID }),
);
const originalNode = initialStateWithLayout.layout.children.find(
(node) => node.uuid === nodeToDuplicateUUID
);
const newNode = stateAfterDuplication.layout.children.find(
(node) => node.uuid !== nodeToDuplicateUUID
);
// Ensure the new node is a duplicate and has a different UUID
expect(newNode).to.not.be.undefined;
expect(newNode.uuid).to.not.equal(nodeToDuplicateUUID);
expect(newNode.name).to.equal(originalNode.name);
expect(newNode.nodeType).to.equal(originalNode.nodeType);
expect(newNode.children.length).to.equal(originalNode.children.length);
// Verify each child node's UUID in the new node
originalNode.children.forEach((originalChild, index) => {
const newChild = newNode.children[index];
expect(newChild).to.not.be.undefined;
expect(newChild.uuid).to.not.equal(originalChild.uuid);
expect(newChild.name).to.equal(originalChild.name);
expect(newChild.nodeType).to.equal(originalChild.nodeType);
expect(newChild.children).to.deep.equal(originalChild.children);
});
// Verify the model for the new node and its children
expect(stateAfterDuplication.model[newNode.uuid]).to.deep.equal(
stateAfterDuplication.model[nodeToDuplicateUUID]
);
});
});
......@@ -112,34 +112,42 @@ export interface ComponentModel {
* Replace UUIDs in a layout node and its corresponding model.
* @param node - The layout node to update.
* @param model - The corresponding model to update.
* @returns An updated model and a updated state.
*/
const replaceUUIDsAndUpdateModel = (
node: LayoutNode,
model: ComponentModels,
) => {
): {
updatedNode: LayoutNode;
updatedModel: ComponentModels;
} => {
const oldToNewUUIDMap: Record<string, string> = {};
const updatedModel: ComponentModels = {};
const replaceUUIDs = (node: LayoutNode) => {
if (node.uuid) {
const newUUID = uuidv4();
oldToNewUUIDMap[node.uuid] = newUUID;
node.uuid = newUUID;
}
const replaceUUIDs = (node: LayoutNode): LayoutNode => {
const newNode: LayoutNode = { ...node, uuid: uuidv4() };
oldToNewUUIDMap[node.uuid] = newNode.uuid;
if (node.children) {
node.children.forEach((child) => replaceUUIDs(child));
// Recursively process children
if (newNode.children) {
newNode.children = newNode.children.map(replaceUUIDs);
}
return newNode;
};
replaceUUIDs(node);
const updatedNode = replaceUUIDs(node);
// Update the model keys
for (const oldUUID in model) {
if (oldToNewUUIDMap[oldUUID]) {
model[oldToNewUUIDMap[oldUUID]] = _.cloneDeep(model[oldUUID]);
delete model[oldUUID];
const newUUID = oldToNewUUIDMap[oldUUID];
if (newUUID) {
updatedModel[newUUID] = _.cloneDeep(model[oldUUID]);
}
}
return { updatedNode, updatedModel };
};
export const layoutModelSlice = createSlice({
......@@ -153,14 +161,20 @@ export const layoutModelSlice = createSlice({
duplicateNode: create.reducer(
(state, action: PayloadAction<DuplicateNodePayload>) => {
const { uuid } = action.payload;
const cloneNode = _.cloneDeep(findNodeByUuid(state.layout, uuid));
if (!cloneNode) {
const nodeToDuplicate = findNodeByUuid(state.layout, uuid);
if (!nodeToDuplicate) {
console.error(`Cannot duplicate ${uuid}. Check the uuid is valid.`);
return;
}
const newUuid = uuidv4();
cloneNode.uuid = newUuid;
state.model[newUuid] = _.cloneDeep(state.model[uuid]);
const { updatedNode, updatedModel } = replaceUUIDsAndUpdateModel(
nodeToDuplicate,
state.model,
);
// Add the updated model to the state
state.model = { ...state.model, ...updatedModel };
const nodePath = findNodePathByUuid(state.layout, uuid);
if (nodePath === null) {
......@@ -170,7 +184,7 @@ export const layoutModelSlice = createSlice({
return;
}
nodePath[nodePath.length - 1]++;
state.layout = insertNodeAtPath(state.layout, nodePath, cloneNode);
state.layout = insertNodeAtPath(state.layout, nodePath, updatedNode);
},
),
moveNode: create.reducer(
......@@ -225,22 +239,25 @@ export const layoutModelSlice = createSlice({
);
return;
}
let updatedModel: ComponentModels = { ...state.model };
// The nodes we're inserting into the layout already have UUIDs. We need to make sure they're unique before
// inserting them into the layout., so we need to generate new UUIDs and update. Ww also need the the model to
// reflect the new UUIDs.
// The nodes we're inserting into the layout already have UUIDs. We need to ensure they're unique before
// inserting them into the layout, so we need to generate new UUIDs and update the model accordingly.
const nodesToInsert: LayoutNode[] = _.cloneDeep(newNodes.children);
const updatedModel: ComponentModels = _.cloneDeep(model);
let newLayout: LayoutNode = _.cloneDeep(state.layout);
// Loop backwards so that we don't have to keep incrementing the insert position for each node we insert.
// Loop through each node in reverse order to maintain the correct insert positions
for (let i = nodesToInsert.length - 1; i >= 0; i--) {
const node = nodesToInsert[i];
replaceUUIDsAndUpdateModel(node, updatedModel);
newLayout = insertNodeAtPath(newLayout, to, node);
const { updatedNode, updatedModel: nodeUpdatedModel } =
replaceUUIDsAndUpdateModel(node, model);
updatedModel = { ...updatedModel, ...nodeUpdatedModel };
// Insert the node into the new layout at the specified position
newLayout = insertNodeAtPath(newLayout, to, updatedNode);
}
state.model = { ...state.model, ...updatedModel };
state.model = updatedModel;
state.layout = newLayout;
},
),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment