diff --git a/src/core/models/app.ts b/src/core/models/app.ts index 84a1670066..dedd8b8072 100644 --- a/src/core/models/app.ts +++ b/src/core/models/app.ts @@ -8,15 +8,6 @@ export type AppState = { overlay: undefined | Overlay } -export const generateId = () => { - return `comp-${( - Date.now().toString(36) + - Math.random() - .toString(36) - .substr(2, 5) - ).toUpperCase()}` -} - const app = createModel({ state: { showLayout: true, diff --git a/src/core/models/components.ts b/src/core/models/components.ts index 0b777fa215..809588fe3e 100644 --- a/src/core/models/components.ts +++ b/src/core/models/components.ts @@ -2,7 +2,8 @@ import { createModel } from '@rematch/core' import { DEFAULT_PROPS } from '../../utils/defaultProps' import omit from 'lodash/omit' import templates, { TemplateType } from '../../templates' -import { generateId } from './app' +import { generateId } from '../../utils/generateId' +import { duplicateComponent, deleteComponent } from '../../utils/recursive' export type ComponentsState = { components: IComponents @@ -98,21 +99,7 @@ const components = createModel({ } } - const deleteRecursive = ( - children: IComponent['children'], - id: IComponent['id'], - ) => { - children.forEach(child => { - updatedComponents[child] && - deleteRecursive(updatedComponents[child].children, componentId) - }) - - updatedComponents = omit(updatedComponents, id) - } - - deleteRecursive(component.children, componentId) - - updatedComponents = omit(updatedComponents, componentId) + updatedComponents = deleteComponent(component, updatedComponents) return { ...state, @@ -264,29 +251,10 @@ const components = createModel({ if (selectedComponent.id !== DEFAULT_ID) { const parentElement = state.components[selectedComponent.parent] - const clonedComponents: IComponents = {} - - const cloneComponent = (component: IComponent) => { - const newid = generateId() - const children = component.children.map(child => { - return cloneComponent(state.components[child]) - }) - - clonedComponents[newid] = { - ...component, - id: newid, - props: { ...component.props }, - children, - } - - children.forEach(child => { - clonedComponents[child].parent = newid - }) - - return newid - } - - const newId = cloneComponent(selectedComponent) + const { newId, clonedComponents } = duplicateComponent( + selectedComponent, + state.components, + ) return { ...state, diff --git a/src/core/models/composer/composer.ts b/src/core/models/composer/composer.ts index 86e3858f57..00d421dc35 100644 --- a/src/core/models/composer/composer.ts +++ b/src/core/models/composer/composer.ts @@ -1,5 +1,5 @@ import { DEFAULT_PROPS } from '../../../utils/defaultProps' -import { generateId } from '../app' +import { generateId } from '../../../utils/generateId' type AddNode = { type: ComponentType diff --git a/src/utils/generateId.ts b/src/utils/generateId.ts new file mode 100644 index 0000000000..359d6ea4c1 --- /dev/null +++ b/src/utils/generateId.ts @@ -0,0 +1,8 @@ +export const generateId = () => { + return `comp-${( + Date.now().toString(36) + + Math.random() + .toString(36) + .substr(2, 5) + ).toUpperCase()}` +} diff --git a/src/utils/recursive.test.ts b/src/utils/recursive.test.ts new file mode 100644 index 0000000000..dc28e5d95d --- /dev/null +++ b/src/utils/recursive.test.ts @@ -0,0 +1,127 @@ +import { generateId } from './generateId' +import { duplicateComponent, deleteComponent } from './recursive' + +jest.mock('./generateId') + +const mockGenerateId: jest.Mock = generateId as any + +describe('recursive functions', () => { + let id = 1 + let mockFn: typeof mockGenerateId + + beforeAll(() => { + // @ts-ignore + mockFn = mockGenerateId.mockImplementation(() => { + return `comp-${id++}` + }) + }) + + afterEach(() => { + id = 1 + }) + + afterAll(() => { + mockFn.mockClear() + }) + + it('should duplicate a Box component containing an Avatar', () => { + const initialComponents: IComponents = {} + + const first = generateId() + const second = generateId() + + initialComponents.root = { + id: 'root', + type: 'Box', + parent: 'root', + props: {}, + children: [first], + } + + initialComponents[first] = { + id: first, + type: 'Box', + parent: 'root', + props: {}, + children: [second], + } + + initialComponents[second] = { + id: second, + type: 'Avatar', + parent: first, + props: {}, + children: [], + } + + const { clonedComponents } = duplicateComponent( + initialComponents[first], + initialComponents, + ) + + expect(Object.keys(clonedComponents).length).toEqual(2) + + const finalTree = { + ...initialComponents, + ...clonedComponents, + } + + const avatars = Object.keys(finalTree).filter( + compKey => finalTree[compKey].type === 'Avatar', + ) + const boxes = Object.keys(finalTree).filter( + compKey => finalTree[compKey].type === 'Box', + ) + + expect(avatars.length).toEqual(2) + expect(boxes.length).toEqual(3) + }) + + it('should remove a Box containing an avatar and a badge', () => { + const initialComponents: IComponents = {} + + const first = generateId() + const second = generateId() + const third = generateId() + + initialComponents.root = { + id: 'root', + type: 'Box', + parent: 'root', + props: {}, + children: [first], + } + + initialComponents[first] = { + id: first, + type: 'Box', + parent: 'root', + props: {}, + children: [second], + } + + initialComponents[second] = { + id: second, + type: 'Avatar', + parent: first, + props: {}, + children: [third], + } + + initialComponents[third] = { + id: third, + type: 'AvatarBadge', + parent: second, + props: {}, + children: [], + } + + const updatedComponents = deleteComponent( + initialComponents[first], + initialComponents, + ) + + expect(Object.keys(updatedComponents).length).toEqual(1) + expect(Object.keys(updatedComponents)[0]).toEqual('root') + }) +}) diff --git a/src/utils/recursive.ts b/src/utils/recursive.ts new file mode 100644 index 0000000000..4e1889266d --- /dev/null +++ b/src/utils/recursive.ts @@ -0,0 +1,58 @@ +import omit from 'lodash/omit' +import { generateId } from './generateId' + +export const duplicateComponent = ( + componentToClone: IComponent, + components: IComponents, +) => { + const clonedComponents: IComponents = {} + + const cloneComponent = (component: IComponent) => { + const newid = generateId() + const children = component.children.map(child => { + return cloneComponent(components[child]) + }) + + clonedComponents[newid] = { + ...component, + id: newid, + props: { ...component.props }, + children, + } + + children.forEach(child => { + clonedComponents[child].parent = newid + }) + + return newid + } + + const newId = cloneComponent(componentToClone) + + return { + newId, + clonedComponents, + } +} + +export const deleteComponent = ( + component: IComponent, + components: IComponents, +) => { + let updatedComponents = { ...components } + const deleteRecursive = ( + children: IComponent['children'], + id: IComponent['id'], + ) => { + children.forEach(child => { + updatedComponents[child] && + deleteRecursive(updatedComponents[child].children, child) + }) + + updatedComponents = omit(updatedComponents, id) + } + + deleteRecursive(component.children, component.id) + updatedComponents = omit(updatedComponents, component.id) + return updatedComponents +}