Skip to content

Commit 618b3fc

Browse files
committed
Fix: nested repeaters
1 parent df08d1a commit 618b3fc

File tree

8 files changed

+139
-227
lines changed

8 files changed

+139
-227
lines changed

frontend/js/components/Repeater.vue

+1-3
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
handle: '.block__handle' // drag handle
124124
}
125125
},
126+
inject: {inContentEditor: {default: false}},
126127
computed: {
127128
triggerVariant: function () {
128129
if (this.buttonAsLink) {
@@ -136,9 +137,6 @@
136137
blockSize: function () {
137138
return this.inContentEditor ? 'small' : ''
138139
},
139-
inContentEditor: function () {
140-
return typeof this.$parent.repeaterName !== 'undefined'
141-
},
142140
hasRemainingBlocks: function () {
143141
let max = null
144142
if (this.max && this.max > 0) {

frontend/js/components/blocks/BlocksList.js

+1-1
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default {
2222
return this.name + '[' + id + ']' // output : nameOfBlock[UniqID][name]
2323
},
2424
repeaterName: function (id) {
25-
return this.name.replace('[', '-').replace(']', '') + '|' + id // nameOfBlock-UniqID|name
25+
return this.nestedEditorName(id)
2626
},
2727
nestedEditorName: function (id) {
2828
return this.name.replace('[', '-').replace(']', '') + '|' + id // nameOfBlock-UniqID|name

frontend/js/mixins/blockEditor.js

+5
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

+52-65
Original file line numberDiff line numberDiff line change
@@ -31,83 +31,76 @@ 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, childKey) => {
35+
const parentRepeaters = rootState.repeaters.repeaters;
36+
const repeaterIds = Object.keys(parentRepeaters);
37+
const prefix = 'blocks-' + block.id + '|';
38+
const repeaters = repeaterIds.filter(repeaterKey => {
39+
return repeaterKey.startsWith(prefix)
3840
})
39-
.map(repeaterKey => {
40-
return {
41-
[repeaterKey.replace('blocks-' + block.id + '|', '')]: rootState.repeaters.repeaters[repeaterKey].map(repeaterItem => {
42-
return buildBlock(repeaterItem, rootState, true)
43-
})
44-
}
45-
}))
41+
.reduce((acc, repeaterKey) => {
42+
acc[repeaterKey.replace(prefix, '')] = parentRepeaters[repeaterKey].map(repeaterItem => {
43+
return buildBlock(repeaterItem, rootState, true)
44+
})
45+
46+
return acc
47+
}, {})
4648

4749
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 => {
50+
const blocks = blockIds.filter(blockKey => {
51+
return blockKey.startsWith(prefix)
52+
}).reduce((acc, blockKey) => {
53+
acc.push(...rootState.blocks.blocks[blockKey].map(repeaterItem => {
54+
if (isRepeater) {
55+
repeaterItem = {...repeaterItem, name: repeaterItem.name.replace(prefix, '')}
56+
}
57+
return buildBlock(repeaterItem, rootState, false, blockKey.replace(prefix, ''))
58+
}));
59+
return acc;
60+
}, [])
61+
62+
// retrieve all fields for this block and clean up field names
63+
const content = rootState.form.fields.filter((field) => {
64+
return isBlockField(field.name, block.id)
65+
}).map((field) => {
5166
return {
52-
[blockKey.replace('blocks-' + block.id + '|', '')]: rootState.blocks.blocks[blockKey].map(repeaterItem => {
53-
return buildBlock(repeaterItem, rootState)
54-
})
67+
name: stripOutBlockNamespace(field.name, block.id),
68+
value: field.value
5569
}
56-
}))
70+
}).reduce((content, field) => {
71+
content[field.name] = field.value
72+
return content
73+
}, {});
5774

58-
return {
75+
const base = {
5976
id: block.id,
60-
type: block.type,
61-
is_repeater: isRepeater,
6277
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-
}, {}),
7578
medias: gatherSelected(rootState.mediaLibrary.selected, block),
7679
browsers: gatherSelected(rootState.browser.selected, block),
7780
// gather repeater blocks from the repeater store module
78-
blocks: { ...repeaters, ...blocks }
81+
blocks,
82+
repeaters,
7983
}
84+
return isRepeater
85+
? { ...content, ...base, is_repeater: true, repeater_target_id: block.repeater_target_id}
86+
: { ...base, type: block.type, content, child_key: childKey }
8087
}
8188

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

8693
export const gatherRepeaters = (rootState) => {
87-
return Object.assign({}, ...Object.keys(rootState.repeaters.repeaters).filter(repeaterKey => {
94+
return Object.keys(rootState.repeaters.repeaters).filter(repeaterKey => {
8895
// we start by filtering out repeater blocks
8996
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-
}))
97+
}).reduce((acc, repeater) => {
98+
acc[repeater] = rootState.repeaters.repeaters[repeater].map(repeaterItem => {
99+
// and for each repeater we build a block for each item
100+
return buildBlock(repeaterItem, rootState, true)
101+
})
102+
return acc;
103+
}, {})
111104
}
112105

113106
export const gatherBlocks = (rootState) => {
@@ -124,7 +117,7 @@ export const gatherBlocks = (rootState) => {
124117
}
125118

126119
export const getFormFields = (rootState) => {
127-
const fields = rootState.form.fields.filter((field) => {
120+
return rootState.form.fields.filter((field) => {
128121
// we start by filtering out blocks related form fields
129122
return !field.name.startsWith('blocks[') && !field.name.startsWith('mediaMeta[')
130123
}).reduce((fields, field) => {
@@ -133,12 +126,10 @@ export const getFormFields = (rootState) => {
133126
fields[field.name] = field.value
134127
return fields
135128
}, {})
136-
137-
return fields
138129
}
139130

140131
export const getModalFormFields = (rootState) => {
141-
const fields = rootState.form.modalFields.filter((field) => {
132+
return rootState.form.modalFields.filter((field) => {
142133
// we start by filtering out blocks related form fields
143134
return !field.name.startsWith('blocks[') && !field.name.startsWith('mediaMeta[')
144135
}).reduce((fields, field) => {
@@ -147,8 +138,6 @@ export const getModalFormFields = (rootState) => {
147138
fields[field.name] = field.value
148139
return fields
149140
}, {})
150-
151-
return fields
152141
}
153142

154143
export const getFormData = (rootState) => {
@@ -159,7 +148,7 @@ export const getFormData = (rootState) => {
159148
// - publication properties
160149
// - selected medias and browsers
161150
// - created blocks and repeaters
162-
const data = Object.assign(fields, {
151+
return Object.assign(fields, {
163152
cmsSaveType: rootState.form.type,
164153
published: rootState.publication.published,
165154
public: rootState.publication.visibility === 'public',
@@ -172,6 +161,4 @@ export const getFormData = (rootState) => {
172161
blocks: gatherBlocks(rootState),
173162
repeaters: gatherRepeaters(rootState)
174163
})
175-
176-
return data
177164
}

jsconfig.json

+8
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

+24-13
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,22 +273,32 @@ 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+
if (empty($parentBlockFields['blocks'])) {
277+
return $childBlocksList;
278+
}
279+
280+
// Fallback if frontend or revision is still on the old schema
281+
if (is_int(key(current($parentBlockFields['blocks'])))) {
282+
foreach ($parentBlockFields['blocks'] as $childKey => $childBlocks) {
283+
foreach ($childBlocks as $index => $childBlock) {
284+
$childBlock['child_key'] = $childKey;
285+
$parentBlockFields['blocks'][$index] = $childBlock;
286+
}
287+
unset($parentBlockFields['blocks'][$childKey]);
288288
}
289289
}
290290

291+
foreach ($parentBlockFields['blocks'] as $index => $childBlock) {
292+
$childBlock = $this->buildBlock($childBlock, $object, $childBlock['is_repeater'] ?? false);
293+
$this->validateBlockArray($childBlock, $childBlock['instance'], true);
294+
$childBlock['child_key'] = $childBlock['child_key'] ?? Str::afterLast($childBlock['editor_name'], '|');
295+
$childBlock['position'] = $index + 1;
296+
$childBlock['editor_name'] = $parentBlockFields['editor_name'] ?? 'default';
297+
$childBlock['blocks'] = $this->getChildBlocks($object, $childBlock);
298+
299+
$childBlocksList->push($childBlock);
300+
}
301+
291302
return $childBlocksList;
292303
}
293304

0 commit comments

Comments
 (0)