Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: nested repeaters #2490

Open
wants to merge 9 commits into
base: 3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
120 changes: 52 additions & 68 deletions frontend/js/utils/getFormData.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,87 +27,77 @@ export const isBlockField = (name, id) => {
}

export const stripOutBlockNamespace = (name, id) => {
const nameWithoutBlock = name.replace('blocks[' + id + '][', '')
return nameWithoutBlock.match(/]/gi).length > 1 ? nameWithoutBlock.replace(']', '') : nameWithoutBlock.slice(0, -1)
return name.replace('blocks[' + id + '][', '').replace(']', '')
}

export const buildBlock = (block, rootState, isRepeater = false) => {
export const buildBlock = (block, rootState, isRepeater = false, isInsideRepeater = isRepeater) => {
const repeaterIds = Object.keys(rootState.repeaters.repeaters);
const repeaters = Object.assign({}, ...repeaterIds.filter(repeaterKey => {
return repeaterKey.startsWith('blocks-' + block.id + '|')
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, '')] = rootState.repeaters.repeaters[repeaterKey]
.map(repeaterItem => {
return buildBlock(repeaterItem, rootState, true, isRepeater)
})
}
}))

return acc
}, {})

const blockIds = Object.keys(rootState.blocks.blocks);
const blocks = Object.assign({}, ...blockIds.filter(blockKey => {
return blockKey.startsWith('blocks-' + block.id)
}).map(blockKey => {
return {
[blockKey.replace('blocks-' + block.id + '|', '')]: rootState.blocks.blocks[blockKey].map(repeaterItem => {
return buildBlock(repeaterItem, rootState)
})
}
}))
const blocks = blockIds.filter(blockKey => {
return blockKey.startsWith(prefix)
}).reduce((acc, blockKey) => {
const key = blockKey.replace(prefix, '');
rootState.blocks.blocks[blockKey].forEach(blockItem => {
const block = buildBlock(blockItem, rootState, false);
if (isInsideRepeater) {
acc.push(block);
} else {
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(block)
}
})
return acc;
}, isInsideRepeater ? [] : {})

// retrieve all fields for this block and clean up field names
const content = rootState.form.fields.filter((field) => {
return isBlockField(field.name, block.id)
}).reduce((acc, field) => {
acc[stripOutBlockNamespace(field.name, block.id)] = field.value;
return acc;
}, {})

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 }
}
return isInsideRepeater
? { ...content, ...base, repeater_target_id: block.repeater_target_id, blocks, repeaters}
: { ...base, content, is_repeater: isRepeater, type: block.type, editor_name: block.name?.split('|').pop(), blocks: {...blocks, ...repeaters} }
}

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 +114,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 +123,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 +135,6 @@ export const getModalFormFields = (rootState) => {
fields[field.name] = field.value
return fields
}, {})

return fields
}

export const getFormData = (rootState) => {
Expand All @@ -159,7 +145,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 +158,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/*"]
}
}
}
26 changes: 10 additions & 16 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 @@ -273,11 +274,11 @@ private function getChildBlocks($object, $parentBlockFields)
$childBlocksList = Collection::make();

foreach ($parentBlockFields['blocks'] ?? [] as $childKey => $childBlocks) {
if (strpos($childKey, '|')) {
if (str_contains($childKey, '|')) {
continue;
}
foreach ($childBlocks as $index => $childBlock) {
$childBlock = $this->buildBlock($childBlock, $object, $childBlock['is_repeater'] ?? true);
$childBlock = $this->buildBlock($childBlock, $object, $childBlock['is_repeater'] ?? false);
$this->validateBlockArray($childBlock, $childBlock['instance'], true);
$childBlock['child_key'] = $childKey;
$childBlock['position'] = $index + 1;
Expand Down Expand Up @@ -421,20 +422,11 @@ function ($files, $role) use ($locale, $block) {
}
}

if ($fields['blocksFields'] ?? false) {
$fields['blocksFields'] = call_user_func_array('array_merge', $fields['blocksFields'] ?? []);
}

if ($fields['blocksMedias'] ?? false) {
$fields['blocksMedias'] = call_user_func_array('array_merge', $fields['blocksMedias'] ?? []);
}

if ($fields['blocksFiles'] ?? false) {
$fields['blocksFiles'] = call_user_func_array('array_merge', $fields['blocksFiles'] ?? []);
}

if ($fields['blocksBrowsers'] ?? false) {
$fields['blocksBrowsers'] = call_user_func_array('array_merge', $fields['blocksBrowsers'] ?? []);
foreach (['Fields', 'Medias', 'Files', 'Browsers'] as $fieldKey) {
if ($fields['blocks' . $fieldKey] ?? false) {
$fields['blocks' . $fieldKey] = call_user_func_array('array_merge', $fields['blocks' . $fieldKey] ?? []);
}
}
}

Expand Down Expand Up @@ -473,7 +465,7 @@ protected function getBlockBrowsers($block)
try {
$relationRepository = $this->getModelRepository($relation);
$relatedItems = $relationRepository->get([], ['id' => $ids], [], -1);
} catch (\Throwable $th) {
} catch (\Throwable) {
$relatedItems = collect();
}
$sortedRelatedItems = array_flip($ids);
Expand All @@ -485,6 +477,7 @@ protected function getBlockBrowsers($block)
$items = Collection::make(array_values($sortedRelatedItems))->filter(function ($value) {
return is_object($value);
})->map(function ($relatedElement) use ($relation) {
// TODO this needs refactoring, it's duplicated from HandleBrowsers
return [
'id' => $relatedElement->id,
'name' => $relatedElement->titleInBrowser ?? $relatedElement->title,
Expand All @@ -494,6 +487,7 @@ protected function getBlockBrowsers($block)
'edit',
$relatedElement->id
),
'endpointType' => $relatedElement->getMorphClass(),
] + (classHasTrait($relatedElement, HasMedias::class) ? [
'thumbnail' => $relatedElement->defaultCmsImage(['w' => 100, 'h' => 100]),
] : []);
Expand Down