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.
Files changed (257) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/assets/builds/pages_core/admin-dist.js +19 -8
  4. data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
  5. data/app/assets/builds/pages_core/admin.css +704 -388
  6. data/app/assets/fonts/Inter-Black.woff2 +0 -0
  7. data/app/assets/fonts/Inter-BlackItalic.woff2 +0 -0
  8. data/app/assets/fonts/Inter-Bold.woff2 +0 -0
  9. data/app/assets/fonts/Inter-BoldItalic.woff2 +0 -0
  10. data/app/assets/fonts/Inter-ExtraBold.woff2 +0 -0
  11. data/app/assets/fonts/Inter-ExtraBoldItalic.woff2 +0 -0
  12. data/app/assets/fonts/Inter-ExtraLight.woff2 +0 -0
  13. data/app/assets/fonts/Inter-ExtraLightItalic.woff2 +0 -0
  14. data/app/assets/fonts/Inter-Italic.woff2 +0 -0
  15. data/app/assets/fonts/Inter-Light.woff2 +0 -0
  16. data/app/assets/fonts/Inter-LightItalic.woff2 +0 -0
  17. data/app/assets/fonts/Inter-Medium.woff2 +0 -0
  18. data/app/assets/fonts/Inter-MediumItalic.woff2 +0 -0
  19. data/app/assets/fonts/Inter-Regular.woff2 +0 -0
  20. data/app/assets/fonts/Inter-SemiBold.woff2 +0 -0
  21. data/app/assets/fonts/Inter-SemiBoldItalic.woff2 +0 -0
  22. data/app/assets/fonts/Inter-Thin.woff2 +0 -0
  23. data/app/assets/fonts/Inter-ThinItalic.woff2 +0 -0
  24. data/app/assets/fonts/InterDisplay-Black.woff2 +0 -0
  25. data/app/assets/fonts/InterDisplay-BlackItalic.woff2 +0 -0
  26. data/app/assets/fonts/InterDisplay-Bold.woff2 +0 -0
  27. data/app/assets/fonts/InterDisplay-BoldItalic.woff2 +0 -0
  28. data/app/assets/fonts/InterDisplay-ExtraBold.woff2 +0 -0
  29. data/app/assets/fonts/InterDisplay-ExtraBoldItalic.woff2 +0 -0
  30. data/app/assets/fonts/InterDisplay-ExtraLight.woff2 +0 -0
  31. data/app/assets/fonts/InterDisplay-ExtraLightItalic.woff2 +0 -0
  32. data/app/assets/fonts/InterDisplay-Italic.woff2 +0 -0
  33. data/app/assets/fonts/InterDisplay-Light.woff2 +0 -0
  34. data/app/assets/fonts/InterDisplay-LightItalic.woff2 +0 -0
  35. data/app/assets/fonts/InterDisplay-Medium.woff2 +0 -0
  36. data/app/assets/fonts/InterDisplay-MediumItalic.woff2 +0 -0
  37. data/app/assets/fonts/InterDisplay-Regular.woff2 +0 -0
  38. data/app/assets/fonts/InterDisplay-SemiBold.woff2 +0 -0
  39. data/app/assets/fonts/InterDisplay-SemiBoldItalic.woff2 +0 -0
  40. data/app/assets/fonts/InterDisplay-Thin.woff2 +0 -0
  41. data/app/assets/fonts/InterDisplay-ThinItalic.woff2 +0 -0
  42. data/app/assets/fonts/InterVariable-Italic.woff2 +0 -0
  43. data/app/assets/fonts/InterVariable.woff2 +0 -0
  44. data/app/assets/stylesheets/pages_core/admin/components/archive.css +1 -1
  45. data/app/assets/stylesheets/pages_core/admin/components/attachments.css +22 -34
  46. data/app/assets/stylesheets/pages_core/admin/components/base.css +1 -68
  47. data/app/assets/stylesheets/pages_core/admin/components/forms.css +107 -48
  48. data/app/assets/stylesheets/pages_core/admin/components/header.css +56 -58
  49. data/app/assets/stylesheets/pages_core/admin/components/image_editor.css +35 -24
  50. data/app/assets/stylesheets/pages_core/admin/components/image_grid.css +28 -27
  51. data/app/assets/stylesheets/pages_core/admin/components/image_uploader.css +5 -5
  52. data/app/assets/stylesheets/pages_core/admin/components/layout.css +7 -1
  53. data/app/assets/stylesheets/pages_core/admin/components/list_table.css +24 -15
  54. data/app/assets/stylesheets/pages_core/admin/components/page_tree.css +63 -104
  55. data/app/assets/stylesheets/pages_core/admin/components/pagination.css +12 -13
  56. data/app/assets/stylesheets/pages_core/admin/components/search.css +1 -16
  57. data/app/assets/stylesheets/pages_core/admin/components/sidebar.css +5 -11
  58. data/app/assets/stylesheets/pages_core/admin/components/tag_editor.css +22 -36
  59. data/app/assets/stylesheets/pages_core/admin/components/toast.css +1 -2
  60. data/app/assets/stylesheets/pages_core/admin/components/toolbar.css +10 -10
  61. data/app/assets/stylesheets/pages_core/admin/components/totp.css +26 -0
  62. data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +37 -51
  63. data/app/assets/stylesheets/pages_core/admin/global/fonts.css +271 -0
  64. data/app/assets/stylesheets/pages_core/admin/global/typography.css +109 -0
  65. data/app/assets/stylesheets/pages_core/admin/vars.css +1 -3
  66. data/app/assets/stylesheets/pages_core/admin.postcss.css +1 -0
  67. data/app/controllers/admin/account_recoveries_controller.rb +87 -0
  68. data/app/controllers/admin/invites_controller.rb +3 -2
  69. data/app/controllers/admin/otp_secrets_controller.rb +45 -0
  70. data/app/controllers/admin/pages_controller.rb +22 -42
  71. data/app/controllers/admin/recovery_codes_controller.rb +32 -0
  72. data/app/controllers/admin/sessions_controller.rb +65 -0
  73. data/app/controllers/admin/users_controller.rb +2 -8
  74. data/app/controllers/concerns/pages_core/authentication.rb +12 -10
  75. data/app/controllers/concerns/pages_core/error_reporting.rb +1 -1
  76. data/app/controllers/concerns/pages_core/page_parameters.rb +29 -0
  77. data/app/controllers/concerns/pages_core/policies_helper.rb +1 -1
  78. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +20 -20
  79. data/app/controllers/pages_core/admin_controller.rb +1 -3
  80. data/app/controllers/pages_core/frontend/pages_controller.rb +2 -6
  81. data/app/formatters/pages_core/html_formatter.rb +2 -4
  82. data/app/helpers/admin/menu_helper.rb +5 -4
  83. data/app/helpers/admin/pages_helper.rb +1 -21
  84. data/app/helpers/pages_core/admin/admin_helper.rb +13 -3
  85. data/app/helpers/pages_core/admin/content_tabs_helper.rb +1 -2
  86. data/app/helpers/pages_core/admin/labelled_field_helper.rb +1 -1
  87. data/app/helpers/pages_core/frontend_helper.rb +1 -1
  88. data/app/helpers/pages_core/images_helper.rb +10 -8
  89. data/app/helpers/pages_core/labelled_form_builder.rb +2 -7
  90. data/app/helpers/pages_core/page_path_helper.rb +1 -1
  91. data/app/javascript/components/Attachments/Attachment.tsx +20 -18
  92. data/app/javascript/components/Attachments/AttachmentEditor.tsx +11 -9
  93. data/app/javascript/components/{Attachments.jsx → Attachments/List.tsx} +58 -63
  94. data/app/javascript/components/Attachments/useAttachments.ts +15 -0
  95. data/app/javascript/components/Attachments.tsx +14 -0
  96. data/app/javascript/components/DateRangeSelect.tsx +105 -0
  97. data/app/javascript/components/DateTimeSelect.tsx +136 -0
  98. data/app/javascript/components/EditableImage.tsx +11 -9
  99. data/app/javascript/components/FileUploadButton.tsx +7 -7
  100. data/app/javascript/components/ImageCropper/FocalPoint.tsx +9 -12
  101. data/app/javascript/components/ImageCropper/Image.tsx +10 -8
  102. data/app/javascript/components/ImageCropper/Toolbar.tsx +11 -12
  103. data/app/javascript/components/ImageCropper/useCrop.ts +24 -53
  104. data/app/javascript/components/ImageCropper.tsx +10 -15
  105. data/app/javascript/components/ImageEditor/Form.tsx +12 -8
  106. data/app/javascript/components/ImageEditor.tsx +12 -7
  107. data/app/javascript/components/ImageGrid/DragElement.tsx +9 -12
  108. data/app/javascript/components/{ImageGrid.jsx → ImageGrid/Grid.tsx} +62 -71
  109. data/app/javascript/components/ImageGrid/GridImage.tsx +22 -23
  110. data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -2
  111. data/app/javascript/components/ImageGrid/useImageGrid.ts +26 -0
  112. data/app/javascript/components/ImageGrid.tsx +15 -0
  113. data/app/javascript/components/ImageUploader.tsx +35 -22
  114. data/app/javascript/components/LabelledField.tsx +34 -0
  115. data/app/javascript/components/Modal.tsx +2 -2
  116. data/app/javascript/components/PageForm/Block.tsx +81 -0
  117. data/app/javascript/components/PageForm/Content.tsx +54 -0
  118. data/app/javascript/components/PageForm/Dates.tsx +66 -0
  119. data/app/javascript/components/PageForm/Files.tsx +28 -0
  120. data/app/javascript/components/PageForm/Form.tsx +41 -0
  121. data/app/javascript/components/PageForm/Images.tsx +28 -0
  122. data/app/javascript/components/PageForm/LocaleLinks.tsx +36 -0
  123. data/app/javascript/components/PageForm/Metadata.tsx +67 -0
  124. data/app/javascript/components/PageForm/Options.tsx +180 -0
  125. data/app/javascript/components/PageForm/PageDescription.tsx +48 -0
  126. data/app/javascript/components/PageForm/PathSegment.tsx +65 -0
  127. data/app/javascript/components/PageForm/TabPanel.tsx +21 -0
  128. data/app/javascript/components/PageForm/Tabs.tsx +33 -0
  129. data/app/javascript/components/PageForm/UnconfiguredContent.tsx +42 -0
  130. data/app/javascript/components/PageForm/pageParams.ts +95 -0
  131. data/app/javascript/components/PageForm/preview.ts +23 -0
  132. data/app/javascript/components/PageForm/usePage.ts +169 -0
  133. data/app/javascript/components/PageForm/useTabs.ts +46 -0
  134. data/app/javascript/components/PageForm.tsx +163 -0
  135. data/app/javascript/components/PageImages.tsx +7 -9
  136. data/app/javascript/components/PageTree/Draggable.tsx +40 -39
  137. data/app/javascript/components/PageTree/Node.tsx +62 -56
  138. data/app/javascript/components/PageTree/PageName.tsx +28 -0
  139. data/app/javascript/components/PageTree.tsx +65 -53
  140. data/app/javascript/components/{RichTextArea.jsx → RichTextArea.tsx} +98 -79
  141. data/app/javascript/components/RichTextToolbarButton.tsx +4 -6
  142. data/app/javascript/components/TagEditor/AddTagForm.tsx +19 -12
  143. data/app/javascript/components/TagEditor/Editor.tsx +32 -0
  144. data/app/javascript/components/TagEditor/Tag.tsx +6 -4
  145. data/app/javascript/components/TagEditor/useTags.ts +58 -0
  146. data/app/javascript/components/TagEditor.tsx +8 -58
  147. data/app/javascript/components/Toast.tsx +3 -3
  148. data/app/javascript/components/drag/draggedOrder.ts +22 -14
  149. data/app/javascript/components/drag/useDragCollection.ts +35 -30
  150. data/app/javascript/components/drag/useDragUploader.ts +32 -21
  151. data/app/javascript/components/drag/useDraggable.ts +7 -6
  152. data/app/javascript/components/drag.ts +0 -1
  153. data/app/javascript/components.ts +1 -3
  154. data/app/javascript/features/RichText.tsx +2 -3
  155. data/app/javascript/features/contentTabs.ts +79 -0
  156. data/app/javascript/index.ts +5 -14
  157. data/app/javascript/lib/Tree.ts +31 -45
  158. data/app/javascript/lib/request.ts +11 -11
  159. data/app/javascript/stores/useToastStore.ts +1 -1
  160. data/app/javascript/types/Attachments.ts +29 -0
  161. data/app/javascript/types/Crop.ts +36 -0
  162. data/app/javascript/types/Drag.ts +34 -0
  163. data/app/javascript/types/Images.ts +47 -0
  164. data/app/javascript/types/PageEditor.ts +26 -0
  165. data/app/javascript/types/Pages.ts +75 -0
  166. data/app/javascript/types/Tags.ts +9 -0
  167. data/app/javascript/types/Template.ts +24 -0
  168. data/app/javascript/types/Trees.ts +19 -0
  169. data/app/javascript/types.ts +2 -25
  170. data/app/mailers/admin_mailer.rb +2 -2
  171. data/app/models/attachment.rb +1 -1
  172. data/app/models/concerns/pages_core/authenticable_user.rb +63 -0
  173. data/app/models/concerns/pages_core/emailable.rb +16 -0
  174. data/app/models/concerns/pages_core/page_model/templateable.rb +2 -16
  175. data/app/models/invite.rb +2 -6
  176. data/app/models/otp_secret.rb +101 -0
  177. data/app/models/page.rb +0 -3
  178. data/app/models/user.rb +2 -68
  179. data/app/policies/page_policy.rb +6 -2
  180. data/app/policies/user_policy.rb +4 -0
  181. data/app/resources/admin/page_resource.rb +95 -0
  182. data/app/resources/admin/page_tree_resource.rb +27 -0
  183. data/app/resources/admin/template_configuration_resource.rb +50 -0
  184. data/app/views/admin/account_recoveries/new.html.erb +22 -0
  185. data/app/views/admin/account_recoveries/show.html.erb +37 -0
  186. data/app/views/admin/invites/show.html.erb +1 -1
  187. data/app/views/admin/news/_sidebar.html.erb +2 -4
  188. data/app/views/admin/news/index.html.erb +0 -1
  189. data/app/views/admin/otp_secrets/create.html.erb +7 -0
  190. data/app/views/admin/otp_secrets/new.html.erb +60 -0
  191. data/app/views/admin/pages/_form.html.erb +10 -30
  192. data/app/views/admin/pages/_search_bar.html.erb +1 -1
  193. data/app/views/admin/pages/edit.html.erb +1 -57
  194. data/app/views/admin/pages/index.html.erb +1 -1
  195. data/app/views/admin/pages/new.html.erb +1 -44
  196. data/app/views/admin/recovery_codes/_codes.html.erb +14 -0
  197. data/app/views/admin/recovery_codes/create.html.erb +7 -0
  198. data/app/views/admin/recovery_codes/new.html.erb +11 -0
  199. data/app/views/admin/sessions/_otp_form.html.erb +13 -0
  200. data/app/views/admin/sessions/new.html.erb +31 -0
  201. data/app/views/admin/sessions/verify_otp.html.erb +19 -0
  202. data/app/views/admin/users/_access_control.html.erb +5 -1
  203. data/app/views/admin/users/_list.html.erb +12 -7
  204. data/app/views/admin/users/edit.html.erb +31 -1
  205. data/app/views/admin/users/new.html.erb +1 -1
  206. data/app/views/admin_mailer/account_recovery.text.erb +10 -0
  207. data/app/views/layouts/admin/_header.html.erb +3 -5
  208. data/app/views/layouts/admin/_page_header.html.erb +1 -2
  209. data/app/views/layouts/admin/_toast.html.erb +12 -0
  210. data/app/views/layouts/admin.html.erb +2 -2
  211. data/config/locales/en.yml +11 -7
  212. data/config/routes.rb +13 -12
  213. data/db/migrate/20240126160700_add_2fa_fields.rb +26 -0
  214. data/db/migrate/20240129201300_remove_password_reset_tokens.rb +13 -0
  215. data/db/migrate/20240131140700_change_email_to_citext.rb +18 -0
  216. data/db/migrate/20240201160700_remove_persistent_data.rb +7 -0
  217. data/db/migrate/20240508145300_remove_categories.rb +21 -0
  218. data/lib/pages_core/configuration/base.rb +2 -2
  219. data/lib/pages_core/templates/configuration.rb +1 -1
  220. data/lib/pages_core/templates/configuration_proxy.rb +2 -2
  221. data/lib/pages_core/templates/template_configuration.rb +11 -1
  222. data/lib/pages_core/templates.rb +6 -4
  223. data/lib/pages_core/version.rb +1 -1
  224. data/lib/pages_core.rb +6 -0
  225. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +6 -7
  226. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +17 -12
  227. data/lib/rails/generators/pages_core/rspec/rspec_generator.rb +0 -2
  228. data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +3 -4
  229. metadata +143 -35
  230. data/app/assets/stylesheets/pages_core/admin/components/login.css +0 -33
  231. data/app/controllers/admin/categories_controller.rb +0 -56
  232. data/app/controllers/admin/password_resets_controller.rb +0 -85
  233. data/app/controllers/concerns/pages_core/admin/persistent_params.rb +0 -75
  234. data/app/controllers/sessions_controller.rb +0 -27
  235. data/app/helpers/pages_core/admin/page_blocks_helper.rb +0 -66
  236. data/app/helpers/pages_core/admin/page_json_helper.rb +0 -23
  237. data/app/javascript/components/DateRangeSelect.jsx +0 -225
  238. data/app/javascript/components/PageDates.jsx +0 -73
  239. data/app/javascript/components/PageFiles.jsx +0 -25
  240. data/app/javascript/components/PageTree/types.ts +0 -15
  241. data/app/javascript/components/drag/types.ts +0 -28
  242. data/app/javascript/controllers/EditPageController.ts +0 -22
  243. data/app/javascript/controllers/LoginController.ts +0 -32
  244. data/app/javascript/controllers/MainController.ts +0 -74
  245. data/app/javascript/controllers/PageOptionsController.js +0 -67
  246. data/app/models/category.rb +0 -22
  247. data/app/models/page_category.rb +0 -6
  248. data/app/models/password_reset_token.rb +0 -34
  249. data/app/views/admin/pages/_edit_content.html.erb +0 -19
  250. data/app/views/admin/pages/_edit_files.html.erb +0 -4
  251. data/app/views/admin/pages/_edit_images.html.erb +0 -4
  252. data/app/views/admin/pages/_edit_metadata.html.erb +0 -35
  253. data/app/views/admin/pages/_edit_options.html.erb +0 -91
  254. data/app/views/admin/password_resets/show.html.erb +0 -21
  255. data/app/views/admin/users/login.html.erb +0 -65
  256. data/app/views/admin_mailer/password_reset.text.erb +0 -11
  257. 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
- interface PageImage {
6
- id: number | null;
7
- image: ImageResource;
8
- }
3
+ import * as Images from "../types/Images";
4
+ import { Locale } from "../types";
5
+
6
+ import ImageGrid from "./ImageGrid";
9
7
 
10
- interface PageImagesProps {
8
+ interface Props {
11
9
  locale: string;
12
10
  locales: { [index: string]: Locale };
13
- records: PageImage[];
11
+ records: Images.Record[];
14
12
  }
15
13
 
16
- export default function PageImages(props: PageImagesProps) {
14
+ export default function PageImages(props: Props) {
17
15
  return (
18
16
  <div className="page-images">
19
17
  <ImageGrid