pages_core 3.14.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 (227) 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 +672 -379
  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 +1 -1
  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 +2 -2
  68. data/app/controllers/admin/pages_controller.rb +22 -42
  69. data/app/controllers/concerns/pages_core/error_reporting.rb +1 -1
  70. data/app/controllers/concerns/pages_core/page_parameters.rb +29 -0
  71. data/app/controllers/concerns/pages_core/policies_helper.rb +1 -1
  72. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +20 -20
  73. data/app/controllers/pages_core/admin_controller.rb +0 -2
  74. data/app/controllers/pages_core/frontend/pages_controller.rb +2 -6
  75. data/app/formatters/pages_core/html_formatter.rb +2 -4
  76. data/app/helpers/admin/menu_helper.rb +5 -4
  77. data/app/helpers/admin/pages_helper.rb +1 -21
  78. data/app/helpers/pages_core/admin/admin_helper.rb +2 -3
  79. data/app/helpers/pages_core/admin/content_tabs_helper.rb +1 -2
  80. data/app/helpers/pages_core/admin/labelled_field_helper.rb +1 -1
  81. data/app/helpers/pages_core/frontend_helper.rb +1 -1
  82. data/app/helpers/pages_core/images_helper.rb +10 -8
  83. data/app/helpers/pages_core/labelled_form_builder.rb +2 -7
  84. data/app/helpers/pages_core/page_path_helper.rb +1 -1
  85. data/app/javascript/components/Attachments/Attachment.tsx +20 -18
  86. data/app/javascript/components/Attachments/AttachmentEditor.tsx +11 -9
  87. data/app/javascript/components/{Attachments.jsx → Attachments/List.tsx} +58 -63
  88. data/app/javascript/components/Attachments/useAttachments.ts +15 -0
  89. data/app/javascript/components/Attachments.tsx +14 -0
  90. data/app/javascript/components/DateRangeSelect.tsx +105 -0
  91. data/app/javascript/components/DateTimeSelect.tsx +136 -0
  92. data/app/javascript/components/EditableImage.tsx +11 -9
  93. data/app/javascript/components/FileUploadButton.tsx +7 -7
  94. data/app/javascript/components/ImageCropper/FocalPoint.tsx +9 -12
  95. data/app/javascript/components/ImageCropper/Image.tsx +10 -8
  96. data/app/javascript/components/ImageCropper/Toolbar.tsx +11 -12
  97. data/app/javascript/components/ImageCropper/useCrop.ts +24 -53
  98. data/app/javascript/components/ImageCropper.tsx +10 -15
  99. data/app/javascript/components/ImageEditor/Form.tsx +12 -8
  100. data/app/javascript/components/ImageEditor.tsx +12 -7
  101. data/app/javascript/components/ImageGrid/DragElement.tsx +9 -12
  102. data/app/javascript/components/{ImageGrid.jsx → ImageGrid/Grid.tsx} +62 -71
  103. data/app/javascript/components/ImageGrid/GridImage.tsx +22 -23
  104. data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -2
  105. data/app/javascript/components/ImageGrid/useImageGrid.ts +26 -0
  106. data/app/javascript/components/ImageGrid.tsx +15 -0
  107. data/app/javascript/components/ImageUploader.tsx +35 -22
  108. data/app/javascript/components/LabelledField.tsx +34 -0
  109. data/app/javascript/components/Modal.tsx +2 -2
  110. data/app/javascript/components/PageForm/Block.tsx +81 -0
  111. data/app/javascript/components/PageForm/Content.tsx +54 -0
  112. data/app/javascript/components/PageForm/Dates.tsx +66 -0
  113. data/app/javascript/components/PageForm/Files.tsx +28 -0
  114. data/app/javascript/components/PageForm/Form.tsx +41 -0
  115. data/app/javascript/components/PageForm/Images.tsx +28 -0
  116. data/app/javascript/components/PageForm/LocaleLinks.tsx +36 -0
  117. data/app/javascript/components/PageForm/Metadata.tsx +67 -0
  118. data/app/javascript/components/PageForm/Options.tsx +180 -0
  119. data/app/javascript/components/PageForm/PageDescription.tsx +48 -0
  120. data/app/javascript/components/PageForm/PathSegment.tsx +65 -0
  121. data/app/javascript/components/PageForm/TabPanel.tsx +21 -0
  122. data/app/javascript/components/PageForm/Tabs.tsx +33 -0
  123. data/app/javascript/components/PageForm/UnconfiguredContent.tsx +42 -0
  124. data/app/javascript/components/PageForm/pageParams.ts +95 -0
  125. data/app/javascript/components/PageForm/preview.ts +23 -0
  126. data/app/javascript/components/PageForm/usePage.ts +169 -0
  127. data/app/javascript/components/PageForm/useTabs.ts +46 -0
  128. data/app/javascript/components/PageForm.tsx +163 -0
  129. data/app/javascript/components/PageImages.tsx +7 -9
  130. data/app/javascript/components/PageTree/Draggable.tsx +40 -39
  131. data/app/javascript/components/PageTree/Node.tsx +62 -56
  132. data/app/javascript/components/PageTree/PageName.tsx +28 -0
  133. data/app/javascript/components/PageTree.tsx +65 -53
  134. data/app/javascript/components/{RichTextArea.jsx → RichTextArea.tsx} +98 -79
  135. data/app/javascript/components/RichTextToolbarButton.tsx +4 -6
  136. data/app/javascript/components/TagEditor/AddTagForm.tsx +19 -12
  137. data/app/javascript/components/TagEditor/Editor.tsx +32 -0
  138. data/app/javascript/components/TagEditor/Tag.tsx +6 -4
  139. data/app/javascript/components/TagEditor/useTags.ts +58 -0
  140. data/app/javascript/components/TagEditor.tsx +8 -58
  141. data/app/javascript/components/Toast.tsx +3 -3
  142. data/app/javascript/components/drag/draggedOrder.ts +22 -14
  143. data/app/javascript/components/drag/useDragCollection.ts +35 -30
  144. data/app/javascript/components/drag/useDragUploader.ts +32 -21
  145. data/app/javascript/components/drag/useDraggable.ts +7 -6
  146. data/app/javascript/components/drag.ts +0 -1
  147. data/app/javascript/components.ts +1 -3
  148. data/app/javascript/features/RichText.tsx +2 -3
  149. data/app/javascript/features/contentTabs.ts +79 -0
  150. data/app/javascript/index.ts +5 -12
  151. data/app/javascript/lib/Tree.ts +31 -45
  152. data/app/javascript/lib/request.ts +11 -11
  153. data/app/javascript/stores/useToastStore.ts +1 -1
  154. data/app/javascript/types/Attachments.ts +29 -0
  155. data/app/javascript/types/Crop.ts +36 -0
  156. data/app/javascript/types/Drag.ts +34 -0
  157. data/app/javascript/types/Images.ts +47 -0
  158. data/app/javascript/types/PageEditor.ts +26 -0
  159. data/app/javascript/types/Pages.ts +75 -0
  160. data/app/javascript/types/Tags.ts +9 -0
  161. data/app/javascript/types/Template.ts +24 -0
  162. data/app/javascript/types/Trees.ts +19 -0
  163. data/app/javascript/types.ts +2 -25
  164. data/app/models/attachment.rb +1 -1
  165. data/app/models/concerns/pages_core/authenticable_user.rb +63 -0
  166. data/app/models/concerns/pages_core/emailable.rb +16 -0
  167. data/app/models/concerns/pages_core/page_model/templateable.rb +2 -16
  168. data/app/models/invite.rb +2 -6
  169. data/app/models/otp_secret.rb +4 -4
  170. data/app/models/page.rb +0 -3
  171. data/app/models/user.rb +2 -46
  172. data/app/policies/page_policy.rb +6 -2
  173. data/app/resources/admin/page_resource.rb +95 -0
  174. data/app/resources/admin/page_tree_resource.rb +27 -0
  175. data/app/resources/admin/template_configuration_resource.rb +50 -0
  176. data/app/views/admin/news/_sidebar.html.erb +2 -4
  177. data/app/views/admin/news/index.html.erb +0 -1
  178. data/app/views/admin/pages/_form.html.erb +10 -30
  179. data/app/views/admin/pages/_search_bar.html.erb +1 -1
  180. data/app/views/admin/pages/edit.html.erb +1 -57
  181. data/app/views/admin/pages/index.html.erb +1 -1
  182. data/app/views/admin/pages/new.html.erb +1 -44
  183. data/app/views/admin/sessions/new.html.erb +9 -11
  184. data/app/views/admin/users/_access_control.html.erb +5 -1
  185. data/app/views/admin/users/_list.html.erb +12 -7
  186. data/app/views/layouts/admin/_header.html.erb +2 -4
  187. data/app/views/layouts/admin/_page_header.html.erb +1 -2
  188. data/app/views/layouts/admin.html.erb +1 -1
  189. data/config/locales/en.yml +0 -4
  190. data/config/routes.rb +3 -7
  191. data/db/migrate/20240126160700_add_2fa_fields.rb +5 -1
  192. data/db/migrate/20240131140700_change_email_to_citext.rb +18 -0
  193. data/db/migrate/20240201160700_remove_persistent_data.rb +7 -0
  194. data/db/migrate/20240508145300_remove_categories.rb +21 -0
  195. data/lib/pages_core/configuration/base.rb +2 -2
  196. data/lib/pages_core/templates/configuration.rb +1 -1
  197. data/lib/pages_core/templates/configuration_proxy.rb +2 -2
  198. data/lib/pages_core/templates/template_configuration.rb +11 -1
  199. data/lib/pages_core/templates.rb +6 -4
  200. data/lib/pages_core/version.rb +1 -1
  201. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +6 -7
  202. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +17 -12
  203. data/lib/rails/generators/pages_core/rspec/rspec_generator.rb +0 -2
  204. data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +3 -4
  205. metadata +95 -29
  206. data/app/assets/stylesheets/pages_core/admin/components/login.css +0 -27
  207. data/app/controllers/admin/categories_controller.rb +0 -56
  208. data/app/controllers/concerns/pages_core/admin/persistent_params.rb +0 -75
  209. data/app/helpers/pages_core/admin/page_blocks_helper.rb +0 -66
  210. data/app/helpers/pages_core/admin/page_json_helper.rb +0 -23
  211. data/app/javascript/components/DateRangeSelect.jsx +0 -225
  212. data/app/javascript/components/PageDates.jsx +0 -73
  213. data/app/javascript/components/PageFiles.jsx +0 -25
  214. data/app/javascript/components/PageTree/types.ts +0 -15
  215. data/app/javascript/components/drag/types.ts +0 -28
  216. data/app/javascript/controllers/EditPageController.ts +0 -22
  217. data/app/javascript/controllers/MainController.ts +0 -74
  218. data/app/javascript/controllers/PageOptionsController.js +0 -67
  219. data/app/models/category.rb +0 -22
  220. data/app/models/concerns/pages_core/has_otp.rb +0 -27
  221. data/app/models/page_category.rb +0 -6
  222. data/app/views/admin/pages/_edit_content.html.erb +0 -19
  223. data/app/views/admin/pages/_edit_files.html.erb +0 -4
  224. data/app/views/admin/pages/_edit_images.html.erb +0 -4
  225. data/app/views/admin/pages/_edit_metadata.html.erb +0 -35
  226. data/app/views/admin/pages/_edit_options.html.erb +0 -91
  227. data/lib/rails/generators/pages_core/rspec/templates/mailer_macros.rb +0 -11
@@ -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
+ }
@@ -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
+ }