simple-module-pagebuilder 0.0.4__tar.gz → 0.0.6__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.4 → simple_module_pagebuilder-0.0.6}/PKG-INFO +2 -2
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/package.json +1 -1
- simple_module_pagebuilder-0.0.6/pagebuilder/board.py +137 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/ConfirmDialog.tsx +138 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/DiffSummary.tsx +14 -13
- simple_module_pagebuilder-0.0.6/pagebuilder/components/NewPageDialog.tsx +218 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/NoteDialog.tsx +116 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/PageBoard.tsx +123 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/PageBoardCard.tsx +143 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/PageListFilters.tsx +104 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/blocks/Image.test.tsx +171 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Image.tsx +74 -9
- simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/BlockOutline.tsx +76 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/NarrowCanvas.tsx +59 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/PageActions.tsx +103 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/editor/PageEditorToolbar.tsx +54 -16
- simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/PageInspectorDrawer.tsx +136 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/PageSettingsPanel.tsx +147 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/editor/RevisionHistoryPanel.tsx +36 -19
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/editor/SchedulePanel.tsx +38 -21
- simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/SeoPreview.tsx +93 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/SeoSettingsPanel.tsx +188 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaFilters.tsx +4 -4
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaFolderSidebar.tsx +4 -4
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaGrid.tsx +22 -9
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaUploadQueue.tsx +10 -7
- simple_module_pagebuilder-0.0.6/pagebuilder/components/media/MediaUsageList.tsx +58 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/components/widgets/divider-widget.test.tsx +48 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/divider-widget.tsx +9 -2
- simple_module_pagebuilder-0.0.6/pagebuilder/contracts/schemas/__init__.py +74 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/contracts/schemas/_layout.py +52 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/contracts/schemas/_media.py +95 -0
- simple_module_pagebuilder-0.0.4/pagebuilder/contracts/schemas.py → simple_module_pagebuilder-0.0.6/pagebuilder/contracts/schemas/_pages.py +43 -92
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/__init__.py +10 -1
- simple_module_pagebuilder-0.0.6/pagebuilder/endpoints/api/media_detail.py +102 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/endpoints/api/pages.py +149 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/endpoints/public_views.py +162 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/seo.py +9 -1
- simple_module_pagebuilder-0.0.6/pagebuilder/endpoints/views.py +251 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/useAutosave.ts +14 -1
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/useEditorForm.ts +44 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/hooks/useIsNarrow.ts +45 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/hooks/useMediaDetail.ts +78 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/usePageRevisions.ts +2 -3
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/usePageWorkflow.ts +19 -23
- simple_module_pagebuilder-0.0.6/pagebuilder/hooks/usePendingDialog.ts +43 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/media_service.py +3 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/media_usage.py +101 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/models/__init__.py +35 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/models/_base.py +14 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/models/_layout.py +45 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/models/_media.py +58 -0
- simple_module_pagebuilder-0.0.4/pagebuilder/models.py → simple_module_pagebuilder-0.0.6/pagebuilder/models/_page.py +60 -71
- simple_module_pagebuilder-0.0.6/pagebuilder/models/_redirect.py +28 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/module.py +13 -7
- simple_module_pagebuilder-0.0.6/pagebuilder/pages/LayoutEditor.tsx +225 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/pages/MediaDetail.tsx +185 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/pages/MediaLibrary.tsx +17 -13
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/pages/PageEditor.tsx +59 -52
- simple_module_pagebuilder-0.0.6/pagebuilder/pages/PageList.tsx +291 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/pages/PendingReview.tsx +37 -43
- simple_module_pagebuilder-0.0.6/pagebuilder/pages/Trash.tsx +150 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/public_claims.py +73 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/redirects.py +60 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/service/__init__.py +94 -30
- simple_module_pagebuilder-0.0.6/pagebuilder/service/_trash.py +166 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/service/_workflow.py +5 -1
- simple_module_pagebuilder-0.0.6/pagebuilder/utils/blockOutline.test.ts +139 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/utils/blockOutline.ts +130 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/editorSnapshot.ts +13 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/utils/measure.ts +54 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/mediaApi.ts +17 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/pagesApi.ts +18 -20
- simple_module_pagebuilder-0.0.6/pagebuilder/utils/request.test.ts +106 -0
- simple_module_pagebuilder-0.0.6/pagebuilder/utils/request.ts +120 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/types.ts +37 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pyproject.toml +5 -1
- simple_module_pagebuilder-0.0.6/tests/db_fixture.py +37 -0
- simple_module_pagebuilder-0.0.6/tests/test_admin_list_view.py +82 -0
- simple_module_pagebuilder-0.0.6/tests/test_draft_preview.py +92 -0
- simple_module_pagebuilder-0.0.6/tests/test_media_detail.py +175 -0
- simple_module_pagebuilder-0.0.6/tests/test_media_usage_escaping.py +58 -0
- simple_module_pagebuilder-0.0.6/tests/test_page_templates.py +164 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_pages_api.py +1 -1
- simple_module_pagebuilder-0.0.6/tests/test_pages_search.py +98 -0
- simple_module_pagebuilder-0.0.6/tests/test_public_claims.py +158 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_public_view_headers.py +1 -1
- simple_module_pagebuilder-0.0.6/tests/test_redirects.py +144 -0
- simple_module_pagebuilder-0.0.6/tests/test_trash.py +177 -0
- simple_module_pagebuilder-0.0.6/tests/test_trash_preconditions.py +93 -0
- simple_module_pagebuilder-0.0.4/pagebuilder/components/blocks/Image.test.tsx +0 -94
- simple_module_pagebuilder-0.0.4/pagebuilder/components/editor/SeoSettingsPanel.tsx +0 -119
- simple_module_pagebuilder-0.0.4/pagebuilder/endpoints/api/pages.py +0 -85
- simple_module_pagebuilder-0.0.4/pagebuilder/endpoints/views.py +0 -228
- simple_module_pagebuilder-0.0.4/pagebuilder/pages/LayoutEditor.tsx +0 -183
- simple_module_pagebuilder-0.0.4/pagebuilder/pages/PageList.tsx +0 -129
- simple_module_pagebuilder-0.0.4/pagebuilder/utils/request.ts +0 -52
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/.gitignore +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/README.md +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/__init__.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/ScheduledBadge.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/StatusBadge.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blockRegistry.test.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blockRegistry.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Button.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Columns.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Heading.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/MediaPicker.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Spacer.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Text.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/layoutPuckConfig.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/lazyBlock.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaHeader.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/types.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/migrateContent.test.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/migrateContent.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/puckConfig.test.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/puckConfig.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgetCatalog.test.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgetCatalog.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/accent-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/link-arrow.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/rich-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/solid-icons.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/background-media.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/content-media.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/cta-button.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/empty-placeholder.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/eyebrow-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/heading.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/image-srcset.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/index.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/mask.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/section.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/social-icon.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/text-link.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/accordion-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/alert-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/app-store-badges-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/article-cards-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/article-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/call-to-action-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/call-to-action-styles.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/call-to-action-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/carousel-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/centered-hero-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/colored-action-list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/column-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/contact-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/contact-form-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/contact-form-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/container-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/definition-list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/eyebrow-section-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/faq-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/faq-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-card-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-cards-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-cards-styles.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-grid-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-showcase-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/gallery-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/grid-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/hero-widget-props.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/hero-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/html-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/iframe-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/leaderboard-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/logo-cloud-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/markdown-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/media-object-layout.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/media-object-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/media-object-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/media-object-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/newsletter-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/page-header-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/prose-section-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/quote-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/row-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/section-intro-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/signup-banner-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/simple-action-list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/site-footer-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/site-header-nav.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/site-header-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/social-banner-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/stats-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/steps-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/story-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/table-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/tabs-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/tags-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/testimonial-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/timeline-layout.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/timeline-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/timeline-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/under-construction-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/video-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/welcome-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/contracts/__init__.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/contracts/events.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/conversion/blocks-to-html.test.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/conversion/blocks-to-html.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/conversion/index.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/conversion/types.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/deps.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/diff.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/__init__.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/_deps.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/layout.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/revisions.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/uploads.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/workflow.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/checkbox-field.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/file-picker/file-gallery.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/file-picker/image-picker-field.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/file-picker/index.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/file-picker/types.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/index.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/media-adapter.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/useMediaUploads.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/usePageSchedule.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/layout_service.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/media_files.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/media_images.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/media_queries.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/pages/PublicPage.tsx +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/permissions.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/py.typed +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/security.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/service/_common.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/service/_revisions.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/services.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/settings.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/static/widgets-base.css +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/apply.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/extract.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/index.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/translatable-fields.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/translation.test.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/api.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/datetime.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/decorative-image.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/markdown.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/mediaFormat.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/parse-list.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/slugify.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/widgetUtils.ts +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/conftest.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_approval_workflow.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_auth_gating.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_csrf.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_image_processing.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_media_files.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_media_security.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_module.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_public_routes.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_public_viewer.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_revision_notes_and_diff.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_revisions_api.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_scheduled_publish.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_seo_meta.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_site_layout.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_sitemap_robots.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_uploads_api.py +0 -0
- {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_pagebuilder
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Drag-and-drop visual page builder for simple_module apps
|
|
5
5
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python_modules
|
|
6
6
|
Author-email: Anto Subash <antosubash@live.com>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Stage-grouped view of the page list — the board behind ``?view=board``.
|
|
2
|
+
|
|
3
|
+
The pipeline is two-state: a page is a draft or it is published. "Scheduled" is
|
|
4
|
+
not a third state, it is a draft carrying a future ``publish_at`` that will flip
|
|
5
|
+
itself; splitting it into its own column is a *display* decision, made here
|
|
6
|
+
rather than in the model precisely so nothing downstream has to learn a status
|
|
7
|
+
that does not exist.
|
|
8
|
+
|
|
9
|
+
Each column is capped and reports its own total, so a site with 400 published
|
|
10
|
+
pages renders three short columns and says how much it is not showing, instead
|
|
11
|
+
of paginating a board — which nobody can read anyway.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
|
|
19
|
+
from sqlalchemy import func, select
|
|
20
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
21
|
+
|
|
22
|
+
from pagebuilder.models import NOT_TRASHED, Page, PageStatus
|
|
23
|
+
|
|
24
|
+
DRAFT = "draft"
|
|
25
|
+
SCHEDULED = "scheduled"
|
|
26
|
+
IN_REVIEW = "in_review"
|
|
27
|
+
PUBLISHED = "published"
|
|
28
|
+
|
|
29
|
+
#: Rows shown per column before the "N more" line takes over.
|
|
30
|
+
PER_STAGE = 8
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Stage:
|
|
35
|
+
key: str
|
|
36
|
+
label: str
|
|
37
|
+
items: list[Page]
|
|
38
|
+
total: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _search_filter(search: str | None):
|
|
42
|
+
"""Reuses the escaping rule from ``list_pages`` — a title containing ``%``
|
|
43
|
+
is searched for literally rather than matching everything."""
|
|
44
|
+
if not search or not search.strip():
|
|
45
|
+
return None
|
|
46
|
+
term = search.strip().translate(str.maketrans({"%": r"\%", "_": r"\_", "\\": "\\\\"}))
|
|
47
|
+
pattern = f"%{term}%"
|
|
48
|
+
return Page.title.ilike(pattern, escape="\\") | Page.slug.ilike(pattern, escape="\\")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _conditions(stage: str, now: datetime) -> list:
|
|
52
|
+
"""What puts a page in this column.
|
|
53
|
+
|
|
54
|
+
Templates are excluded from every column. They are ordinary pages, but they
|
|
55
|
+
are not *in* the pipeline — nobody publishes a template — so listing them
|
|
56
|
+
under Draft would put permanent residents in a column that is meant to read
|
|
57
|
+
as a queue. They stay reachable in the list view, and in the New page
|
|
58
|
+
dialog, which is where they are actually used.
|
|
59
|
+
|
|
60
|
+
Draft and scheduled are deliberately complementary halves of
|
|
61
|
+
``status == DRAFT``: a page with no ``publish_at``, or one whose date has
|
|
62
|
+
already passed, is simply a draft. Without the second half a scheduled page
|
|
63
|
+
whose moment arrived but whose job has not yet run would vanish from the
|
|
64
|
+
board entirely.
|
|
65
|
+
"""
|
|
66
|
+
not_template = Page.is_template.is_(False)
|
|
67
|
+
if stage == DRAFT:
|
|
68
|
+
return [
|
|
69
|
+
NOT_TRASHED,
|
|
70
|
+
not_template,
|
|
71
|
+
Page.status == PageStatus.DRAFT,
|
|
72
|
+
(Page.publish_at.is_(None)) | (Page.publish_at <= now),
|
|
73
|
+
]
|
|
74
|
+
if stage == SCHEDULED:
|
|
75
|
+
return [NOT_TRASHED, not_template, Page.status == PageStatus.DRAFT, Page.publish_at > now]
|
|
76
|
+
if stage == IN_REVIEW:
|
|
77
|
+
return [NOT_TRASHED, not_template, Page.status == PageStatus.SUBMITTED_FOR_REVIEW]
|
|
78
|
+
return [NOT_TRASHED, not_template, Page.status == PageStatus.PUBLISHED]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_ORDER = [
|
|
82
|
+
(DRAFT, "Draft"),
|
|
83
|
+
(SCHEDULED, "Scheduled"),
|
|
84
|
+
(IN_REVIEW, "In review"),
|
|
85
|
+
(PUBLISHED, "Published"),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def load(
|
|
90
|
+
db: AsyncSession, *, search: str | None = None, per_stage: int = PER_STAGE
|
|
91
|
+
) -> list[Stage]:
|
|
92
|
+
"""Every column, in pipeline order.
|
|
93
|
+
|
|
94
|
+
``In review`` is returned even when empty; the caller drops it, because
|
|
95
|
+
whether the approval workflow is in use is a question about the site rather
|
|
96
|
+
than about this query.
|
|
97
|
+
"""
|
|
98
|
+
now = datetime.now(UTC)
|
|
99
|
+
search_clause = _search_filter(search)
|
|
100
|
+
|
|
101
|
+
stages: list[Stage] = []
|
|
102
|
+
for key, label in _ORDER:
|
|
103
|
+
clauses = _conditions(key, now)
|
|
104
|
+
if search_clause is not None:
|
|
105
|
+
clauses = [*clauses, search_clause]
|
|
106
|
+
|
|
107
|
+
total = await db.scalar(select(func.count()).select_from(Page).where(*clauses))
|
|
108
|
+
# Scheduled sorts by when it fires — the next one to go live is the one
|
|
109
|
+
# worth seeing first. Every other column is newest-touched first.
|
|
110
|
+
order = Page.publish_at.asc() if key == SCHEDULED else Page.id.desc()
|
|
111
|
+
rows = await db.execute(select(Page).where(*clauses).order_by(order).limit(per_stage))
|
|
112
|
+
stages.append(
|
|
113
|
+
Stage(key=key, label=label, items=list(rows.scalars().all()), total=int(total or 0))
|
|
114
|
+
)
|
|
115
|
+
return stages
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def to_payload(stages: list[Stage], serialize) -> list[dict]:
|
|
119
|
+
"""Stages as JSON for the Inertia page.
|
|
120
|
+
|
|
121
|
+
``serialize`` turns one ``Page`` into a dict — passed in rather than
|
|
122
|
+
imported so this module keeps knowing nothing about the schemas.
|
|
123
|
+
|
|
124
|
+
``In review`` is dropped when empty: an empty column would advertise an
|
|
125
|
+
approval workflow the site may not use, while a non-empty one must never be
|
|
126
|
+
hidden or its pages are stranded with no route to them.
|
|
127
|
+
"""
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
"key": stage.key,
|
|
131
|
+
"label": stage.label,
|
|
132
|
+
"total": stage.total,
|
|
133
|
+
"items": [serialize(item) for item in stage.items],
|
|
134
|
+
}
|
|
135
|
+
for stage in stages
|
|
136
|
+
if stage.key != IN_REVIEW or stage.total
|
|
137
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertDialog,
|
|
3
|
+
AlertDialogAction,
|
|
4
|
+
AlertDialogCancel,
|
|
5
|
+
AlertDialogContent,
|
|
6
|
+
AlertDialogDescription,
|
|
7
|
+
AlertDialogFooter,
|
|
8
|
+
AlertDialogHeader,
|
|
9
|
+
AlertDialogTitle,
|
|
10
|
+
AlertDialogTrigger,
|
|
11
|
+
} from '@simple-module-py/ui/components/ui/alert-dialog';
|
|
12
|
+
import { Input } from '@simple-module-py/ui/components/ui/input';
|
|
13
|
+
import { Label } from '@simple-module-py/ui/components/ui/label';
|
|
14
|
+
import { type ReactNode, useState } from 'react';
|
|
15
|
+
|
|
16
|
+
import { usePendingDialog } from '../hooks/usePendingDialog';
|
|
17
|
+
|
|
18
|
+
/** How much a confirmation is allowed to cost the person clicking it.
|
|
19
|
+
*
|
|
20
|
+
* A deliberate scale rather than a style choice: friction has to track blast
|
|
21
|
+
* radius, or people learn to click through the cheap confirmations and then
|
|
22
|
+
* click through the expensive one exactly the same way.
|
|
23
|
+
*
|
|
24
|
+
* - `low` — reversible; nothing is public. A plain button.
|
|
25
|
+
* - `medium` — this screen cannot undo it, but nothing live is lost: either it
|
|
26
|
+
* changes the public site while the content survives (unpublish), or it
|
|
27
|
+
* removes something for good that nothing references (an unused asset).
|
|
28
|
+
* - `high` — irreversible past the retention window. Requires typing a phrase.
|
|
29
|
+
*
|
|
30
|
+
* The axis is how hard the result is to walk back, not whether the public site
|
|
31
|
+
* changes. An irreversible delete does not get `low` because it happens to be
|
|
32
|
+
* invisible from outside.
|
|
33
|
+
*/
|
|
34
|
+
export type ConfirmLevel = 'low' | 'medium' | 'high';
|
|
35
|
+
|
|
36
|
+
const CONFIRM_PHRASE_INPUT = 'confirm-dialog-phrase';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Confirm a consequential action without handing the page to the browser.
|
|
40
|
+
*
|
|
41
|
+
* `confirm()` blocks the event loop, cannot say which row it is asking about
|
|
42
|
+
* beyond a bare sentence, is unstyleable, and — the reason it matters here —
|
|
43
|
+
* gives the failure nowhere to go: the call site learns the user said yes and
|
|
44
|
+
* then swallows whatever the request answered. This keeps the dialog open
|
|
45
|
+
* while the work runs and shows the error inside it, so a delete that comes
|
|
46
|
+
* back 403 is visible instead of looking like it worked.
|
|
47
|
+
*/
|
|
48
|
+
export function ConfirmDialog({
|
|
49
|
+
trigger,
|
|
50
|
+
title,
|
|
51
|
+
description,
|
|
52
|
+
confirmLabel = 'Confirm',
|
|
53
|
+
destructive = false,
|
|
54
|
+
level,
|
|
55
|
+
confirmPhrase,
|
|
56
|
+
children,
|
|
57
|
+
onConfirm,
|
|
58
|
+
}: {
|
|
59
|
+
trigger: ReactNode;
|
|
60
|
+
title: string;
|
|
61
|
+
description: ReactNode;
|
|
62
|
+
confirmLabel?: string;
|
|
63
|
+
destructive?: boolean;
|
|
64
|
+
/** Blast radius. Defaults from `destructive` so existing call sites keep
|
|
65
|
+
* the behaviour they had before the scale existed. */
|
|
66
|
+
level?: ConfirmLevel;
|
|
67
|
+
/** At `high`, the phrase that must be typed before the action unlocks.
|
|
68
|
+
* Ignored at the other levels. */
|
|
69
|
+
confirmPhrase?: string;
|
|
70
|
+
/** Extra controls — a reassignment target, a checkbox — above the buttons. */
|
|
71
|
+
children?: ReactNode;
|
|
72
|
+
/** Rejecting keeps the dialog open and surfaces the message. */
|
|
73
|
+
onConfirm: () => Promise<unknown>;
|
|
74
|
+
}) {
|
|
75
|
+
const [typed, setTyped] = useState('');
|
|
76
|
+
|
|
77
|
+
const resolvedLevel: ConfirmLevel = level ?? (destructive ? 'medium' : 'low');
|
|
78
|
+
// Compared exactly, case included. A forgiving match would defeat the point:
|
|
79
|
+
// the delay is the safeguard, and it only works while the phrase is actually
|
|
80
|
+
// being read off the screen.
|
|
81
|
+
const needsPhrase = resolvedLevel === 'high' && !!confirmPhrase;
|
|
82
|
+
const unlocked = !needsPhrase || typed === confirmPhrase;
|
|
83
|
+
|
|
84
|
+
// The open/pending/error lifecycle is shared with NoteDialog — see the hook
|
|
85
|
+
// for the invariants (in-flight request owns the dialog, failure keeps it
|
|
86
|
+
// open, closing clears the error). The `onClose` callback clears the typed
|
|
87
|
+
// phrase, because reopening must not inherit the one typed last time: that
|
|
88
|
+
// would put a second delete one click from confirmed, which is the exact
|
|
89
|
+
// property this level exists to remove.
|
|
90
|
+
const { open, pending, error, change, run } = usePendingDialog(`${confirmLabel} failed`, () =>
|
|
91
|
+
setTyped(''),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<AlertDialog open={open} onOpenChange={change}>
|
|
96
|
+
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
|
|
97
|
+
<AlertDialogContent>
|
|
98
|
+
<AlertDialogHeader>
|
|
99
|
+
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
100
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
101
|
+
</AlertDialogHeader>
|
|
102
|
+
{children && <div className="grid gap-2 py-2">{children}</div>}
|
|
103
|
+
{needsPhrase && (
|
|
104
|
+
<div className="grid gap-2 py-2">
|
|
105
|
+
<Label htmlFor={CONFIRM_PHRASE_INPUT}>
|
|
106
|
+
Type <code className="font-mono font-semibold">{confirmPhrase}</code> to confirm
|
|
107
|
+
</Label>
|
|
108
|
+
<Input
|
|
109
|
+
id={CONFIRM_PHRASE_INPUT}
|
|
110
|
+
value={typed}
|
|
111
|
+
autoComplete="off"
|
|
112
|
+
disabled={pending}
|
|
113
|
+
onChange={(e) => setTyped(e.target.value)}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
118
|
+
<AlertDialogFooter>
|
|
119
|
+
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
|
|
120
|
+
<AlertDialogAction
|
|
121
|
+
disabled={pending || !unlocked}
|
|
122
|
+
className={
|
|
123
|
+
resolvedLevel === 'low' ? '' : 'bg-destructive text-white hover:bg-destructive/90'
|
|
124
|
+
}
|
|
125
|
+
// Radix closes on click; the close has to wait for the request so
|
|
126
|
+
// a failure stays on screen.
|
|
127
|
+
onClick={(e) => {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
void run(onConfirm);
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{pending ? 'Working…' : confirmLabel}
|
|
133
|
+
</AlertDialogAction>
|
|
134
|
+
</AlertDialogFooter>
|
|
135
|
+
</AlertDialogContent>
|
|
136
|
+
</AlertDialog>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -15,6 +15,12 @@ function renderValue(value: unknown): string {
|
|
|
15
15
|
* Compact, scannable diff: metadata table (only changed fields) plus
|
|
16
16
|
* three lists of blocks (added / removed / changed). Designed to fit
|
|
17
17
|
* inside the editor's history panel without a separate page.
|
|
18
|
+
*
|
|
19
|
+
* The added/removed greens and reds below are deliberately literal rather than
|
|
20
|
+
* theme tokens. They are a matched pair carrying meaning, and the token set has
|
|
21
|
+
* `--destructive` but no success counterpart — converting only the red half
|
|
22
|
+
* would leave the pair mismatched. Give the design system a success token and
|
|
23
|
+
* both sides can move together.
|
|
18
24
|
*/
|
|
19
25
|
export function DiffSummary({ diff }: Props): React.JSX.Element {
|
|
20
26
|
const { metadata, blocks } = diff;
|
|
@@ -25,7 +31,7 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
|
|
|
25
31
|
if (totalChanges === 0) {
|
|
26
32
|
return (
|
|
27
33
|
<div
|
|
28
|
-
className="mt-3 rounded border
|
|
34
|
+
className="mt-3 rounded border bg-card p-3 text-xs text-muted-foreground"
|
|
29
35
|
data-testid="diff-summary"
|
|
30
36
|
>
|
|
31
37
|
No changes between these revisions.
|
|
@@ -34,13 +40,10 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
|
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
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
|
-
>
|
|
43
|
+
<div className="mt-3 space-y-2 rounded border bg-card p-3 text-xs" data-testid="diff-summary">
|
|
41
44
|
{metaKeys.length > 0 && (
|
|
42
45
|
<div>
|
|
43
|
-
<div className="font-semibold
|
|
46
|
+
<div className="font-semibold">Metadata</div>
|
|
44
47
|
<ul className="ml-3 list-disc">
|
|
45
48
|
{metaKeys.map((key) => (
|
|
46
49
|
<li key={key}>
|
|
@@ -61,7 +64,7 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
|
|
|
61
64
|
{blocks.added.map((b) => (
|
|
62
65
|
<li key={b.id}>
|
|
63
66
|
<span className="font-mono">{b.type ?? '?'}</span>{' '}
|
|
64
|
-
<span className="text-
|
|
67
|
+
<span className="text-muted-foreground">({b.id})</span>
|
|
65
68
|
</li>
|
|
66
69
|
))}
|
|
67
70
|
</ul>
|
|
@@ -74,7 +77,7 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
|
|
|
74
77
|
{blocks.removed.map((b) => (
|
|
75
78
|
<li key={b.id}>
|
|
76
79
|
<span className="font-mono">{b.type ?? '?'}</span>{' '}
|
|
77
|
-
<span className="text-
|
|
80
|
+
<span className="text-muted-foreground">({b.id})</span>
|
|
78
81
|
</li>
|
|
79
82
|
))}
|
|
80
83
|
</ul>
|
|
@@ -87,16 +90,14 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
|
|
|
87
90
|
{blocks.changed.map((b) => (
|
|
88
91
|
<li key={b.id}>
|
|
89
92
|
<span className="font-mono">{b.type ?? '?'}</span>{' '}
|
|
90
|
-
<span className="text-
|
|
93
|
+
<span className="text-muted-foreground">({b.id})</span>
|
|
91
94
|
{b.type_before && (
|
|
92
|
-
<span className="text-
|
|
95
|
+
<span className="text-muted-foreground">
|
|
93
96
|
{' '}
|
|
94
97
|
— was <span className="font-mono">{b.type_before}</span>
|
|
95
98
|
</span>
|
|
96
99
|
)}
|
|
97
|
-
{b.fields.length > 0 && (
|
|
98
|
-
<span className="text-gray-700">: {b.fields.join(', ')}</span>
|
|
99
|
-
)}
|
|
100
|
+
{b.fields.length > 0 && <span>: {b.fields.join(', ')}</span>}
|
|
100
101
|
</li>
|
|
101
102
|
))}
|
|
102
103
|
</ul>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { router } from '@inertiajs/react';
|
|
2
|
+
import { Button } from '@simple-module-py/ui/components/ui/button';
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
DialogTrigger,
|
|
11
|
+
} from '@simple-module-py/ui/components/ui/dialog';
|
|
12
|
+
import { Input } from '@simple-module-py/ui/components/ui/input';
|
|
13
|
+
import { Label } from '@simple-module-py/ui/components/ui/label';
|
|
14
|
+
import {
|
|
15
|
+
NativeSelect,
|
|
16
|
+
NativeSelectOptGroup,
|
|
17
|
+
} from '@simple-module-py/ui/components/ui/native-select';
|
|
18
|
+
import { useEffect, useState } from 'react';
|
|
19
|
+
|
|
20
|
+
import type { PageRead } from '../utils/api';
|
|
21
|
+
import { createPage, listPages, listTemplates } from '../utils/pagesApi';
|
|
22
|
+
import { slugify } from '../utils/slugify';
|
|
23
|
+
|
|
24
|
+
const TITLE_ID = 'new-page-title';
|
|
25
|
+
const SLUG_ID = 'new-page-slug';
|
|
26
|
+
const START_ID = 'new-page-start';
|
|
27
|
+
const PARENT_ID = 'new-page-parent';
|
|
28
|
+
|
|
29
|
+
const BLANK = '';
|
|
30
|
+
|
|
31
|
+
/** An empty Puck document — what "Blank" starts from. */
|
|
32
|
+
const EMPTY_DRAFT = { root: { props: { title: '', width: 'full' } }, content: [], zones: {} };
|
|
33
|
+
|
|
34
|
+
/** "New page" — title, URL, a starting point, and an optional breadcrumb parent.
|
|
35
|
+
*
|
|
36
|
+
* Four fields, all changeable later. The dialog exists because two of them are
|
|
37
|
+
* awkward to change *after* the fact: the URL leaves a stale link behind, and
|
|
38
|
+
* the starting point cannot be applied to a page that already has content.
|
|
39
|
+
*/
|
|
40
|
+
export function NewPageDialog({ publicPrefix = '/p' }: { publicPrefix?: string }) {
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const [title, setTitle] = useState('');
|
|
43
|
+
// Null until the author edits it: while it is null the slug tracks the title,
|
|
44
|
+
// and the moment they type their own it stops being overwritten.
|
|
45
|
+
const [slugOverride, setSlugOverride] = useState<string | null>(null);
|
|
46
|
+
const [startFrom, setStartFrom] = useState(BLANK);
|
|
47
|
+
const [parentId, setParentId] = useState('');
|
|
48
|
+
const [templates, setTemplates] = useState<PageRead[]>([]);
|
|
49
|
+
const [pages, setPages] = useState<PageRead[]>([]);
|
|
50
|
+
const [pending, setPending] = useState(false);
|
|
51
|
+
const [error, setError] = useState<string | null>(null);
|
|
52
|
+
|
|
53
|
+
const slug = slugOverride ?? slugify(title);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!open) return;
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
// The signal is threaded into the requests, not just checked afterwards:
|
|
59
|
+
// without it, closing the dialog left both fetches running to completion.
|
|
60
|
+
void Promise.all([listTemplates(controller.signal), listPages(controller.signal)])
|
|
61
|
+
.then(([t, p]) => {
|
|
62
|
+
if (controller.signal.aborted) return;
|
|
63
|
+
setTemplates(t.items);
|
|
64
|
+
setPages(p.items);
|
|
65
|
+
})
|
|
66
|
+
// Not worth a banner: without these the dialog still creates a blank
|
|
67
|
+
// page, which is the common case anyway.
|
|
68
|
+
.catch(() => {});
|
|
69
|
+
return () => controller.abort();
|
|
70
|
+
}, [open]);
|
|
71
|
+
|
|
72
|
+
const reset = (next: boolean) => {
|
|
73
|
+
if (pending) return;
|
|
74
|
+
setOpen(next);
|
|
75
|
+
if (!next) {
|
|
76
|
+
setTitle('');
|
|
77
|
+
setSlugOverride(null);
|
|
78
|
+
setStartFrom(BLANK);
|
|
79
|
+
setParentId('');
|
|
80
|
+
setError(null);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const create = async () => {
|
|
85
|
+
setPending(true);
|
|
86
|
+
setError(null);
|
|
87
|
+
try {
|
|
88
|
+
const page = await createPage({
|
|
89
|
+
title: title.trim(),
|
|
90
|
+
slug,
|
|
91
|
+
draft_data: EMPTY_DRAFT,
|
|
92
|
+
...(startFrom ? { copy_from_page_id: Number(startFrom) } : {}),
|
|
93
|
+
...(parentId ? { parent_id: Number(parentId) } : {}),
|
|
94
|
+
});
|
|
95
|
+
router.visit(`/pagebuilder/${page.id}/edit`);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
setError((e as Error).message);
|
|
98
|
+
setPending(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const copyable = pages.filter((p) => !p.is_template);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Dialog open={open} onOpenChange={reset}>
|
|
106
|
+
<DialogTrigger asChild>
|
|
107
|
+
<Button type="button">New page</Button>
|
|
108
|
+
</DialogTrigger>
|
|
109
|
+
<DialogContent>
|
|
110
|
+
<form
|
|
111
|
+
onSubmit={(e) => {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
if (title.trim() && slug && !pending) void create();
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<DialogHeader>
|
|
117
|
+
<DialogTitle>New page</DialogTitle>
|
|
118
|
+
<DialogDescription>
|
|
119
|
+
Four fields, all changeable later. Creating opens the editor.
|
|
120
|
+
</DialogDescription>
|
|
121
|
+
</DialogHeader>
|
|
122
|
+
|
|
123
|
+
<div className="grid gap-4 py-4">
|
|
124
|
+
<div className="grid gap-2">
|
|
125
|
+
<Label htmlFor={TITLE_ID}>Title</Label>
|
|
126
|
+
<Input
|
|
127
|
+
id={TITLE_ID}
|
|
128
|
+
value={title}
|
|
129
|
+
autoFocus
|
|
130
|
+
disabled={pending}
|
|
131
|
+
placeholder="Field methods"
|
|
132
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="grid gap-2">
|
|
137
|
+
<Label htmlFor={SLUG_ID}>URL</Label>
|
|
138
|
+
<div className="flex items-center gap-1">
|
|
139
|
+
<span className="text-sm text-muted-foreground">{publicPrefix}/</span>
|
|
140
|
+
<Input
|
|
141
|
+
id={SLUG_ID}
|
|
142
|
+
value={slug}
|
|
143
|
+
disabled={pending}
|
|
144
|
+
onChange={(e) => setSlugOverride(e.target.value)}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="grid gap-2">
|
|
150
|
+
<Label htmlFor={START_ID}>Start from</Label>
|
|
151
|
+
<NativeSelect
|
|
152
|
+
id={START_ID}
|
|
153
|
+
value={startFrom}
|
|
154
|
+
disabled={pending}
|
|
155
|
+
onChange={(e) => setStartFrom(e.target.value)}
|
|
156
|
+
>
|
|
157
|
+
<option value={BLANK}>Blank</option>
|
|
158
|
+
{templates.length > 0 && (
|
|
159
|
+
<NativeSelectOptGroup label="Templates">
|
|
160
|
+
{templates.map((t) => (
|
|
161
|
+
<option key={t.id} value={String(t.id)}>
|
|
162
|
+
{t.title}
|
|
163
|
+
</option>
|
|
164
|
+
))}
|
|
165
|
+
</NativeSelectOptGroup>
|
|
166
|
+
)}
|
|
167
|
+
{copyable.length > 0 && (
|
|
168
|
+
<NativeSelectOptGroup label="Copy a page">
|
|
169
|
+
{copyable.map((p) => (
|
|
170
|
+
<option key={p.id} value={String(p.id)}>
|
|
171
|
+
{p.title}
|
|
172
|
+
</option>
|
|
173
|
+
))}
|
|
174
|
+
</NativeSelectOptGroup>
|
|
175
|
+
)}
|
|
176
|
+
</NativeSelect>
|
|
177
|
+
<p className="text-xs text-muted-foreground">
|
|
178
|
+
Templates are ordinary pages flagged as templates, so the set grows without a
|
|
179
|
+
developer.
|
|
180
|
+
</p>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="grid gap-2">
|
|
184
|
+
<Label htmlFor={PARENT_ID}>Parent</Label>
|
|
185
|
+
<NativeSelect
|
|
186
|
+
id={PARENT_ID}
|
|
187
|
+
value={parentId}
|
|
188
|
+
disabled={pending}
|
|
189
|
+
onChange={(e) => setParentId(e.target.value)}
|
|
190
|
+
>
|
|
191
|
+
<option value="">None</option>
|
|
192
|
+
{pages.map((p) => (
|
|
193
|
+
<option key={p.id} value={String(p.id)}>
|
|
194
|
+
{p.title}
|
|
195
|
+
</option>
|
|
196
|
+
))}
|
|
197
|
+
</NativeSelect>
|
|
198
|
+
<p className="text-xs text-muted-foreground">
|
|
199
|
+
Optional. Affects the breadcrumb, not the URL.
|
|
200
|
+
</p>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<DialogFooter>
|
|
207
|
+
<Button type="button" variant="outline" disabled={pending} onClick={() => reset(false)}>
|
|
208
|
+
Cancel
|
|
209
|
+
</Button>
|
|
210
|
+
<Button type="submit" disabled={pending || !title.trim() || !slug}>
|
|
211
|
+
{pending ? 'Creating…' : 'Create and open editor'}
|
|
212
|
+
</Button>
|
|
213
|
+
</DialogFooter>
|
|
214
|
+
</form>
|
|
215
|
+
</DialogContent>
|
|
216
|
+
</Dialog>
|
|
217
|
+
);
|
|
218
|
+
}
|