simple-module-pagebuilder 0.0.1__tar.gz
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.
- simple_module_pagebuilder-0.0.1/.gitignore +11 -0
- simple_module_pagebuilder-0.0.1/PKG-INFO +152 -0
- simple_module_pagebuilder-0.0.1/README.md +130 -0
- simple_module_pagebuilder-0.0.1/package.json +18 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/__init__.py +0 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/DiffSummary.tsx +107 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/ScheduledBadge.tsx +34 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/StatusBadge.tsx +21 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/blockRegistry.test.ts +56 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/blockRegistry.ts +80 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Button.tsx +142 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Columns.tsx +64 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Heading.tsx +92 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Image.tsx +125 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/MediaPicker.tsx +178 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Spacer.tsx +31 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Text.tsx +80 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/editor/PageEditorToolbar.tsx +169 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/editor/RevisionHistoryPanel.tsx +95 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/editor/SchedulePanel.tsx +85 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/editor/SeoSettingsPanel.tsx +119 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/layoutPuckConfig.tsx +109 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaFilters.tsx +83 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaFolderSidebar.tsx +97 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaGrid.tsx +76 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaHeader.tsx +37 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaUploadQueue.tsx +103 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/media/types.ts +20 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/migrateContent.test.ts +146 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/migrateContent.ts +135 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/puckConfig.test.tsx +118 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/puckConfig.tsx +121 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgetCatalog.test.tsx +123 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgetCatalog.ts +208 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/accent-text.tsx +60 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +14 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +71 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/link-arrow.tsx +23 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/rich-text.tsx +243 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/solid-icons.tsx +73 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/background-media.tsx +74 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/content-media.tsx +43 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/cta-button.tsx +69 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/empty-placeholder.tsx +15 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/eyebrow-text.tsx +27 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/heading.tsx +88 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/index.ts +39 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/mask.ts +54 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/section.tsx +60 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/social-icon.tsx +71 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/text-link.tsx +47 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/accordion-widget.tsx +64 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/alert-widget.tsx +47 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/app-store-badges-widget.tsx +91 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/article-cards-render.tsx +132 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/article-cards-widget.tsx +72 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/call-to-action-render.tsx +285 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/call-to-action-styles.ts +28 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/call-to-action-widget.tsx +83 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/carousel-widget.tsx +97 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/centered-hero-widget.tsx +54 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/colored-action-list-widget.tsx +117 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/column-widget.tsx +63 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/contact-cards-widget.tsx +172 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/contact-form-render.tsx +293 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/contact-form-widget.tsx +69 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/container-widget.tsx +93 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/definition-list-widget.tsx +144 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/divider-widget.tsx +56 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/eyebrow-section-widget.tsx +56 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/faq-render.tsx +268 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/faq-widget.tsx +64 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-card-widget.tsx +98 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-cards-render.tsx +258 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-cards-styles.ts +107 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-cards-widget.tsx +133 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-grid-widget.tsx +118 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-showcase-widget.tsx +118 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/gallery-widget.tsx +139 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/grid-widget.tsx +59 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/hero-widget.tsx +292 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/html-widget.tsx +42 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/iframe-widget.tsx +44 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/leaderboard-widget.tsx +203 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/list-widget.tsx +56 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/logo-cloud-widget.tsx +127 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/markdown-widget.tsx +148 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/media-object-layout.ts +156 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/media-object-render.tsx +284 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/media-object-text.tsx +202 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/media-object-widget.tsx +176 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/newsletter-widget.tsx +217 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/page-header-widget.tsx +118 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/prose-section-widget.tsx +99 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/quote-widget.tsx +51 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/row-widget.tsx +74 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/section-intro-widget.tsx +143 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/signup-banner-widget.tsx +50 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/simple-action-list-widget.tsx +91 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/site-footer-widget.tsx +97 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/site-header-nav.tsx +153 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/site-header-widget.tsx +182 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/social-banner-widget.tsx +83 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/stats-widget.tsx +200 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/steps-widget.tsx +211 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/story-cards-widget.tsx +182 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/table-widget.tsx +86 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/tabs-widget.tsx +95 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/tags-widget.tsx +85 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/testimonial-widget.tsx +102 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/timeline-layout.ts +73 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/timeline-render.tsx +294 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/timeline-widget.tsx +106 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/under-construction-widget.tsx +40 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/video-widget.tsx +103 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/welcome-widget.tsx +38 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/contracts/__init__.py +57 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/contracts/events.py +25 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/contracts/schemas.py +239 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/conversion/blocks-to-html.test.ts +105 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/conversion/blocks-to-html.ts +102 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/conversion/index.ts +2 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/conversion/types.ts +5 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/deps.py +26 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/diff.py +129 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/__init__.py +0 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/__init__.py +21 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/_deps.py +21 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/layout.py +78 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/pages.py +85 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/revisions.py +79 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/uploads.py +92 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/workflow.py +124 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/seo.py +95 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/views.py +228 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/fields/checkbox-field.tsx +22 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/fields/file-picker/file-gallery.tsx +152 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/fields/file-picker/image-picker-field.tsx +214 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/fields/file-picker/index.ts +6 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/fields/file-picker/types.ts +15 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/fields/index.ts +9 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/fields/media-adapter.ts +47 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/hooks/useAutosave.ts +134 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/hooks/useEditorForm.ts +193 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/hooks/useMediaUploads.ts +96 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/hooks/usePageRevisions.ts +59 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/hooks/usePageSchedule.ts +72 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/hooks/usePageWorkflow.ts +173 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/layout_service.py +101 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/media_images.py +185 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/media_queries.py +92 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/media_service.py +222 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/models.py +209 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/module.py +279 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/pages/LayoutEditor.tsx +183 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/pages/MediaLibrary.tsx +249 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/pages/PageEditor.tsx +181 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/pages/PageList.tsx +129 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/pages/PendingReview.tsx +128 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/pages/PublicPage.tsx +126 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/permissions.py +44 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/py.typed +0 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/security.py +230 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/service/__init__.py +141 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/service/_common.py +24 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/service/_revisions.py +92 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/service/_workflow.py +207 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/services.py +19 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/settings.py +161 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/static/widgets-base.css +57 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/translation/apply.ts +54 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/translation/extract.ts +141 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/translation/index.ts +20 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/translation/translatable-fields.ts +65 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/translation/translation.test.ts +167 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/api.ts +12 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/datetime.ts +25 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/decorative-image.ts +15 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/editorSnapshot.ts +39 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/markdown.ts +160 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/mediaApi.ts +112 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/mediaFormat.ts +19 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/pagesApi.ts +111 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/parse-list.ts +7 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/request.ts +52 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/slugify.ts +9 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/types.ts +171 -0
- simple_module_pagebuilder-0.0.1/pagebuilder/utils/widgetUtils.ts +11 -0
- simple_module_pagebuilder-0.0.1/pyproject.toml +54 -0
- simple_module_pagebuilder-0.0.1/tests/conftest.py +300 -0
- simple_module_pagebuilder-0.0.1/tests/test_approval_workflow.py +251 -0
- simple_module_pagebuilder-0.0.1/tests/test_auth_gating.py +53 -0
- simple_module_pagebuilder-0.0.1/tests/test_csrf.py +73 -0
- simple_module_pagebuilder-0.0.1/tests/test_image_processing.py +183 -0
- simple_module_pagebuilder-0.0.1/tests/test_media_security.py +118 -0
- simple_module_pagebuilder-0.0.1/tests/test_module.py +99 -0
- simple_module_pagebuilder-0.0.1/tests/test_pages_api.py +101 -0
- simple_module_pagebuilder-0.0.1/tests/test_public_routes.py +84 -0
- simple_module_pagebuilder-0.0.1/tests/test_public_view_headers.py +46 -0
- simple_module_pagebuilder-0.0.1/tests/test_public_viewer.py +79 -0
- simple_module_pagebuilder-0.0.1/tests/test_revision_notes_and_diff.py +265 -0
- simple_module_pagebuilder-0.0.1/tests/test_revisions_api.py +101 -0
- simple_module_pagebuilder-0.0.1/tests/test_scheduled_publish.py +283 -0
- simple_module_pagebuilder-0.0.1/tests/test_seo_meta.py +97 -0
- simple_module_pagebuilder-0.0.1/tests/test_site_layout.py +264 -0
- simple_module_pagebuilder-0.0.1/tests/test_sitemap_robots.py +94 -0
- simple_module_pagebuilder-0.0.1/tests/test_uploads_api.py +221 -0
- simple_module_pagebuilder-0.0.1/tsconfig.json +10 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple_module_pagebuilder
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Drag-and-drop visual page builder for simple_module apps
|
|
5
|
+
Project-URL: Repository, https://github.com/antosubash/simple_module_python_modules
|
|
6
|
+
Author-email: Anto Subash <antosubash@live.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: cms,pagebuilder,puck,simple-module
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: pillow>=10.0
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
12
|
+
Requires-Dist: simple-module-core<0.1,>=0.0.25
|
|
13
|
+
Requires-Dist: simple-module-db<0.1,>=0.0.25
|
|
14
|
+
Requires-Dist: simple-module-hosting<0.1,>=0.0.25
|
|
15
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
16
|
+
Requires-Dist: sqlmodel>=0.0.21
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: simple-module-test<0.1,>=0.0.25; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# simple_module_pagebuilder
|
|
24
|
+
|
|
25
|
+
Drag-and-drop visual page builder for [simple_module](https://github.com/antosubash/simple_module_python)
|
|
26
|
+
apps. Pages are composed from a reusable block library using
|
|
27
|
+
[Puck](https://puckeditor.com) and stored as JSON; published pages are served
|
|
28
|
+
at `/p/{slug}`.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Visual editor** — Puck-based drag-and-drop with a block library (heading,
|
|
33
|
+
text, image, button, columns, spacer) and a media picker.
|
|
34
|
+
- **Site layout** — a shared header/footer layout, edited the same way, wrapped
|
|
35
|
+
around every public page.
|
|
36
|
+
- **Media library** — uploads with magic-byte content sniffing, folder
|
|
37
|
+
organisation, and server-generated WebP thumbnails.
|
|
38
|
+
- **Revisions** — every status transition snapshots the page, with a diff view
|
|
39
|
+
and restore-as-draft.
|
|
40
|
+
- **Approval workflow** — edit → submit for review → approve / reject, gated by
|
|
41
|
+
three separate permissions.
|
|
42
|
+
- **Scheduled publishing** — set `publish_at` / `unpublish_at`; a background
|
|
43
|
+
poller flips status at the due time.
|
|
44
|
+
- **SEO** — per-page meta description, canonical URL, OG image, JSON-LD, plus
|
|
45
|
+
`/sitemap.xml` and `/robots.txt`.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv add simple_module_pagebuilder
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then add it to your host's `pyproject.toml` dependencies. The module is
|
|
54
|
+
discovered at boot through the `simple_module` entry point — no registration
|
|
55
|
+
code required.
|
|
56
|
+
|
|
57
|
+
Its frontend ships inside the wheel. Pull its JS dependencies into your host's
|
|
58
|
+
client app and regenerate the page manifest with:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
smpy host sync-js-deps --host-client-app=host/client_app
|
|
62
|
+
smpy host gen-pages --host-dir=host/client_app
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
Once installed and migrated, sign in and open `/pagebuilder`:
|
|
68
|
+
|
|
69
|
+
1. **Create a page** — `/pagebuilder/new`. Give it a title; the slug is derived
|
|
70
|
+
from it and stays editable.
|
|
71
|
+
2. **Compose it** — drag blocks from the Puck sidebar. Images come from the
|
|
72
|
+
media library, which uploads and thumbnails them for you.
|
|
73
|
+
3. **Publish it** — publish directly with `pagebuilder.publish`, or submit for
|
|
74
|
+
review if your role only carries `pagebuilder.edit`. Reviewers work through
|
|
75
|
+
`/pagebuilder/pending`.
|
|
76
|
+
4. **Schedule it** — set `publish_at` / `unpublish_at` instead of publishing
|
|
77
|
+
now, and the poller flips the status at the due time.
|
|
78
|
+
5. **View it** — the page is live at `/p/{slug}`, wrapped in the site layout
|
|
79
|
+
from `/pagebuilder/layout`.
|
|
80
|
+
|
|
81
|
+
Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
|
|
82
|
+
two revisions and restore either one as a draft.
|
|
83
|
+
|
|
84
|
+
## Migrations
|
|
85
|
+
|
|
86
|
+
This module ships **no** migrations — that is the framework convention. Its
|
|
87
|
+
SQLModel tables are picked up by your host's `build_module_metadata()`, so
|
|
88
|
+
after installing it run:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
make migration msg="add pagebuilder"
|
|
92
|
+
make migrate
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Add `branch_labels = ("pagebuilder",)` to that first generated revision so the
|
|
96
|
+
module can later be removed on its own with
|
|
97
|
+
`alembic downgrade pagebuilder@base`.
|
|
98
|
+
|
|
99
|
+
## Permissions
|
|
100
|
+
|
|
101
|
+
| Permission | Grants |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `pagebuilder.edit` | Draft, save, submit for review, restore a revision |
|
|
104
|
+
| `pagebuilder.publish` | Publish and unpublish directly |
|
|
105
|
+
| `pagebuilder.approve` | Approve or reject a submitted page |
|
|
106
|
+
|
|
107
|
+
`DEFAULT_ROLE_MAP` in `pagebuilder.permissions` suggests three roles —
|
|
108
|
+
`pagebuilder_editor`, `pagebuilder_publisher`, `pagebuilder_approver` —
|
|
109
|
+
cumulatively mapped to those permissions.
|
|
110
|
+
|
|
111
|
+
## Settings
|
|
112
|
+
|
|
113
|
+
All settings use the `SM_PAGEBUILDER_` env prefix.
|
|
114
|
+
|
|
115
|
+
| Setting | Default | Purpose |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `public_route_prefix` | `/p` | Where published pages are served |
|
|
118
|
+
| `requires_auth` | `true` | Gate the admin surface behind authentication |
|
|
119
|
+
| `csrf_protect` | `true` | Require a CSRF token on mutating admin requests |
|
|
120
|
+
| `media_root` | `var/pagebuilder/media` | Upload storage directory |
|
|
121
|
+
| `media_url_prefix` | `/media/pagebuilder` | Public URL prefix for uploads |
|
|
122
|
+
| `media_max_bytes` | `10485760` | Per-upload size ceiling |
|
|
123
|
+
| `media_thumbnail_widths` | `320,640,1280,1920` | Widths generated as WebP |
|
|
124
|
+
| `media_webp_quality` | `82` | WebP encoder quality |
|
|
125
|
+
| `public_csp` | see `settings.py` | CSP header on public pages |
|
|
126
|
+
| `public_cache_max_age` | `60` | `max-age` on public pages |
|
|
127
|
+
| `public_base_url` | `None` | Absolute base for canonical URLs and the sitemap |
|
|
128
|
+
| `site_name` | `None` | OpenGraph site name |
|
|
129
|
+
| `sitemap_enabled` | `true` | Serve `/sitemap.xml` |
|
|
130
|
+
| `robots_enabled` | `true` | Serve `/robots.txt` |
|
|
131
|
+
| `scheduler_enabled` | `true` | Run the scheduled publish/unpublish poller |
|
|
132
|
+
| `scheduler_interval_seconds` | `30` | Poll interval |
|
|
133
|
+
|
|
134
|
+
## Routes
|
|
135
|
+
|
|
136
|
+
**Admin views** (under `/pagebuilder`): `/`, `/new`, `/{page_id}/edit`,
|
|
137
|
+
`/pending`, `/layout`, `/media`.
|
|
138
|
+
|
|
139
|
+
**Admin API** (under `/api/pagebuilder`): pages CRUD, workflow transitions,
|
|
140
|
+
revisions and diffs, layout and its revisions, uploads.
|
|
141
|
+
|
|
142
|
+
**Public**: `/p/{slug}`, `/sitemap.xml`, `/robots.txt`.
|
|
143
|
+
|
|
144
|
+
## Contracts
|
|
145
|
+
|
|
146
|
+
DTOs other modules may depend on are exported from `pagebuilder.contracts`.
|
|
147
|
+
Everything else in the package is internal and may change without a major
|
|
148
|
+
version bump.
|
|
149
|
+
|
|
150
|
+
## Licence
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# simple_module_pagebuilder
|
|
2
|
+
|
|
3
|
+
Drag-and-drop visual page builder for [simple_module](https://github.com/antosubash/simple_module_python)
|
|
4
|
+
apps. Pages are composed from a reusable block library using
|
|
5
|
+
[Puck](https://puckeditor.com) and stored as JSON; published pages are served
|
|
6
|
+
at `/p/{slug}`.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Visual editor** — Puck-based drag-and-drop with a block library (heading,
|
|
11
|
+
text, image, button, columns, spacer) and a media picker.
|
|
12
|
+
- **Site layout** — a shared header/footer layout, edited the same way, wrapped
|
|
13
|
+
around every public page.
|
|
14
|
+
- **Media library** — uploads with magic-byte content sniffing, folder
|
|
15
|
+
organisation, and server-generated WebP thumbnails.
|
|
16
|
+
- **Revisions** — every status transition snapshots the page, with a diff view
|
|
17
|
+
and restore-as-draft.
|
|
18
|
+
- **Approval workflow** — edit → submit for review → approve / reject, gated by
|
|
19
|
+
three separate permissions.
|
|
20
|
+
- **Scheduled publishing** — set `publish_at` / `unpublish_at`; a background
|
|
21
|
+
poller flips status at the due time.
|
|
22
|
+
- **SEO** — per-page meta description, canonical URL, OG image, JSON-LD, plus
|
|
23
|
+
`/sitemap.xml` and `/robots.txt`.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv add simple_module_pagebuilder
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then add it to your host's `pyproject.toml` dependencies. The module is
|
|
32
|
+
discovered at boot through the `simple_module` entry point — no registration
|
|
33
|
+
code required.
|
|
34
|
+
|
|
35
|
+
Its frontend ships inside the wheel. Pull its JS dependencies into your host's
|
|
36
|
+
client app and regenerate the page manifest with:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
smpy host sync-js-deps --host-client-app=host/client_app
|
|
40
|
+
smpy host gen-pages --host-dir=host/client_app
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
Once installed and migrated, sign in and open `/pagebuilder`:
|
|
46
|
+
|
|
47
|
+
1. **Create a page** — `/pagebuilder/new`. Give it a title; the slug is derived
|
|
48
|
+
from it and stays editable.
|
|
49
|
+
2. **Compose it** — drag blocks from the Puck sidebar. Images come from the
|
|
50
|
+
media library, which uploads and thumbnails them for you.
|
|
51
|
+
3. **Publish it** — publish directly with `pagebuilder.publish`, or submit for
|
|
52
|
+
review if your role only carries `pagebuilder.edit`. Reviewers work through
|
|
53
|
+
`/pagebuilder/pending`.
|
|
54
|
+
4. **Schedule it** — set `publish_at` / `unpublish_at` instead of publishing
|
|
55
|
+
now, and the poller flips the status at the due time.
|
|
56
|
+
5. **View it** — the page is live at `/p/{slug}`, wrapped in the site layout
|
|
57
|
+
from `/pagebuilder/layout`.
|
|
58
|
+
|
|
59
|
+
Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
|
|
60
|
+
two revisions and restore either one as a draft.
|
|
61
|
+
|
|
62
|
+
## Migrations
|
|
63
|
+
|
|
64
|
+
This module ships **no** migrations — that is the framework convention. Its
|
|
65
|
+
SQLModel tables are picked up by your host's `build_module_metadata()`, so
|
|
66
|
+
after installing it run:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
make migration msg="add pagebuilder"
|
|
70
|
+
make migrate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Add `branch_labels = ("pagebuilder",)` to that first generated revision so the
|
|
74
|
+
module can later be removed on its own with
|
|
75
|
+
`alembic downgrade pagebuilder@base`.
|
|
76
|
+
|
|
77
|
+
## Permissions
|
|
78
|
+
|
|
79
|
+
| Permission | Grants |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `pagebuilder.edit` | Draft, save, submit for review, restore a revision |
|
|
82
|
+
| `pagebuilder.publish` | Publish and unpublish directly |
|
|
83
|
+
| `pagebuilder.approve` | Approve or reject a submitted page |
|
|
84
|
+
|
|
85
|
+
`DEFAULT_ROLE_MAP` in `pagebuilder.permissions` suggests three roles —
|
|
86
|
+
`pagebuilder_editor`, `pagebuilder_publisher`, `pagebuilder_approver` —
|
|
87
|
+
cumulatively mapped to those permissions.
|
|
88
|
+
|
|
89
|
+
## Settings
|
|
90
|
+
|
|
91
|
+
All settings use the `SM_PAGEBUILDER_` env prefix.
|
|
92
|
+
|
|
93
|
+
| Setting | Default | Purpose |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `public_route_prefix` | `/p` | Where published pages are served |
|
|
96
|
+
| `requires_auth` | `true` | Gate the admin surface behind authentication |
|
|
97
|
+
| `csrf_protect` | `true` | Require a CSRF token on mutating admin requests |
|
|
98
|
+
| `media_root` | `var/pagebuilder/media` | Upload storage directory |
|
|
99
|
+
| `media_url_prefix` | `/media/pagebuilder` | Public URL prefix for uploads |
|
|
100
|
+
| `media_max_bytes` | `10485760` | Per-upload size ceiling |
|
|
101
|
+
| `media_thumbnail_widths` | `320,640,1280,1920` | Widths generated as WebP |
|
|
102
|
+
| `media_webp_quality` | `82` | WebP encoder quality |
|
|
103
|
+
| `public_csp` | see `settings.py` | CSP header on public pages |
|
|
104
|
+
| `public_cache_max_age` | `60` | `max-age` on public pages |
|
|
105
|
+
| `public_base_url` | `None` | Absolute base for canonical URLs and the sitemap |
|
|
106
|
+
| `site_name` | `None` | OpenGraph site name |
|
|
107
|
+
| `sitemap_enabled` | `true` | Serve `/sitemap.xml` |
|
|
108
|
+
| `robots_enabled` | `true` | Serve `/robots.txt` |
|
|
109
|
+
| `scheduler_enabled` | `true` | Run the scheduled publish/unpublish poller |
|
|
110
|
+
| `scheduler_interval_seconds` | `30` | Poll interval |
|
|
111
|
+
|
|
112
|
+
## Routes
|
|
113
|
+
|
|
114
|
+
**Admin views** (under `/pagebuilder`): `/`, `/new`, `/{page_id}/edit`,
|
|
115
|
+
`/pending`, `/layout`, `/media`.
|
|
116
|
+
|
|
117
|
+
**Admin API** (under `/api/pagebuilder`): pages CRUD, workflow transitions,
|
|
118
|
+
revisions and diffs, layout and its revisions, uploads.
|
|
119
|
+
|
|
120
|
+
**Public**: `/p/{slug}`, `/sitemap.xml`, `/robots.txt`.
|
|
121
|
+
|
|
122
|
+
## Contracts
|
|
123
|
+
|
|
124
|
+
DTOs other modules may depend on are exported from `pagebuilder.contracts`.
|
|
125
|
+
Everything else in the package is internal and may change without a major
|
|
126
|
+
version bump.
|
|
127
|
+
|
|
128
|
+
## Licence
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simple-module-py/pagebuilder",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Frontend assets for the PageBuilder module",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"react": "^19.0.0",
|
|
8
|
+
"react-dom": "^19.0.0",
|
|
9
|
+
"@inertiajs/react": "^2.0.0",
|
|
10
|
+
"@simple-module-py/ui": "*"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@simple-module-py/tsconfig": "*"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@puckeditor/core": "^0.22.4"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { RevisionDiff } from '../utils/api';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
diff: RevisionDiff;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function renderValue(value: unknown): string {
|
|
9
|
+
if (value === null || value === undefined) return '∅';
|
|
10
|
+
if (typeof value === 'string') return value || '""';
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compact, scannable diff: metadata table (only changed fields) plus
|
|
16
|
+
* three lists of blocks (added / removed / changed). Designed to fit
|
|
17
|
+
* inside the editor's history panel without a separate page.
|
|
18
|
+
*/
|
|
19
|
+
export function DiffSummary({ diff }: Props): React.JSX.Element {
|
|
20
|
+
const { metadata, blocks } = diff;
|
|
21
|
+
const metaKeys = Object.keys(metadata);
|
|
22
|
+
const totalChanges =
|
|
23
|
+
metaKeys.length + blocks.added.length + blocks.removed.length + blocks.changed.length;
|
|
24
|
+
|
|
25
|
+
if (totalChanges === 0) {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className="mt-3 rounded border border-gray-200 bg-white p-3 text-xs text-gray-500"
|
|
29
|
+
data-testid="diff-summary"
|
|
30
|
+
>
|
|
31
|
+
No changes between these revisions.
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className="mt-3 rounded border border-gray-200 bg-white p-3 text-xs space-y-2"
|
|
39
|
+
data-testid="diff-summary"
|
|
40
|
+
>
|
|
41
|
+
{metaKeys.length > 0 && (
|
|
42
|
+
<div>
|
|
43
|
+
<div className="font-semibold text-gray-700">Metadata</div>
|
|
44
|
+
<ul className="ml-3 list-disc">
|
|
45
|
+
{metaKeys.map((key) => (
|
|
46
|
+
<li key={key}>
|
|
47
|
+
<span className="font-mono">{key}</span>:{' '}
|
|
48
|
+
<span className="text-red-600 line-through">
|
|
49
|
+
{renderValue(metadata[key].before)}
|
|
50
|
+
</span>{' '}
|
|
51
|
+
→ <span className="text-green-700">{renderValue(metadata[key].after)}</span>
|
|
52
|
+
</li>
|
|
53
|
+
))}
|
|
54
|
+
</ul>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
{blocks.added.length > 0 && (
|
|
58
|
+
<div>
|
|
59
|
+
<div className="font-semibold text-green-700">Added ({blocks.added.length})</div>
|
|
60
|
+
<ul className="ml-3 list-disc">
|
|
61
|
+
{blocks.added.map((b) => (
|
|
62
|
+
<li key={b.id}>
|
|
63
|
+
<span className="font-mono">{b.type ?? '?'}</span>{' '}
|
|
64
|
+
<span className="text-gray-500">({b.id})</span>
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
{blocks.removed.length > 0 && (
|
|
71
|
+
<div>
|
|
72
|
+
<div className="font-semibold text-red-700">Removed ({blocks.removed.length})</div>
|
|
73
|
+
<ul className="ml-3 list-disc">
|
|
74
|
+
{blocks.removed.map((b) => (
|
|
75
|
+
<li key={b.id}>
|
|
76
|
+
<span className="font-mono">{b.type ?? '?'}</span>{' '}
|
|
77
|
+
<span className="text-gray-500">({b.id})</span>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
{blocks.changed.length > 0 && (
|
|
84
|
+
<div>
|
|
85
|
+
<div className="font-semibold text-amber-700">Changed ({blocks.changed.length})</div>
|
|
86
|
+
<ul className="ml-3 list-disc">
|
|
87
|
+
{blocks.changed.map((b) => (
|
|
88
|
+
<li key={b.id}>
|
|
89
|
+
<span className="font-mono">{b.type ?? '?'}</span>{' '}
|
|
90
|
+
<span className="text-gray-500">({b.id})</span>
|
|
91
|
+
{b.type_before && (
|
|
92
|
+
<span className="text-gray-500">
|
|
93
|
+
{' '}
|
|
94
|
+
— was <span className="font-mono">{b.type_before}</span>
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
{b.fields.length > 0 && (
|
|
98
|
+
<span className="text-gray-700">: {b.fields.join(', ')}</span>
|
|
99
|
+
)}
|
|
100
|
+
</li>
|
|
101
|
+
))}
|
|
102
|
+
</ul>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PageStatus } from '../utils/api';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
status: PageStatus;
|
|
5
|
+
publishAt: string | null;
|
|
6
|
+
unpublishAt: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Inline pill rendered next to ``StatusBadge`` when a page has a
|
|
11
|
+
* pending scheduler-driven flip. Drafts surface the upcoming publish
|
|
12
|
+
* time; published pages surface the upcoming unpublish time. Pages
|
|
13
|
+
* with no schedule for their current state render nothing — so callers
|
|
14
|
+
* can drop this in unconditionally.
|
|
15
|
+
*/
|
|
16
|
+
export function ScheduledBadge({ status, publishAt, unpublishAt }: Props) {
|
|
17
|
+
const target =
|
|
18
|
+
status === 'draft' && publishAt
|
|
19
|
+
? { kind: 'publish' as const, at: publishAt }
|
|
20
|
+
: status === 'published' && unpublishAt
|
|
21
|
+
? { kind: 'unpublish' as const, at: unpublishAt }
|
|
22
|
+
: null;
|
|
23
|
+
if (!target) return null;
|
|
24
|
+
const label = target.kind === 'publish' ? 'Scheduled to publish' : 'Scheduled to unpublish';
|
|
25
|
+
return (
|
|
26
|
+
<span
|
|
27
|
+
className="ml-2 inline-block text-xs font-medium px-2 py-1 rounded bg-blue-100 text-blue-800"
|
|
28
|
+
title={new Date(target.at).toLocaleString()}
|
|
29
|
+
data-testid="scheduled-badge"
|
|
30
|
+
>
|
|
31
|
+
{label} {new Date(target.at).toLocaleString()}
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Badge } from '@simple-module-py/ui/components/ui/badge';
|
|
2
|
+
|
|
3
|
+
import type { PageStatus } from '../utils/api';
|
|
4
|
+
|
|
5
|
+
// The e2e suite asserts on this text verbatim — getByText('published',
|
|
6
|
+
// { exact: true }) — so these labels stay lowercase and unabbreviated.
|
|
7
|
+
const LABELS: Record<PageStatus, string> = {
|
|
8
|
+
published: 'published',
|
|
9
|
+
submitted_for_review: 'pending review',
|
|
10
|
+
draft: 'draft',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const VARIANTS: Record<PageStatus, 'default' | 'secondary' | 'outline'> = {
|
|
14
|
+
published: 'default',
|
|
15
|
+
submitted_for_review: 'secondary',
|
|
16
|
+
draft: 'outline',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function StatusBadge({ status }: { status: PageStatus }) {
|
|
20
|
+
return <Badge variant={VARIANTS[status]}>{LABELS[status]}</Badge>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyBlock,
|
|
5
|
+
declareBuiltInBlocks,
|
|
6
|
+
registeredBlocks,
|
|
7
|
+
registerPuckBlocks,
|
|
8
|
+
registryVersion,
|
|
9
|
+
resetBlockRegistry,
|
|
10
|
+
} from './blockRegistry';
|
|
11
|
+
|
|
12
|
+
const stub = { render: () => null } as unknown as AnyBlock;
|
|
13
|
+
|
|
14
|
+
describe('block registry', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
declareBuiltInBlocks(['Heading', 'Text']);
|
|
17
|
+
resetBlockRegistry();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('collects a registration', () => {
|
|
21
|
+
registerPuckBlocks({ blocks: { Widget: stub } });
|
|
22
|
+
expect(registeredBlocks().flatMap((r) => Object.keys(r.blocks))).toContain('Widget');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('bumps the version on each registration', () => {
|
|
26
|
+
const before = registryVersion();
|
|
27
|
+
registerPuckBlocks({ blocks: { Widget: stub } });
|
|
28
|
+
expect(registryVersion()).toBeGreaterThan(before);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('refuses to shadow a block pagebuilder already ships', () => {
|
|
32
|
+
// Block names are the `type` stored in page content, so a duplicate would
|
|
33
|
+
// make two blocks each render the other's saved data.
|
|
34
|
+
expect(() => registerPuckBlocks({ blocks: { Heading: stub } })).toThrow(/Heading/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('refuses to register the same name twice', () => {
|
|
38
|
+
registerPuckBlocks({ blocks: { Widget: stub } });
|
|
39
|
+
expect(() => registerPuckBlocks({ blocks: { Widget: stub } })).toThrow(/Widget/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('keeps the built-ins reserved across a reset', () => {
|
|
43
|
+
resetBlockRegistry();
|
|
44
|
+
expect(() => registerPuckBlocks({ blocks: { Text: stub } })).toThrow(/Text/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('carries the category through', () => {
|
|
48
|
+
registerPuckBlocks({ blocks: { Widget: stub }, category: { key: 'feeds', title: 'Feeds' } });
|
|
49
|
+
expect(registeredBlocks()[0].category).toEqual({ key: 'feeds', title: 'Feeds' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('defaults to page-only, not layout', () => {
|
|
53
|
+
registerPuckBlocks({ blocks: { Widget: stub } });
|
|
54
|
+
expect(registeredBlocks()[0].layout).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lets another module contribute Puck blocks without pagebuilder importing it.
|
|
3
|
+
*
|
|
4
|
+
* The framework's registries — menu, permissions, public routes, design packs —
|
|
5
|
+
* are all backend. This is the frontend equivalent, and the reason it exists is
|
|
6
|
+
* dependency direction: the news module knows about pagebuilder, so pagebuilder
|
|
7
|
+
* must not know about news.
|
|
8
|
+
*
|
|
9
|
+
* Registration is a side effect of importing a module's `puck-blocks.ts`. The
|
|
10
|
+
* host imports them all eagerly at app start (see `host/client_app/blocks.ts`),
|
|
11
|
+
* which is what guarantees the registry is populated before the first render.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
15
|
+
|
|
16
|
+
// Puck's ComponentConfig is invariant in its props, so a registry that holds
|
|
17
|
+
// configs for unrelated prop shapes cannot be typed precisely. The runtime
|
|
18
|
+
// shape is what matters here; each module keeps its own widget types honest.
|
|
19
|
+
// biome-ignore lint/suspicious/noExplicitAny: a heterogeneous registry has no single prop type
|
|
20
|
+
export type AnyBlock = ComponentConfig<any>;
|
|
21
|
+
|
|
22
|
+
export interface BlockRegistration {
|
|
23
|
+
/** Block name → config. The name is the `type` stored in page content. */
|
|
24
|
+
blocks: Record<string, AnyBlock>;
|
|
25
|
+
/** Palette group. Blocks with no category land in Puck's "Other". */
|
|
26
|
+
category?: { key: string; title: string };
|
|
27
|
+
/** Also offer these in the site-layout palette. Default false. */
|
|
28
|
+
layout?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let registrations: BlockRegistration[] = [];
|
|
32
|
+
let takenNames = new Set<string>();
|
|
33
|
+
let builtIns: readonly string[] = [];
|
|
34
|
+
let version = 0;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Declare the names pagebuilder itself ships, so a module cannot shadow one.
|
|
38
|
+
*
|
|
39
|
+
* Called once by puckConfig at import time rather than hardcoded here, which
|
|
40
|
+
* would be a second list to keep in sync.
|
|
41
|
+
*/
|
|
42
|
+
export function declareBuiltInBlocks(names: readonly string[]): void {
|
|
43
|
+
builtIns = names;
|
|
44
|
+
for (const name of names) takenNames.add(name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function registerPuckBlocks(registration: BlockRegistration): void {
|
|
48
|
+
for (const name of Object.keys(registration.blocks)) {
|
|
49
|
+
if (takenNames.has(name)) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Puck block "${name}" is already registered. Block names are the \`type\` ` +
|
|
52
|
+
'stored in page content, so two blocks sharing one would each render ' +
|
|
53
|
+
"the other's saved data.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
takenNames.add(name);
|
|
57
|
+
}
|
|
58
|
+
registrations = [...registrations, registration];
|
|
59
|
+
version += 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function registeredBlocks(): readonly BlockRegistration[] {
|
|
63
|
+
return registrations;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Bumped on every registration. The config builders memoise on this rather
|
|
68
|
+
* than caching unconditionally, so a registration that lands after the first
|
|
69
|
+
* read is not silently dropped.
|
|
70
|
+
*/
|
|
71
|
+
export function registryVersion(): number {
|
|
72
|
+
return version;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Test-only: drop every registration, keeping the declared built-ins. */
|
|
76
|
+
export function resetBlockRegistry(): void {
|
|
77
|
+
registrations = [];
|
|
78
|
+
takenNames = new Set(builtIns);
|
|
79
|
+
version += 1;
|
|
80
|
+
}
|