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,54 @@
1
+ import React from "react";
2
+
3
+ import * as PageEditor from "../../types/PageEditor";
4
+ import * as Tags from "../../types/Tags";
5
+ import { MaybeLocalizedValue } from "../../types";
6
+
7
+ import { blockValue, errorsOn } from "./usePage";
8
+ import LabelledField from "../LabelledField";
9
+ import { default as TagEditor } from "../TagEditor/Editor";
10
+ import Block from "./Block";
11
+ import Dates from "./Dates";
12
+
13
+ interface Props {
14
+ state: PageEditor.State;
15
+ dispatch: (action: PageEditor.Action) => void;
16
+ tagsState: Tags.State;
17
+ tagsDispatch: (action: Tags.Action) => void;
18
+ }
19
+
20
+ export default function Content(props: Props) {
21
+ const { state, dispatch, tagsState, tagsDispatch } = props;
22
+
23
+ const { page, locale, inputDir, templateConfig } = state;
24
+
25
+ const handleChange = (attr: string) => (value: MaybeLocalizedValue) => {
26
+ dispatch({ type: "updateBlocks", payload: { [attr]: value } });
27
+ };
28
+
29
+ return (
30
+ <React.Fragment>
31
+ {templateConfig.blocks.map((b) => (
32
+ <Block
33
+ key={b.name}
34
+ block={b}
35
+ errors={errorsOn(page, b.name)}
36
+ dir={inputDir}
37
+ lang={locale}
38
+ onChange={handleChange(b.name)}
39
+ value={blockValue(state, b)}
40
+ />
41
+ ))}
42
+ {templateConfig.dates && <Dates state={state} dispatch={dispatch} />}
43
+ {templateConfig.tags && (
44
+ <LabelledField label="Tags">
45
+ <TagEditor
46
+ name="page[serialized_tags]"
47
+ state={tagsState}
48
+ dispatch={tagsDispatch}
49
+ />
50
+ </LabelledField>
51
+ )}
52
+ </React.Fragment>
53
+ );
54
+ }
@@ -0,0 +1,66 @@
1
+ import React from "react";
2
+
3
+ import * as PageEditor from "../../types/PageEditor";
4
+
5
+ import DateRangeSelect from "../DateRangeSelect";
6
+
7
+ interface Props {
8
+ state: PageEditor.State;
9
+ dispatch: (action: PageEditor.Action) => void;
10
+ }
11
+
12
+ export default function Dates(props: Props) {
13
+ const { state, dispatch } = props;
14
+ const { datesEnabled, page } = state;
15
+
16
+ const toggleAllDay = () => {
17
+ dispatch({ type: "update", payload: { all_day: !page.all_day } });
18
+ };
19
+
20
+ const toggleDatesEnabled = () => {
21
+ dispatch({ type: "setDatesEnabled", payload: !datesEnabled });
22
+ };
23
+
24
+ const setDate = (attr: "starts_at" | "ends_at") => (date: Date) => {
25
+ dispatch({ type: "update", payload: { [attr]: date } });
26
+ };
27
+
28
+ return (
29
+ <div className="page-dates field">
30
+ <input
31
+ type="hidden"
32
+ name="page[all_day]"
33
+ value={datesEnabled && page.all_day ? "1" : "0"}
34
+ />
35
+ <label>Dates</label>
36
+ <div className="toggles">
37
+ <label className="has-dates-toggle">
38
+ <input
39
+ type="checkbox"
40
+ checked={datesEnabled}
41
+ onChange={toggleDatesEnabled}
42
+ />
43
+ Enabled
44
+ </label>
45
+ <label className={!datesEnabled && "disabled"}>
46
+ <input
47
+ type="checkbox"
48
+ disabled={!datesEnabled}
49
+ checked={page.all_day}
50
+ onChange={toggleAllDay}
51
+ />
52
+ All day event
53
+ </label>
54
+ </div>
55
+ <DateRangeSelect
56
+ objectName="page"
57
+ startsAt={page.starts_at}
58
+ setStartsAt={setDate("starts_at")}
59
+ endsAt={page.ends_at}
60
+ setEndsAt={setDate("ends_at")}
61
+ disabled={!datesEnabled}
62
+ disableTime={page.all_day}
63
+ />
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+
3
+ import * as Attachments from "../../types/Attachments";
4
+ import { Locale } from "../../types";
5
+
6
+ import List from "../Attachments/List";
7
+
8
+ interface Props {
9
+ locale: string;
10
+ locales: { [index: string]: Locale };
11
+ state: Attachments.State;
12
+ }
13
+
14
+ export default function Files(props: Props) {
15
+ const { locale, locales, state } = props;
16
+
17
+ return (
18
+ <div className="page-files">
19
+ <List
20
+ attribute="page[page_files_attributes]"
21
+ showEmbed={true}
22
+ locale={locale}
23
+ locales={locales}
24
+ state={state}
25
+ />
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+
3
+ import { csrfToken } from "../../lib/request";
4
+ import * as PageEditor from "../../types/PageEditor";
5
+
6
+ interface FormProps {
7
+ state: PageEditor.State;
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ function pageUrl(state: PageEditor.State): string {
12
+ if (state.page.id) {
13
+ return `/admin/${state.locale}/pages/${state.page.id}`;
14
+ } else {
15
+ return `/admin/${state.locale}/pages`;
16
+ }
17
+ }
18
+
19
+ export default function Form(props: FormProps) {
20
+ const { state, children } = props;
21
+ const { page } = state;
22
+
23
+ return (
24
+ <form
25
+ className="edit-page main-wrapper"
26
+ method="post"
27
+ acceptCharset="UTF-8"
28
+ action={pageUrl(state)}>
29
+ {page.id && (
30
+ <input type="hidden" name="_method" value="put" autoComplete="off" />
31
+ )}
32
+ <input
33
+ type="hidden"
34
+ autoComplete="off"
35
+ name="authenticity_token"
36
+ value={csrfToken()}
37
+ />
38
+ {children}
39
+ </form>
40
+ );
41
+ }
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+
3
+ import { GridState } from "../../types/Images";
4
+ import { Locale } from "../../types";
5
+
6
+ import Grid from "../ImageGrid/Grid";
7
+
8
+ interface Props {
9
+ locale: string;
10
+ locales: { [index: string]: Locale };
11
+ state: GridState;
12
+ }
13
+
14
+ export default function Images(props: Props) {
15
+ return (
16
+ <div className="page-images">
17
+ <Grid
18
+ attribute="page[page_images_attributes]"
19
+ primaryAttribute="page[image_id]"
20
+ enablePrimary={true}
21
+ showEmbed={true}
22
+ locale={props.locale}
23
+ locales={props.locales}
24
+ state={props.state}
25
+ />
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+
3
+ import * as PageEditor from "../../types/PageEditor";
4
+
5
+ interface Props {
6
+ state: PageEditor.State;
7
+ dispatch: (action: PageEditor.Action) => void;
8
+ }
9
+
10
+ export default function LocaleLinks(props: Props) {
11
+ const { state, dispatch } = props;
12
+ const { locale, locales } = state;
13
+
14
+ const handleClick = (newLocale: string) => (evt: React.MouseEvent) => {
15
+ evt.preventDefault();
16
+ dispatch({ type: "setLocale", payload: newLocale });
17
+ };
18
+
19
+ if (!locales) {
20
+ return "";
21
+ }
22
+
23
+ return (
24
+ <div className="links">
25
+ {Object.keys(locales).map((l) => (
26
+ <a
27
+ key={l}
28
+ className={locale == l ? "current" : ""}
29
+ href="#"
30
+ onClick={handleClick(l)}>
31
+ {locales[l].name}
32
+ </a>
33
+ ))}
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+
3
+ import * as PageEditor from "../../types/PageEditor";
4
+ import * as Pages from "../../types/Pages";
5
+ import { MaybeLocalizedValue } from "../../types";
6
+
7
+ import { blockValue, errorsOn } from "./usePage";
8
+ import Block from "./Block";
9
+ import PathSegment from "./PathSegment";
10
+ import LabelledField from "../LabelledField";
11
+ import ImageUploader from "../ImageUploader";
12
+
13
+ interface Props {
14
+ state: PageEditor.State;
15
+ dispatch: (action: PageEditor.Action) => void;
16
+ }
17
+
18
+ export default function Metadata(props: Props) {
19
+ const { state, dispatch } = props;
20
+
21
+ const { page, locale, locales, inputDir, templateConfig } = state;
22
+
23
+ const handleChange = (attr: string) => (value: MaybeLocalizedValue) => {
24
+ dispatch({ type: "updateBlocks", payload: { [attr]: value } });
25
+ };
26
+
27
+ const handleMetaImage = (value: Pages.MetaImage) => {
28
+ dispatch({ type: "update", payload: { meta_image: value } });
29
+ };
30
+
31
+ return (
32
+ <React.Fragment>
33
+ <PathSegment state={state} dispatch={dispatch} />
34
+ <LabelledField
35
+ htmlFor="page_meta_image_id"
36
+ label="Image"
37
+ description={
38
+ "Image displayed when sharing on social media. " +
39
+ "Will fall back to the primary image if absent. " +
40
+ "Recommended size is at least 1200x630."
41
+ }
42
+ errors={errorsOn(page, "meta_image_id")}>
43
+ <ImageUploader
44
+ attr="page[meta_image_id]"
45
+ locale={locale}
46
+ locales={locales}
47
+ image={page.meta_image.image}
48
+ src={page.meta_image.src}
49
+ onChange={handleMetaImage}
50
+ width={250}
51
+ caption={false}
52
+ />
53
+ </LabelledField>
54
+ {templateConfig.metadata_blocks.map((b) => (
55
+ <Block
56
+ key={b.name}
57
+ block={b}
58
+ errors={errorsOn(page, b.name)}
59
+ dir={inputDir}
60
+ lang={locale}
61
+ onChange={handleChange(b.name)}
62
+ value={blockValue(state, b)}
63
+ />
64
+ ))}
65
+ </React.Fragment>
66
+ );
67
+ }
@@ -0,0 +1,180 @@
1
+ import React, { useState, ChangeEvent } from "react";
2
+
3
+ import * as PageEditor from "../../types/PageEditor";
4
+ import * as Pages from "../../types/Pages";
5
+ import LabelledField from "../LabelledField";
6
+ import DateTimeSelect from "../DateTimeSelect";
7
+ import { errorsOn } from "./usePage";
8
+
9
+ interface OptionsProps {
10
+ state: PageEditor.State;
11
+ dispatch: (action: PageEditor.Action) => void;
12
+ authors: Pages.Author[];
13
+ statuses: Pages.StatusLabels;
14
+ }
15
+
16
+ export default function Options(props: OptionsProps) {
17
+ const { state, dispatch, authors, statuses } = props;
18
+
19
+ const { page, locale, templates } = state;
20
+
21
+ const [showAdvanced, setShowAdvanced] = useState(false);
22
+
23
+ const published = page.status == 2;
24
+ const autopublish = published && page.published_at > new Date();
25
+ const url = page.urls[locale];
26
+
27
+ const handleChange =
28
+ (attr: string) =>
29
+ (evt: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLSelectElement>) => {
30
+ dispatch({ type: "update", payload: { [attr]: evt.target.value } });
31
+ };
32
+
33
+ const handleChecked =
34
+ (attr: string) => (evt: ChangeEvent<HTMLInputElement>) => {
35
+ dispatch({ type: "update", payload: { [attr]: evt.target.checked } });
36
+ };
37
+
38
+ const changePublishedAt = (value: Date) => {
39
+ dispatch({ type: "update", payload: { published_at: value } });
40
+ };
41
+
42
+ const toggleAdvanced = (evt: React.MouseEvent) => {
43
+ evt.preventDefault();
44
+ setShowAdvanced(!showAdvanced);
45
+ };
46
+
47
+ return (
48
+ <div className="page-options">
49
+ <LabelledField
50
+ htmlFor="page_status"
51
+ label="Status"
52
+ errors={errorsOn(page, "status")}>
53
+ <select
54
+ id="page_status"
55
+ name="page[status]"
56
+ onChange={handleChange("status")}
57
+ value={page.status}>
58
+ {Object.keys(statuses).map((id) => (
59
+ <option key={id} value={id}>
60
+ {statuses[id]}
61
+ </option>
62
+ ))}
63
+ </select>
64
+ </LabelledField>
65
+ {published && (
66
+ <div>
67
+ <LabelledField label="Date" errors={errorsOn(page, "published_at")}>
68
+ <DateTimeSelect
69
+ name={"page[published_at]"}
70
+ onChange={changePublishedAt}
71
+ value={page.published_at}
72
+ />
73
+ </LabelledField>
74
+ {autopublish && <p>This page will publish later</p>}
75
+ </div>
76
+ )}
77
+ <LabelledField
78
+ htmlFor="page_user_id"
79
+ label="Author"
80
+ errors={errorsOn(page, "user_id")}>
81
+ <select
82
+ id="page_user_id"
83
+ name="page[user_id]"
84
+ onChange={handleChange("user_id")}
85
+ value={page.user_id}>
86
+ {authors.map((u) => (
87
+ <option key={u[1]} value={u[1]}>
88
+ {u[0]}
89
+ </option>
90
+ ))}
91
+ </select>
92
+ </LabelledField>
93
+ <LabelledField label="Pin to top">
94
+ <label className="check-box">
95
+ <input
96
+ name="page[pinned]"
97
+ type="checkbox"
98
+ onChange={handleChecked("pinned")}
99
+ checked={page.pinned}
100
+ />{" "}
101
+ Make post featured
102
+ </label>
103
+ </LabelledField>
104
+ <LabelledField
105
+ htmlFor="page_template"
106
+ label="Template"
107
+ errors={errorsOn(page, "template")}>
108
+ <select
109
+ id="page_template"
110
+ name="page[template]"
111
+ onChange={handleChange("template")}
112
+ value={page.template}>
113
+ {templates.map((t) => (
114
+ <option key={t.template_name} value={t.template_name}>
115
+ {t.name}
116
+ </option>
117
+ ))}
118
+ </select>
119
+ </LabelledField>
120
+ <p>
121
+ <a href="#" onClick={toggleAdvanced}>
122
+ Advanced options
123
+ </a>
124
+ </p>
125
+ {showAdvanced && (
126
+ <React.Fragment>
127
+ <LabelledField label="Subpages">
128
+ <label className="check-box">
129
+ <input
130
+ name="page[feed_enabled]"
131
+ type="checkbox"
132
+ onChange={handleChecked("feed_enabled")}
133
+ checked={page.feed_enabled}
134
+ />{" "}
135
+ RSS feed enabled
136
+ </label>
137
+ <label className="check-box">
138
+ <input
139
+ name="page[news_page]"
140
+ type="checkbox"
141
+ onChange={handleChecked("news_page")}
142
+ checked={page.news_page}
143
+ />{" "}
144
+ Show in news
145
+ </label>
146
+ </LabelledField>
147
+ <LabelledField
148
+ htmlFor="page_unique_name"
149
+ label="Unique name"
150
+ errors={errorsOn(page, "unique_name")}>
151
+ <input
152
+ type="text"
153
+ id="page_unique_name"
154
+ name="page[unique_name]"
155
+ value={page.unique_name}
156
+ onChange={handleChange("unique_name")}
157
+ />
158
+ </LabelledField>
159
+ <LabelledField
160
+ htmlFor="page_redirect_to"
161
+ label="Redirect"
162
+ errors={errorsOn(page, "redirect_to")}>
163
+ <input
164
+ type="text"
165
+ id="page_redirect_to"
166
+ name="page[redirect_to]"
167
+ value={page.redirect_to}
168
+ onChange={handleChange("redirect_to")}
169
+ />
170
+ </LabelledField>
171
+ </React.Fragment>
172
+ )}
173
+ {url && (
174
+ <LabelledField label="Page link">
175
+ <a href={url}>{url}</a>
176
+ </LabelledField>
177
+ )}
178
+ </div>
179
+ );
180
+ }
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+
3
+ import * as PageEditor from "../../types/PageEditor";
4
+ import * as Pages from "../../types/Pages";
5
+
6
+ import LocaleLinks from "./LocaleLinks";
7
+
8
+ interface PageDescriptionProps {
9
+ state: PageEditor.State;
10
+ dispatch: (action: PageEditor.Action) => void;
11
+ children: React.ReactNode;
12
+ }
13
+
14
+ function editLink(locale: string, page: Pages.Ancestor | Pages.Resource) {
15
+ return (
16
+ <a href={`/admin/${locale}/pages/${page.id}/edit`}>
17
+ {pageName(locale, page)}
18
+ </a>
19
+ );
20
+ }
21
+
22
+ function pageName(locale: string, page: Pages.Ancestor | Pages.Resource) {
23
+ if ("name" in page) {
24
+ return page.name[locale];
25
+ }
26
+ return page.blocks.name[locale] || <i>Untitled</i>;
27
+ }
28
+
29
+ export default function PageDescription(props: PageDescriptionProps) {
30
+ const { state, dispatch, children } = props;
31
+ const { locale, page } = state;
32
+
33
+ return (
34
+ <div className="page-description with_content_tabs">
35
+ <LocaleLinks state={state} dispatch={dispatch} />
36
+ <h3>
37
+ {page.ancestors.map((p) => (
38
+ <React.Fragment key={p.id}>
39
+ {editLink(locale, p)}
40
+ {" » "}
41
+ </React.Fragment>
42
+ ))}
43
+ {page.id ? editLink(locale, page) : "New Page"}
44
+ </h3>
45
+ {children}
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,65 @@
1
+ import React, { ChangeEvent } from "react";
2
+
3
+ import * as PageEditor from "../../types/PageEditor";
4
+ import * as Pages from "../../types/Pages";
5
+ import { LocalizedValue } from "../../types";
6
+
7
+ import { errorsOn } from "./usePage";
8
+ import LabelledField from "../LabelledField";
9
+
10
+ interface Props {
11
+ state: PageEditor.State;
12
+ dispatch: (action: PageEditor.Action) => void;
13
+ }
14
+
15
+ function missingPathSegment(ancestors: Pages.Ancestor[], locale: string) {
16
+ for (let i = 0; i < ancestors.length; i++) {
17
+ if (!ancestors[i].path_segment[locale]) {
18
+ return ancestors[i];
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export default function PathSegment(props: Props) {
25
+ const { state, dispatch } = props;
26
+ const { page, locale } = state;
27
+
28
+ const value = (page.path_segment as LocalizedValue)[locale];
29
+
30
+ const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
31
+ dispatch({ type: "update", payload: { path_segment: evt.target.value } });
32
+ };
33
+
34
+ const editAncestor = missingPathSegment(page.ancestors, locale);
35
+
36
+ if (editAncestor) {
37
+ const editUrl = `/admin/${locale}/pages/${editAncestor.id}/edit#metadata`;
38
+ return (
39
+ <LabelledField label="Path segment">
40
+ <p className="description">
41
+ Unable to add a path segment to this page, please add one to{" "}
42
+ <a href={editUrl}>this page&apos;s ancestor</a> first.
43
+ </p>
44
+ </LabelledField>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <LabelledField
50
+ htmlFor="page_path_segment"
51
+ label="Path segment"
52
+ description="Only alpanumeric characters and dashes are allowed."
53
+ errors={errorsOn(page, "path_segment")}>
54
+ <input
55
+ type="text"
56
+ id="page_path_segment"
57
+ name="page[path_segment]"
58
+ lang={state.locale}
59
+ dir="ltr"
60
+ onChange={handleChange}
61
+ value={value}
62
+ />
63
+ </LabelledField>
64
+ );
65
+ }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+
3
+ interface TabPanelProps {
4
+ active: boolean;
5
+ children: React.ReactNode;
6
+ }
7
+
8
+ export default function TabPanel(props: TabPanelProps) {
9
+ const { active, children } = props;
10
+
11
+ const classNames = ["content-tab"];
12
+ if (!active) {
13
+ classNames.push("hidden");
14
+ }
15
+
16
+ return (
17
+ <div className={classNames.join(" ")} role="tabpanel">
18
+ {children}
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+
3
+ import * as PageEditor from "../../types/PageEditor";
4
+
5
+ interface Props {
6
+ tab: string;
7
+ tabs: PageEditor.Tab[];
8
+ setTab: (tab: string) => void;
9
+ }
10
+
11
+ export default function Tabs(props: Props) {
12
+ const { tab, tabs, setTab } = props;
13
+
14
+ const handleTabChange = (tab: PageEditor.Tab) => (evt: React.MouseEvent) => {
15
+ evt.preventDefault();
16
+ setTab(tab.id);
17
+ };
18
+
19
+ return (
20
+ <ul className="content-tabs" role="tablist">
21
+ {tabs.map((t) => (
22
+ <li key={t.id} className={t.id == tab ? "current" : ""}>
23
+ {!t.enabled && t.name}
24
+ {t.enabled && (
25
+ <a href={`#${t.id}`} onClick={handleTabChange(t)}>
26
+ {t.name}
27
+ </a>
28
+ )}
29
+ </li>
30
+ ))}
31
+ </ul>
32
+ );
33
+ }