@nextbridgehq/payload-block-builder 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -39
- package/dist/bin/init.js +79 -25
- package/dist/client.cjs +473 -302
- package/dist/client.d.cts +5 -1
- package/dist/client.d.ts +5 -1
- package/dist/client.js +308 -124
- package/dist/index.cjs +45 -15
- package/dist/index.js +45 -15
- package/package.json +2 -2
- package/src/components/BlockDataField/BlockDataField.css +170 -0
package/README.md
CHANGED
|
@@ -1,38 +1,84 @@
|
|
|
1
1
|
# Payload Block Builder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@nextbridgehq/payload-block-builder)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://payloadcms.com)
|
|
6
|
+
|
|
7
|
+
> A visual block builder plugin for Payload CMS v3. Design content blocks through a drag-and-drop UI, store schemas in your database, and let editors build pages without waiting on a developer.
|
|
8
|
+
|
|
9
|
+
Developed and open-sourced by [Nextbridge](https://nextbridge.com).
|
|
10
|
+
|
|
11
|
+
## Screenshots
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
4
16
|
|
|
5
17
|
---
|
|
6
18
|
|
|
7
|
-
|
|
19
|
+
## 🎯 The Problem
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
Content editors need to manage flexible page layouts — but every new block type or layout change requires a developer to update code, redeploy, and migrate. This creates bottlenecks, slows down marketing teams, and turns simple content tasks into engineering tickets.
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
- **Multi-tenant platforms:** Each tenant can have its own block definitions without touching shared config or triggering redeployments.
|
|
13
|
-
- **Marketing teams:** Give marketing full control to create, update, and reorder blocks on any page, any time.
|
|
14
|
-
- **Evolving content schemas:** Roll out new block versions without breaking content that was built against older ones.
|
|
15
|
-
- **Headless frontends:** Fetch structured block data from the Payload API and render it with any framework.
|
|
23
|
+
## 💡 The Solution
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
Payload Block Builder moves block schema definitions from code into your database. Editors design blocks visually, publish them instantly, and use them across any collection — all without touching code or triggering deployments.
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ✨ Features
|
|
30
|
+
|
|
31
|
+
- **Visual drag-and-drop block designer** — No code required to create new block types
|
|
32
|
+
- **Database-stored schemas** — Block definitions live in your DB, not your codebase
|
|
33
|
+
- **Version snapshots** — Every publish creates an immutable version; existing content never breaks
|
|
34
|
+
- **Collection integration** — Adds a "DB Layout" tab to any collection with one line of config
|
|
35
|
+
- **13 field types** — text, textarea, number, email, date, checkbox, select, radio, upload, relationship, json, and more
|
|
36
|
+
- **Multi-tenant ready** — Each tenant can have its own block definitions without shared config changes
|
|
37
|
+
- **Framework agnostic frontend** — Fetch structured JSON and render with React, Vue, Svelte, or anything else
|
|
38
|
+
- **Automatic init command** — Get up and running in under 2 minutes
|
|
39
|
+
- **Works with all Payload databases** — PostgreSQL, SQLite, MongoDB — no database-specific code
|
|
40
|
+
|
|
41
|
+
---
|
|
20
42
|
|
|
21
|
-
|
|
43
|
+
## 📋 Compatibility
|
|
44
|
+
|
|
45
|
+
| Requirement | Version |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Payload CMS | v3.x |
|
|
48
|
+
| Node.js | ≥ 18 |
|
|
49
|
+
| Next.js | ≥ 14 |
|
|
50
|
+
|
|
51
|
+
### Database Support
|
|
52
|
+
|
|
53
|
+
| Database | Adapter |
|
|
54
|
+
|---|---|
|
|
55
|
+
| PostgreSQL / Supabase / Neon | `@payloadcms/db-postgres` |
|
|
56
|
+
| SQLite / Turso / LibSQL | `@payloadcms/db-sqlite` |
|
|
57
|
+
| MongoDB | `@payloadcms/db-mongodb` |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 🚀 Quick Start
|
|
62
|
+
|
|
63
|
+
### Option A — Automatic Setup (Recommended)
|
|
22
64
|
|
|
23
65
|
```bash
|
|
66
|
+
# Install
|
|
24
67
|
pnpm add @nextbridgehq/payload-block-builder
|
|
25
68
|
# or: npm install @nextbridgehq/payload-block-builder
|
|
26
69
|
|
|
70
|
+
# Initialize
|
|
27
71
|
npx payload-block-builder init
|
|
28
72
|
```
|
|
29
73
|
|
|
30
74
|
The init command automatically:
|
|
31
75
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
76
|
+
| What it does | File |
|
|
77
|
+
|---|---|
|
|
78
|
+
| Creates the builder UI page | `src/app/block-builder/page.tsx` |
|
|
79
|
+
| Creates a standalone layout | `src/app/block-builder/layout.tsx` |
|
|
80
|
+
| Injects admin field styles | `src/app/(payload)/custom.scss` |
|
|
81
|
+
| Adds plugin config | `payload.config.ts` |
|
|
36
82
|
|
|
37
83
|
Then regenerate the import map and start your dev server:
|
|
38
84
|
|
|
@@ -41,9 +87,10 @@ pnpm generate:importmap
|
|
|
41
87
|
pnpm dev
|
|
42
88
|
```
|
|
43
89
|
|
|
44
|
-
Visit `
|
|
90
|
+
Visit `https://your-domain.com/block-builder` and you're ready to build.
|
|
45
91
|
|
|
46
|
-
> **PostgreSQL users:** Payload will automatically push
|
|
92
|
+
> **PostgreSQL users:** Payload will automatically push new schema tables on first startup in dev mode. For production migrations:
|
|
93
|
+
>
|
|
47
94
|
> ```bash
|
|
48
95
|
> pnpm payload migrate:create --name=add_block_builder
|
|
49
96
|
> pnpm payload migrate
|
|
@@ -51,7 +98,8 @@ Visit `http://localhost:3000/block-builder` and you're ready to build.
|
|
|
51
98
|
|
|
52
99
|
---
|
|
53
100
|
|
|
54
|
-
|
|
101
|
+
<details>
|
|
102
|
+
<summary><strong>Option B — Manual Setup</strong></summary>
|
|
55
103
|
|
|
56
104
|
**1. Install:**
|
|
57
105
|
|
|
@@ -77,11 +125,19 @@ export default buildConfig({
|
|
|
77
125
|
|
|
78
126
|
```tsx
|
|
79
127
|
import React from 'react'
|
|
128
|
+
import { headers } from 'next/headers'
|
|
129
|
+
import { redirect } from 'next/navigation'
|
|
130
|
+
import { getPayload } from 'payload'
|
|
131
|
+
import config from '@payload-config'
|
|
80
132
|
import '@nextbridgehq/payload-block-builder/builder.css'
|
|
81
133
|
|
|
82
134
|
export const metadata = { title: 'Block Builder' }
|
|
83
135
|
|
|
84
|
-
export default function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
|
|
136
|
+
export default async function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
|
|
137
|
+
const payload = await getPayload({ config })
|
|
138
|
+
const { user } = await payload.auth({ headers: await headers() })
|
|
139
|
+
if (!user) redirect('/admin/login')
|
|
140
|
+
|
|
85
141
|
return (
|
|
86
142
|
<html lang="en">
|
|
87
143
|
<body style={{ margin: 0, padding: 0, height: '100vh', overflow: 'hidden' }}>
|
|
@@ -118,19 +174,28 @@ pnpm generate:importmap
|
|
|
118
174
|
pnpm dev
|
|
119
175
|
```
|
|
120
176
|
|
|
177
|
+
</details>
|
|
178
|
+
|
|
121
179
|
---
|
|
122
180
|
|
|
123
|
-
##
|
|
181
|
+
## ⚙️ Configuration Options
|
|
124
182
|
|
|
125
183
|
```ts
|
|
126
184
|
dynamicBlocksPlugin({
|
|
127
|
-
enabled?: boolean
|
|
128
|
-
collections?: string[]
|
|
129
|
-
fieldName?: string
|
|
130
|
-
tabLabel?: string
|
|
185
|
+
enabled?: boolean, // Disable without removing. Default: true
|
|
186
|
+
collections?: string[], // Collection slugs that get the DB Layout tab. Default: []
|
|
187
|
+
fieldName?: string, // Name of the layout array field. Default: 'dbLayout'
|
|
188
|
+
tabLabel?: string, // Label shown on the tab in the admin UI. Default: 'DB Layout'
|
|
131
189
|
})
|
|
132
190
|
```
|
|
133
191
|
|
|
192
|
+
| Option | Type | Default | Description |
|
|
193
|
+
|---|---|---|---|
|
|
194
|
+
| `enabled` | `boolean` | `true` | Toggle the plugin on/off without removing it from config |
|
|
195
|
+
| `collections` | `string[]` | `[]` | Collection slugs that receive the DB Layout tab |
|
|
196
|
+
| `fieldName` | `string` | `'dbLayout'` | The field name for the layout array stored on documents |
|
|
197
|
+
| `tabLabel` | `string` | `'DB Layout'` | Label displayed on the tab in the Payload admin UI |
|
|
198
|
+
|
|
134
199
|
The `--collections` flag is also supported in the init command:
|
|
135
200
|
|
|
136
201
|
```bash
|
|
@@ -139,17 +204,17 @@ npx payload-block-builder init --collections=pages,posts
|
|
|
139
204
|
|
|
140
205
|
---
|
|
141
206
|
|
|
142
|
-
## Usage
|
|
207
|
+
## 📖 Usage
|
|
143
208
|
|
|
144
|
-
### Creating a
|
|
209
|
+
### Creating a Block
|
|
145
210
|
|
|
146
211
|
1. Open `/block-builder` in your browser.
|
|
147
212
|
2. Click "Add Block" and give it a name and slug.
|
|
148
213
|
3. Drag fields from the panel on the right onto the canvas.
|
|
149
214
|
4. Configure each field (label, name, required, options, etc.).
|
|
150
|
-
5. Click Publish
|
|
215
|
+
5. Click Publish — the block schema is saved to your database and a version snapshot is created.
|
|
151
216
|
|
|
152
|
-
### Using
|
|
217
|
+
### Using Blocks in a Collection
|
|
153
218
|
|
|
154
219
|
Any collection listed in the `collections` option gets a new "DB Layout" tab in the Payload admin. Editors can:
|
|
155
220
|
|
|
@@ -159,33 +224,179 @@ Any collection listed in the `collections` option gets a new "DB Layout" tab in
|
|
|
159
224
|
4. Reorder, hide, or add anchor IDs to individual block instances.
|
|
160
225
|
5. Save the document as normal.
|
|
161
226
|
|
|
162
|
-
### Reading
|
|
227
|
+
### Reading Block Data on the Frontend
|
|
163
228
|
|
|
164
229
|
```ts
|
|
165
230
|
const res = await fetch('/api/pages/my-page?depth=2')
|
|
166
231
|
const page = await res.json()
|
|
167
232
|
|
|
168
233
|
for (const block of page.dbLayout) {
|
|
169
|
-
const type = block.blockDefinition.slug
|
|
170
|
-
const fields = block.data
|
|
234
|
+
const type = block.blockDefinition.slug // e.g. "hero"
|
|
235
|
+
const fields = block.data // { heading: '...', image: '...', ... }
|
|
171
236
|
const isHidden = block.hidden
|
|
237
|
+
const anchor = block.anchorId
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Example: React Component Map
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
const blockComponents = {
|
|
245
|
+
hero: HeroBlock,
|
|
246
|
+
features: FeaturesBlock,
|
|
247
|
+
testimonials: TestimonialsBlock,
|
|
248
|
+
cta: CTABlock,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function PageRenderer({ blocks }) {
|
|
252
|
+
return (
|
|
253
|
+
<>
|
|
254
|
+
{blocks
|
|
255
|
+
.filter((block) => !block.hidden)
|
|
256
|
+
.map((block, i) => {
|
|
257
|
+
const Component = blockComponents[block.blockDefinition.slug]
|
|
258
|
+
if (!Component) return null
|
|
259
|
+
return (
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
)
|
|
264
|
+
})}
|
|
265
|
+
</>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
> **Note:** The inner JSX of the `return (` in the React Component Map example is intentionally left blank in this snippet — fill in with your `<section>` / `<Component>` rendering as appropriate for your app.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 🧩 Supported Field Types
|
|
275
|
+
|
|
276
|
+
| Type | Description | Admin UI |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| `text` | Single-line text input | Standard text field |
|
|
279
|
+
| `textarea` | Multi-line text input | Expandable textarea |
|
|
280
|
+
| `number` | Numeric input | Number field with validation |
|
|
281
|
+
| `email` | Email address | Email field with validation |
|
|
282
|
+
| `date` | Date picker | Calendar date picker |
|
|
283
|
+
| `checkbox` | Boolean toggle | Checkbox input |
|
|
284
|
+
| `select` | Dropdown with custom options | Select dropdown |
|
|
285
|
+
| `radio` | Radio button group | Radio buttons |
|
|
286
|
+
| `upload` | File/image picker | Media library picker |
|
|
287
|
+
| `relationship` | Document picker from any collection | Relationship field |
|
|
288
|
+
| `json` | Raw JSON data | JSON editor |
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 🏗️ Architecture
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
┌─────────────────────────────────────────────────────┐
|
|
296
|
+
│ Block Builder UI │
|
|
297
|
+
│ /block-builder (drag & drop) │
|
|
298
|
+
└──────────────────────────┬──────────────────────────┘
|
|
299
|
+
│ Publish
|
|
300
|
+
▼
|
|
301
|
+
┌─────────────────────────────────────────────────────┐
|
|
302
|
+
│ block-definitions collection │
|
|
303
|
+
│ (name, slug, field definitions) │
|
|
304
|
+
└──────────────────────────┬──────────────────────────┘
|
|
305
|
+
│ Snapshot
|
|
306
|
+
▼
|
|
307
|
+
┌─────────────────────────────────────────────────────┐
|
|
308
|
+
│ block-definition-versions collection │
|
|
309
|
+
│ (immutable schema snapshots per publish) │
|
|
310
|
+
└──────────────────────────┬──────────────────────────┘
|
|
311
|
+
│ Referenced by
|
|
312
|
+
▼
|
|
313
|
+
┌─────────────────────────────────────────────────────┐
|
|
314
|
+
│ Your Collection (e.g. "pages") │
|
|
315
|
+
│ dbLayout: [{ blockDefinition, version, data }] │
|
|
316
|
+
└─────────────────────────────────────────────────────┘
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Key design decisions:
|
|
320
|
+
|
|
321
|
+
- Block definitions are stored in a `block-definitions` collection. Each document is a named block type with a slug and a list of field definitions.
|
|
322
|
+
- Versions are stored in a `block-definition-versions` collection. Every publish creates an immutable snapshot.
|
|
323
|
+
- Documents in opted-in collections store a reference to the exact block version they were built against — updating a block schema later does not break existing content.
|
|
324
|
+
- The DB Layout tab is injected automatically into each collection you list. It renders a dynamic array field where editors pick a block and version.
|
|
325
|
+
- Four internal API endpoints power the builder UI and admin field components. You do not need to call them directly.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 🔧 Advanced Usage
|
|
330
|
+
|
|
331
|
+
### Using `dbLayoutField` Directly
|
|
332
|
+
|
|
333
|
+
If you prefer not to use the plugin's `collections` option, you can add the layout tab manually to any collection:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { dbLayoutField } from '@nextbridgehq/payload-block-builder'
|
|
337
|
+
|
|
338
|
+
export const Pages: CollectionConfig = {
|
|
339
|
+
slug: 'pages',
|
|
340
|
+
fields: [
|
|
341
|
+
{
|
|
342
|
+
type: 'tabs',
|
|
343
|
+
tabs: [
|
|
344
|
+
{ label: 'Content', fields: [/* your fields */] },
|
|
345
|
+
dbLayoutField(), // fieldName='dbLayout', tab label='DB Layout'
|
|
346
|
+
dbLayoutField('heroBlocks', 'Hero'), // custom field name and tab label
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
],
|
|
172
350
|
}
|
|
173
351
|
```
|
|
174
352
|
|
|
175
|
-
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 📦 CSS Imports Reference
|
|
356
|
+
|
|
357
|
+
| Import path | Purpose |
|
|
358
|
+
|---|---|
|
|
359
|
+
| `@nextbridgehq/payload-block-builder/builder.css` | Block Builder UI page styles |
|
|
360
|
+
| `@nextbridgehq/payload-block-builder/block-data-field.css` | DB Layout field styles in admin |
|
|
361
|
+
| `@nextbridgehq/payload-block-builder/schema-builder-field.css` | Schema Builder field styles in admin |
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 🗺️ Use Cases
|
|
366
|
+
|
|
367
|
+
| Use Case | How It Helps |
|
|
368
|
+
|---|---|
|
|
369
|
+
| Dynamic landing pages | Editors compose pages from a library of blocks (hero, features, testimonials, CTA) without code changes |
|
|
370
|
+
| Multi-tenant platforms | Each tenant gets its own block definitions without touching shared config or triggering redeployments |
|
|
371
|
+
| Marketing teams | Full control to create, update, and reorder blocks on any page, any time |
|
|
372
|
+
| Evolving content schemas | Roll out new block versions without breaking content built against older ones |
|
|
373
|
+
| Headless frontends | Fetch structured block data from the Payload API and render with any framework |
|
|
176
374
|
|
|
177
375
|
---
|
|
178
376
|
|
|
179
|
-
##
|
|
377
|
+
## 🤝 Contributing
|
|
180
378
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
379
|
+
Contributions are welcome! Please see our Contributing Guide for details.
|
|
380
|
+
|
|
381
|
+
- Fork the repository
|
|
382
|
+
- Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
383
|
+
- Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
384
|
+
- Push to the branch (`git push origin feature/amazing-feature`)
|
|
385
|
+
- Open a Pull Request
|
|
186
386
|
|
|
187
387
|
---
|
|
188
388
|
|
|
189
|
-
## License
|
|
389
|
+
## 📄 License
|
|
190
390
|
|
|
191
391
|
MIT © [Nextbridge](https://nextbridge.com)
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## 🔗 Links
|
|
396
|
+
|
|
397
|
+
- [npm Package](https://www.npmjs.com/package/@nextbridgehq/payload-block-builder)
|
|
398
|
+
- [GitHub Repository](https://github.com/nextbridgehq/block-builder)
|
|
399
|
+
- [Report a Bug](https://github.com/nextbridgehq/block-builder/issues)
|
|
400
|
+
- [Payload CMS](https://payloadcms.com)
|
|
401
|
+
|
|
402
|
+
Built with ❤️ by Nextbridge
|
package/dist/bin/init.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/bin/init.ts
|
|
5
4
|
import fs from "fs";
|
|
@@ -13,13 +12,21 @@ export default function BlockBuilderPage() {
|
|
|
13
12
|
}
|
|
14
13
|
`;
|
|
15
14
|
var LAYOUT_CONTENT = `import React from 'react'
|
|
15
|
+
import { headers } from 'next/headers'
|
|
16
|
+
import { redirect } from 'next/navigation'
|
|
17
|
+
import { getPayload } from 'payload'
|
|
18
|
+
import config from '@payload-config'
|
|
16
19
|
import '@nextbridgehq/payload-block-builder/builder.css'
|
|
17
20
|
|
|
18
21
|
export const metadata = {
|
|
19
22
|
title: 'Block Builder',
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
export default function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
|
|
25
|
+
export default async function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
|
|
26
|
+
const payload = await getPayload({ config })
|
|
27
|
+
const { user } = await payload.auth({ headers: await headers() })
|
|
28
|
+
if (!user) redirect('/admin/login')
|
|
29
|
+
|
|
23
30
|
return (
|
|
24
31
|
<html lang="en">
|
|
25
32
|
<body style={{ margin: 0, padding: 0, height: '100vh', overflow: 'hidden' }}>
|
|
@@ -76,7 +83,7 @@ function addImport(content) {
|
|
|
76
83
|
while ((m = lastFromRegex.exec(content)) !== null) lastMatch = m;
|
|
77
84
|
if (!lastMatch) return newImport + "\n" + content;
|
|
78
85
|
const insertPos = lastMatch.index + lastMatch[0].length;
|
|
79
|
-
return content.slice(0, insertPos) + "\n" + newImport + content.slice(insertPos);
|
|
86
|
+
return content.slice(0, insertPos) + "\n" + newImport + "\n" + content.slice(insertPos);
|
|
80
87
|
}
|
|
81
88
|
function insertIntoPluginsArray(content, collectionsArg) {
|
|
82
89
|
const pluginsMatch = /\bplugins\s*:\s*\[/.exec(content);
|
|
@@ -84,13 +91,19 @@ function insertIntoPluginsArray(content, collectionsArg) {
|
|
|
84
91
|
const openPos = content.indexOf("[", pluginsMatch.index);
|
|
85
92
|
const closePos = findClosingBracket(content, openPos);
|
|
86
93
|
if (closePos === -1) return null;
|
|
94
|
+
const beforePlugins = content.slice(0, pluginsMatch.index);
|
|
95
|
+
const pluginsLineStart = beforePlugins.lastIndexOf("\n") + 1;
|
|
96
|
+
const outerIndent = content.slice(pluginsLineStart, pluginsMatch.index).match(/^([ \t]*)/)?.[1] ?? " ";
|
|
97
|
+
const entryIndent = outerIndent + " ";
|
|
87
98
|
const beforeClose = content.slice(0, closePos);
|
|
88
99
|
const prevNL = beforeClose.lastIndexOf("\n");
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
const isSingleLine = prevNL < openPos;
|
|
101
|
+
const newEntry = `${entryIndent}dynamicBlocksPlugin({ collections: [${collectionsArg}] }),`;
|
|
102
|
+
if (isSingleLine) {
|
|
103
|
+
return content.slice(0, openPos + 1) + "\n" + newEntry + "\n" + outerIndent + content.slice(closePos);
|
|
104
|
+
} else {
|
|
105
|
+
return content.slice(0, prevNL + 1) + newEntry + "\n" + content.slice(prevNL + 1);
|
|
106
|
+
}
|
|
94
107
|
}
|
|
95
108
|
function injectPluginsBlock(content, collectionsArg) {
|
|
96
109
|
const collMatch = /\bcollections\s*:\s*\[/.exec(content);
|
|
@@ -110,7 +123,13 @@ ${outerIndent}],`;
|
|
|
110
123
|
return content.slice(0, afterCollLine) + "\n" + pluginsBlock + content.slice(afterCollLine);
|
|
111
124
|
}
|
|
112
125
|
function modifyPayloadConfig(configPath, collectionsArg) {
|
|
113
|
-
let content
|
|
126
|
+
let content;
|
|
127
|
+
try {
|
|
128
|
+
content = fs.readFileSync(configPath, "utf8");
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`Error: Could not read ${configPath}: ${err.message}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
114
133
|
if (content.includes("dynamicBlocksPlugin")) {
|
|
115
134
|
console.log(`Skipped: dynamicBlocksPlugin already present in ${configPath}`);
|
|
116
135
|
return;
|
|
@@ -119,8 +138,16 @@ function modifyPayloadConfig(configPath, collectionsArg) {
|
|
|
119
138
|
const noComments = content.replace(/\/\/[^\n]*/g, "");
|
|
120
139
|
const hasPluginsArray = /\bplugins\s*:\s*\[/.test(noComments);
|
|
121
140
|
const hasPluginsShorthand = /^\s*plugins\s*,/m.test(noComments);
|
|
141
|
+
function writeConfig(data) {
|
|
142
|
+
try {
|
|
143
|
+
fs.writeFileSync(configPath, data, "utf8");
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(`Error: Could not write ${configPath}: ${err.message}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
122
149
|
if (hasPluginsShorthand && !hasPluginsArray) {
|
|
123
|
-
|
|
150
|
+
writeConfig(content);
|
|
124
151
|
console.log(`Updated: ${configPath} (added import)`);
|
|
125
152
|
console.log(` Note: 'plugins' is imported from another file.`);
|
|
126
153
|
console.log(` Add dynamicBlocksPlugin({ collections: ['pages'] }) to that file manually.`);
|
|
@@ -133,13 +160,13 @@ function modifyPayloadConfig(configPath, collectionsArg) {
|
|
|
133
160
|
result = injectPluginsBlock(content, collectionsArg);
|
|
134
161
|
}
|
|
135
162
|
if (result === null) {
|
|
136
|
-
|
|
163
|
+
writeConfig(content);
|
|
137
164
|
console.log(`Updated: ${configPath} (added import only)`);
|
|
138
165
|
console.log(` Could not auto-detect plugins array. Add manually:`);
|
|
139
166
|
console.log(` plugins: [ dynamicBlocksPlugin({ collections: [${collectionsArg}] }) ]`);
|
|
140
167
|
return;
|
|
141
168
|
}
|
|
142
|
-
|
|
169
|
+
writeConfig(result);
|
|
143
170
|
console.log(`Updated: ${configPath} (added dynamicBlocksPlugin)`);
|
|
144
171
|
}
|
|
145
172
|
function printNextSteps(dbAdapter) {
|
|
@@ -165,7 +192,14 @@ function printNextSteps(dbAdapter) {
|
|
|
165
192
|
function main() {
|
|
166
193
|
const args = process.argv.slice(2);
|
|
167
194
|
const collectionsFlag = args.find((a) => a.startsWith("--collections="));
|
|
168
|
-
const
|
|
195
|
+
const rawCollections = collectionsFlag ? collectionsFlag.replace("--collections=", "").split(",").map((s) => s.trim()) : ["pages"];
|
|
196
|
+
const invalidSlugs = rawCollections.filter((c) => !/^[a-z0-9_-]+$/i.test(c));
|
|
197
|
+
if (invalidSlugs.length > 0) {
|
|
198
|
+
console.error(`Error: Invalid collection slug(s): ${invalidSlugs.join(", ")}`);
|
|
199
|
+
console.error("Collection slugs may only contain letters, numbers, hyphens, and underscores.");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const collectionsValue = rawCollections;
|
|
169
203
|
const collectionsArg = collectionsValue.map((c) => `'${c}'`).join(", ");
|
|
170
204
|
const appDir = findAppDir();
|
|
171
205
|
if (!appDir) {
|
|
@@ -173,32 +207,52 @@ function main() {
|
|
|
173
207
|
process.exit(1);
|
|
174
208
|
}
|
|
175
209
|
const builderDir = path.join(appDir, "block-builder");
|
|
176
|
-
|
|
177
|
-
fs.
|
|
210
|
+
try {
|
|
211
|
+
if (!fs.existsSync(builderDir)) {
|
|
212
|
+
fs.mkdirSync(builderDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error(`Error: Could not create directory ${builderDir}: ${err.message}`);
|
|
216
|
+
process.exit(1);
|
|
178
217
|
}
|
|
179
218
|
const pagePath = path.join(builderDir, "page.tsx");
|
|
180
219
|
const layoutPath = path.join(builderDir, "layout.tsx");
|
|
181
220
|
if (fs.existsSync(pagePath)) {
|
|
182
221
|
console.log(`Skipped: ${pagePath} already exists`);
|
|
183
222
|
} else {
|
|
184
|
-
|
|
185
|
-
|
|
223
|
+
try {
|
|
224
|
+
fs.writeFileSync(pagePath, PAGE_CONTENT);
|
|
225
|
+
console.log(`Created: ${pagePath}`);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error(`Error: Could not write ${pagePath}: ${err.message}`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
186
230
|
}
|
|
187
231
|
if (fs.existsSync(layoutPath)) {
|
|
188
232
|
console.log(`Skipped: ${layoutPath} already exists`);
|
|
189
233
|
} else {
|
|
190
|
-
|
|
191
|
-
|
|
234
|
+
try {
|
|
235
|
+
fs.writeFileSync(layoutPath, LAYOUT_CONTENT);
|
|
236
|
+
console.log(`Created: ${layoutPath}`);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error(`Error: Could not write ${layoutPath}: ${err.message}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
192
241
|
}
|
|
193
242
|
const payloadRouteDir = path.join(appDir, "(payload)");
|
|
194
243
|
const customScssPath = path.join(payloadRouteDir, "custom.scss");
|
|
195
244
|
if (fs.existsSync(customScssPath)) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
245
|
+
try {
|
|
246
|
+
const existing = fs.readFileSync(customScssPath, "utf8");
|
|
247
|
+
if (!existing.includes("@nextbridgehq/payload-block-builder")) {
|
|
248
|
+
fs.appendFileSync(customScssPath, "\n" + CUSTOM_SCSS_IMPORTS);
|
|
249
|
+
console.log(`Updated: ${customScssPath} (added admin field styles)`);
|
|
250
|
+
} else {
|
|
251
|
+
console.log(`Skipped: ${customScssPath} already has block-builder imports`);
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error(`Error: Could not update ${customScssPath}: ${err.message}`);
|
|
255
|
+
process.exit(1);
|
|
202
256
|
}
|
|
203
257
|
}
|
|
204
258
|
const configPath = findPayloadConfig();
|