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,26 @@
1
+ import { useState } from "react";
2
+
3
+ import * as Images from "../../types/Images";
4
+
5
+ import { useDragCollection } from "../drag";
6
+
7
+ export default function useImageGrid(
8
+ records: Images.Record[],
9
+ enablePrimary = false
10
+ ): Images.GridState {
11
+ const primaryRecords = enablePrimary
12
+ ? records.filter((r) => r.primary).slice(0, 1)
13
+ : [];
14
+ const imageRecords = records.filter((r) => primaryRecords.indexOf(r) === -1);
15
+
16
+ const primary = useDragCollection(primaryRecords);
17
+ const images = useDragCollection(imageRecords);
18
+ const [deleted, setDeleted] = useState<Images.Record[]>([]);
19
+
20
+ return {
21
+ primary: primary,
22
+ images: images,
23
+ deleted: deleted,
24
+ setDeleted: setDeleted
25
+ };
26
+ }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+
3
+ import * as Images from "../types/Images";
4
+ import useImageGrid from "./ImageGrid/useImageGrid";
5
+ import Grid from "./ImageGrid/Grid";
6
+
7
+ interface Props extends Images.GridOptions {
8
+ records: Images.Record[];
9
+ }
10
+
11
+ export default function ImageGrid(props: Props) {
12
+ const state = useImageGrid(props.records, props.showEmbed);
13
+
14
+ return <Grid state={state} {...props} />;
15
+ }
@@ -1,21 +1,28 @@
1
- import React, { useState } from "react";
2
- import EditableImage from "./EditableImage";
3
- import FileUploadButton from "./FileUploadButton";
1
+ import React, { DragEvent, MouseEvent, useState } from "react";
2
+
4
3
  import useToastStore from "../stores/useToastStore";
5
- import { ImageResource, Locale } from "../types";
6
4
  import { post } from "../lib/request";
5
+ import * as Images from "../types/Images";
6
+ import { Locale } from "../types";
7
7
 
8
- type ImageResponse = ImageResource | { status: "error"; error: string };
8
+ import EditableImage from "./EditableImage";
9
+ import FileUploadButton from "./FileUploadButton";
9
10
 
