Skip to content

Commit 8c24885

Browse files
committed
Fix: nested repeaters
1 parent df08d1a commit 8c24885

File tree

8 files changed

+162
-228
lines changed

8 files changed

+162
-228
lines changed

frontend/js/components/Repeater.vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@
123123
handle: '.block__handle' // drag handle
124124
}
125125
},
126+
provide() {
127+
const sepIndex = this.name.indexOf('|');
128+
if (sepIndex >= 0) {
129+
return {
130+
nestedBlockName: this.name.substring(0, sepIndex) + '|'
131+
}
132+
}
133+
},
134+
inject: {inContentEditor: {default: false}},
126135
computed: {
127136
triggerVariant: function () {
128137
if (this.buttonAsLink) {
@@ -136,9 +145,6 @@
136145
blockSize: function () {
137146
return this.inContentEditor ? 'small' : ''
138147
},
139-
inContentEditor: function () {
140-
return typeof this.$parent.repeaterName !== 'undefined'
141-
},
142148
hasRemainingBlocks: function () {
143149
let max = null
144150
if (this.max && this.max > 0) {

frontend/js/components/blocks/BlocksList.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default {
2020
return this.blocks(this.editorName)
2121
},
2222
allSavedBlocks () {
23-
return this.used && Object.keys(this.used).reduce((acc, editorName) => acc.concat(this.used[editorName]), [])
23+
return this.used && Object.values(this.used).flat()
2424
},
2525
hasBlockActive () {
2626
return Object.keys(this.activeBlock).length > 0

frontend/js/mixins/block.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default {
1414
opened: this.isOpen
1515
}
1616
},
17+
inject: {nestedBlockName: {default: ''}},
1718
methods: {
1819
open: function () {
1920
this.opened = true
@@ -22,7 +23,7 @@ export default {
2223
return this.name + '[' + id + ']' // output : nameOfBlock[UniqID][name]
2324
},
2425
repeaterName: function (id) {
25-
return this.name.replace('[', '-').replace(']', '') + '|' + id // nameOfBlock-UniqID|name
26+
return this.nestedBlockName + this.nestedEditorName(id)
2627
},
2728
nestedEditorName: function (id) {
2829
return this.name.replace('[', '-').replace(']', '') + '|' + id // nameOfBlock-UniqID|name

frontend/js/mixins/blockEditor.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export default {
1515
default: 0
1616
}
1717
},
18+
provide() {
19+
return {
20+
inContentEditor: true,
21+
}
22+
},
1823
methods: {
1924
addAndEditBlock (add, edit, { block, index }) {
2025
window[process.env.VUE_APP_NAME].PREVSTATE = cloneDeep(this.$store.state)

frontend/js/utils/getFormData.js

Lines changed: 62 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import isEmpty from 'lodash/isEmpty'
66
* and strip it out from the key to clean things up and make it easier for the backend
77
*/
88
const gatherSelected = (selected, block = null) => {
9-
return Object.assign({}, ...Object.keys(selected).map(key => {
9+
return Object.keys(selected).map(key => {
1010
if (block) {
1111
if (isBlockField(key, block.id)) {
1212
return {
@@ -19,7 +19,7 @@ const gatherSelected = (selected, block = null) => {
1919
}
2020
}
2121
return null
22-
}).filter(x => x))
22+
}).filter(x => x)
2323
}
2424

2525
export const isBlockField = (name, id) => {
@@ -31,83 +31,85 @@ export const stripOutBlockNamespace = (name, id) => {
3131
return nameWithoutBlock.match(/]/gi).length > 1 ? nameWithoutBlock.replace(']', '') : nameWithoutBlock.slice(0, -1)
3232
}
3333

34-
export const buildBlock = (block, rootState, isRepeater = false) => {
35-
const repeaterIds = Object.keys(rootState.repeaters.repeaters);
36-
const repeaters = Object.assign({}, ...repeaterIds.filter(repeaterKey => {
37-
return repeaterKey.startsWith('blocks-' + block.id + '|')
34+
export const buildBlock = (block, rootState, isRepeater = false, subRepeaters = null, childKey) => {
35+
const parentRepeaters = subRepeaters || rootState.repeaters.repeaters;
36+
subRepeaters = {};
37+
const repeaterIds = Object.keys(parentRepeaters);
38+
const prefix = 'blocks-' + block.id + '|';
39+
const repeaters = repeaterIds.filter(repeaterKey => {
40+
return repeaterKey.startsWith(prefix)
3841
})
39-
.map(repeaterKey => {
40-
return {
41-
[repeaterKey.replace('blocks-' + block.id + '|', '')]: rootState.repeaters.repeaters[repeaterKey].map(repeaterItem => {
42-
return buildBlock(repeaterItem, rootState, true)
42+
.reduce((acc, repeaterKey) => {
43+
if (repeaterKey.split('|').length > 2) {
44+
subRepeaters[repeaterKey.replace(prefix, '')] = parentRepeaters[repeaterKey];
45+
} else {
46+
acc[repeaterKey.replace(prefix, '')] = parentRepeaters[repeaterKey].map(repeaterItem => {
47+
return buildBlock(repeaterItem, rootState, true, subRepeaters)
4348
})
4449
}
45-
}))
50+
51+
return acc
52+
}, {})
4653

4754
const blockIds = Object.keys(rootState.blocks.blocks);
48-
const blocks = Object.assign({}, ...blockIds.filter(blockKey => {
49-
return blockKey.startsWith('blocks-' + block.id)
50-
}).map(blockKey => {
55+
const blocks = blockIds.filter(blockKey => {
56+
return blockKey.startsWith(prefix)
57+
}).reduce((acc, blockKey) => {
58+
acc.push(...rootState.blocks.blocks[blockKey].map(repeaterItem => {
59+
if (isRepeater) {
60+
repeaterItem = {...repeaterItem, name: repeaterItem.name.replace(prefix, '')}
61+
}
62+
return buildBlock(repeaterItem, rootState, false, null, blockKey.replace(prefix, ''))
63+
}));
64+
return acc;
65+
}, [])
66+
67+
// retrieve all fields for this block and clean up field names
68+
const content = rootState.form.fields.filter((field) => {
69+
return isBlockField(field.name, block.id)
70+
}).map((field) => {
5171
return {
52-
[blockKey.replace('blocks-' + block.id + '|', '')]: rootState.blocks.blocks[blockKey].map(repeaterItem => {
53-
return buildBlock(repeaterItem, rootState)
54-
})
72+
name: stripOutBlockNamespace(field.name, block.id),
73+
value: field.value
5574
}
56-
}))
75+
}).reduce((content, field) => {
76+
content[field.name] = field.value
77+
return content
78+
}, {});
5779

58-
return {
80+
const base = {
5981
id: block.id,
60-
type: block.type,
61-
is_repeater: isRepeater,
6282
editor_name: block.name,
63-
// retrieve all fields for this block and clean up field names
64-
content: rootState.form.fields.filter((field) => {
65-
return isBlockField(field.name, block.id)
66-
}).map((field) => {
67-
return {
68-
name: stripOutBlockNamespace(field.name, block.id),
69-
value: field.value
70-
}
71-
}).reduce((content, field) => {
72-
content[field.name] = field.value
73-
return content
74-
}, {}),
7583
medias: gatherSelected(rootState.mediaLibrary.selected, block),
7684
browsers: gatherSelected(rootState.browser.selected, block),
7785
// gather repeater blocks from the repeater store module
78-
blocks: { ...repeaters, ...blocks }
86+
blocks,
87+
repeaters,
7988
}
89+
return isRepeater
90+
? { ...content, ...base, is_repeater: true, repeater_target_id: block.repeater_target_id}
91+
: { ...base, type: block.type, content, child_key: childKey }
8092
}
8193

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

86-
export const gatherRepeaters = (rootState) => {
87-
return Object.assign({}, ...Object.keys(rootState.repeaters.repeaters).filter(repeaterKey => {
98+
99+
const buildRepeaters = (repeaters, rootState) => {
100+
return Object.keys(repeaters).filter(repeaterKey => {
88101
// we start by filtering out repeater blocks
89102
return !repeaterKey.startsWith('blocks-')
90-
}).map(repeater => {
91-
return {
92-
[repeater]: rootState.repeaters.repeaters[repeater].map(repeaterItem => {
93-
// and for each repeater we build a block for each item
94-
const repeaterBlock = buildBlock(repeaterItem, rootState)
95-
96-
// we want to inline fields in the repeater object
97-
// and we don't need the type of component used
98-
const fields = repeaterBlock.content
99-
delete repeaterBlock.content
100-
delete repeaterBlock.type
101-
102-
// and lastly we want to keep the id to update existing items
103-
fields.id = repeaterItem.id
104-
// If the repeater has a target id we are referencing an existing item.
105-
fields.repeater_target_id = repeaterItem.repeater_target_id ?? null
106-
107-
return Object.assign(repeaterBlock, fields)
108-
})
109-
}
110-
}))
103+
}).reduce((acc, repeater) => {
104+
acc[repeater] = repeaters[repeater].map(repeaterItem => {
105+
// and for each repeater we build a block for each item
106+
return buildBlock(repeaterItem, rootState, true)
107+
})
108+
return acc;
109+
}, {})
110+
}
111+
export const gatherRepeaters = (rootState) => {
112+
return buildRepeaters(rootState.repeaters.repeaters, rootState)
111113
}
112114

113115
export const gatherBlocks = (rootState) => {
@@ -124,7 +126,7 @@ export const gatherBlocks = (rootState) => {
124126
}
125127

126128
export const getFormFields = (rootState) => {
127-
const fields = rootState.form.fields.filter((field) => {
129+
return rootState.form.fields.filter((field) => {
128130
// we start by filtering out blocks related form fields
129131
return !field.name.startsWith('blocks[') && !field.name.startsWith('mediaMeta[')
130132
}).reduce((fields, field) => {
@@ -133,12 +135,10 @@ export const getFormFields = (rootState) => {
133135
fields[field.name] = field.value
134136
return fields
135137
}, {})
136-
137-
return fields
138138
}
139139

140140
export const getModalFormFields = (rootState) => {
141-
const fields = rootState.form.modalFields.filter((field) => {
141+
return rootState.form.modalFields.filter((field) => {
142142
// we start by filtering out blocks related form fields
143143
return !field.name.startsWith('blocks[') && !field.name.startsWith('mediaMeta[')
144144
}).reduce((fields, field) => {
@@ -147,8 +147,6 @@ export const getModalFormFields = (rootState) => {
147147
fields[field.name] = field.value
148148
return fields
149149
}, {})
150-
151-
return fields
152150
}
153151

154152
export const getFormData = (rootState) => {
@@ -159,7 +157,7 @@ export const getFormData = (rootState) => {
159157
// - publication properties
160158
// - selected medias and browsers
161159
// - created blocks and repeaters
162-
const data = Object.assign(fields, {
160+
return Object.assign(fields, {
163161
cmsSaveType: rootState.form.type,
164162
published: rootState.publication.published,
165163
public: rootState.publication.visibility === 'public',
@@ -172,6 +170,4 @@ export const getFormData = (rootState) => {
172170
blocks: gatherBlocks(rootState),
173171
repeaters: gatherRepeaters(rootState)
174172
})
175-
176-
return data
177173
}

jsconfig.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": ".",
4+
"paths": {
5+
"@/*": ["./frontend/js/*"]
6+
}
7+
}
8+
}

src/Repositories/Behaviors/HandleBlocks.php

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Illuminate\Support\Facades\Log;
1414
use Illuminate\Support\Facades\Schema;
1515
use Illuminate\Support\Facades\Validator;
16+
use Illuminate\Support\Str;
1617
use Illuminate\Validation\ValidationException;
1718
use Symfony\Component\Routing\Exception\RouteNotFoundException;
1819

@@ -272,20 +273,20 @@ private function getChildBlocks($object, $parentBlockFields)
272273
{
273274
$childBlocksList = Collection::make();
274275

275-
foreach ($parentBlockFields['blocks'] ?? [] as $childKey => $childBlocks) {
276-
if (strpos($childKey, '|')) {
277-
continue;
278-
}
279-
foreach ($childBlocks as $index => $childBlock) {
280-
$childBlock = $this->buildBlock($childBlock, $object, $childBlock['is_repeater'] ?? true);
281-
$this->validateBlockArray($childBlock, $childBlock['instance'], true);
282-
$childBlock['child_key'] = $childKey;
283-
$childBlock['position'] = $index + 1;
284-
$childBlock['editor_name'] = $parentBlockFields['editor_name'] ?? 'default';
285-
$childBlock['blocks'] = $this->getChildBlocks($object, $childBlock);
286-
287-
$childBlocksList->push($childBlock);
276+
foreach ($parentBlockFields['blocks'] ?? [] as $index => $childBlock) {
277+
// Fallback if frontend is still on the old schema
278+
if (!is_int($index)) {
279+
$childBlock = current($childBlock);
280+
$childBlock['child_key'] = key($childBlock);
288281
}
282+
$childBlock = $this->buildBlock($childBlock, $object, $childBlock['is_repeater'] ?? false);
283+
$this->validateBlockArray($childBlock, $childBlock['instance'], true);
284+
$childBlock['child_key'] = $childBlock['child_key'] ?? Str::afterLast($childBlock['editor_name'], '|');
285+
$childBlock['position'] = $index + 1;
286+
$childBlock['editor_name'] = $parentBlockFields['editor_name'] ?? 'default';
287+
$childBlock['blocks'] = $this->getChildBlocks($object, $childBlock);
288+
289+
$childBlocksList->push($childBlock);
289290
}
290291

291292
return $childBlocksList;

0 commit comments

Comments
 (0)