Skip to content

Commit b3e21c7

Browse files
committed
feat: Custom Blocks with Tier-based Limits (v1.4.0-alpha.1)
- Add Custom Blocks feature (Simple + Embedded Entry blocks) - Implement tier-based limits (FREE: 3 Simple, PREMIUM: 10 Total, ADVANCED: Unlimited) - Add Export/Import for Advanced tier - Backend validation with 403 Forbidden on limit exceeded - Modern Admin UI with limits banner, progress bar, upgrade hints - Contentful-style Inline Entries via Embedded Entry blocks - Replaces need for Dynamic Zones in many use cases
1 parent a99f37a commit b3e21c7

File tree

20 files changed

+4991
-26
lines changed

20 files changed

+4991
-26
lines changed

README.md

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,36 @@
77
[![Strapi v5](https://img.shields.io/badge/Strapi-v5-7C3AED.svg)](https://strapi.io)
88
[![Editor.js](https://img.shields.io/badge/Editor.js-2.31.0-000.svg)](https://editorjs.io)
99

10+
> **ALPHA RELEASE v1.4.0-alpha.1** - Custom Blocks with Tier-based Limits now available! Create your own editor blocks without writing code.
11+
1012
---
1113

12-
## 🆕 What's New in v1.2.0
14+
## What's New in v1.4.0-alpha.1
15+
16+
### Custom Blocks (Contentful-style)
17+
18+
Create custom editor blocks directly from the Admin UI - no code required!
19+
20+
- **Simple Blocks** - Text/HTML blocks with configurable fields (text, textarea, select, color, checkbox)
21+
- **Embedded Entry Blocks** - Reference other Strapi content types with preview fields (like Contentful Inline Entries)
22+
- **HTML Templates** - Define custom templates with `{{fieldName}}` placeholders
23+
- **Replaces Dynamic Zones** - Simpler alternative for structured content within the editor
24+
25+
### Tier-Based Limits
26+
27+
| Feature | FREE | PREMIUM | ADVANCED |
28+
|---------|------|---------|----------|
29+
| Simple Blocks | 3 | 10 (combined) | Unlimited |
30+
| Embedded Entry | - | 10 (combined) | Unlimited |
31+
| Export/Import | - | - | Yes |
32+
| API Access | - | - | Yes |
33+
34+
### Previous Updates (v1.2.0)
1335

1436
- **Character-Level Collaboration** - Multiple users can now type in the same paragraph simultaneously without conflicts
1537
- **Webtools Links Integration** - Optional integration with PluginPal's Webtools Links addon for internal/external link management
1638
- **Direct Link Editing** - Click on any link to instantly open the link editor modal
1739
- **Improved Fullscreen Mode** - Blocks now stretch to full width, Media Library modal works correctly
18-
- **Performance Improvements** - Removed debug logging, optimized Y.js sync
1940

2041
---
2142

@@ -314,6 +335,114 @@ Magic Editor X comes with a comprehensive collection of tools, categorized for e
314335
| **Undo/Redo** | History management | `editorjs-undo` |
315336
| **Drag & Drop** | Reorder blocks by dragging | `editorjs-drag-drop` |
316337

338+
### Custom Blocks (User-Defined)
339+
340+
Magic Editor X allows you to create your own custom blocks without writing code! This feature is similar to Contentful's embedded entries and can replace Strapi's Components + Dynamic Zones setup.
341+
342+
#### Creating Custom Blocks via Admin UI
343+
344+
1. Go to **Settings > Magic Editor X > Custom Blocks**
345+
2. Click **Create Block**
346+
3. Fill in the form:
347+
- **Block Name** - Unique identifier (e.g., `productCard`)
348+
- **Display Label** - Shown in the editor toolbox
349+
- **Block Type**:
350+
- **Simple Block** - Text/HTML content with custom fields
351+
- **Embedded Entry** - Links to existing Strapi content
352+
4. Configure additional options (fields, template, icon)
353+
5. Click **Create Block**
354+
355+
The new block appears immediately in the editor toolbox!
356+
357+
#### Block Types
358+
359+
**Simple Blocks:**
360+
Custom text/HTML blocks with configurable fields.
361+
362+
| Option | Description |
363+
|--------|-------------|
364+
| Placeholder | Hint text for empty content |
365+
| Template | Custom HTML with `{{fieldName}}` placeholders |
366+
| Fields | Add custom fields (text, select, color, checkbox) |
367+
| Styles | Custom CSS as JSON object |
368+
369+
**Embedded Entry Blocks:**
370+
Embed existing Strapi content directly in your editor.
371+
372+
| Option | Description |
373+
|--------|-------------|
374+
| Content Type | Target content type (e.g., `api::product.product`) |
375+
| Title Field | Field to display as entry title |
376+
| Display Fields | Fields shown in preview |
377+
378+
#### Creating Custom Blocks via Config (Developers)
379+
380+
For developers who prefer code-based configuration:
381+
382+
```javascript
383+
// config/plugins.js
384+
'magic-editor-x': {
385+
enabled: true,
386+
config: {
387+
customBlocks: [
388+
{
389+
name: 'productCard',
390+
label: 'Product Card',
391+
blockType: 'embedded-entry',
392+
contentType: 'api::product.product',
393+
displayFields: ['title', 'price', 'thumbnail'],
394+
titleField: 'title',
395+
icon: '<svg>...</svg>',
396+
},
397+
{
398+
name: 'callToAction',
399+
label: 'Call to Action',
400+
blockType: 'simple',
401+
placeholder: 'Enter CTA text...',
402+
fields: [
403+
{ name: 'buttonText', label: 'Button Text', type: 'text' },
404+
{ name: 'buttonUrl', label: 'Button URL', type: 'text' },
405+
{ name: 'variant', label: 'Style', type: 'select', options: ['primary', 'secondary'] },
406+
],
407+
template: `
408+
<div class="cta-block">
409+
<p data-field="content">{{content}}</p>
410+
<a href="{{buttonUrl}}" class="btn btn-{{variant}}">{{buttonText}}</a>
411+
</div>
412+
`,
413+
},
414+
],
415+
},
416+
},
417+
```
418+
419+
#### Import/Export Custom Blocks
420+
421+
Custom blocks can be exported and imported between environments:
422+
423+
1. Go to **Settings > Magic Editor X > Custom Blocks**
424+
2. Click **Export** to download a JSON file
425+
3. On another instance, click **Import** and select the file
426+
427+
#### Custom Block Output Format
428+
429+
Custom blocks save their data in the standard Editor.js format:
430+
431+
```json
432+
{
433+
"type": "productCard",
434+
"data": {
435+
"entry": {
436+
"id": 42,
437+
"documentId": "abc123",
438+
"title": "Product Name",
439+
"price": 99.99
440+
},
441+
"contentType": "api::product.product"
442+
}
443+
}
444+
```
445+
317446
### 4. Media Library Integration
318447

319448
Seamless integration with Strapi's built-in Media Library:

admin/src/components/EditorJS/index.jsx

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { useLicense } from '../../hooks/useLicense';
3838
import { useAIActions } from '../../hooks/useAIActions';
3939
import { useWebtoolsLinks } from '../../hooks/useWebtoolsLinks';
4040
import { useVersionHistory } from '../../hooks/useVersionHistory';
41+
import { useCustomBlocks } from '../../hooks/useCustomBlocks';
4142
import AIAssistantPopup from '../AIAssistantPopup';
4243
import VersionHistoryPanel from '../VersionHistoryPanel';
4344
import AIInlineToolbar from '../AIInlineToolbar';
@@ -213,44 +214,62 @@ const EditorJSGlobalStyles = createGlobalStyle`
213214
z-index: 99999 !important;
214215
}
215216
217+
/* ============================================
218+
NESTED POPOVER FIX - Remove min-width constraint
219+
============================================ */
220+
.ce-popover--inline .ce-popover--nested .ce-popover__container,
221+
.ce-popover--inline .ce-popover--nested.ce-popover--nested-level-1 .ce-popover__container {
222+
min-width: 0 !important;
223+
}
224+
216225
/* ============================================
217226
TOOLBOX POPOVER (Plus Button) - CRITICAL FIX
218227
Make sure items are visible and properly displayed
219228
============================================ */
220229
221230
.ce-popover:not(.ce-popover--inline) {
222-
display: block !important;
231+
display: flex !important;
232+
flex-direction: column !important;
223233
visibility: visible !important;
224234
opacity: 1 !important;
225235
z-index: 99999 !important;
226236
}
227237
228238
.ce-popover--opened {
229-
display: block !important;
239+
display: flex !important;
240+
flex-direction: column !important;
230241
visibility: visible !important;
231242
opacity: 1 !important;
232243
}
233244
234245
.ce-popover__container {
235-
display: block !important;
246+
display: flex !important;
247+
flex-direction: column !important;
236248
visibility: visible !important;
237249
opacity: 1 !important;
250+
flex: 1 !important;
251+
overflow: hidden !important;
238252
}
239253
240254
.ce-popover__items {
241255
display: block !important;
242256
visibility: visible !important;
243257
opacity: 1 !important;
244-
max-height: 400px !important;
258+
flex: 1 !important;
245259
overflow-y: auto !important;
246260
}
247261
262+
/* Popover items - don't force display, let EditorJS control visibility for search */
248263
.ce-popover-item {
249-
display: flex !important;
250264
visibility: visible !important;
251265
opacity: 1 !important;
252266
}
253267
268+
/* Only show flex when not hidden by search */
269+
.ce-popover-item:not([hidden]) {
270+
display: flex !important;
271+
}
272+
254273
.ce-popover-item__icon {
255274
display: flex !important;
256275
visibility: visible !important;
@@ -263,16 +282,48 @@ const EditorJSGlobalStyles = createGlobalStyle`
263282
opacity: 1 !important;
264283
}
265284
266-
/* Hide empty/nothing-found message for main toolbox */
285+
/* Nothing found message - EditorJS controls visibility */
267286
.ce-popover:not(.ce-popover--inline) .ce-popover__nothing-found-message {
268-
display: none !important;
287+
padding: 16px !important;
288+
text-align: center !important;
289+
color: #64748b !important;
290+
font-size: 14px !important;
269291
}
270292
271-
/* Make sure search is visible */
293+
/* Search styling - let EditorJS control visibility */
272294
.ce-popover__search {
273-
display: block !important;
274-
visibility: visible !important;
275-
opacity: 1 !important;
295+
padding: 12px !important;
296+
margin: 0 !important;
297+
border-bottom: 1px solid #e2e8f0 !important;
298+
background: white !important;
299+
}
300+
301+
/* Hide the search icon */
302+
.ce-popover__search-icon {
303+
display: none !important;
304+
}
305+
306+
.ce-popover__search-input {
307+
width: 100% !important;
308+
padding: 10px 14px !important;
309+
border: 1px solid #e2e8f0 !important;
310+
border-radius: 10px !important;
311+
background: #f8fafc !important;
312+
font-size: 14px !important;
313+
color: #334155 !important;
314+
outline: none !important;
315+
transition: all 0.15s ease !important;
316+
box-sizing: border-box !important;
317+
}
318+
319+
.ce-popover__search-input:focus {
320+
border-color: #7C3AED !important;
321+
background: white !important;
322+
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1) !important;
323+
}
324+
325+
.ce-popover__search-input::placeholder {
326+
color: #94a3b8 !important;
276327
}
277328
278329
/* ============================================
@@ -2082,6 +2133,9 @@ const Editor = forwardRef(({
20822133
// Webtools Link Picker integration (optional)
20832134
const { isAvailable: isWebtoolsAvailable, openLinkPicker: webtoolsOpenLinkPicker } = useWebtoolsLinks();
20842135

2136+
// Custom Blocks (user-defined blocks from database)
2137+
const { customBlocks, isLoading: isLoadingCustomBlocks } = useCustomBlocks();
2138+
20852139
// Refs - must be defined before useAIActions
20862140
const editorRef = useRef(null);
20872141
const editorInstanceRef = useRef(null);
@@ -3358,12 +3412,21 @@ const Editor = forwardRef(({
33583412

33593413
// Initialize Editor.js
33603414
useEffect(() => {
3415+
// Wait for custom blocks to finish loading before initializing
3416+
if (isLoadingCustomBlocks) {
3417+
console.log('[Magic Editor X] Waiting for custom blocks to load...');
3418+
return;
3419+
}
3420+
33613421
if (editorRef.current && !editorInstanceRef.current) {
33623422
const tools = getTools({
33633423
mediaLibToggleFunc,
33643424
pluginId: PLUGIN_ID,
33653425
openLinkPicker: isWebtoolsAvailable ? webtoolsOpenLinkPicker : null,
3426+
customBlocks: customBlocks || [],
33663427
});
3428+
3429+
console.log('[Magic Editor X] Custom blocks loaded:', customBlocks?.length || 0);
33673430

33683431
let initialData = undefined;
33693432
if (value) {
@@ -3610,7 +3673,8 @@ const Editor = forwardRef(({
36103673
}
36113674
document.body.classList.remove('editor-fullscreen');
36123675
};
3613-
}, []);
3676+
// eslint-disable-next-line react-hooks/exhaustive-deps
3677+
}, [isLoadingCustomBlocks, customBlocks]);
36143678

36153679
// Dynamically toggle readOnly when collaboration role changes (viewer can't edit)
36163680
useEffect(() => {

0 commit comments

Comments
 (0)