From ad8512f945a87eee734a9017f72882cdf03b1143 Mon Sep 17 00:00:00 2001 From: Justin Jongstra Date: Fri, 8 May 2026 11:05:40 +0200 Subject: [PATCH] enhance block duplication logic and improve state management --- frontend/js/store/modules/blocks.js | 78 +++++++++++++++++++++- frontend/js/store/modules/browser.js | 15 ----- frontend/js/store/modules/form.js | 8 +-- frontend/js/store/modules/media-library.js | 15 ----- frontend/js/store/modules/repeaters.js | 33 +-------- 5 files changed, 80 insertions(+), 69 deletions(-) diff --git a/frontend/js/store/modules/blocks.js b/frontend/js/store/modules/blocks.js index 033644ec13..30f50df2f1 100644 --- a/frontend/js/store/modules/blocks.js +++ b/frontend/js/store/modules/blocks.js @@ -11,7 +11,7 @@ import ACTIONS from '@/store/actions' import { buildBlock, isBlockEmpty } from '@/utils/getFormData.js' import api from '../api/blocks' -import { BLOCKS } from '../mutations' +import { BLOCKS, BROWSER, FORM, MEDIA_LIBRARY } from '../mutations' const state = { /** @@ -107,7 +107,7 @@ const mutations = { [BLOCKS.DUPLICATE_BLOCK] (state, { editorName, index, block, id }) { const updated = state.blocks[editorName] || [] - updated.splice(index, 0, { ...block, id, name: editorName }) + updated.splice(index, 0, { ...JSON.parse(JSON.stringify(block)), id, name: editorName }) Vue.set(state.blocks, editorName, updated) }, @@ -188,7 +188,81 @@ const actions = { } }, async [ACTIONS.DUPLICATE_BLOCK] ({ commit, state, rootState }, { editorName, futureIndex, block, id }) { + const clone = (v) => v == null ? v : JSON.parse(JSON.stringify(v)) + + const repeaters = (rootState.repeaters && rootState.repeaters.repeaters) || {} + const nestedBlocks = (rootState.blocks && rootState.blocks.blocks) || {} + const fields = (rootState.form && rootState.form.fields) || [] + const mediaSelected = (rootState.mediaLibrary && rootState.mediaLibrary.selected) || {} + const browserSelected = (rootState.browser && rootState.browser.selected) || {} + + const idMap = { [block.id]: id } + const queue = [block.id] + while (queue.length) { + const currentId = queue.shift() + const prefix = `blocks-${currentId}|` + ;[repeaters, nestedBlocks].forEach(bucket => { + Object.keys(bucket).forEach(key => { + if (!key.startsWith(prefix)) return + ;(bucket[key] || []).forEach(item => { + if (!item || item.id == null || idMap[item.id] != null) return + idMap[item.id] = setBlockID() + queue.push(item.id) + }) + }) + }) + } + + const rewrite = (str, before, after) => Object.keys(idMap).reduce( + (acc, oldId) => acc.split(`${before}${oldId}${after}`).join(`${before}${idMap[oldId]}${after}`), str + ) + const inSubtree = (key) => Object.keys(idMap).some(oldId => key.startsWith(`blocks-${oldId}|`)) + commit(BLOCKS.DUPLICATE_BLOCK, { editorName, index: futureIndex, block, id }) + + Object.keys(nestedBlocks).forEach(key => { + if (!inSubtree(key)) return + const newKey = rewrite(key, 'blocks-', '|') + const cloned = (nestedBlocks[key] || []).map(nested => { + if (!nested || nested.id == null || idMap[nested.id] == null) return null + return { ...clone(nested), id: idMap[nested.id], name: newKey } + }).filter(Boolean) + if (!cloned.length) return + const existing = state.blocks[newKey] || [] + commit(BLOCKS.REORDER_BLOCKS, { editorName: newKey, value: [...existing, ...cloned] }) + }) + + const clonedRepeaters = {} + Object.keys(repeaters).forEach(key => { + if (!inSubtree(key)) return + clonedRepeaters[rewrite(key, 'blocks-', '|')] = (repeaters[key] || []).map(item => { + const c = clone(item) + if (c && c.id != null && idMap[c.id] != null) c.id = idMap[c.id] + if (c) c.twillUi = { isNew: true } + return c + }) + }) + if (Object.keys(clonedRepeaters).length) commit(FORM.ADD_REPEATERS, { repeaters: clonedRepeaters }) + + const fieldCopies = [] + const clonedMedias = {} + const clonedBrowsers = {} + Object.keys(idMap).forEach(oldId => { + const fp = `blocks[${oldId}]` + fields.forEach(f => { + if (typeof f.name !== 'string' || !f.name.startsWith(fp)) return + fieldCopies.push({ name: rewrite(f.name, 'blocks[', ']'), value: clone(f.value) }) + }) + Object.keys(mediaSelected).forEach(k => { + if (k.startsWith(fp)) clonedMedias[rewrite(k, 'blocks[', ']')] = clone(mediaSelected[k]) + }) + Object.keys(browserSelected).forEach(k => { + if (k.startsWith(fp)) clonedBrowsers[rewrite(k, 'blocks[', ']')] = clone(browserSelected[k]) + }) + }) + if (fieldCopies.length) commit(FORM.ADD_FORM_FIELDS, fieldCopies) + if (Object.keys(clonedMedias).length) commit(MEDIA_LIBRARY.ADD_MEDIAS, { medias: clonedMedias }) + if (Object.keys(clonedBrowsers).length) commit(BROWSER.ADD_BROWSERS, { browsers: clonedBrowsers }) }, async [ACTIONS.MOVE_BLOCK_TO_EDITOR] ({ commit, dispatch }, { editorName, index, block, futureIndex, id }) { await dispatch(ACTIONS.DUPLICATE_BLOCK, { diff --git a/frontend/js/store/modules/browser.js b/frontend/js/store/modules/browser.js index 1f5f78f421..8cae3565ac 100644 --- a/frontend/js/store/modules/browser.js +++ b/frontend/js/store/modules/browser.js @@ -1,7 +1,5 @@ import Vue from 'vue' -import ACTIONS from '@/store/actions' - import { BROWSER } from '../mutations' const state = { @@ -107,21 +105,8 @@ const mutations = { } } -const actions = { - async [ACTIONS.DUPLICATE_BLOCK] ({ commit, getters }, { block, id }) { - // copy browsers and update with the provided id - const browsers = { ...getters.browsersByBlockId(block.id) } - const browserIds = Object.keys(browsers) - const duplicates = {} - browserIds.forEach(browserId => (duplicates[browserId.replace(block.id, id)] = [...browsers[browserId]])) - - commit(BROWSER.ADD_BROWSERS, { browsers: duplicates }) - } -} - export default { state, getters, mutations, - actions } diff --git a/frontend/js/store/modules/form.js b/frontend/js/store/modules/form.js index 5107a063c3..86678bb164 100644 --- a/frontend/js/store/modules/form.js +++ b/frontend/js/store/modules/form.js @@ -4,8 +4,6 @@ * Save all the fields of the form. Submit the form. Display errors. */ -import cloneDeep from 'lodash/cloneDeep' - import ACTIONS from '@/store/actions' import { getFormData, getFormFields, getModalFormFields } from '@/utils/getFormData.js' @@ -156,7 +154,7 @@ const mutations = { fields.forEach(field => { newFields.push({ name: field.name.replace(oldId, newId), - value: cloneDeep(field.value) + value: JSON.parse(JSON.stringify(field.value)) }) }) state.fields = [...state.fields, ...newFields] @@ -356,10 +354,6 @@ const actions = { commit(NOTIFICATION.SET_NOTIF, { message: 'Your submission could not be validated, please fix and retry', variant: 'error' }) } }) - }, - async [ACTIONS.DUPLICATE_BLOCK] ({ commit, getters }, { block, id }) { - const fields = getters.fieldsByBlockId(block.id) - commit(FORM.DUPLICATE_BLOCK_FORM_FIELDS, { fields, oldId: block.id, newId: id }) } } diff --git a/frontend/js/store/modules/media-library.js b/frontend/js/store/modules/media-library.js index 2a82dfd1ac..e174c10bee 100644 --- a/frontend/js/store/modules/media-library.js +++ b/frontend/js/store/modules/media-library.js @@ -7,8 +7,6 @@ import cloneDeep from 'lodash/cloneDeep' import Vue from 'vue' -import ACTIONS from '@/store/actions' - import { MEDIA_LIBRARY } from '../mutations' const state = { @@ -286,21 +284,8 @@ const mutations = { } } -const actions = { - async [ACTIONS.DUPLICATE_BLOCK] ({ commit, getters }, { block, id }) { - // copy medias and update with the provided id - const medias = { ...getters.mediasByBlockId(block.id) } - const mediaIds = Object.keys(medias) - const duplicates = {} - mediaIds.forEach(mediaId => (duplicates[mediaId.replace(block.id, id)] = [...medias[mediaId]])) - - commit(MEDIA_LIBRARY.ADD_MEDIAS, { medias: duplicates }) - } -} - export default { state, getters, mutations, - actions } diff --git a/frontend/js/store/modules/repeaters.js b/frontend/js/store/modules/repeaters.js index e01c287d23..b07148c473 100644 --- a/frontend/js/store/modules/repeaters.js +++ b/frontend/js/store/modules/repeaters.js @@ -107,7 +107,7 @@ const mutations = { state.repeaters[blockInfos.name].splice(blockInfos.index, 1) }, [FORM.DUPLICATE_FORM_BLOCK] (state, blockInfos) { - const clone = Object.assign({}, state.repeaters[blockInfos.name][blockInfos.index]) + const clone = JSON.parse(JSON.stringify(state.repeaters[blockInfos.name][blockInfos.index])) clone.id = setBlockID() // Metadata for rendering @@ -122,7 +122,7 @@ const mutations = { fields.forEach(field => { fieldCopies.push({ name: field.name.replace(blockInfos.id, clone.id), - value: field.value + value: JSON.parse(JSON.stringify(field.value)) }) }) this.commit(FORM.ADD_FORM_FIELDS, fieldCopies) @@ -139,7 +139,7 @@ const mutations = { const actions = { async [ACTIONS.DUPLICATE_REPEATER] ({ state, commit, getters }, { editorName, block, index, id }) { - const clone = Object.assign({}, state.repeaters[editorName][index]) + const clone = JSON.parse(JSON.stringify(state.repeaters[editorName][index])) clone.id = id // Metadata for rendering @@ -163,33 +163,6 @@ const actions = { commit(FORM.ADD_FORM_FIELDS, fieldCopies) commit(FORM.ADD_REPEATERS, { repeaters: duplicates }) - }, - async [ACTIONS.DUPLICATE_BLOCK] ({ commit, getters }, { block, id }) { - // copy repeaters and update with the provided id - const repeaters = { ...getters.repeatersByBlockId(block.id) } - const repeaterIds = Object.keys(repeaters) - const duplicates = {} - repeaterIds.forEach(repeaterId => (duplicates[repeaterId.replace(block.id, id)] = [...repeaters[repeaterId]])) - - // copy fields and give them a new id - const fieldCopies = [] - Object.keys(duplicates).forEach(duplicateId => { - duplicates[duplicateId].forEach((block, index) => { - const id = Date.now() + Math.floor(Math.random() * 1000) - const fields = [...getters.fieldsByBlockId(block.id)] - duplicates[duplicateId][index] = { ...duplicates[duplicateId][index], id } - - fields.forEach(field => { - fieldCopies.push({ - name: field.name.replace(block.id, id), - value: JSON.parse(JSON.stringify(field.value)) - }) - }) - }) - }) - - commit(FORM.ADD_REPEATERS, { repeaters: duplicates }) - commit(FORM.ADD_FORM_FIELDS, fieldCopies) } }