Skip to content

Commit

Permalink
Fix: nested repeaters
Browse files Browse the repository at this point in the history
  • Loading branch information
Tofandel committed Feb 28, 2024
1 parent df08d1a commit c4f61d7
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 227 deletions.
4 changes: 1 addition & 3 deletions frontend/js/components/Repeater.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
handle: '.block__handle' // drag handle
}
},
inject: {inContentEditor: {default: false}},
computed: {
triggerVariant: function () {
if (this.buttonAsLink) {
Expand All @@ -136,9 +137,6 @@
blockSize: function () {
return this.inContentEditor ? 'small' : ''
},
inContentEditor: function () {
return typeof this.$parent.repeaterName !== 'undefined'
},
hasRemainingBlocks: function () {
let max = null
if (this.max && this.max > 0) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/js/components/blocks/BlocksList.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default {
return this.blocks(this.editorName)
},
allSavedBlocks () {
return this.used && Object.keys(this.used).reduce((acc, editorName) => acc.concat(this.used[editorName]), [])
return this.used && Object.values(this.used).flat()
},
hasBlockActive () {
return Object.keys(this.activeBlock).length > 0
Expand Down
2 changes: 1 addition & 1 deletion frontend/js/mixins/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default {
return this.name + '[' + id + ']' // output : nameOfBlock[UniqID][name]
},
repeaterName: function (id) {
return this.name.replace('[', '-').replace(']', '') + '|' + id // nameOfBlock-UniqID|name
return this.nestedEditorName(id)
},
nestedEditorName: function (id) {
return this.name.replace('[', '-').replace(']', '') + '|' + id // nameOfBlock-UniqID|name
Expand Down
5 changes: 5 additions & 0 deletions frontend/js/mixins/blockEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export default {
default: 0
}
},
provide() {
return {
inContentEditor: true,
}
},
methods: {
addAndEditBlock (add, edit, { block, index }) {
window[process.env.VUE_APP_NAME].PREVSTATE = cloneDeep(this.$store.state)
Expand Down
117 changes: 52 additions & 65 deletions frontend/js/utils/getFormData.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,83 +31,76 @@ export const stripOutBlockNamespace = (name, id) => {
return nameWithoutBlock.match(/]/gi).length > 1 ? nameWithoutBlock.replace(']', '') : nameWithoutBlock.slice(0, -1)
}

export const buildBlock = (block, rootState, isRepeater = false) => {
const repeaterIds = Object.keys(rootState.repeaters.repeaters);
const repeaters = Object.assign({}, ...repeaterIds.filter(repeaterKey => {
return repeaterKey.startsWith('blocks-' + block.id + '|')
export const buildBlock = (block, rootState, isRepeater = false, childKey) => {
const parentRepeaters = rootState.repeaters.repeaters;
const repeaterIds = Object.keys(parentRepeaters);
const prefix = 'blocks-' + block.id + '|';
const repeaters = repeaterIds.filter(repeaterKey => {
return repeaterKey.startsWith(prefix)
})
.map(repeaterKey => {
return {
[repeaterKey.replace('blocks-' + block.id + '|', '')]: rootState.repeaters.repeaters[repeaterKey].map(repeaterItem => {
return buildBlock(repeaterItem, rootState, true)
})
}
}))
.reduce((acc, repeaterKey) => {
acc[repeaterKey.replace(prefix, '')] = parentRepeaters[repeaterKey].map(repeaterItem => {
return buildBlock(repeaterItem, rootState, true)
})

return acc
}, {})

const blockIds = Object.keys(rootState.blocks.blocks);
const blocks = Object.assign({}, ...blockIds.filter(blockKey => {
return blockKey.startsWith('blocks-' + block.id)
}).map(blockKey => {
const blocks = blockIds.filter(blockKey => {
return blockKey.startsWith(prefix)
}).reduce((acc, blockKey) => {
acc.push(...rootState.blocks.blocks[blockKey].map(repeaterItem => {
if (isRepeater) {
repeaterItem = {...repeaterItem, name: repeaterItem.name.replace(prefix, '')}
}
return buildBlock(repeaterItem, rootState, false, blockKey.replace(prefix, ''))
}));
return acc;
}, [])

// retrieve all fields for this block and clean up field names
const content = rootState.form.fields.filter((field) => {
return isBlockField(field.name, block.id)
}).map((field) => {
return {
[blockKey.replace('blocks-' + block.id + '|', '')]: rootState.blocks.blocks[blockKey].map(repeaterItem => {
return buildBlock(repeaterItem, rootState)
})
name: stripOutBlockNamespace(field.name, block.id),
value: field.value
}
}))
}).reduce((content, field) => {
content[field.name] = field.value
return content
}, {});

return {
const base = {
id: block.id,
type: block.type,
is_repeater: isRepeater,
editor_name: block.name,
// retrieve all fields for this block and clean up field names
content: rootState.form.fields.filter((field) => {
return isBlockField(field.name, block.id)
}).map((field) => {
return {
name: stripOutBlockNamespace(field.name, block.id),
value: field.value
}
}).reduce((content, field) => {
content[field.name] = field.value
return content
}, {}),
medias: gatherSelected(rootState.mediaLibrary.selected, block),
browsers: gatherSelected(rootState.browser.selected, block),
// gather repeater blocks from the repeater store module
blocks: { ...repeaters, ...blocks }
blocks,
repeaters,
}
return isRepeater
? { ...content, ...base, is_repeater: true, repeater_target_id: block.repeater_target_id}
: { ...base, type: block.type, content, child_key: childKey }
}

export const isBlockEmpty = (blockData) => {
return isEmpty(blockData.content) && isEmpty(blockData.browsers) && isEmpty(blockData.medias) && isEmpty(blockData.blocks)
}

export const gatherRepeaters = (rootState) => {
return Object.assign({}, ...Object.keys(rootState.repeaters.repeaters).filter(repeaterKey => {
return Object.keys(rootState.repeaters.repeaters).filter(repeaterKey => {
// we start by filtering out repeater blocks
return !repeaterKey.startsWith('blocks-')
}).map(repeater => {
return {
[repeater]: rootState.repeaters.repeaters[repeater].map(repeaterItem => {
// and for each repeater we build a block for each item
const repeaterBlock = buildBlock(repeaterItem, rootState)

// we want to inline fields in the repeater object
// and we don't need the type of component used
const fields = repeaterBlock.content
delete repeaterBlock.content
delete repeaterBlock.type

// and lastly we want to keep the id to update existing items
fields.id = repeaterItem.id
// If the repeater has a target id we are referencing an existing item.
fields.repeater_target_id = repeaterItem.repeater_target_id ?? null

return Object.assign(repeaterBlock, fields)
})
}
}))
}).reduce((acc, repeater) => {
acc[repeater] = rootState.repeaters.repeaters[repeater].map(repeaterItem => {
// and for each repeater we build a block for each item
return buildBlock(repeaterItem, rootState, true)
})
return acc;
}, {})
}

export const gatherBlocks = (rootState) => {
Expand All @@ -124,7 +117,7 @@ export const gatherBlocks = (rootState) => {
}

export const getFormFields = (rootState) => {
const fields = rootState.form.fields.filter((field) => {
return rootState.form.fields.filter((field) => {
// we start by filtering out blocks related form fields
return !field.name.startsWith('blocks[') && !field.name.startsWith('mediaMeta[')
}).reduce((fields, field) => {
Expand All @@ -133,12 +126,10 @@ export const getFormFields = (rootState) => {
fields[field.name] = field.value
return fields
}, {})

return fields
}

export const getModalFormFields = (rootState) => {
const fields = rootState.form.modalFields.filter((field) => {
return rootState.form.modalFields.filter((field) => {
// we start by filtering out blocks related form fields
return !field.name.startsWith('blocks[') && !field.name.startsWith('mediaMeta[')
}).reduce((fields, field) => {
Expand All @@ -147,8 +138,6 @@ export const getModalFormFields = (rootState) => {
fields[field.name] = field.value
return fields
}, {})

return fields
}

export const getFormData = (rootState) => {
Expand All @@ -159,7 +148,7 @@ export const getFormData = (rootState) => {
// - publication properties
// - selected medias and browsers
// - created blocks and repeaters
const data = Object.assign(fields, {
return Object.assign(fields, {
cmsSaveType: rootState.form.type,
published: rootState.publication.published,
public: rootState.publication.visibility === 'public',
Expand All @@ -172,6 +161,4 @@ export const getFormData = (rootState) => {
blocks: gatherBlocks(rootState),
repeaters: gatherRepeaters(rootState)
})

return data
}
8 changes: 8 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./frontend/js/*"]
}
}
}
37 changes: 24 additions & 13 deletions src/Repositories/Behaviors/HandleBlocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;

Expand Down Expand Up @@ -272,22 +273,32 @@ private function getChildBlocks($object, $parentBlockFields)
{
$childBlocksList = Collection::make();

foreach ($parentBlockFields['blocks'] ?? [] as $childKey => $childBlocks) {
if (strpos($childKey, '|')) {
continue;
}
foreach ($childBlocks as $index => $childBlock) {
$childBlock = $this->buildBlock($childBlock, $object, $childBlock['is_repeater'] ?? true);
$this->validateBlockArray($childBlock, $childBlock['instance'], true);
$childBlock['child_key'] = $childKey;
$childBlock['position'] = $index + 1;
$childBlock['editor_name'] = $parentBlockFields['editor_name'] ?? 'default';
$childBlock['blocks'] = $this->getChildBlocks($object, $childBlock);

$childBlocksList->push($childBlock);
if (empty($parentBlockFields['blocks'])) {
return $childBlocksList;
}

// Fallback if frontend or revision is still on the old schema
if (is_int(key(current($parentBlockFields['blocks'])))) {
foreach ($parentBlockFields['blocks'] as $childKey => $childBlocks) {
foreach ($childBlocks as $index => $childBlock) {
$childBlock['child_key'] = $childKey;
$parentBlockFields['blocks'][$index] = $childBlock;
}
unset($parentBlockFields['blocks'][$childKey]);
}
}

foreach ($parentBlockFields['blocks'] as $index => $childBlock) {
$childBlock = $this->buildBlock($childBlock, $object, $childBlock['is_repeater'] ?? false);
$this->validateBlockArray($childBlock, $childBlock['instance'], true);
$childBlock['child_key'] = $childBlock['child_key'] ?? Str::afterLast($childBlock['editor_name'], '|');
$childBlock['position'] = $index + 1;
$childBlock['editor_name'] = $parentBlockFields['editor_name'] ?? 'default';
$childBlock['blocks'] = $this->getChildBlocks($object, $childBlock);

$childBlocksList->push($childBlock);
}

return $childBlocksList;
}

Expand Down

0 comments on commit c4f61d7

Please sign in to comment.