simple-module-pagebuilder 0.0.1__py3-none-any.whl
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.
- pagebuilder/__init__.py +0 -0
- pagebuilder/components/DiffSummary.tsx +107 -0
- pagebuilder/components/ScheduledBadge.tsx +34 -0
- pagebuilder/components/StatusBadge.tsx +21 -0
- pagebuilder/components/blockRegistry.test.ts +56 -0
- pagebuilder/components/blockRegistry.ts +80 -0
- pagebuilder/components/blocks/Button.tsx +142 -0
- pagebuilder/components/blocks/Columns.tsx +64 -0
- pagebuilder/components/blocks/Heading.tsx +92 -0
- pagebuilder/components/blocks/Image.tsx +125 -0
- pagebuilder/components/blocks/MediaPicker.tsx +178 -0
- pagebuilder/components/blocks/Spacer.tsx +31 -0
- pagebuilder/components/blocks/Text.tsx +80 -0
- pagebuilder/components/editor/PageEditorToolbar.tsx +169 -0
- pagebuilder/components/editor/RevisionHistoryPanel.tsx +95 -0
- pagebuilder/components/editor/SchedulePanel.tsx +85 -0
- pagebuilder/components/editor/SeoSettingsPanel.tsx +119 -0
- pagebuilder/components/layoutPuckConfig.tsx +109 -0
- pagebuilder/components/media/MediaFilters.tsx +83 -0
- pagebuilder/components/media/MediaFolderSidebar.tsx +97 -0
- pagebuilder/components/media/MediaGrid.tsx +76 -0
- pagebuilder/components/media/MediaHeader.tsx +37 -0
- pagebuilder/components/media/MediaUploadQueue.tsx +103 -0
- pagebuilder/components/media/types.ts +20 -0
- pagebuilder/components/migrateContent.test.ts +146 -0
- pagebuilder/components/migrateContent.ts +135 -0
- pagebuilder/components/puckConfig.test.tsx +118 -0
- pagebuilder/components/puckConfig.tsx +121 -0
- pagebuilder/components/widgetCatalog.test.tsx +123 -0
- pagebuilder/components/widgetCatalog.ts +208 -0
- pagebuilder/components/widgets/_internal/accent-text.tsx +60 -0
- pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +14 -0
- pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +71 -0
- pagebuilder/components/widgets/_internal/link-arrow.tsx +23 -0
- pagebuilder/components/widgets/_internal/rich-text.tsx +243 -0
- pagebuilder/components/widgets/_internal/solid-icons.tsx +73 -0
- pagebuilder/components/widgets/_shared/background-media.tsx +74 -0
- pagebuilder/components/widgets/_shared/content-media.tsx +43 -0
- pagebuilder/components/widgets/_shared/cta-button.tsx +69 -0
- pagebuilder/components/widgets/_shared/empty-placeholder.tsx +15 -0
- pagebuilder/components/widgets/_shared/eyebrow-text.tsx +27 -0
- pagebuilder/components/widgets/_shared/heading.tsx +88 -0
- pagebuilder/components/widgets/_shared/index.ts +39 -0
- pagebuilder/components/widgets/_shared/mask.ts +54 -0
- pagebuilder/components/widgets/_shared/section.tsx +60 -0
- pagebuilder/components/widgets/_shared/social-icon.tsx +71 -0
- pagebuilder/components/widgets/_shared/text-link.tsx +47 -0
- pagebuilder/components/widgets/accordion-widget.tsx +64 -0
- pagebuilder/components/widgets/alert-widget.tsx +47 -0
- pagebuilder/components/widgets/app-store-badges-widget.tsx +91 -0
- pagebuilder/components/widgets/article-cards-render.tsx +132 -0
- pagebuilder/components/widgets/article-cards-widget.tsx +72 -0
- pagebuilder/components/widgets/call-to-action-render.tsx +285 -0
- pagebuilder/components/widgets/call-to-action-styles.ts +28 -0
- pagebuilder/components/widgets/call-to-action-widget.tsx +83 -0
- pagebuilder/components/widgets/carousel-widget.tsx +97 -0
- pagebuilder/components/widgets/centered-hero-widget.tsx +54 -0
- pagebuilder/components/widgets/colored-action-list-widget.tsx +117 -0
- pagebuilder/components/widgets/column-widget.tsx +63 -0
- pagebuilder/components/widgets/contact-cards-widget.tsx +172 -0
- pagebuilder/components/widgets/contact-form-render.tsx +293 -0
- pagebuilder/components/widgets/contact-form-widget.tsx +69 -0
- pagebuilder/components/widgets/container-widget.tsx +93 -0
- pagebuilder/components/widgets/definition-list-widget.tsx +144 -0
- pagebuilder/components/widgets/divider-widget.tsx +56 -0
- pagebuilder/components/widgets/eyebrow-section-widget.tsx +56 -0
- pagebuilder/components/widgets/faq-render.tsx +268 -0
- pagebuilder/components/widgets/faq-widget.tsx +64 -0
- pagebuilder/components/widgets/feature-card-widget.tsx +98 -0
- pagebuilder/components/widgets/feature-cards-render.tsx +258 -0
- pagebuilder/components/widgets/feature-cards-styles.ts +107 -0
- pagebuilder/components/widgets/feature-cards-widget.tsx +133 -0
- pagebuilder/components/widgets/feature-grid-widget.tsx +118 -0
- pagebuilder/components/widgets/feature-showcase-widget.tsx +118 -0
- pagebuilder/components/widgets/gallery-widget.tsx +139 -0
- pagebuilder/components/widgets/grid-widget.tsx +59 -0
- pagebuilder/components/widgets/hero-widget.tsx +292 -0
- pagebuilder/components/widgets/html-widget.tsx +42 -0
- pagebuilder/components/widgets/iframe-widget.tsx +44 -0
- pagebuilder/components/widgets/leaderboard-widget.tsx +203 -0
- pagebuilder/components/widgets/list-widget.tsx +56 -0
- pagebuilder/components/widgets/logo-cloud-widget.tsx +127 -0
- pagebuilder/components/widgets/markdown-widget.tsx +148 -0
- pagebuilder/components/widgets/media-object-layout.ts +156 -0
- pagebuilder/components/widgets/media-object-render.tsx +284 -0
- pagebuilder/components/widgets/media-object-text.tsx +202 -0
- pagebuilder/components/widgets/media-object-widget.tsx +176 -0
- pagebuilder/components/widgets/newsletter-widget.tsx +217 -0
- pagebuilder/components/widgets/page-header-widget.tsx +118 -0
- pagebuilder/components/widgets/prose-section-widget.tsx +99 -0
- pagebuilder/components/widgets/quote-widget.tsx +51 -0
- pagebuilder/components/widgets/row-widget.tsx +74 -0
- pagebuilder/components/widgets/section-intro-widget.tsx +143 -0
- pagebuilder/components/widgets/signup-banner-widget.tsx +50 -0
- pagebuilder/components/widgets/simple-action-list-widget.tsx +91 -0
- pagebuilder/components/widgets/site-footer-widget.tsx +97 -0
- pagebuilder/components/widgets/site-header-nav.tsx +153 -0
- pagebuilder/components/widgets/site-header-widget.tsx +182 -0
- pagebuilder/components/widgets/social-banner-widget.tsx +83 -0
- pagebuilder/components/widgets/stats-widget.tsx +200 -0
- pagebuilder/components/widgets/steps-widget.tsx +211 -0
- pagebuilder/components/widgets/story-cards-widget.tsx +182 -0
- pagebuilder/components/widgets/table-widget.tsx +86 -0
- pagebuilder/components/widgets/tabs-widget.tsx +95 -0
- pagebuilder/components/widgets/tags-widget.tsx +85 -0
- pagebuilder/components/widgets/testimonial-widget.tsx +102 -0
- pagebuilder/components/widgets/timeline-layout.ts +73 -0
- pagebuilder/components/widgets/timeline-render.tsx +294 -0
- pagebuilder/components/widgets/timeline-widget.tsx +106 -0
- pagebuilder/components/widgets/under-construction-widget.tsx +40 -0
- pagebuilder/components/widgets/video-widget.tsx +103 -0
- pagebuilder/components/widgets/welcome-widget.tsx +38 -0
- pagebuilder/contracts/__init__.py +57 -0
- pagebuilder/contracts/events.py +25 -0
- pagebuilder/contracts/schemas.py +239 -0
- pagebuilder/conversion/blocks-to-html.test.ts +105 -0
- pagebuilder/conversion/blocks-to-html.ts +102 -0
- pagebuilder/conversion/index.ts +2 -0
- pagebuilder/conversion/types.ts +5 -0
- pagebuilder/deps.py +26 -0
- pagebuilder/diff.py +129 -0
- pagebuilder/endpoints/__init__.py +0 -0
- pagebuilder/endpoints/api/__init__.py +21 -0
- pagebuilder/endpoints/api/_deps.py +21 -0
- pagebuilder/endpoints/api/layout.py +78 -0
- pagebuilder/endpoints/api/pages.py +85 -0
- pagebuilder/endpoints/api/revisions.py +79 -0
- pagebuilder/endpoints/api/uploads.py +92 -0
- pagebuilder/endpoints/api/workflow.py +124 -0
- pagebuilder/endpoints/seo.py +95 -0
- pagebuilder/endpoints/views.py +228 -0
- pagebuilder/fields/checkbox-field.tsx +22 -0
- pagebuilder/fields/file-picker/file-gallery.tsx +152 -0
- pagebuilder/fields/file-picker/image-picker-field.tsx +214 -0
- pagebuilder/fields/file-picker/index.ts +6 -0
- pagebuilder/fields/file-picker/types.ts +15 -0
- pagebuilder/fields/index.ts +9 -0
- pagebuilder/fields/media-adapter.ts +47 -0
- pagebuilder/hooks/useAutosave.ts +134 -0
- pagebuilder/hooks/useEditorForm.ts +193 -0
- pagebuilder/hooks/useMediaUploads.ts +96 -0
- pagebuilder/hooks/usePageRevisions.ts +59 -0
- pagebuilder/hooks/usePageSchedule.ts +72 -0
- pagebuilder/hooks/usePageWorkflow.ts +173 -0
- pagebuilder/layout_service.py +101 -0
- pagebuilder/media_images.py +185 -0
- pagebuilder/media_queries.py +92 -0
- pagebuilder/media_service.py +222 -0
- pagebuilder/models.py +209 -0
- pagebuilder/module.py +279 -0
- pagebuilder/package.json +18 -0
- pagebuilder/pages/LayoutEditor.tsx +183 -0
- pagebuilder/pages/MediaLibrary.tsx +249 -0
- pagebuilder/pages/PageEditor.tsx +181 -0
- pagebuilder/pages/PageList.tsx +129 -0
- pagebuilder/pages/PendingReview.tsx +128 -0
- pagebuilder/pages/PublicPage.tsx +126 -0
- pagebuilder/permissions.py +44 -0
- pagebuilder/py.typed +0 -0
- pagebuilder/security.py +230 -0
- pagebuilder/service/__init__.py +141 -0
- pagebuilder/service/_common.py +24 -0
- pagebuilder/service/_revisions.py +92 -0
- pagebuilder/service/_workflow.py +207 -0
- pagebuilder/services.py +19 -0
- pagebuilder/settings.py +161 -0
- pagebuilder/static/widgets-base.css +57 -0
- pagebuilder/translation/apply.ts +54 -0
- pagebuilder/translation/extract.ts +141 -0
- pagebuilder/translation/index.ts +20 -0
- pagebuilder/translation/translatable-fields.ts +65 -0
- pagebuilder/translation/translation.test.ts +167 -0
- pagebuilder/utils/api.ts +12 -0
- pagebuilder/utils/datetime.ts +25 -0
- pagebuilder/utils/decorative-image.ts +15 -0
- pagebuilder/utils/editorSnapshot.ts +39 -0
- pagebuilder/utils/markdown.ts +160 -0
- pagebuilder/utils/mediaApi.ts +112 -0
- pagebuilder/utils/mediaFormat.ts +19 -0
- pagebuilder/utils/pagesApi.ts +111 -0
- pagebuilder/utils/parse-list.ts +7 -0
- pagebuilder/utils/request.ts +52 -0
- pagebuilder/utils/slugify.ts +9 -0
- pagebuilder/utils/types.ts +171 -0
- pagebuilder/utils/widgetUtils.ts +11 -0
- simple_module_pagebuilder-0.0.1.dist-info/METADATA +152 -0
- simple_module_pagebuilder-0.0.1.dist-info/RECORD +189 -0
- simple_module_pagebuilder-0.0.1.dist-info/WHEEL +4 -0
- simple_module_pagebuilder-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Page, layout, and revision calls against the pagebuilder admin API. */
|
|
2
|
+
|
|
3
|
+
import { request } from './request';
|
|
4
|
+
import type {
|
|
5
|
+
LayoutDetail,
|
|
6
|
+
LayoutRevisionRead,
|
|
7
|
+
LayoutUpdate,
|
|
8
|
+
PageDetail,
|
|
9
|
+
PageRead,
|
|
10
|
+
PageRevisionRead,
|
|
11
|
+
PageWritePayload,
|
|
12
|
+
RevisionDiff,
|
|
13
|
+
ScheduleRequest,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
export const createPage = (data: PageWritePayload & { title: string; slug: string }) =>
|
|
17
|
+
request<PageDetail>('/pages', { method: 'POST', body: JSON.stringify(data) });
|
|
18
|
+
|
|
19
|
+
export const savePage = (id: number, data: PageWritePayload) =>
|
|
20
|
+
request<PageDetail>(`/pages/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
21
|
+
|
|
22
|
+
export const deletePage = (id: number) => request<void>(`/pages/${id}`, { method: 'DELETE' });
|
|
23
|
+
|
|
24
|
+
function noteBody(note?: string | null): BodyInit | undefined {
|
|
25
|
+
return note ? JSON.stringify({ note }) : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const publishPage = (id: number, note?: string | null) =>
|
|
29
|
+
request<PageRead>(`/pages/${id}/publish`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
body: noteBody(note),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const unpublishPage = (id: number, note?: string | null) =>
|
|
35
|
+
request<PageRead>(`/pages/${id}/unpublish`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
body: noteBody(note),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const submitPage = (id: number, note?: string | null) =>
|
|
41
|
+
request<PageRead>(`/pages/${id}/submit`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body: noteBody(note),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const approvePage = (id: number, note?: string | null) =>
|
|
47
|
+
request<PageRead>(`/pages/${id}/approve`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
body: noteBody(note),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const rejectPage = (id: number, note: string) =>
|
|
53
|
+
request<PageRead>(`/pages/${id}/reject`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
body: JSON.stringify({ note }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const listPendingPages = () => request<{ items: PageRead[] }>('/pages/pending');
|
|
59
|
+
|
|
60
|
+
export const schedulePage = (id: number, body: ScheduleRequest) =>
|
|
61
|
+
request<PageRead>(`/pages/${id}/schedule`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Drive a rejection prompt → reject API round-trip. Returns the
|
|
68
|
+
* updated page on success, or a string describing why the call was
|
|
69
|
+
* skipped (cancelled / empty note / API error).
|
|
70
|
+
*
|
|
71
|
+
* Shared by the editor and the pending-review queue so both surfaces
|
|
72
|
+
* stay in sync on prompt copy + empty-note handling.
|
|
73
|
+
*/
|
|
74
|
+
export async function promptAndReject(id: number): Promise<PageRead | { skipped: string }> {
|
|
75
|
+
const note = window.prompt('Reason for rejection (shown to the editor):');
|
|
76
|
+
if (note === null) return { skipped: 'cancelled' };
|
|
77
|
+
const trimmed = note.trim();
|
|
78
|
+
if (!trimmed) return { skipped: 'A rejection note is required.' };
|
|
79
|
+
try {
|
|
80
|
+
return await rejectPage(id, trimmed);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return { skipped: e instanceof Error ? e.message : 'Reject failed' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const listRevisions = (id: number) =>
|
|
87
|
+
request<{ items: PageRevisionRead[] }>(`/pages/${id}/revisions`);
|
|
88
|
+
|
|
89
|
+
export const restoreRevision = (pageId: number, revisionId: number) =>
|
|
90
|
+
request<PageDetail>(`/pages/${pageId}/revisions/${revisionId}/restore`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const diffRevisions = (pageId: number, beforeId: number, afterId: number) =>
|
|
95
|
+
request<RevisionDiff>(`/pages/${pageId}/revisions/${beforeId}/diff/${afterId}`);
|
|
96
|
+
|
|
97
|
+
export const getLayout = () => request<LayoutDetail>('/layout');
|
|
98
|
+
|
|
99
|
+
export const saveLayout = (body: LayoutUpdate) =>
|
|
100
|
+
request<LayoutDetail>('/layout', {
|
|
101
|
+
method: 'PUT',
|
|
102
|
+
body: JSON.stringify(body),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const listLayoutRevisions = () =>
|
|
106
|
+
request<{ items: LayoutRevisionRead[] }>('/layout/revisions');
|
|
107
|
+
|
|
108
|
+
export const restoreLayoutRevision = (revisionId: number) =>
|
|
109
|
+
request<LayoutDetail>(`/layout/revisions/${revisionId}/restore`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fetch plumbing for the pagebuilder JSON admin API.
|
|
3
|
+
*
|
|
4
|
+
* All endpoints live under `/api/pagebuilder` and return JSON. We use
|
|
5
|
+
* plain fetch + same-origin cookies for auth, plus an `X-CSRF-Token`
|
|
6
|
+
* header on every mutating request (token is read from the
|
|
7
|
+
* `pagebuilder_csrf` cookie set by the view layer).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const BASE = '/api/pagebuilder';
|
|
11
|
+
|
|
12
|
+
export const CSRF_COOKIE = 'pagebuilder_csrf';
|
|
13
|
+
|
|
14
|
+
const UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
15
|
+
|
|
16
|
+
export function readCookie(name: string): string | null {
|
|
17
|
+
if (typeof document === 'undefined') return null;
|
|
18
|
+
const target = `${name}=`;
|
|
19
|
+
for (const piece of document.cookie.split(';')) {
|
|
20
|
+
const trimmed = piece.trim();
|
|
21
|
+
if (trimmed.startsWith(target)) {
|
|
22
|
+
return decodeURIComponent(trimmed.slice(target.length));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
29
|
+
const isFormData = init.body instanceof FormData;
|
|
30
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
31
|
+
const csrfHeaders: Record<string, string> = {};
|
|
32
|
+
if (UNSAFE_METHODS.has(method)) {
|
|
33
|
+
const token = readCookie(CSRF_COOKIE);
|
|
34
|
+
if (token) csrfHeaders['X-CSRF-Token'] = token;
|
|
35
|
+
}
|
|
36
|
+
const response = await fetch(`${BASE}${path}`, {
|
|
37
|
+
credentials: 'same-origin',
|
|
38
|
+
headers: {
|
|
39
|
+
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
|
40
|
+
Accept: 'application/json',
|
|
41
|
+
...csrfHeaders,
|
|
42
|
+
...(init.headers || {}),
|
|
43
|
+
},
|
|
44
|
+
...init,
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const text = await response.text();
|
|
48
|
+
throw new Error(`Request failed (${response.status}): ${text}`);
|
|
49
|
+
}
|
|
50
|
+
if (response.status === 204) return undefined as T;
|
|
51
|
+
return (await response.json()) as T;
|
|
52
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for the pagebuilder JSON admin API.
|
|
3
|
+
*
|
|
4
|
+
* These mirror the Pydantic DTOs in `pagebuilder.contracts.schemas`. Keep
|
|
5
|
+
* the two in sync when either changes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type PageStatus = 'draft' | 'submitted_for_review' | 'published';
|
|
9
|
+
|
|
10
|
+
export type RevisionEvent = 'publish' | 'unpublish' | 'submit' | 'approve' | 'reject';
|
|
11
|
+
|
|
12
|
+
export interface PageRead {
|
|
13
|
+
id: number;
|
|
14
|
+
slug: string;
|
|
15
|
+
title: string;
|
|
16
|
+
status: PageStatus;
|
|
17
|
+
has_published: boolean;
|
|
18
|
+
meta_description: string | null;
|
|
19
|
+
og_image: string | null;
|
|
20
|
+
canonical_url: string | null;
|
|
21
|
+
index_in_search: boolean;
|
|
22
|
+
rejection_note: string | null;
|
|
23
|
+
publish_at: string | null;
|
|
24
|
+
unpublish_at: string | null;
|
|
25
|
+
created_at: string;
|
|
26
|
+
updated_at: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PageDetail extends PageRead {
|
|
30
|
+
draft_data: Record<string, unknown>;
|
|
31
|
+
published_data: Record<string, unknown> | null;
|
|
32
|
+
json_ld: Record<string, unknown> | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LayoutDetail {
|
|
36
|
+
id: number;
|
|
37
|
+
header_data: Record<string, unknown>;
|
|
38
|
+
footer_data: Record<string, unknown>;
|
|
39
|
+
created_at: string;
|
|
40
|
+
updated_at: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LayoutRevisionRead {
|
|
44
|
+
id: number;
|
|
45
|
+
layout_id: number;
|
|
46
|
+
note: string | null;
|
|
47
|
+
created_at: string;
|
|
48
|
+
created_by: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LayoutUpdate {
|
|
52
|
+
header_data?: Record<string, unknown> | null;
|
|
53
|
+
footer_data?: Record<string, unknown> | null;
|
|
54
|
+
note?: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PageRevisionRead {
|
|
58
|
+
id: number;
|
|
59
|
+
page_id: number;
|
|
60
|
+
title: string;
|
|
61
|
+
meta_description: string | null;
|
|
62
|
+
og_image: string | null;
|
|
63
|
+
event: RevisionEvent;
|
|
64
|
+
note: string | null;
|
|
65
|
+
created_at: string;
|
|
66
|
+
created_by: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface MediaAssetVariant {
|
|
70
|
+
filename: string;
|
|
71
|
+
url: string;
|
|
72
|
+
content_type: string;
|
|
73
|
+
width: number;
|
|
74
|
+
height: number | null;
|
|
75
|
+
size_bytes: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface MediaAssetRead {
|
|
79
|
+
id: number;
|
|
80
|
+
filename: string;
|
|
81
|
+
original_filename: string;
|
|
82
|
+
content_type: string;
|
|
83
|
+
size_bytes: number;
|
|
84
|
+
url: string;
|
|
85
|
+
width: number | null;
|
|
86
|
+
height: number | null;
|
|
87
|
+
folder: string | null;
|
|
88
|
+
variants: Record<string, MediaAssetVariant>;
|
|
89
|
+
created_at: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MediaListResponse {
|
|
93
|
+
items: MediaAssetRead[];
|
|
94
|
+
next_cursor: number | null;
|
|
95
|
+
folders: string[];
|
|
96
|
+
/** Only present when the request used offset paging. */
|
|
97
|
+
total?: number | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface MediaListQuery {
|
|
101
|
+
search?: string;
|
|
102
|
+
content_type?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Folder filter. Omit for "any folder", pass an empty string for
|
|
105
|
+
* "Unfiled" (rows where folder IS NULL), or pass a path like
|
|
106
|
+
* "marketing/heros" to match an exact folder.
|
|
107
|
+
*/
|
|
108
|
+
folder?: string;
|
|
109
|
+
min_size_bytes?: number;
|
|
110
|
+
max_size_bytes?: number;
|
|
111
|
+
cursor?: number;
|
|
112
|
+
/** Offset paging, used by the image-picker gallery. Makes the response
|
|
113
|
+
* carry `total`; takes precedence over `cursor`. */
|
|
114
|
+
offset?: number;
|
|
115
|
+
limit?: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface PageWritePayload {
|
|
119
|
+
title?: string;
|
|
120
|
+
slug?: string;
|
|
121
|
+
meta_description?: string | null;
|
|
122
|
+
og_image?: string | null;
|
|
123
|
+
canonical_url?: string | null;
|
|
124
|
+
index_in_search?: boolean;
|
|
125
|
+
json_ld?: Record<string, unknown> | null;
|
|
126
|
+
draft_data?: Record<string, unknown>;
|
|
127
|
+
publish_at?: string | null;
|
|
128
|
+
unpublish_at?: string | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Body for ``POST /pages/{id}/schedule``.
|
|
133
|
+
*
|
|
134
|
+
* A field's *presence* tells the server "act on me" — omit a field to
|
|
135
|
+
* leave the stored value alone, send ``null`` to clear it, send an ISO
|
|
136
|
+
* timestamp to set it. So ``{publish_at: "2026-…"}`` schedules a publish
|
|
137
|
+
* without touching ``unpublish_at``.
|
|
138
|
+
*/
|
|
139
|
+
export interface ScheduleRequest {
|
|
140
|
+
publish_at?: string | null;
|
|
141
|
+
unpublish_at?: string | null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface BlockChange {
|
|
145
|
+
id: string;
|
|
146
|
+
type?: string;
|
|
147
|
+
fields: string[];
|
|
148
|
+
type_before?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface MetadataChange {
|
|
152
|
+
before: unknown;
|
|
153
|
+
after: unknown;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface RevisionDiff {
|
|
157
|
+
before_id: number;
|
|
158
|
+
after_id: number;
|
|
159
|
+
metadata: Record<string, MetadataChange>;
|
|
160
|
+
blocks: {
|
|
161
|
+
added: BlockChange[];
|
|
162
|
+
removed: BlockChange[];
|
|
163
|
+
changed: BlockChange[];
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface UploadMediaOptions {
|
|
168
|
+
folder?: string | null;
|
|
169
|
+
onProgress?: (loaded: number, total: number) => void;
|
|
170
|
+
signal?: AbortSignal;
|
|
171
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Barrel for helpers the ported GCA widgets import as a group.
|
|
2
|
+
*
|
|
3
|
+
* Mirrors the source package's `utils` barrel. `cn` came from @geowiki/shared
|
|
4
|
+
* there; here it comes from the UI package we already depend on, so there's no
|
|
5
|
+
* second clsx/tailwind-merge implementation in the tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { cn } from '@simple-module-py/ui/lib/utils';
|
|
9
|
+
|
|
10
|
+
export { decorativeAriaProps } from './decorative-image';
|
|
11
|
+
export { parseList } from './parse-list';
|
|
@@ -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
|