pages_core 3.14.0 → 3.15.1

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/assets/builds/fonts/6569749d.ttf +0 -0
  4. data/app/assets/builds/fonts/7b7db107.woff2 +0 -0
  5. data/app/assets/builds/fonts/921961e9.woff2 +0 -0
  6. data/app/assets/builds/fonts/ee32bc60.ttf +0 -0
  7. data/app/assets/builds/pages_core/admin-dist.js +19 -8
  8. data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
  9. data/app/assets/builds/pages_core/admin.css +699 -394
  10. data/app/assets/builds/pages_core/mailer.css +99 -0
  11. data/app/assets/fonts/Inter-Black.woff2 +0 -0
  12. data/app/assets/fonts/Inter-BlackItalic.woff2 +0 -0
  13. data/app/assets/fonts/Inter-Bold.woff2 +0 -0
  14. data/app/assets/fonts/Inter-BoldItalic.woff2 +0 -0
  15. data/app/assets/fonts/Inter-ExtraBold.woff2 +0 -0
  16. data/app/assets/fonts/Inter-ExtraBoldItalic.woff2 +0 -0
  17. data/app/assets/fonts/Inter-ExtraLight.woff2 +0 -0
  18. data/app/assets/fonts/Inter-ExtraLightItalic.woff2 +0 -0
  19. data/app/assets/fonts/Inter-Italic.woff2 +0 -0
  20. data/app/assets/fonts/Inter-Light.woff2 +0 -0
  21. data/app/assets/fonts/Inter-LightItalic.woff2 +0 -0
  22. data/app/assets/fonts/Inter-Medium.woff2 +0 -0
  23. data/app/assets/fonts/Inter-MediumItalic.woff2 +0 -0
  24. data/app/assets/fonts/Inter-Regular.woff2 +0 -0
  25. data/app/assets/fonts/Inter-SemiBold.woff2 +0 -0
  26. data/app/assets/fonts/Inter-SemiBoldItalic.woff2 +0 -0
  27. data/app/assets/fonts/Inter-Thin.woff2 +0 -0
  28. data/app/assets/fonts/Inter-ThinItalic.woff2 +0 -0
  29. data/app/assets/fonts/InterDisplay-Black.woff2 +0 -0
  30. data/app/assets/fonts/InterDisplay-BlackItalic.woff2 +0 -0
  31. data/app/assets/fonts/InterDisplay-Bold.woff2 +0 -0
  32. data/app/assets/fonts/InterDisplay-BoldItalic.woff2 +0 -0
  33. data/app/assets/fonts/InterDisplay-ExtraBold.woff2 +0 -0
  34. data/app/assets/fonts/InterDisplay-ExtraBoldItalic.woff2 +0 -0
  35. data/app/assets/fonts/InterDisplay-ExtraLight.woff2 +0 -0
  36. data/app/assets/fonts/InterDisplay-ExtraLightItalic.woff2 +0 -0
  37. data/app/assets/fonts/InterDisplay-Italic.woff2 +0 -0
  38. data/app/assets/fonts/InterDisplay-Light.woff2 +0 -0
  39. data/app/assets/fonts/InterDisplay-LightItalic.woff2 +0 -0
  40. data/app/assets/fonts/InterDisplay-Medium.woff2 +0 -0
  41. data/app/assets/fonts/InterDisplay-MediumItalic.woff2 +0 -0
  42. data/app/assets/fonts/InterDisplay-Regular.woff2 +0 -0
  43. data/app/assets/fonts/InterDisplay-SemiBold.woff2 +0 -0
  44. data/app/assets/fonts/InterDisplay-SemiBoldItalic.woff2 +0 -0
  45. data/app/assets/fonts/InterDisplay-Thin.woff2 +0 -0
  46. data/app/assets/fonts/InterDisplay-ThinItalic.woff2 +0 -0
  47. data/app/assets/fonts/InterVariable-Italic.woff2 +0 -0
  48. data/app/assets/fonts/InterVariable.woff2 +0 -0
  49. data/app/assets/stylesheets/pages_core/admin/components/archive.css +1 -1
  50. data/app/assets/stylesheets/pages_core/admin/components/attachments.css +22 -34
  51. data/app/assets/stylesheets/pages_core/admin/components/base.css +1 -68
  52. data/app/assets/stylesheets/pages_core/admin/components/forms.css +109 -48
  53. data/app/assets/stylesheets/pages_core/admin/components/header.css +56 -58
  54. data/app/assets/stylesheets/pages_core/admin/components/image_editor.css +35 -24
  55. data/app/assets/stylesheets/pages_core/admin/components/image_grid.css +28 -27
  56. data/app/assets/stylesheets/pages_core/admin/components/image_uploader.css +5 -5
  57. data/app/assets/stylesheets/pages_core/admin/components/layout.css +7 -1
  58. data/app/assets/stylesheets/pages_core/admin/components/list_table.css +24 -15
  59. data/app/assets/stylesheets/pages_core/admin/components/page_tree.css +63 -104
  60. data/app/assets/stylesheets/pages_core/admin/components/pagination.css +12 -13
  61. data/app/assets/stylesheets/pages_core/admin/components/search.css +1 -16
  62. data/app/assets/stylesheets/pages_core/admin/components/sidebar.css +5 -11
  63. data/app/assets/stylesheets/pages_core/admin/components/tag_editor.css +22 -36
  64. data/app/assets/stylesheets/pages_core/admin/components/toast.css +1 -2
  65. data/app/assets/stylesheets/pages_core/admin/components/toolbar.css +10 -10
  66. data/app/assets/stylesheets/pages_core/admin/components/totp.css +1 -1
  67. data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +37 -51
  68. data/app/assets/stylesheets/pages_core/admin/global/fonts.css +271 -0
  69. data/app/assets/stylesheets/pages_core/admin/global/typography.css +109 -0
  70. data/app/assets/stylesheets/pages_core/admin/vars.css +1 -3
  71. data/app/assets/stylesheets/pages_core/{admin.postcss.css → admin.css} +1 -0
  72. data/app/assets/stylesheets/pages_core/mailer.css +90 -0
  73. data/app/controllers/admin/account_recoveries_controller.rb +2 -2
  74. data/app/controllers/admin/pages_controller.rb +22 -42
  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 +0 -2
  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 +2 -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/attachments_helper.rb +1 -1
  88. data/app/helpers/pages_core/frontend_helper.rb +1 -1
  89. data/app/helpers/pages_core/images_helper.rb +10 -8
  90. data/app/helpers/pages_core/labelled_form_builder.rb +2 -7
  91. data/app/helpers/pages_core/page_path_helper.rb +1 -1
  92. data/app/javascript/components/Attachments/Attachment.tsx +20 -18
  93. data/app/javascript/components/Attachments/AttachmentEditor.tsx +11 -9
  94. data/app/javascript/components/{Attachments.jsx → Attachments/List.tsx} +58 -63
  95. data/app/javascript/components/Attachments/useAttachments.ts +15 -0
  96. data/app/javascript/components/Attachments.tsx +14 -0
  97. data/app/javascript/components/DateRangeSelect.tsx +105 -0
  98. data/app/javascript/components/DateTimeSelect.tsx +136 -0
  99. data/app/javascript/components/EditableImage.tsx +11 -9
  100. data/app/javascript/components/FileUploadButton.tsx +7 -7
  101. data/app/javascript/components/ImageCropper/FocalPoint.tsx +9 -12
  102. data/app/javascript/components/ImageCropper/Image.tsx +10 -8
  103. data/app/javascript/components/ImageCropper/Toolbar.tsx +11 -12
  104. data/app/javascript/components/ImageCropper/useCrop.ts +24 -53
  105. data/app/javascript/components/ImageCropper.tsx +10 -15
  106. data/app/javascript/components/ImageEditor/Form.tsx +12 -8
  107. data/app/javascript/components/ImageEditor.tsx +12 -7
  108. data/app/javascript/components/ImageGrid/DragElement.tsx +9 -12
  109. data/app/javascript/components/{ImageGrid.jsx → ImageGrid/Grid.tsx} +62 -71
  110. data/app/javascript/components/ImageGrid/GridImage.tsx +22 -23
  111. data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -2
  112. data/app/javascript/components/ImageGrid/useImageGrid.ts +26 -0
  113. data/app/javascript/components/ImageGrid.tsx +15 -0
  114. data/app/javascript/components/ImageUploader.tsx +35 -22
  115. data/app/javascript/components/LabelledField.tsx +34 -0
  116. data/app/javascript/components/Modal.tsx +2 -2
  117. data/app/javascript/components/PageForm/Block.tsx +81 -0
  118. data/app/javascript/components/PageForm/Content.tsx +54 -0
  119. data/app/javascript/components/PageForm/Dates.tsx +66 -0
  120. data/app/javascript/components/PageForm/Files.tsx +28 -0
  121. data/app/javascript/components/PageForm/Form.tsx +41 -0
  122. data/app/javascript/components/PageForm/Images.tsx +28 -0
  123. data/app/javascript/components/PageForm/LocaleLinks.tsx +36 -0
  124. data/app/javascript/components/PageForm/Metadata.tsx +67 -0
  125. data/app/javascript/components/PageForm/Options.tsx +180 -0
  126. data/app/javascript/components/PageForm/PageDescription.tsx +48 -0
  127. data/app/javascript/components/PageForm/PathSegment.tsx +65 -0
  128. data/app/javascript/components/PageForm/TabPanel.tsx +21 -0
  129. data/app/javascript/components/PageForm/Tabs.tsx +33 -0
  130. data/app/javascript/components/PageForm/UnconfiguredContent.tsx +42 -0
  131. data/app/javascript/components/PageForm/pageParams.ts +95 -0
  132. data/app/javascript/components/PageForm/preview.ts +23 -0
  133. data/app/javascript/components/PageForm/usePage.ts +169 -0
  134. data/app/javascript/components/PageForm/useTabs.ts +46 -0
  135. data/app/javascript/components/PageForm.tsx +169 -0
  136. data/app/javascript/components/PageImages.tsx +7 -9
  137. data/app/javascript/components/PageTree/Draggable.tsx +40 -39
  138. data/app/javascript/components/PageTree/Node.tsx +62 -56
  139. data/app/javascript/components/PageTree/PageName.tsx +28 -0
  140. data/app/javascript/components/PageTree.tsx +65 -53
  141. data/app/javascript/components/{RichTextArea.jsx → RichTextArea.tsx} +98 -79
  142. data/app/javascript/components/RichTextToolbarButton.tsx +4 -6
  143. data/app/javascript/components/TagEditor/AddTagForm.tsx +19 -12
  144. data/app/javascript/components/TagEditor/Editor.tsx +32 -0
  145. data/app/javascript/components/TagEditor/Tag.tsx +6 -4
  146. data/app/javascript/components/TagEditor/useTags.ts +58 -0
  147. data/app/javascript/components/TagEditor.tsx +8 -58
  148. data/app/javascript/components/Toast.tsx +3 -3
  149. data/app/javascript/components/drag/draggedOrder.ts +22 -14
  150. data/app/javascript/components/drag/useDragCollection.ts +35 -30
  151. data/app/javascript/components/drag/useDragUploader.ts +32 -21
  152. data/app/javascript/components/drag/useDraggable.ts +7 -6
  153. data/app/javascript/components/drag.ts +0 -1
  154. data/app/javascript/components.ts +1 -3
  155. data/app/javascript/features/RichText.tsx +2 -3
  156. data/app/javascript/features/contentTabs.ts +79 -0
  157. data/app/javascript/index.ts +5 -12
  158. data/app/javascript/lib/Tree.ts +31 -45
  159. data/app/javascript/lib/request.ts +11 -11
  160. data/app/javascript/stores/useToastStore.ts +1 -1
  161. data/app/javascript/types/Attachments.ts +29 -0
  162. data/app/javascript/types/Crop.ts +36 -0
  163. data/app/javascript/types/Drag.ts +34 -0
  164. data/app/javascript/types/Images.ts +47 -0
  165. data/app/javascript/types/PageEditor.ts +26 -0
  166. data/app/javascript/types/Pages.ts +75 -0
  167. data/app/javascript/types/Tags.ts +9 -0
  168. data/app/javascript/types/Template.ts +24 -0
  169. data/app/javascript/types/Trees.ts +19 -0
  170. data/app/javascript/types.ts +2 -25
  171. data/app/mailers/admin_mailer.rb +5 -9
  172. data/app/models/attachment.rb +1 -1
  173. data/app/models/autopublisher.rb +1 -1
  174. data/app/models/concerns/pages_core/authenticable_user.rb +63 -0
  175. data/app/models/concerns/pages_core/emailable.rb +16 -0
  176. data/app/models/concerns/pages_core/page_model/dated_page.rb +3 -3
  177. data/app/models/concerns/pages_core/page_model/templateable.rb +2 -16
  178. data/app/models/concerns/pages_core/taggable.rb +2 -19
  179. data/app/models/invite.rb +2 -6
  180. data/app/models/otp_secret.rb +4 -4
  181. data/app/models/page.rb +0 -3
  182. data/app/models/user.rb +2 -46
  183. data/app/policies/page_policy.rb +6 -2
  184. data/app/resources/admin/page_resource.rb +95 -0
  185. data/app/resources/admin/page_tree_resource.rb +27 -0
  186. data/app/resources/admin/template_configuration_resource.rb +50 -0
  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/pages/_form.html.erb +10 -30
  190. data/app/views/admin/pages/_search_bar.html.erb +1 -1
  191. data/app/views/admin/pages/edit.html.erb +1 -57
  192. data/app/views/admin/pages/index.html.erb +1 -1
  193. data/app/views/admin/pages/new.html.erb +1 -44
  194. data/app/views/admin/sessions/new.html.erb +9 -11
  195. data/app/views/admin/users/_access_control.html.erb +5 -1
  196. data/app/views/admin/users/_list.html.erb +12 -7
  197. data/app/views/admin_mailer/account_recovery.html.erb +20 -0
  198. data/app/views/admin_mailer/invite.html.erb +11 -0
  199. data/app/views/layouts/admin/_header.html.erb +2 -4
  200. data/app/views/layouts/admin/_page_header.html.erb +1 -2
  201. data/app/views/layouts/admin.html.erb +1 -1
  202. data/app/views/layouts/pages_core/mailer.html.erb +11 -0
  203. data/config/locales/en.yml +0 -4
  204. data/config/routes.rb +3 -7
  205. data/db/migrate/20240126160700_add_2fa_fields.rb +5 -1
  206. data/db/migrate/20240131140700_change_email_to_citext.rb +18 -0
  207. data/db/migrate/20240201160700_remove_persistent_data.rb +7 -0
  208. data/db/migrate/20240508145300_remove_categories.rb +21 -0
  209. data/lib/pages_core/configuration/base.rb +2 -2
  210. data/lib/pages_core/engine.rb +1 -0
  211. data/lib/pages_core/templates/configuration.rb +1 -1
  212. data/lib/pages_core/templates/configuration_proxy.rb +2 -2
  213. data/lib/pages_core/templates/template_configuration.rb +11 -1
  214. data/lib/pages_core/templates.rb +6 -4
  215. data/lib/pages_core/version.rb +1 -1
  216. data/lib/pages_core.rb +1 -0
  217. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +6 -7
  218. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +17 -12
  219. data/lib/rails/generators/pages_core/rspec/rspec_generator.rb +0 -2
  220. data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +3 -4
  221. metadata +119 -36
  222. data/app/assets/builds/fonts/2a3059ad.ttf +0 -0
  223. data/app/assets/builds/fonts/47262711.woff2 +0 -0
  224. data/app/assets/builds/fonts/500ddeb0.woff2 +0 -0
  225. data/app/assets/builds/fonts/81221036.ttf +0 -0
  226. data/app/assets/stylesheets/pages_core/admin/components/login.css +0 -27
  227. data/app/controllers/admin/categories_controller.rb +0 -56
  228. data/app/controllers/concerns/pages_core/admin/persistent_params.rb +0 -75
  229. data/app/helpers/pages_core/admin/page_blocks_helper.rb +0 -66
  230. data/app/helpers/pages_core/admin/page_json_helper.rb +0 -23
  231. data/app/javascript/components/DateRangeSelect.jsx +0 -225
  232. data/app/javascript/components/PageDates.jsx +0 -73
  233. data/app/javascript/components/PageFiles.jsx +0 -25
  234. data/app/javascript/components/PageTree/types.ts +0 -15
  235. data/app/javascript/components/drag/types.ts +0 -28
  236. data/app/javascript/controllers/EditPageController.ts +0 -22
  237. data/app/javascript/controllers/MainController.ts +0 -74
  238. data/app/javascript/controllers/PageOptionsController.js +0 -67
  239. data/app/models/category.rb +0 -22
  240. data/app/models/concerns/pages_core/has_otp.rb +0 -27
  241. data/app/models/page_category.rb +0 -6
  242. data/app/views/admin/pages/_edit_content.html.erb +0 -19
  243. data/app/views/admin/pages/_edit_files.html.erb +0 -4
  244. data/app/views/admin/pages/_edit_images.html.erb +0 -4
  245. data/app/views/admin/pages/_edit_metadata.html.erb +0 -35
  246. data/app/views/admin/pages/_edit_options.html.erb +0 -91
  247. data/app/views/admin_mailer/account_recovery.text.erb +0 -10
  248. data/app/views/admin_mailer/invite.text.erb +0 -7
  249. 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,169 @@
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 clearDeletedObjects = () => {
86
+ filesState.setDeleted([]);
87
+ imagesState.setDeleted([]);
88
+ };
89
+
90
+ const handleSubmit = (evt: React.MouseEvent) => {
91
+ evt.preventDefault();
92
+ let method = postJson;
93
+ let url = `/admin/${locale}/pages.json`;
94
+ const data = {
95
+ page: pageParams(state, {
96
+ files: filesState,
97
+ images: imagesState,
98
+ tags: tagsState
99
+ })
100
+ };
101
+
102
+ if (page.id) {
103
+ method = putJson;
104
+ url = `/admin/${locale}/pages/${page.id}.json`;
105
+ }
106
+
107
+ method(url, data)
108
+ .then((response: Pages.SerializedResource) => {
109
+ dispatch({ type: "setPage", payload: response });
110
+ if (response.errors && response.errors.length > 0) {
111
+ errorToast("A validation error prevented the page from being saved.");
112
+ } else {
113
+ clearDeletedObjects();
114
+ noticeToast("Your changes were saved");
115
+ }
116
+ })
117
+ .catch(() => {
118
+ errorToast("An error occured while saving your changes.");
119
+ });
120
+ };
121
+
122
+ return (
123
+ <Form state={state}>
124
+ <main>
125
+ <PageDescription state={state} dispatch={dispatch}>
126
+ <Tabs tabs={tabs} tab={tab} setTab={setTab} />
127
+ </PageDescription>
128
+ <div className="content">
129
+ <TabPanel active={tab == "content"}>
130
+ <Content
131
+ state={state}
132
+ dispatch={dispatch}
133
+ tagsState={tagsState}
134
+ tagsDispatch={tagsDispatch}
135
+ />
136
+ </TabPanel>
137
+ <TabPanel active={tab == "unconfigured-content"}>
138
+ <UnconfiguredContent state={state} dispatch={dispatch} />
139
+ </TabPanel>
140
+ <TabPanel active={tab == "images"}>
141
+ <Images locale={locale} locales={locales} state={imagesState} />
142
+ </TabPanel>
143
+ <TabPanel active={tab == "files"}>
144
+ <Files locale={locale} locales={locales} state={filesState} />
145
+ </TabPanel>
146
+ <TabPanel active={tab == "metadata"}>
147
+ <Metadata state={state} dispatch={dispatch} />
148
+ </TabPanel>
149
+ <div className="buttons">
150
+ <button type="button" onClick={handlePreview}>
151
+ Preview
152
+ </button>
153
+ <button type="submit" onClick={handleSubmit}>
154
+ Save
155
+ </button>
156
+ </div>
157
+ </div>
158
+ </main>
159
+ <aside className="sidebar">
160
+ <Options
161
+ state={state}
162
+ dispatch={dispatch}
163
+ authors={props.authors}
164
+ statuses={props.statuses}
165
+ />
166
+ </aside>
167
+ </Form>
168
+ );
169
+ }
@@ -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