WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit aeb156f

Browse files
djayclaude
andcommitted
Add unified hierarchical sidebar for container block navigation
Implements PLIP 6569: Replace tabbed sidebar with unified hierarchical view. - Page metadata always at top - Parent blocks collapsible with sticky headers - Current block settings highlighted - Child blocks widget at bottom for containers New components: - ParentBlocksWidget: Renders parent chain with navigation - ChildBlocksWidget: Shows child blocks for containers - Sidebar customization: Unified layout without tabs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 341c0f2 commit aeb156f

File tree

4 files changed

+832
-0
lines changed

4 files changed

+832
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/**
2+
* ChildBlocksWidget - Shows child blocks for each container field in the current block.
3+
* A block can have multiple `type: 'blocks'` fields, each rendered as a separate section.
4+
*
5+
* Example: A block with both 'slides' and 'footnotes' fields would show:
6+
* Slides [+]
7+
* ⋮⋮ Slide 1 >
8+
* ⋮⋮ Slide 2 >
9+
*
10+
* Footnotes [+]
11+
* ⋮⋮ Note 1 >
12+
* ⋮⋮ Note 2 >
13+
*/
14+
15+
import React from 'react';
16+
import PropTypes from 'prop-types';
17+
import { createPortal } from 'react-dom';
18+
import { defineMessages, useIntl } from 'react-intl';
19+
import config from '@plone/volto/registry';
20+
21+
const messages = defineMessages({
22+
blocks: {
23+
id: 'Blocks',
24+
defaultMessage: 'Blocks',
25+
},
26+
addBlock: {
27+
id: 'Add block',
28+
defaultMessage: 'Add block',
29+
},
30+
});
31+
32+
/**
33+
* Get all container fields (type: 'blocks') from a block's schema
34+
*/
35+
const getContainerFields = (blockType) => {
36+
const blockConfig = config.blocks?.blocksConfig?.[blockType];
37+
const schema = blockConfig?.blockSchema;
38+
if (!schema?.properties) return [];
39+
40+
const containerFields = [];
41+
for (const [fieldName, fieldDef] of Object.entries(schema.properties)) {
42+
if (fieldDef.type === 'blocks') {
43+
containerFields.push({
44+
name: fieldName,
45+
title: fieldDef.title || fieldName,
46+
allowedBlocks: fieldDef.allowedBlocks,
47+
maxLength: fieldDef.maxLength,
48+
});
49+
}
50+
}
51+
return containerFields;
52+
};
53+
54+
/**
55+
* Get child blocks for a specific container field
56+
*/
57+
const getChildBlocks = (blockData, fieldName, formData) => {
58+
if (!blockData || !blockData[fieldName]) return [];
59+
60+
const layoutField = `${fieldName}_layout`;
61+
const items = blockData[layoutField]?.items || [];
62+
const blocksData = blockData[fieldName] || {};
63+
64+
return items.map((blockId) => {
65+
const childBlock = blocksData[blockId];
66+
const blockType = childBlock?.['@type'] || 'unknown';
67+
const blockConfig = config.blocks?.blocksConfig?.[blockType];
68+
const title = blockConfig?.title || blockType;
69+
70+
return {
71+
id: blockId,
72+
type: blockType,
73+
title: title,
74+
data: childBlock,
75+
};
76+
});
77+
};
78+
79+
/**
80+
* Get the display title for a block
81+
*/
82+
const getBlockTitle = (blockData) => {
83+
const blockType = blockData?.['@type'];
84+
if (!blockType) return 'Block';
85+
86+
const blockConfig = config.blocks?.blocksConfig?.[blockType];
87+
return blockConfig?.title || blockType;
88+
};
89+
90+
/**
91+
* Single container field section
92+
*/
93+
const ContainerFieldSection = ({
94+
fieldName,
95+
fieldTitle,
96+
childBlocks,
97+
allowedBlocks,
98+
maxLength,
99+
onSelectBlock,
100+
onAddBlock,
101+
parentBlockId,
102+
}) => {
103+
const intl = useIntl();
104+
const canAdd = !maxLength || childBlocks.length < maxLength;
105+
106+
return (
107+
<div className="container-field-section">
108+
<div className="widget-header">
109+
<span className="widget-title">{fieldTitle}</span>
110+
<div className="widget-actions">
111+
{canAdd && (
112+
<button
113+
onClick={() => onAddBlock(parentBlockId, fieldName)}
114+
title={intl.formatMessage(messages.addBlock)}
115+
aria-label={intl.formatMessage(messages.addBlock)}
116+
>
117+
+
118+
</button>
119+
)}
120+
</div>
121+
</div>
122+
<div className="child-blocks-list">
123+
{childBlocks.map((child) => (
124+
<div
125+
key={child.id}
126+
className="child-block-item"
127+
onClick={() => onSelectBlock(child.id)}
128+
role="button"
129+
tabIndex={0}
130+
onKeyDown={(e) => {
131+
if (e.key === 'Enter' || e.key === ' ') {
132+
onSelectBlock(child.id);
133+
}
134+
}}
135+
>
136+
<span className="drag-handle">⋮⋮</span>
137+
<span className="block-type">{child.title}</span>
138+
<span className="nav-arrow"></span>
139+
</div>
140+
))}
141+
{childBlocks.length === 0 && (
142+
<div className="empty-container-message">
143+
No blocks yet
144+
</div>
145+
)}
146+
</div>
147+
</div>
148+
);
149+
};
150+
151+
/**
152+
* ChildBlocksWidget - Main component
153+
* Renders all container fields for the current block
154+
*/
155+
const ChildBlocksWidget = ({
156+
selectedBlock,
157+
formData,
158+
blockPathMap,
159+
onSelectBlock,
160+
onAddBlock,
161+
}) => {
162+
const intl = useIntl();
163+
const [isClient, setIsClient] = React.useState(false);
164+
165+
React.useEffect(() => {
166+
setIsClient(true);
167+
}, []);
168+
169+
// Get the selected block's data
170+
const getBlockData = (blockId) => {
171+
if (!blockId || !formData) return null;
172+
173+
// Check blockPathMap for nested blocks
174+
const pathInfo = blockPathMap?.[blockId];
175+
if (pathInfo?.path) {
176+
let current = formData;
177+
for (const key of pathInfo.path) {
178+
if (current && typeof current === 'object') {
179+
current = current[key];
180+
} else {
181+
return null;
182+
}
183+
}
184+
return current;
185+
}
186+
187+
// Fall back to top-level blocks
188+
return formData.blocks?.[blockId];
189+
};
190+
191+
// If no block selected, show page-level blocks
192+
if (!selectedBlock) {
193+
const pageBlocks = formData?.blocks_layout?.items || [];
194+
const blocksData = formData?.blocks || {};
195+
196+
const childBlocks = pageBlocks.map((blockId) => {
197+
const blockData = blocksData[blockId];
198+
return {
199+
id: blockId,
200+
type: blockData?.['@type'] || 'unknown',
201+
title: getBlockTitle(blockData),
202+
data: blockData,
203+
};
204+
});
205+
206+
if (!isClient) return null;
207+
208+
// Note: Uses sidebar-order for backwards compatibility with tests
209+
const target = document.getElementById('sidebar-order');
210+
if (!target) return null;
211+
212+
return createPortal(
213+
<div className="child-blocks-widget">
214+
<ContainerFieldSection
215+
fieldName="blocks"
216+
fieldTitle={intl.formatMessage(messages.blocks)}
217+
childBlocks={childBlocks}
218+
onSelectBlock={onSelectBlock}
219+
onAddBlock={onAddBlock}
220+
parentBlockId={null}
221+
/>
222+
</div>,
223+
target,
224+
);
225+
}
226+
227+
// Get selected block data and find container fields
228+
const blockData = getBlockData(selectedBlock);
229+
if (!blockData) return null;
230+
231+
const blockType = blockData['@type'];
232+
const containerFields = getContainerFields(blockType);
233+
234+
// If no container fields, don't render anything
235+
if (containerFields.length === 0) return null;
236+
237+
if (!isClient) return null;
238+
239+
// Note: Uses sidebar-order for backwards compatibility with tests
240+
const target = document.getElementById('sidebar-order');
241+
if (!target) return null;
242+
243+
return createPortal(
244+
<div className="child-blocks-widget">
245+
{containerFields.map((field) => {
246+
const childBlocks = getChildBlocks(blockData, field.name, formData);
247+
return (
248+
<ContainerFieldSection
249+
key={field.name}
250+
fieldName={field.name}
251+
fieldTitle={field.title}
252+
childBlocks={childBlocks}
253+
allowedBlocks={field.allowedBlocks}
254+
maxLength={field.maxLength}
255+
onSelectBlock={onSelectBlock}
256+
onAddBlock={onAddBlock}
257+
parentBlockId={selectedBlock}
258+
/>
259+
);
260+
})}
261+
</div>,
262+
target,
263+
);
264+
};
265+
266+
ChildBlocksWidget.propTypes = {
267+
selectedBlock: PropTypes.string,
268+
formData: PropTypes.object,
269+
blockPathMap: PropTypes.object,
270+
onSelectBlock: PropTypes.func,
271+
onAddBlock: PropTypes.func,
272+
};
273+
274+
export default ChildBlocksWidget;

0 commit comments

Comments
 (0)