pages_core 3.13.0 → 3.15.0
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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/assets/builds/pages_core/admin-dist.js +19 -8
- data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
- data/app/assets/builds/pages_core/admin.css +704 -388
- data/app/assets/fonts/Inter-Black.woff2 +0 -0
- data/app/assets/fonts/Inter-BlackItalic.woff2 +0 -0
- data/app/assets/fonts/Inter-Bold.woff2 +0 -0
- data/app/assets/fonts/Inter-BoldItalic.woff2 +0 -0
- data/app/assets/fonts/Inter-ExtraBold.woff2 +0 -0
- data/app/assets/fonts/Inter-ExtraBoldItalic.woff2 +0 -0
- data/app/assets/fonts/Inter-ExtraLight.woff2 +0 -0
- data/app/assets/fonts/Inter-ExtraLightItalic.woff2 +0 -0
- data/app/assets/fonts/Inter-Italic.woff2 +0 -0
- data/app/assets/fonts/Inter-Light.woff2 +0 -0
- data/app/assets/fonts/Inter-LightItalic.woff2 +0 -0
- data/app/assets/fonts/Inter-Medium.woff2 +0 -0
- data/app/assets/fonts/Inter-MediumItalic.woff2 +0 -0
- data/app/assets/fonts/Inter-Regular.woff2 +0 -0
- data/app/assets/fonts/Inter-SemiBold.woff2 +0 -0
- data/app/assets/fonts/Inter-SemiBoldItalic.woff2 +0 -0
- data/app/assets/fonts/Inter-Thin.woff2 +0 -0
- data/app/assets/fonts/Inter-ThinItalic.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-Black.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-BlackItalic.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-Bold.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-BoldItalic.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-ExtraBold.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-ExtraBoldItalic.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-ExtraLight.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-ExtraLightItalic.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-Italic.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-Light.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-LightItalic.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-Medium.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-MediumItalic.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-Regular.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-SemiBold.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-SemiBoldItalic.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-Thin.woff2 +0 -0
- data/app/assets/fonts/InterDisplay-ThinItalic.woff2 +0 -0
- data/app/assets/fonts/InterVariable-Italic.woff2 +0 -0
- data/app/assets/fonts/InterVariable.woff2 +0 -0
- data/app/assets/stylesheets/pages_core/admin/components/archive.css +1 -1
- data/app/assets/stylesheets/pages_core/admin/components/attachments.css +22 -34
- data/app/assets/stylesheets/pages_core/admin/components/base.css +1 -68
- data/app/assets/stylesheets/pages_core/admin/components/forms.css +107 -48
- data/app/assets/stylesheets/pages_core/admin/components/header.css +56 -58
- data/app/assets/stylesheets/pages_core/admin/components/image_editor.css +35 -24
- data/app/assets/stylesheets/pages_core/admin/components/image_grid.css +28 -27
- data/app/assets/stylesheets/pages_core/admin/components/image_uploader.css +5 -5
- data/app/assets/stylesheets/pages_core/admin/components/layout.css +7 -1
- data/app/assets/stylesheets/pages_core/admin/components/list_table.css +24 -15
- data/app/assets/stylesheets/pages_core/admin/components/page_tree.css +63 -104
- data/app/assets/stylesheets/pages_core/admin/components/pagination.css +12 -13
- data/app/assets/stylesheets/pages_core/admin/components/search.css +1 -16
- data/app/assets/stylesheets/pages_core/admin/components/sidebar.css +5 -11
- data/app/assets/stylesheets/pages_core/admin/components/tag_editor.css +22 -36
- data/app/assets/stylesheets/pages_core/admin/components/toast.css +1 -2
- data/app/assets/stylesheets/pages_core/admin/components/toolbar.css +10 -10
- data/app/assets/stylesheets/pages_core/admin/components/totp.css +26 -0
- data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +37 -51
- data/app/assets/stylesheets/pages_core/admin/global/fonts.css +271 -0
- data/app/assets/stylesheets/pages_core/admin/global/typography.css +109 -0
- data/app/assets/stylesheets/pages_core/admin/vars.css +1 -3
- data/app/assets/stylesheets/pages_core/admin.postcss.css +1 -0
- data/app/controllers/admin/account_recoveries_controller.rb +87 -0
- data/app/controllers/admin/invites_controller.rb +3 -2
- data/app/controllers/admin/otp_secrets_controller.rb +45 -0
- data/app/controllers/admin/pages_controller.rb +22 -42
- data/app/controllers/admin/recovery_codes_controller.rb +32 -0
- data/app/controllers/admin/sessions_controller.rb +65 -0
- data/app/controllers/admin/users_controller.rb +2 -8
- data/app/controllers/concerns/pages_core/authentication.rb +12 -10
- data/app/controllers/concerns/pages_core/error_reporting.rb +1 -1
- data/app/controllers/concerns/pages_core/page_parameters.rb +29 -0
- data/app/controllers/concerns/pages_core/policies_helper.rb +1 -1
- data/app/controllers/concerns/pages_core/preview_pages_controller.rb +20 -20
- data/app/controllers/pages_core/admin_controller.rb +1 -3
- data/app/controllers/pages_core/frontend/pages_controller.rb +2 -6
- data/app/formatters/pages_core/html_formatter.rb +2 -4
- data/app/helpers/admin/menu_helper.rb +5 -4
- data/app/helpers/admin/pages_helper.rb +1 -21
- data/app/helpers/pages_core/admin/admin_helper.rb +13 -3
- data/app/helpers/pages_core/admin/content_tabs_helper.rb +1 -2
- data/app/helpers/pages_core/admin/labelled_field_helper.rb +1 -1
- data/app/helpers/pages_core/frontend_helper.rb +1 -1
- data/app/helpers/pages_core/images_helper.rb +10 -8
- data/app/helpers/pages_core/labelled_form_builder.rb +2 -7
- data/app/helpers/pages_core/page_path_helper.rb +1 -1
- data/app/javascript/components/Attachments/Attachment.tsx +20 -18
- data/app/javascript/components/Attachments/AttachmentEditor.tsx +11 -9
- data/app/javascript/components/{Attachments.jsx → Attachments/List.tsx} +58 -63
- data/app/javascript/components/Attachments/useAttachments.ts +15 -0
- data/app/javascript/components/Attachments.tsx +14 -0
- data/app/javascript/components/DateRangeSelect.tsx +105 -0
- data/app/javascript/components/DateTimeSelect.tsx +136 -0
- data/app/javascript/components/EditableImage.tsx +11 -9
- data/app/javascript/components/FileUploadButton.tsx +7 -7
- data/app/javascript/components/ImageCropper/FocalPoint.tsx +9 -12
- data/app/javascript/components/ImageCropper/Image.tsx +10 -8
- data/app/javascript/components/ImageCropper/Toolbar.tsx +11 -12
- data/app/javascript/components/ImageCropper/useCrop.ts +24 -53
- data/app/javascript/components/ImageCropper.tsx +10 -15
- data/app/javascript/components/ImageEditor/Form.tsx +12 -8
- data/app/javascript/components/ImageEditor.tsx +12 -7
- data/app/javascript/components/ImageGrid/DragElement.tsx +9 -12
- data/app/javascript/components/{ImageGrid.jsx → ImageGrid/Grid.tsx} +62 -71
- data/app/javascript/components/ImageGrid/GridImage.tsx +22 -23
- data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -2
- data/app/javascript/components/ImageGrid/useImageGrid.ts +26 -0
- data/app/javascript/components/ImageGrid.tsx +15 -0
- data/app/javascript/components/ImageUploader.tsx +35 -22
- data/app/javascript/components/LabelledField.tsx +34 -0
- data/app/javascript/components/Modal.tsx +2 -2
- data/app/javascript/components/PageForm/Block.tsx +81 -0
- data/app/javascript/components/PageForm/Content.tsx +54 -0
- data/app/javascript/components/PageForm/Dates.tsx +66 -0
- data/app/javascript/components/PageForm/Files.tsx +28 -0
- data/app/javascript/components/PageForm/Form.tsx +41 -0
- data/app/javascript/components/PageForm/Images.tsx +28 -0
- data/app/javascript/components/PageForm/LocaleLinks.tsx +36 -0
- data/app/javascript/components/PageForm/Metadata.tsx +67 -0
- data/app/javascript/components/PageForm/Options.tsx +180 -0
- data/app/javascript/components/PageForm/PageDescription.tsx +48 -0
- data/app/javascript/components/PageForm/PathSegment.tsx +65 -0
- data/app/javascript/components/PageForm/TabPanel.tsx +21 -0
- data/app/javascript/components/PageForm/Tabs.tsx +33 -0
- data/app/javascript/components/PageForm/UnconfiguredContent.tsx +42 -0
- data/app/javascript/components/PageForm/pageParams.ts +95 -0
- data/app/javascript/components/PageForm/preview.ts +23 -0
- data/app/javascript/components/PageForm/usePage.ts +169 -0
- data/app/javascript/components/PageForm/useTabs.ts +46 -0
- data/app/javascript/components/PageForm.tsx +163 -0
- data/app/javascript/components/PageImages.tsx +7 -9
- data/app/javascript/components/PageTree/Draggable.tsx +40 -39
- data/app/javascript/components/PageTree/Node.tsx +62 -56
- data/app/javascript/components/PageTree/PageName.tsx +28 -0
- data/app/javascript/components/PageTree.tsx +65 -53
- data/app/javascript/components/{RichTextArea.jsx → RichTextArea.tsx} +98 -79
- data/app/javascript/components/RichTextToolbarButton.tsx +4 -6
- data/app/javascript/components/TagEditor/AddTagForm.tsx +19 -12
- data/app/javascript/components/TagEditor/Editor.tsx +32 -0
- data/app/javascript/components/TagEditor/Tag.tsx +6 -4
- data/app/javascript/components/TagEditor/useTags.ts +58 -0
- data/app/javascript/components/TagEditor.tsx +8 -58
- data/app/javascript/components/Toast.tsx +3 -3
- data/app/javascript/components/drag/draggedOrder.ts +22 -14
- data/app/javascript/components/drag/useDragCollection.ts +35 -30
- data/app/javascript/components/drag/useDragUploader.ts +32 -21
- data/app/javascript/components/drag/useDraggable.ts +7 -6
- data/app/javascript/components/drag.ts +0 -1
- data/app/javascript/components.ts +1 -3
- data/app/javascript/features/RichText.tsx +2 -3
- data/app/javascript/features/contentTabs.ts +79 -0
- data/app/javascript/index.ts +5 -14
- data/app/javascript/lib/Tree.ts +31 -45
- data/app/javascript/lib/request.ts +11 -11
- data/app/javascript/stores/useToastStore.ts +1 -1
- data/app/javascript/types/Attachments.ts +29 -0
- data/app/javascript/types/Crop.ts +36 -0
- data/app/javascript/types/Drag.ts +34 -0
- data/app/javascript/types/Images.ts +47 -0
- data/app/javascript/types/PageEditor.ts +26 -0
- data/app/javascript/types/Pages.ts +75 -0
- data/app/javascript/types/Tags.ts +9 -0
- data/app/javascript/types/Template.ts +24 -0
- data/app/javascript/types/Trees.ts +19 -0
- data/app/javascript/types.ts +2 -25
- data/app/mailers/admin_mailer.rb +2 -2
- data/app/models/attachment.rb +1 -1
- data/app/models/concerns/pages_core/authenticable_user.rb +63 -0
- data/app/models/concerns/pages_core/emailable.rb +16 -0
- data/app/models/concerns/pages_core/page_model/templateable.rb +2 -16
- data/app/models/invite.rb +2 -6
- data/app/models/otp_secret.rb +101 -0
- data/app/models/page.rb +0 -3
- data/app/models/user.rb +2 -68
- data/app/policies/page_policy.rb +6 -2
- data/app/policies/user_policy.rb +4 -0
- data/app/resources/admin/page_resource.rb +95 -0
- data/app/resources/admin/page_tree_resource.rb +27 -0
- data/app/resources/admin/template_configuration_resource.rb +50 -0
- data/app/views/admin/account_recoveries/new.html.erb +22 -0
- data/app/views/admin/account_recoveries/show.html.erb +37 -0
- data/app/views/admin/invites/show.html.erb +1 -1
- data/app/views/admin/news/_sidebar.html.erb +2 -4
- data/app/views/admin/news/index.html.erb +0 -1
- data/app/views/admin/otp_secrets/create.html.erb +7 -0
- data/app/views/admin/otp_secrets/new.html.erb +60 -0
- data/app/views/admin/pages/_form.html.erb +10 -30
- data/app/views/admin/pages/_search_bar.html.erb +1 -1
- data/app/views/admin/pages/edit.html.erb +1 -57
- data/app/views/admin/pages/index.html.erb +1 -1
- data/app/views/admin/pages/new.html.erb +1 -44
- data/app/views/admin/recovery_codes/_codes.html.erb +14 -0
- data/app/views/admin/recovery_codes/create.html.erb +7 -0
- data/app/views/admin/recovery_codes/new.html.erb +11 -0
- data/app/views/admin/sessions/_otp_form.html.erb +13 -0
- data/app/views/admin/sessions/new.html.erb +31 -0
- data/app/views/admin/sessions/verify_otp.html.erb +19 -0
- data/app/views/admin/users/_access_control.html.erb +5 -1
- data/app/views/admin/users/_list.html.erb +12 -7
- data/app/views/admin/users/edit.html.erb +31 -1
- data/app/views/admin/users/new.html.erb +1 -1
- data/app/views/admin_mailer/account_recovery.text.erb +10 -0
- data/app/views/layouts/admin/_header.html.erb +3 -5
- data/app/views/layouts/admin/_page_header.html.erb +1 -2
- data/app/views/layouts/admin/_toast.html.erb +12 -0
- data/app/views/layouts/admin.html.erb +2 -2
- data/config/locales/en.yml +11 -7
- data/config/routes.rb +13 -12
- data/db/migrate/20240126160700_add_2fa_fields.rb +26 -0
- data/db/migrate/20240129201300_remove_password_reset_tokens.rb +13 -0
- data/db/migrate/20240131140700_change_email_to_citext.rb +18 -0
- data/db/migrate/20240201160700_remove_persistent_data.rb +7 -0
- data/db/migrate/20240508145300_remove_categories.rb +21 -0
- data/lib/pages_core/configuration/base.rb +2 -2
- data/lib/pages_core/templates/configuration.rb +1 -1
- data/lib/pages_core/templates/configuration_proxy.rb +2 -2
- data/lib/pages_core/templates/template_configuration.rb +11 -1
- data/lib/pages_core/templates.rb +6 -4
- data/lib/pages_core/version.rb +1 -1
- data/lib/pages_core.rb +6 -0
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +6 -7
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +17 -12
- data/lib/rails/generators/pages_core/rspec/rspec_generator.rb +0 -2
- data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +3 -4
- metadata +143 -35
- data/app/assets/stylesheets/pages_core/admin/components/login.css +0 -33
- data/app/controllers/admin/categories_controller.rb +0 -56
- data/app/controllers/admin/password_resets_controller.rb +0 -85
- data/app/controllers/concerns/pages_core/admin/persistent_params.rb +0 -75
- data/app/controllers/sessions_controller.rb +0 -27
- data/app/helpers/pages_core/admin/page_blocks_helper.rb +0 -66
- data/app/helpers/pages_core/admin/page_json_helper.rb +0 -23
- data/app/javascript/components/DateRangeSelect.jsx +0 -225
- data/app/javascript/components/PageDates.jsx +0 -73
- data/app/javascript/components/PageFiles.jsx +0 -25
- data/app/javascript/components/PageTree/types.ts +0 -15
- data/app/javascript/components/drag/types.ts +0 -28
- data/app/javascript/controllers/EditPageController.ts +0 -22
- data/app/javascript/controllers/LoginController.ts +0 -32
- data/app/javascript/controllers/MainController.ts +0 -74
- data/app/javascript/controllers/PageOptionsController.js +0 -67
- data/app/models/category.rb +0 -22
- data/app/models/page_category.rb +0 -6
- data/app/models/password_reset_token.rb +0 -34
- data/app/views/admin/pages/_edit_content.html.erb +0 -19
- data/app/views/admin/pages/_edit_files.html.erb +0 -4
- data/app/views/admin/pages/_edit_images.html.erb +0 -4
- data/app/views/admin/pages/_edit_metadata.html.erb +0 -35
- data/app/views/admin/pages/_edit_options.html.erb +0 -91
- data/app/views/admin/password_resets/show.html.erb +0 -21
- data/app/views/admin/users/login.html.erb +0 -65
- data/app/views/admin_mailer/password_reset.text.erb +0 -11
- data/lib/rails/generators/pages_core/rspec/templates/mailer_macros.rb +0 -11
@@ -0,0 +1,42 @@
|
|
1
|
+
import React from "react";
|
2
|
+
|
3
|
+
import * as PageEditor from "../../types/PageEditor";
|
4
|
+
import { MaybeLocalizedValue } from "../../types";
|
5
|
+
import { blockValue, errorsOn, unconfiguredBlocks } from "./usePage";
|
6
|
+
|
7
|
+
import Block from "./Block";
|
8
|
+
|
9
|
+
interface Props {
|
10
|
+
state: PageEditor.State;
|
11
|
+
dispatch: (action: PageEditor.Action) => void;
|
12
|
+
}
|
13
|
+
|
14
|
+
export default function UnconfiguredContent(props: Props) {
|
15
|
+
const { state, dispatch } = props;
|
16
|
+
|
17
|
+
const { page, locale, inputDir } = state;
|
18
|
+
|
19
|
+
const handleChange = (attr: string) => (value: MaybeLocalizedValue) => {
|
20
|
+
dispatch({ type: "updateBlocks", payload: { [attr]: value } });
|
21
|
+
};
|
22
|
+
|
23
|
+
return (
|
24
|
+
<React.Fragment>
|
25
|
+
<p>
|
26
|
+
This page has additional content fields not enabled by the selected
|
27
|
+
template.
|
28
|
+
</p>
|
29
|
+
{unconfiguredBlocks(state).map((b) => (
|
30
|
+
<Block
|
31
|
+
key={b.name}
|
32
|
+
block={b}
|
33
|
+
errors={errorsOn(page, b.name)}
|
34
|
+
dir={inputDir}
|
35
|
+
lang={locale}
|
36
|
+
onChange={handleChange(b.name)}
|
37
|
+
value={blockValue(state, b)}
|
38
|
+
/>
|
39
|
+
))}
|
40
|
+
</React.Fragment>
|
41
|
+
);
|
42
|
+
}
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import * as Attachments from "../../types/Attachments";
|
2
|
+
import * as Drag from "../../types/Drag";
|
3
|
+
import * as PageEditor from "../../types/PageEditor";
|
4
|
+
import * as Images from "../../types/Images";
|
5
|
+
import * as Tags from "../../types/Tags";
|
6
|
+
|
7
|
+
interface Options {
|
8
|
+
files: Attachments.State;
|
9
|
+
images: Images.GridState;
|
10
|
+
tags: Tags.State;
|
11
|
+
}
|
12
|
+
|
13
|
+
function dates(state: PageEditor.State) {
|
14
|
+
if (state.datesEnabled) {
|
15
|
+
return {
|
16
|
+
all_day: state.page.all_day,
|
17
|
+
starts_at: state.page.starts_at,
|
18
|
+
ends_at: state.page.ends_at
|
19
|
+
};
|
20
|
+
} else {
|
21
|
+
return {
|
22
|
+
all_day: false,
|
23
|
+
starts_at: null,
|
24
|
+
ends_at: null
|
25
|
+
};
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
function pageFiles(state: Attachments.State) {
|
30
|
+
const files = state.collection.draggables
|
31
|
+
.filter((r) => r !== "Files")
|
32
|
+
.map((r: Drag.Draggable<Attachments.Record>, i: number) => {
|
33
|
+
const a = r.record;
|
34
|
+
return { id: a.id, attachment_id: a.attachment.id, position: i + 1 };
|
35
|
+
});
|
36
|
+
const deleted = state.deleted.map((a) => {
|
37
|
+
return { id: a.id, attachment_id: a.attachment.id, _destroy: "true" };
|
38
|
+
});
|
39
|
+
return [...files, ...deleted];
|
40
|
+
}
|
41
|
+
|
42
|
+
function pageImages(state: Images.GridState) {
|
43
|
+
const primary = state.primary.draggables
|
44
|
+
.filter((r) => r !== "Files")
|
45
|
+
.map((r: Drag.Draggable<Images.Record>, i: number) => {
|
46
|
+
const pi = r.record;
|
47
|
+
return {
|
48
|
+
id: pi.id,
|
49
|
+
image_id: pi.image.id,
|
50
|
+
primary: true,
|
51
|
+
position: i + 1
|
52
|
+
};
|
53
|
+
});
|
54
|
+
const images = state.images.draggables
|
55
|
+
.filter((r) => r !== "Files")
|
56
|
+
.map((r: Drag.Draggable<Images.Record>, i: number) => {
|
57
|
+
const pi = r.record;
|
58
|
+
return {
|
59
|
+
id: pi.id,
|
60
|
+
image_id: pi.image.id,
|
61
|
+
primary: false,
|
62
|
+
position: primary.length + i + 1
|
63
|
+
};
|
64
|
+
});
|
65
|
+
|
66
|
+
const deleted = state.deleted.map((i) => {
|
67
|
+
return { id: i.id, image_id: i.image.id, _destroy: "true" };
|
68
|
+
});
|
69
|
+
return [...primary, ...images, ...deleted];
|
70
|
+
}
|
71
|
+
|
72
|
+
export default function pageParams(state: PageEditor.State, options: Options) {
|
73
|
+
const { files, images, tags } = options;
|
74
|
+
const { page } = state;
|
75
|
+
|
76
|
+
return {
|
77
|
+
...dates(state),
|
78
|
+
...page.blocks,
|
79
|
+
status: page.status,
|
80
|
+
published_at: page.published_at,
|
81
|
+
pinned: page.pinned,
|
82
|
+
template: page.template,
|
83
|
+
unique_name: page.unique_name,
|
84
|
+
feed_enabled: page.feed_enabled,
|
85
|
+
news_page: page.news_page,
|
86
|
+
user_id: page.user_id,
|
87
|
+
redirect_to: page.redirect_to,
|
88
|
+
serialized_tags: JSON.stringify(tags.enabled),
|
89
|
+
path_segment: page.path_segment,
|
90
|
+
meta_image_id: page.meta_image.image && page.meta_image.image.id,
|
91
|
+
parent_page_id: page.parent_page_id,
|
92
|
+
page_files_attributes: pageFiles(files),
|
93
|
+
page_images_attributes: pageImages(images)
|
94
|
+
};
|
95
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { csrfToken } from "../../lib/request";
|
2
|
+
|
3
|
+
function buildForm(url: string, body: Record<string, string>) {
|
4
|
+
const form = document.createElement("form");
|
5
|
+
form.action = url;
|
6
|
+
form.method = "POST";
|
7
|
+
form.target = "_blank";
|
8
|
+
for (const [name, value] of Object.entries(body)) {
|
9
|
+
const input = document.createElement("input");
|
10
|
+
input.type = "hidden";
|
11
|
+
input.name = name;
|
12
|
+
input.value = value;
|
13
|
+
form.appendChild(input);
|
14
|
+
}
|
15
|
+
return form;
|
16
|
+
}
|
17
|
+
|
18
|
+
export function openPreview(url: string, body: Record<string, string>) {
|
19
|
+
const form = buildForm(url, { authenticity_token: csrfToken(), ...body });
|
20
|
+
document.body.appendChild(form);
|
21
|
+
form.submit();
|
22
|
+
document.body.removeChild(form);
|
23
|
+
}
|
@@ -0,0 +1,169 @@
|
|
1
|
+
import { useReducer } from "react";
|
2
|
+
|
3
|
+
import * as PageEditor from "../../types/PageEditor";
|
4
|
+
import * as Pages from "../../types/Pages";
|
5
|
+
import * as Template from "../../types/Template";
|
6
|
+
import { LocalizedValue, MaybeLocalizedValue } from "../../types";
|
7
|
+
|
8
|
+
export function blockValue(
|
9
|
+
state: PageEditor.State,
|
10
|
+
block: Template.Block
|
11
|
+
): string {
|
12
|
+
if (block.localized) {
|
13
|
+
const value: LocalizedValue =
|
14
|
+
(state.page.blocks[block.name] as LocalizedValue) || {};
|
15
|
+
|
16
|
+
return value[state.locale] || "";
|
17
|
+
} else {
|
18
|
+
return (state.page.blocks[block.name] as string) || "";
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
export function errorsOn(page: Pages.Resource, attribute: string): string[] {
|
23
|
+
return page.errors
|
24
|
+
.filter((e) => e.attribute === attribute)
|
25
|
+
.map((e) => e.message);
|
26
|
+
}
|
27
|
+
|
28
|
+
export function unconfiguredBlocks(state: PageEditor.State): Template.Block[] {
|
29
|
+
const allBlocks: Record<string, Template.Block> = state.templates
|
30
|
+
.flatMap((t) => t.blocks)
|
31
|
+
.reduce((bs, b) => ({ [b.name]: b, ...bs }), {});
|
32
|
+
|
33
|
+
const anyValue = (v: MaybeLocalizedValue) => {
|
34
|
+
if (typeof v === "string") {
|
35
|
+
return v ? true : false;
|
36
|
+
} else {
|
37
|
+
return Object.values(v).filter((v) => v).length > 0;
|
38
|
+
}
|
39
|
+
};
|
40
|
+
|
41
|
+
const hasValue = Object.keys(allBlocks).filter((k) => {
|
42
|
+
const value = state.page.blocks[k] || "";
|
43
|
+
return anyValue(value);
|
44
|
+
});
|
45
|
+
|
46
|
+
const enabled = state.templateConfig.blocks.map((b) => b.name);
|
47
|
+
|
48
|
+
return hasValue
|
49
|
+
.filter((b) => enabled.indexOf(b) === -1)
|
50
|
+
.map((n) => allBlocks[n]);
|
51
|
+
}
|
52
|
+
|
53
|
+
function parseDate(str: string): Date | null {
|
54
|
+
if (!str) {
|
55
|
+
return null;
|
56
|
+
} else if (typeof str === "string") {
|
57
|
+
return new Date(str);
|
58
|
+
} else {
|
59
|
+
return str;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
function derivedState(state: PageEditor.State): PageEditor.State {
|
64
|
+
const { locale, locales, page, templates } = state;
|
65
|
+
return {
|
66
|
+
...state,
|
67
|
+
inputDir: (locales && locales[locale] && locales[locale].dir) || "ltr",
|
68
|
+
templateConfig: templates.filter(
|
69
|
+
(t) => t.template_name === page.template
|
70
|
+
)[0]
|
71
|
+
};
|
72
|
+
}
|
73
|
+
|
74
|
+
function parsedDates(page: Pages.SerializedResource) {
|
75
|
+
return {
|
76
|
+
published_at: parseDate(page.published_at),
|
77
|
+
starts_at: parseDate(page.starts_at),
|
78
|
+
ends_at: parseDate(page.ends_at)
|
79
|
+
};
|
80
|
+
}
|
81
|
+
|
82
|
+
function localizedAttributes(templates: Template.Config[]): string[] {
|
83
|
+
const allBlocks = (t: Template.Config): Template.Block[] => {
|
84
|
+
return [...t.blocks, ...t.metadata_blocks];
|
85
|
+
};
|
86
|
+
|
87
|
+
const blockNames = templates
|
88
|
+
.map(allBlocks)
|
89
|
+
.reduce((acc, val) => acc.concat(val), [])
|
90
|
+
.filter((b) => b.localized)
|
91
|
+
.map((b) => b.name)
|
92
|
+
.filter((value, index, array) => array.indexOf(value) === index);
|
93
|
+
|
94
|
+
return ["path_segment", ...blockNames];
|
95
|
+
}
|
96
|
+
|
97
|
+
function prepare(
|
98
|
+
state: PageEditor.State<Pages.SerializedResource>
|
99
|
+
): PageEditor.State {
|
100
|
+
const page = { ...state.page, ...parsedDates(state.page) };
|
101
|
+
return { ...state, page: page, datesEnabled: page.starts_at ? true : false };
|
102
|
+
}
|
103
|
+
|
104
|
+
function reducer(
|
105
|
+
state: PageEditor.State,
|
106
|
+
action: PageEditor.Action
|
107
|
+
): PageEditor.State {
|
108
|
+
const { type, payload } = action;
|
109
|
+
switch (type) {
|
110
|
+
case "setPage":
|
111
|
+
return prepare({ ...state, page: payload });
|
112
|
+
case "setDatesEnabled":
|
113
|
+
return { ...state, datesEnabled: payload };
|
114
|
+
case "setLocale":
|
115
|
+
return { ...state, locale: payload };
|
116
|
+
case "update":
|
117
|
+
return updatePage(state, payload);
|
118
|
+
case "updateBlocks":
|
119
|
+
return updatePageBlocks(state, payload);
|
120
|
+
default:
|
121
|
+
return state;
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
function updateLocalized<T>(
|
126
|
+
state: PageEditor.State,
|
127
|
+
obj: T,
|
128
|
+
attributes: Partial<T>
|
129
|
+
): T {
|
130
|
+
const { locale, templates } = state;
|
131
|
+
const nextObj = {};
|
132
|
+
|
133
|
+
Object.keys(attributes).forEach((attr: string) => {
|
134
|
+
const value = attributes[attr] as MaybeLocalizedValue;
|
135
|
+
if (localizedAttributes(templates).indexOf(attr) !== -1) {
|
136
|
+
nextObj[attr] = { ...obj[attr], [locale]: value } as LocalizedValue;
|
137
|
+
} else {
|
138
|
+
nextObj[attr] = value;
|
139
|
+
}
|
140
|
+
});
|
141
|
+
|
142
|
+
return { ...obj, ...nextObj };
|
143
|
+
}
|
144
|
+
|
145
|
+
function updatePageBlocks(
|
146
|
+
state: PageEditor.State,
|
147
|
+
attributes: Partial<Pages.Blocks>
|
148
|
+
): PageEditor.State {
|
149
|
+
const { page } = state;
|
150
|
+
|
151
|
+
return {
|
152
|
+
...state,
|
153
|
+
page: { ...page, blocks: updateLocalized(state, page.blocks, attributes) }
|
154
|
+
};
|
155
|
+
}
|
156
|
+
|
157
|
+
function updatePage(
|
158
|
+
state: PageEditor.State,
|
159
|
+
attributes: Partial<Pages.Resource>
|
160
|
+
): PageEditor.State {
|
161
|
+
return { ...state, page: updateLocalized(state, state.page, attributes) };
|
162
|
+
}
|
163
|
+
|
164
|
+
export default function usePage(
|
165
|
+
initialState: PageEditor.State<Pages.SerializedResource>
|
166
|
+
): [PageEditor.State, (action: PageEditor.Action) => void] {
|
167
|
+
const [state, dispatch] = useReducer(reducer, prepare(initialState));
|
168
|
+
return [derivedState(state), dispatch];
|
169
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { useState } from "react";
|
2
|
+
|
3
|
+
import * as PageEditor from "../../types/PageEditor";
|
4
|
+
import { unconfiguredBlocks } from "./usePage";
|
5
|
+
|
6
|
+
function tabsList(state: PageEditor.State): PageEditor.Tab[] {
|
7
|
+
const { templates, templateConfig } = state;
|
8
|
+
const tabs: PageEditor.Tab[] = [
|
9
|
+
{ id: "content", name: "Content", enabled: true }
|
10
|
+
];
|
11
|
+
if (templates.filter((t) => t.images).length > 0) {
|
12
|
+
tabs.push({ id: "images", name: "Images", enabled: templateConfig.images });
|
13
|
+
}
|
14
|
+
if (templates.filter((t) => t.files).length > 0) {
|
15
|
+
tabs.push({ id: "files", name: "Files", enabled: templateConfig.files });
|
16
|
+
}
|
17
|
+
tabs.push({ id: "metadata", name: "Metadata", enabled: true });
|
18
|
+
if (unconfiguredBlocks(state).length > 0) {
|
19
|
+
tabs.push({
|
20
|
+
id: "unconfigured-content",
|
21
|
+
name: "Unconfigured content",
|
22
|
+
enabled: true
|
23
|
+
});
|
24
|
+
}
|
25
|
+
return tabs;
|
26
|
+
}
|
27
|
+
|
28
|
+
function initialTab(tabs: PageEditor.Tab[]): string {
|
29
|
+
const tabExpression = /#(.*)$/;
|
30
|
+
if (document.location.toString().match(tabExpression)) {
|
31
|
+
const id = document.location.toString().match(tabExpression)[1];
|
32
|
+
const matchingTab = tabs.filter((t) => t.id == id)[0];
|
33
|
+
if (matchingTab) {
|
34
|
+
return matchingTab.id;
|
35
|
+
}
|
36
|
+
}
|
37
|
+
return tabs[0].id;
|
38
|
+
}
|
39
|
+
|
40
|
+
export default function useTabs(
|
41
|
+
state: PageEditor.State
|
42
|
+
): [PageEditor.Tab[], string, (tab: string) => void] {
|
43
|
+
const tabs = tabsList(state);
|
44
|
+
const [tab, setTab] = useState<string>(initialTab(tabs));
|
45
|
+
return [tabs, tab, setTab];
|
46
|
+
}
|
@@ -0,0 +1,163 @@
|
|
1
|
+
import React, { useEffect } from "react";
|
2
|
+
|
3
|
+
import { putJson, postJson } from "../lib/request";
|
4
|
+
import useToastStore from "../stores/useToastStore";
|
5
|
+
import * as Pages from "../types/Pages";
|
6
|
+
import * as Template from "../types/Template";
|
7
|
+
import { Locale } from "../types";
|
8
|
+
|
9
|
+
import { openPreview } from "./PageForm/preview";
|
10
|
+
import useAttachments from "./Attachments/useAttachments";
|
11
|
+
import useImageGrid from "./ImageGrid/useImageGrid";
|
12
|
+
import useTags from "./TagEditor/useTags";
|
13
|
+
import usePage from "./PageForm/usePage";
|
14
|
+
import useTabs from "./PageForm/useTabs";
|
15
|
+
import pageParams from "./PageForm/pageParams";
|
16
|
+
import Content from "./PageForm/Content";
|
17
|
+
import UnconfiguredContent from "./PageForm/UnconfiguredContent";
|
18
|
+
import Metadata from "./PageForm/Metadata";
|
19
|
+
import Form from "./PageForm/Form";
|
20
|
+
import PageDescription from "./PageForm/PageDescription";
|
21
|
+
import Options from "./PageForm/Options";
|
22
|
+
import Tabs from "./PageForm/Tabs";
|
23
|
+
import TabPanel from "./PageForm/TabPanel";
|
24
|
+
import Files from "./PageForm/Files";
|
25
|
+
import Images from "./PageForm/Images";
|
26
|
+
|
27
|
+
interface Props {
|
28
|
+
locale: string;
|
29
|
+
locales: { [index: string]: Locale };
|
30
|
+
page: Pages.SerializedResource;
|
31
|
+
templates: Template.Config[];
|
32
|
+
authors: Pages.Author[];
|
33
|
+
statuses: Pages.StatusLabels;
|
34
|
+
}
|
35
|
+
|
36
|
+
export default function PageForm(props: Props) {
|
37
|
+
const [state, dispatch] = usePage({
|
38
|
+
locales: props.locales,
|
39
|
+
locale: props.locale,
|
40
|
+
page: props.page,
|
41
|
+
templates: props.templates
|
42
|
+
});
|
43
|
+
const { page, locale, locales } = state;
|
44
|
+
|
45
|
+
const filesState = useAttachments(page.page_files);
|
46
|
+
const imagesState = useImageGrid(page.page_images, true);
|
47
|
+
const [tagsState, tagsDispatch] = useTags(
|
48
|
+
page.tags_and_suggestions,
|
49
|
+
page.enabled_tags
|
50
|
+
);
|
51
|
+
const [tabs, tab, setTab] = useTabs(state);
|
52
|
+
|
53
|
+
const errorToast = useToastStore((state) => state.error);
|
54
|
+
const noticeToast = useToastStore((state) => state.notice);
|
55
|
+
|
56
|
+
const params = () => {
|
57
|
+
return pageParams(state, {
|
58
|
+
files: filesState,
|
59
|
+
images: imagesState,
|
60
|
+
tags: tagsState
|
61
|
+
});
|
62
|
+
};
|
63
|
+
|
64
|
+
useEffect(() => {
|
65
|
+
const parentParam = page.parent_page_id
|
66
|
+
? `?parent=${page.parent_page_id}`
|
67
|
+
: "";
|
68
|
+
const pageUrl =
|
69
|
+
`/admin/${locale}/pages/` +
|
70
|
+
(page.id ? `${page.id}/edit` : `new${parentParam}`) +
|
71
|
+
`#${tab}`;
|
72
|
+
if (history) {
|
73
|
+
history.replaceState(null, "", pageUrl);
|
74
|
+
}
|
75
|
+
}, [page.id, locale, tab]);
|
76
|
+
|
77
|
+
const handlePreview = (evt: React.MouseEvent) => {
|
78
|
+
evt.preventDefault();
|
79
|
+
openPreview(`/${locale}/pages/preview`, {
|
80
|
+
page_id: `${page.id}`,
|
81
|
+
preview_page: JSON.stringify(params())
|
82
|
+
});
|
83
|
+
};
|
84
|
+
|
85
|
+
const handleSubmit = (evt: React.MouseEvent) => {
|
86
|
+
evt.preventDefault();
|
87
|
+
let method = postJson;
|
88
|
+
let url = `/admin/${locale}/pages.json`;
|
89
|
+
const data = {
|
90
|
+
page: pageParams(state, {
|
91
|
+
files: filesState,
|
92
|
+
images: imagesState,
|
93
|
+
tags: tagsState
|
94
|
+
})
|
95
|
+
};
|
96
|
+
|
97
|
+
if (page.id) {
|
98
|
+
method = putJson;
|
99
|
+
url = `/admin/${locale}/pages/${page.id}.json`;
|
100
|
+
}
|
101
|
+
|
102
|
+
method(url, data)
|
103
|
+
.then((response: Pages.SerializedResource) => {
|
104
|
+
dispatch({ type: "setPage", payload: response });
|
105
|
+
if (response.errors && response.errors.length > 0) {
|
106
|
+
errorToast("A validation error prevented the page from being saved.");
|
107
|
+
} else {
|
108
|
+
noticeToast("Your changes were saved");
|
109
|
+
}
|
110
|
+
})
|
111
|
+
.catch(() => {
|
112
|
+
errorToast("An error occured while saving your changes.");
|
113
|
+
});
|
114
|
+
};
|
115
|
+
|
116
|
+
return (
|
117
|
+
<Form state={state}>
|
118
|
+
<main>
|
119
|
+
<PageDescription state={state} dispatch={dispatch}>
|
120
|
+
<Tabs tabs={tabs} tab={tab} setTab={setTab} />
|
121
|
+
</PageDescription>
|
122
|
+
<div className="content">
|
123
|
+
<TabPanel active={tab == "content"}>
|
124
|
+
<Content
|
125
|
+
state={state}
|
126
|
+
dispatch={dispatch}
|
127
|
+
tagsState={tagsState}
|
128
|
+
tagsDispatch={tagsDispatch}
|
129
|
+
/>
|
130
|
+
</TabPanel>
|
131
|
+
<TabPanel active={tab == "unconfigured-content"}>
|
132
|
+
<UnconfiguredContent state={state} dispatch={dispatch} />
|
133
|
+
</TabPanel>
|
134
|
+
<TabPanel active={tab == "images"}>
|
135
|
+
<Images locale={locale} locales={locales} state={imagesState} />
|
136
|
+
</TabPanel>
|
137
|
+
<TabPanel active={tab == "files"}>
|
138
|
+
<Files locale={locale} locales={locales} state={filesState} />
|
139
|
+
</TabPanel>
|
140
|
+
<TabPanel active={tab == "metadata"}>
|
141
|
+
<Metadata state={state} dispatch={dispatch} />
|
142
|
+
</TabPanel>
|
143
|
+
<div className="buttons">
|
144
|
+
<button type="button" onClick={handlePreview}>
|
145
|
+
Preview
|
146
|
+
</button>
|
147
|
+
<button type="submit" onClick={handleSubmit}>
|
148
|
+
Save
|
149
|
+
</button>
|
150
|
+
</div>
|
151
|
+
</div>
|
152
|
+
</main>
|
153
|
+
<aside className="sidebar">
|
154
|
+
<Options
|
155
|
+
state={state}
|
156
|
+
dispatch={dispatch}
|
157
|
+
authors={props.authors}
|
158
|
+
statuses={props.statuses}
|
159
|
+
/>
|
160
|
+
</aside>
|
161
|
+
</Form>
|
162
|
+
);
|
163
|
+
}
|
@@ -1,19 +1,17 @@
|
|
1
1
|
import React from "react";
|
2
|
-
import { ImageResource, Locale } from "../types";
|
3
|
-
import ImageGrid from "./ImageGrid";
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
import * as Images from "../types/Images";
|
4
|
+
import { Locale } from "../types";
|
5
|
+
|
6
|
+
import ImageGrid from "./ImageGrid";
|
9
7
|
|
10
|
-
interface
|
8
|
+
interface Props {
|
11
9
|
locale: string;
|
12
10
|
locales: { [index: string]: Locale };
|
13
|
-
records:
|
11
|
+
records: Images.Record[];
|
14
12
|
}
|
15
13
|
|
16
|
-
export default function PageImages(props:
|
14
|
+
export default function PageImages(props: Props) {
|
17
15
|
return (
|
18
16
|
<div className="page-images">
|
19
17
|
<ImageGrid
|