10
- interface ImageUploaderProps {
11
+ interface Props {
11
12
  locale: string;
12
13
  locales: { [index: string]: Locale };
13
- image: ImageResource;
14
+ image: Images.Resource;
14
15
  src: string;
15
16
  width: number;
16
17
  caption: boolean;
17
18
  attr: string;
18
- alternative: string;
19
+ alternative?: string;
20
+ onChange?: (state: State) => void;
21
+ }
22
+
23
+ interface State {
24
+ image?: Images.Resource;
25
+ src?: string;
19
26
  }
20
27
 
21
28
  function getFiles(dt: DataTransfer): File[] {
@@ -34,14 +41,23 @@ function getFiles(dt: DataTransfer): File[] {
34
41
  return files;
35
42
  }
36
43
 
37
- export default function ImageUploader(props: ImageUploaderProps) {
44
+ export default function ImageUploader(props: Props) {
38
45
  const [uploading, setUploading] = useState(false);
39
46
  const [dragover, setDragover] = useState(false);
40
- const [image, setImage] = useState(props.image);
41
- const [src, setSrc] = useState(props.src);
47
+ const [state, setState] = useState<State>({
48
+ image: props.image,
49
+ src: props.src
50
+ });
51
+ const { image, src } = props.onChange ? props : state;
52
+
42
53
  const error = useToastStore((state) => state.error);
43
54
 
44
- const handleDragOver = (evt: Event) => {
55
+ const update = (image: Images.Resource | null, src?: string) => {
56
+ const handler = props.onChange || setState;
57
+ handler({ image: image, src: src || null });
58
+ };
59
+
60
+ const handleDragOver = (evt: DragEvent) => {
45
61
  evt.preventDefault();
46
62
  setDragover(true);
47
63
  };
@@ -50,7 +66,7 @@ export default function ImageUploader(props: ImageUploaderProps) {
50
66
  setDragover(false);
51
67
  };
52
68
 
53
- const handleDragEnd = (evt: Event) => {
69
+ const handleDragEnd = (evt: DragEvent) => {
54
70
  if ("dataTransfer" in evt) {
55
71
  if ("items" in evt.dataTransfer && "remove" in evt.dataTransfer.items) {
56
72
  for (let i = 0; i < evt.dataTransfer.items.length; i++) {
@@ -63,7 +79,7 @@ export default function ImageUploader(props: ImageUploaderProps) {
63
79
  setDragover(false);
64
80
  };
65
81
 
66
- const handleDrop = (evt: Event) => {
82
+ const handleDrop = (evt: DragEvent) => {
67
83
  let files: File[] = [];
68
84
  if ("dataTransfer" in evt) {
69
85
  files = getFiles(evt.dataTransfer);
@@ -74,10 +90,9 @@ export default function ImageUploader(props: ImageUploaderProps) {
74
90
  }
75
91
  };
76
92
 
77
- const handleRemove = (evt: Event) => {
93
+ const handleRemove = (evt: MouseEvent) => {
78
94
  evt.preventDefault();
79
- setImage(null);
80
- setSrc(null);
95
+ update(null);
81
96
  };
82
97
 
83
98
  const receiveFiles = (files: File[]) => {
@@ -108,8 +123,7 @@ export default function ImageUploader(props: ImageUploaderProps) {
108
123
 
109
124
  const data = new FormData();
110
125
 
111
- setImage(null);
112
- setSrc(null);
126
+ update(null);
113
127
  setDragover(false);
114
128
  setUploading(true);
115
129
 
@@ -118,13 +132,12 @@ export default function ImageUploader(props: ImageUploaderProps) {
118
132
  data.append(`image[alternative][${l}]`, props.alternative || "");
119
133
  });
120
134
 
121
- void post("/admin/images.json", data).then((response: ImageResponse) => {
135
+ void post("/admin/images.json", data).then((response: Images.Response) => {
122
136
  setUploading(false);
123
137
  if ("status" in response && response.status === "error") {
124
138
  error(`Error uploading image: ${response.error}`);
125
139
  } else if ("thumbnail_url" in response) {
126
- setSrc(response.thumbnail_url);
127
- setImage(response);
140
+ update(response, response.thumbnail_url);
128
141
  }
129
142
  });
130
143
  };
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+
3
+ interface LabelledFieldProps {
4
+ label: string;
5
+ children: React.ReactNode;
6
+ htmlFor?: string;
7
+ description?: string;
8
+ errors?: string[];
9
+ }
10
+
11
+ export default function LabelledField(props: LabelledFieldProps) {
12
+ const { htmlFor, description, label, errors, children } = props;
13
+
14
+ const classNames = ["field"];
15
+ if (errors && errors.length > 0) {
16
+ classNames.push("field-with-errors");
17
+ }
18
+
19
+ return (
20
+ <div className={classNames.join(" ")}>
21
+ <label htmlFor={htmlFor}>
22
+ {label}
23
+ {errors && (
24
+ <React.Fragment>
25
+ {" "}
26
+ <span className="error">{errors[errors.length - 1]}</span>
27
+ </React.Fragment>
28
+ )}
29
+ </label>
30
+ {description && <p className="description">{description}</p>}
31
+ {children}
32
+ </div>
33
+ );
34
+ }
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from "react";
1
+ import React, { MouseEvent, useEffect } from "react";
2
2
 
3
3
  import useModalStore from "../stores/useModalStore";
4
4
 
@@ -6,7 +6,7 @@ export default function Modal() {
6
6
  const component = useModalStore((state) => state.component);
7
7
  const close = useModalStore((state) => state.close);
8
8
 
9
- const handleClose = (evt: Event) => {
9
+ const handleClose = (evt: KeyboardEvent | MouseEvent) => {
10
10
  evt.stopPropagation();
11
11
  evt.preventDefault();
12
12
  close();
@@ -0,0 +1,81 @@
1
+ import React, { ChangeEvent } from "react";
2
+
3
+ import * as Template from "../../types/Template";
4
+
5
+ import LabelledField from "../LabelledField";
6
+ import RichTextArea from "../RichTextArea";
7
+
8
+ interface Props {
9
+ block: Template.Block;
10
+ errors: string[];
11
+ onChange: (value: string) => void;
12
+ lang: string;
13
+ dir: string;
14
+ value: string;
15
+ }
16
+
17
+ export default function Block(props: Props) {
18
+ const { block, errors, onChange, lang, dir, value } = props;
19
+
20
+ const handleChange = (
21
+ evt: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLSelectElement>
22
+ ) => {
23
+ onChange(evt.target.value);
24
+ };
25
+
26
+ const id = `page_${block.name}`;
27
+
28
+ const commonOptions = {
29
+ id: id,
30
+ name: `page[${block.name}]`,
31
+ value: value
32
+ };
33
+
34
+ const textFieldOptions = {
35
+ ...commonOptions,
36
+ className: ["rich", block.class].join(" ").trim(),
37
+ lang: lang,
38
+ dir: dir,
39
+ placeholder: block.placeholder
40
+ };
41
+
42
+ let field: React.ReactNode;
43
+ if (block.type == "select") {
44
+ const options = block.options;
45
+
46
+ // Ensure the current value is part of the options
47
+ if (options.map((o) => o[1]).indexOf(value) === -1) {
48
+ options.push([value, value]);
49
+ }
50
+
51
+ field = (
52
+ <select onChange={handleChange} {...commonOptions}>
53
+ {options.map((opt) => (
54
+ <option key={opt[1]} value={opt[1]}>
55
+ {opt[0]}
56
+ </option>
57
+ ))}
58
+ </select>
59
+ );
60
+ } else if (block.size == "field") {
61
+ field = <input type="text" onChange={handleChange} {...textFieldOptions} />;
62
+ } else {
63
+ field = (
64
+ <RichTextArea
65
+ onChange={onChange}
66
+ rows={block.size == "large" ? 15 : 5}
67
+ {...textFieldOptions}
68
+ />
69
+ );
70
+ }
71
+
72
+ return (
73
+ <LabelledField
74
+ htmlFor={id}
75
+ label={block.title}
76
+ description={block.description}
77
+ errors={errors}>
78
+ {field}
79
+ </LabelledField>
80
+ );
81
+ }
@@ -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
+ }