pages_core 3.14.0 → 3.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/assets/builds/fonts/6569749d.ttf +0 -0
  4. data/app/assets/builds/fonts/7b7db107.woff2 +0 -0
  5. data/app/assets/builds/fonts/921961e9.woff2 +0 -0
  6. data/app/assets/builds/fonts/ee32bc60.ttf +0 -0
  7. data/app/assets/builds/pages_core/admin-dist.js +19 -8
  8. data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
  9. data/app/assets/builds/pages_core/admin.css +699 -394
  10. data/app/assets/builds/pages_core/mailer.css +99 -0
  11. data/app/assets/fonts/Inter-Black.woff2 +0 -0
  12. data/app/assets/fonts/Inter-BlackItalic.woff2 +0 -0
  13. data/app/assets/fonts/Inter-Bold.woff2 +0 -0
  14. data/app/assets/fonts/Inter-BoldItalic.woff2 +0 -0
  15. data/app/assets/fonts/Inter-ExtraBold.woff2 +0 -0
  16. data/app/assets/fonts/Inter-ExtraBoldItalic.woff2 +0 -0
  17. data/app/assets/fonts/Inter-ExtraLight.woff2 +0 -0
  18. data/app/assets/fonts/Inter-ExtraLightItalic.woff2 +0 -0
  19. data/app/assets/fonts/Inter-Italic.woff2 +0 -0
  20. data/app/assets/fonts/Inter-Light.woff2 +0 -0
  21. data/app/assets/fonts/Inter-LightItalic.woff2 +0 -0
  22. data/app/assets/fonts/Inter-Medium.woff2 +0 -0
  23. data/app/assets/fonts/Inter-MediumItalic.woff2 +0 -0
  24. data/app/assets/fonts/Inter-Regular.woff2 +0 -0
  25. data/app/assets/fonts/Inter-SemiBold.woff2 +0 -0
  26. data/app/assets/fonts/Inter-SemiBoldItalic.woff2 +0 -0
  27. data/app/assets/fonts/Inter-Thin.woff2 +0 -0
  28. data/app/assets/fonts/Inter-ThinItalic.woff2 +0 -0
  29. data/app/assets/fonts/InterDisplay-Black.woff2 +0 -0
  30. data/app/assets/fonts/InterDisplay-BlackItalic.woff2 +0 -0
  31. data/app/assets/fonts/InterDisplay-Bold.woff2 +0 -0
  32. data/app/assets/fonts/InterDisplay-BoldItalic.woff2 +0 -0
  33. data/app/assets/fonts/InterDisplay-ExtraBold.woff2 +0 -0
  34. data/app/assets/fonts/InterDisplay-ExtraBoldItalic.woff2 +0 -0
  35. data/app/assets/fonts/InterDisplay-ExtraLight.woff2 +0 -0
  36. data/app/assets/fonts/InterDisplay-ExtraLightItalic.woff2 +0 -0
  37. data/app/assets/fonts/InterDisplay-Italic.woff2 +0 -0
  38. data/app/assets/fonts/InterDisplay-Light.woff2 +0 -0
  39. data/app/assets/fonts/InterDisplay-LightItalic.woff2 +0 -0
  40. data/app/assets/fonts/InterDisplay-Medium.woff2 +0 -0
  41. data/app/assets/fonts/InterDisplay-MediumItalic.woff2 +0 -0
  42. data/app/assets/fonts/InterDisplay-Regular.woff2 +0 -0
  43. data/app/assets/fonts/InterDisplay-SemiBold.woff2 +0 -0
  44. data/app/assets/fonts/InterDisplay-SemiBoldItalic.woff2 +0 -0
  45. data/app/assets/fonts/InterDisplay-Thin.woff2 +0 -0
  46. data/app/assets/fonts/InterDisplay-ThinItalic.woff2 +0 -0
  47. data/app/assets/fonts/InterVariable-Italic.woff2 +0 -0
  48. data/app/assets/fonts/InterVariable.woff2 +0 -0
  49. data/app/assets/stylesheets/pages_core/admin/components/archive.css +1 -1
  50. data/app/assets/stylesheets/pages_core/admin/components/attachments.css +22 -34
  51. data/app/assets/stylesheets/pages_core/admin/components/base.css +1 -68
  52. data/app/assets/stylesheets/pages_core/admin/components/forms.css +109 -48
  53. data/app/assets/stylesheets/pages_core/admin/components/header.css +56 -58
  54. data/app/assets/stylesheets/pages_core/admin/components/image_editor.css +35 -24
  55. data/app/assets/stylesheets/pages_core/admin/components/image_grid.css +28 -27
  56. data/app/assets/stylesheets/pages_core/admin/components/image_uploader.css +5 -5
  57. data/app/assets/stylesheets/pages_core/admin/components/layout.css +7 -1
  58. data/app/assets/stylesheets/pages_core/admin/components/list_table.css +24 -15
  59. data/app/assets/stylesheets/pages_core/admin/components/page_tree.css +63 -104
  60. data/app/assets/stylesheets/pages_core/admin/components/pagination.css +12 -13
  61. data/app/assets/stylesheets/pages_core/admin/components/search.css +1 -16
  62. data/app/assets/stylesheets/pages_core/admin/components/sidebar.css +5 -11
  63. data/app/assets/stylesheets/pages_core/admin/components/tag_editor.css +22 -36
  64. data/app/assets/stylesheets/pages_core/admin/components/toast.css +1 -2
  65. data/app/assets/stylesheets/pages_core/admin/components/toolbar.css +10 -10
  66. data/app/assets/stylesheets/pages_core/admin/components/totp.css +1 -1
  67. data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +37 -51
  68. data/app/assets/stylesheets/pages_core/admin/global/fonts.css +271 -0
  69. data/app/assets/stylesheets/pages_core/admin/global/typography.css +109 -0
  70. data/app/assets/stylesheets/pages_core/admin/vars.css +1 -3
  71. data/app/assets/stylesheets/pages_core/{admin.postcss.css → admin.css} +1 -0
  72. data/app/assets/stylesheets/pages_core/mailer.css +90 -0
  73. data/app/controllers/admin/account_recoveries_controller.rb +2 -2
  74. data/app/controllers/admin/pages_controller.rb +22 -42
  75. data/app/controllers/concerns/pages_core/error_reporting.rb +1 -1
  76. data/app/controllers/concerns/pages_core/page_parameters.rb +29 -0
  77. data/app/controllers/concerns/pages_core/policies_helper.rb +1 -1
  78. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +20 -20
  79. data/app/controllers/pages_core/admin_controller.rb +0 -2
  80. data/app/controllers/pages_core/frontend/pages_controller.rb +2 -6
  81. data/app/formatters/pages_core/html_formatter.rb +2 -4
  82. data/app/helpers/admin/menu_helper.rb +5 -4
  83. data/app/helpers/admin/pages_helper.rb +1 -21
  84. data/app/helpers/pages_core/admin/admin_helper.rb +2 -3
  85. data/app/helpers/pages_core/admin/content_tabs_helper.rb +1 -2
  86. data/app/helpers/pages_core/admin/labelled_field_helper.rb +1 -1
  87. data/app/helpers/pages_core/attachments_helper.rb +1 -1
  88. data/app/helpers/pages_core/frontend_helper.rb +1 -1
  89. data/app/helpers/pages_core/images_helper.rb +10 -8
  90. data/app/helpers/pages_core/labelled_form_builder.rb +2 -7
  91. data/app/helpers/pages_core/page_path_helper.rb +1 -1
  92. data/app/javascript/components/Attachments/Attachment.tsx +20 -18
  93. data/app/javascript/components/Attachments/AttachmentEditor.tsx +11 -9
  94. data/app/javascript/components/{Attachments.jsx → Attachments/List.tsx} +58 -63
  95. data/app/javascript/components/Attachments/useAttachments.ts +15 -0
  96. data/app/javascript/components/Attachments.tsx +14 -0
  97. data/app/javascript/components/DateRangeSelect.tsx +105 -0
  98. data/app/javascript/components/DateTimeSelect.tsx +136 -0
  99. data/app/javascript/components/EditableImage.tsx +11 -9
  100. data/app/javascript/components/FileUploadButton.tsx +7 -7
  101. data/app/javascript/components/ImageCropper/FocalPoint.tsx +9 -12
  102. data/app/javascript/components/ImageCropper/Image.tsx +10 -8
  103. data/app/javascript/components/ImageCropper/Toolbar.tsx +11 -12
  104. data/app/javascript/components/ImageCropper/useCrop.ts +24 -53
  105. data/app/javascript/components/ImageCropper.tsx +10 -15
  106. data/app/javascript/components/ImageEditor/Form.tsx +12 -8
  107. data/app/javascript/components/ImageEditor.tsx +12 -7
  108. data/app/javascript/components/ImageGrid/DragElement.tsx +9 -12
  109. data/app/javascript/components/{ImageGrid.jsx → ImageGrid/Grid.tsx} +62 -71
  110. data/app/javascript/components/ImageGrid/GridImage.tsx +22 -23
  111. data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -2
  112. data/app/javascript/components/ImageGrid/useImageGrid.ts +26 -0
  113. data/app/javascript/components/ImageGrid.tsx +15 -0
  114. data/app/javascript/components/ImageUploader.tsx +35 -22
  115. data/app/javascript/components/LabelledField.tsx +34 -0
  116. data/app/javascript/components/Modal.tsx +2 -2
  117. data/app/javascript/components/PageForm/Block.tsx +81 -0
  118. data/app/javascript/components/PageForm/Content.tsx +54 -0
  119. data/app/javascript/components/PageForm/Dates.tsx +66 -0
  120. data/app/javascript/components/PageForm/Files.tsx +28 -0
  121. data/app/javascript/components/PageForm/Form.tsx +41 -0
  122. data/app/javascript/components/PageForm/Images.tsx +28 -0
  123. data/app/javascript/components/PageForm/LocaleLinks.tsx +36 -0
  124. data/app/javascript/components/PageForm/Metadata.tsx +67 -0
  125. data/app/javascript/components/PageForm/Options.tsx +180 -0
  126. data/app/javascript/components/PageForm/PageDescription.tsx +48 -0
  127. data/app/javascript/components/PageForm/PathSegment.tsx +65 -0
  128. data/app/javascript/components/PageForm/TabPanel.tsx +21 -0
  129. data/app/javascript/components/PageForm/Tabs.tsx +33 -0
  130. data/app/javascript/components/PageForm/UnconfiguredContent.tsx +42 -0
  131. data/app/javascript/components/PageForm/pageParams.ts +95 -0
  132. data/app/javascript/components/PageForm/preview.ts +23 -0
  133. data/app/javascript/components/PageForm/usePage.ts +169 -0
  134. data/app/javascript/components/PageForm/useTabs.ts +46 -0
  135. data/app/javascript/components/PageForm.tsx +169 -0
  136. data/app/javascript/components/PageImages.tsx +7 -9
  137. data/app/javascript/components/PageTree/Draggable.tsx +40 -39
  138. data/app/javascript/components/PageTree/Node.tsx +62 -56
  139. data/app/javascript/components/PageTree/PageName.tsx +28 -0
  140. data/app/javascript/components/PageTree.tsx +65 -53
  141. data/app/javascript/components/{RichTextArea.jsx → RichTextArea.tsx} +98 -79
  142. data/app/javascript/components/RichTextToolbarButton.tsx +4 -6
  143. data/app/javascript/components/TagEditor/AddTagForm.tsx +19 -12
  144. data/app/javascript/components/TagEditor/Editor.tsx +32 -0
  145. data/app/javascript/components/TagEditor/Tag.tsx +6 -4
  146. data/app/javascript/components/TagEditor/useTags.ts +58 -0
  147. data/app/javascript/components/TagEditor.tsx +8 -58
  148. data/app/javascript/components/Toast.tsx +3 -3
  149. data/app/javascript/components/drag/draggedOrder.ts +22 -14
  150. data/app/javascript/components/drag/useDragCollection.ts +35 -30
  151. data/app/javascript/components/drag/useDragUploader.ts +32 -21
  152. data/app/javascript/components/drag/useDraggable.ts +7 -6
  153. data/app/javascript/components/drag.ts +0 -1
  154. data/app/javascript/components.ts +1 -3
  155. data/app/javascript/features/RichText.tsx +2 -3
  156. data/app/javascript/features/contentTabs.ts +79 -0
  157. data/app/javascript/index.ts +5 -12
  158. data/app/javascript/lib/Tree.ts +31 -45
  159. data/app/javascript/lib/request.ts +11 -11
  160. data/app/javascript/stores/useToastStore.ts +1 -1
  161. data/app/javascript/types/Attachments.ts +29 -0
  162. data/app/javascript/types/Crop.ts +36 -0
  163. data/app/javascript/types/Drag.ts +34 -0
  164. data/app/javascript/types/Images.ts +47 -0
  165. data/app/javascript/types/PageEditor.ts +26 -0
  166. data/app/javascript/types/Pages.ts +75 -0
  167. data/app/javascript/types/Tags.ts +9 -0
  168. data/app/javascript/types/Template.ts +24 -0
  169. data/app/javascript/types/Trees.ts +19 -0
  170. data/app/javascript/types.ts +2 -25
  171. data/app/mailers/admin_mailer.rb +5 -9
  172. data/app/models/attachment.rb +1 -1
  173. data/app/models/autopublisher.rb +1 -1
  174. data/app/models/concerns/pages_core/authenticable_user.rb +63 -0
  175. data/app/models/concerns/pages_core/emailable.rb +16 -0
  176. data/app/models/concerns/pages_core/page_model/dated_page.rb +3 -3
  177. data/app/models/concerns/pages_core/page_model/templateable.rb +2 -16
  178. data/app/models/concerns/pages_core/taggable.rb +2 -19
  179. data/app/models/invite.rb +2 -6
  180. data/app/models/otp_secret.rb +4 -4
  181. data/app/models/page.rb +0 -3
  182. data/app/models/user.rb +2 -46
  183. data/app/policies/page_policy.rb +6 -2
  184. data/app/resources/admin/page_resource.rb +95 -0
  185. data/app/resources/admin/page_tree_resource.rb +27 -0
  186. data/app/resources/admin/template_configuration_resource.rb +50 -0
  187. data/app/views/admin/news/_sidebar.html.erb +2 -4
  188. data/app/views/admin/news/index.html.erb +0 -1
  189. data/app/views/admin/pages/_form.html.erb +10 -30
  190. data/app/views/admin/pages/_search_bar.html.erb +1 -1
  191. data/app/views/admin/pages/edit.html.erb +1 -57
  192. data/app/views/admin/pages/index.html.erb +1 -1
  193. data/app/views/admin/pages/new.html.erb +1 -44
  194. data/app/views/admin/sessions/new.html.erb +9 -11
  195. data/app/views/admin/users/_access_control.html.erb +5 -1
  196. data/app/views/admin/users/_list.html.erb +12 -7
  197. data/app/views/admin_mailer/account_recovery.html.erb +20 -0
  198. data/app/views/admin_mailer/invite.html.erb +11 -0
  199. data/app/views/layouts/admin/_header.html.erb +2 -4
  200. data/app/views/layouts/admin/_page_header.html.erb +1 -2
  201. data/app/views/layouts/admin.html.erb +1 -1
  202. data/app/views/layouts/pages_core/mailer.html.erb +11 -0
  203. data/config/locales/en.yml +0 -4
  204. data/config/routes.rb +3 -7
  205. data/db/migrate/20240126160700_add_2fa_fields.rb +5 -1
  206. data/db/migrate/20240131140700_change_email_to_citext.rb +18 -0
  207. data/db/migrate/20240201160700_remove_persistent_data.rb +7 -0
  208. data/db/migrate/20240508145300_remove_categories.rb +21 -0
  209. data/lib/pages_core/configuration/base.rb +2 -2
  210. data/lib/pages_core/engine.rb +1 -0
  211. data/lib/pages_core/templates/configuration.rb +1 -1
  212. data/lib/pages_core/templates/configuration_proxy.rb +2 -2
  213. data/lib/pages_core/templates/template_configuration.rb +11 -1
  214. data/lib/pages_core/templates.rb +6 -4
  215. data/lib/pages_core/version.rb +1 -1
  216. data/lib/pages_core.rb +1 -0
  217. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +6 -7
  218. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +17 -12
  219. data/lib/rails/generators/pages_core/rspec/rspec_generator.rb +0 -2
  220. data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +3 -4
  221. metadata +119 -36
  222. data/app/assets/builds/fonts/2a3059ad.ttf +0 -0
  223. data/app/assets/builds/fonts/47262711.woff2 +0 -0
  224. data/app/assets/builds/fonts/500ddeb0.woff2 +0 -0
  225. data/app/assets/builds/fonts/81221036.ttf +0 -0
  226. data/app/assets/stylesheets/pages_core/admin/components/login.css +0 -27
  227. data/app/controllers/admin/categories_controller.rb +0 -56
  228. data/app/controllers/concerns/pages_core/admin/persistent_params.rb +0 -75
  229. data/app/helpers/pages_core/admin/page_blocks_helper.rb +0 -66
  230. data/app/helpers/pages_core/admin/page_json_helper.rb +0 -23
  231. data/app/javascript/components/DateRangeSelect.jsx +0 -225
  232. data/app/javascript/components/PageDates.jsx +0 -73
  233. data/app/javascript/components/PageFiles.jsx +0 -25
  234. data/app/javascript/components/PageTree/types.ts +0 -15
  235. data/app/javascript/components/drag/types.ts +0 -28
  236. data/app/javascript/controllers/EditPageController.ts +0 -22
  237. data/app/javascript/controllers/MainController.ts +0 -74
  238. data/app/javascript/controllers/PageOptionsController.js +0 -67
  239. data/app/models/category.rb +0 -22
  240. data/app/models/concerns/pages_core/has_otp.rb +0 -27
  241. data/app/models/page_category.rb +0 -6
  242. data/app/views/admin/pages/_edit_content.html.erb +0 -19
  243. data/app/views/admin/pages/_edit_files.html.erb +0 -4
  244. data/app/views/admin/pages/_edit_images.html.erb +0 -4
  245. data/app/views/admin/pages/_edit_metadata.html.erb +0 -35
  246. data/app/views/admin/pages/_edit_options.html.erb +0 -91
  247. data/app/views/admin_mailer/account_recovery.text.erb +0 -10
  248. data/app/views/admin_mailer/invite.text.erb +0 -7
  249. data/lib/rails/generators/pages_core/rspec/templates/mailer_macros.rb +0 -11
@@ -1,20 +1,20 @@
1
- import React, { useRef, useState } from "react";
2
- import PropTypes from "prop-types";
3
- import FileUploadButton from "./FileUploadButton";
4
- import DragElement from "./ImageGrid/DragElement";
5
- import FilePlaceholder from "./ImageGrid/FilePlaceholder";
6
- import GridImage from "./ImageGrid/GridImage";
7
- import useToastStore from "../stores/useToastStore";
8
- import { post } from "../lib/request";
9
-
10
- import {
11
- createDraggable,
12
- collectionOrder,
13
- useDragCollection,
14
- useDragUploader
15
- } from "./drag";
1
+ import React, { useRef } from "react";
2
+ import FileUploadButton from "../FileUploadButton";
3
+ import DragElement from "./DragElement";
4
+ import FilePlaceholder from "./FilePlaceholder";
5
+ import GridImage from "./GridImage";
6
+ import useToastStore from "../../stores/useToastStore";
7
+ import { post } from "../../lib/request";
8
+ import * as Drag from "../../types/Drag";
9
+ import * as Images from "../../types/Images";
10
+
11
+ import { createDraggable, collectionOrder, useDragUploader } from "../drag";
12
+
13
+ interface Props extends Images.GridOptions {
14
+ state: Images.GridState;
15
+ }
16
16
 
17
- function filterFiles(files) {
17
+ function filterFiles(files: File[]): File[] {
18
18
  const validMimeTypes = [
19
19
  "image/gif",
20
20
  "image/jpeg",
@@ -25,7 +25,11 @@ function filterFiles(files) {
25
25
  return files.filter((f) => validMimeTypes.indexOf(f.type) !== -1);
26
26
  }
27
27
 
28
- function draggedImageOrder(primaryCollection, imagesCollection, dragState) {
28
+ function draggedImageOrder(
29
+ primaryCollection: Drag.Collection<Images.Record>,
30
+ imagesCollection: Drag.Collection<Images.Record>,
31
+ dragState: Drag.State<Images.Record>
32
+ ): [Drag.Item<Images.Record>, Drag.Item<Images.Record>[]] {
29
33
  const [primary, ...rest] = collectionOrder(primaryCollection, dragState);
30
34
  let images = [...rest, ...collectionOrder(imagesCollection, dragState)];
31
35
 
@@ -45,29 +49,18 @@ function draggedImageOrder(primaryCollection, imagesCollection, dragState) {
45
49
  return [primary, images];
46
50
  }
47
51
 
48
- function initRecords(props) {
49
- const primary = props.enablePrimary
50
- ? props.records.filter((r) => r.primary).slice(0, 1)
51
- : [];
52
-
53
- return [primary, props.records.filter((r) => primary.indexOf(r) === -1)];
54
- }
55
-
56
- export default function ImageGrid(props) {
57
- const [initPrimary, initImages] = initRecords(props);
58
- const primary = useDragCollection(initPrimary);
59
- const images = useDragCollection(initImages);
60
- const [deleted, setDeleted] = useState([]);
61
- const error = useToastStore((state) => state.error);
52
+ export default function Grid(props: Props) {
53
+ const { primary, images, deleted, setDeleted } = props.state;
62
54
 
63
55
  const containerRef = useRef();
56
+ const error = useToastStore((state) => state.error);
64
57
 
65
58
  const dispatchAll = (action) => {
66
59
  primary.dispatch(action);
67
60
  images.dispatch(action);
68
61
  };
69
62
 
70
- const dragEnd = (dragState, files) => {
63
+ const dragEnd = (dragState: Drag.State<Images.Record>, files: File[]) => {
71
64
  const [draggedPrimary, draggedImages] = draggedImageOrder(
72
65
  primary,
73
66
  images,
@@ -86,40 +79,44 @@ export default function ImageGrid(props) {
86
79
  }
87
80
  };
88
81
 
89
- const [dragState, dragStart, listeners] = useDragUploader(
82
+ const [dragState, dragStart, listeners] = useDragUploader<Images.Record>(
90
83
  [primary, images],
91
84
  dragEnd
92
85
  );
93
86
 
94
- const position = (record) => {
87
+ const position = (record: Images.Record) => {
95
88
  return (
96
89
  [
97
- ...primary.draggables.map((d) => d.record),
98
- ...images.draggables.map((d) => d.record),
90
+ ...primary.draggables.map(
91
+ (d: Drag.Draggable<Images.Record>) => d.record
92
+ ),
93
+ ...images.draggables.map(
94
+ (d: Drag.Draggable<Images.Record>) => d.record
95
+ ),
99
96
  ...deleted
100
97
  ].indexOf(record) + 1
101
98
  );
102
99
  };
103
100
 
104
- const attrName = (record) => {
101
+ const attrName = (record: Images.Record) => {
105
102
  return `${props.attribute}[${position(record)}]`;
106
103
  };
107
104
 
108
- const uploadImage = (file) => {
105
+ const uploadImage = (file: File) => {
109
106
  const draggable = createDraggable({ image: null, file: file });
110
107
 
111
- let data = new FormData();
108
+ const data = new FormData();
112
109
 
113
110
  data.append("image[file]", file);
114
111
 
115
- post("/admin/images.json", data).then((json) => {
116
- if (json.status === "error") {
117
- error("Error uploading image: " + json.error);
112
+ void post("/admin/images.json", data).then((json: Images.Response) => {
113
+ if ("status" in json && json.status === "error") {
114
+ error(`Error uploading image: ${json.error}`);
118
115
  dispatchAll({ type: "remove", payload: draggable });
119
116
  } else {
120
117
  dispatchAll({
121
118
  type: "update",
122
- payload: { ...draggable, record: { image: json } }
119
+ payload: { ...draggable, record: { image: json } } as Drag.Draggable
123
120
  });
124
121
  }
125
122
  });
@@ -127,26 +124,30 @@ export default function ImageGrid(props) {
127
124
  return draggable;
128
125
  };
129
126
 
130
- const update = (draggable) => (image) => {
131
- const { record } = draggable;
132
- const updated = {
133
- ...draggable,
134
- record: {
135
- ...record,
136
- image: { ...record.image, ...image }
137
- }
127
+ const update =
128
+ (draggable: Drag.Draggable<Images.Record>) => (image: Images.Resource) => {
129
+ const { record } = draggable;
130
+ const updated = {
131
+ ...draggable,
132
+ record: {
133
+ ...record,
134
+ image: { ...record.image, ...image }
135
+ }
136
+ };
137
+ dispatchAll({ type: "update", payload: updated });
138
138
  };
139
- dispatchAll({ type: "update", payload: updated });
140
- };
141
139
 
142
- const remove = (draggable) => () => {
140
+ const remove = (draggable: Drag.Draggable<Images.Record>) => () => {
143
141
  dispatchAll({ type: "remove", payload: draggable });
144
142
  if (draggable.record.id) {
145
143
  setDeleted([...deleted, draggable.record]);
146
144
  }
147
145
  };
148
146
 
149
- const renderImage = (draggable, isPrimary) => {
147
+ const renderImage = (
148
+ draggable: Drag.Item<Images.Record>,
149
+ isPrimary: boolean
150
+ ) => {
150
151
  const { dragging } = dragState;
151
152
 
152
153
  if (draggable === "Files") {
@@ -172,7 +173,7 @@ export default function ImageGrid(props) {
172
173
  );
173
174
  };
174
175
 
175
- const uploadPrimary = (files) => {
176
+ const uploadPrimary = (files: File[]) => {
176
177
  const [first, ...rest] = filterFiles(files).map((f) => uploadImage(f));
177
178
  if (first) {
178
179
  images.dispatch({
@@ -183,14 +184,14 @@ export default function ImageGrid(props) {
183
184
  }
184
185
  };
185
186
 
186
- const uploadAdditional = (files) => {
187
+ const uploadAdditional = (files: File[]) => {
187
188
  images.dispatch({
188
189
  type: "append",
189
190
  payload: filterFiles(files).map((f) => uploadImage(f))
190
191
  });
191
192
  };
192
193
 
193
- let classNames = ["image-grid"];
194
+ const classNames = ["image-grid"];
194
195
  if (props.enablePrimary) {
195
196
  classNames.push("with-primary-image");
196
197
  }
@@ -221,7 +222,7 @@ export default function ImageGrid(props) {
221
222
  type="hidden"
222
223
  name={props.primaryAttribute}
223
224
  value={
224
- (draggedPrimary.record &&
225
+ (draggedPrimary !== "Files" &&
225
226
  draggedPrimary.record.image &&
226
227
  draggedPrimary.record.image.id) ||
227
228
  ""
@@ -260,14 +261,14 @@ export default function ImageGrid(props) {
260
261
  <span className="deleted-image" key={r.id}>
261
262
  <input name={`${attrName(r)}[id]`} type="hidden" value={r.id} />
262
263
  <input
263
- name={`${attrName(r)}[image_id]`}
264
+ name={`${attrName(r)}[attachment_id]`}
264
265
  type="hidden"
265
266
  value={(r.image && r.image.id) || ""}
266
267
  />
267
268
  <input
268
269
  name={`${attrName(r)}[_destroy]`}
269
270
  type="hidden"
270
- value={true}
271
+ value={"true"}
271
272
  />
272
273
  </span>
273
274
  ))}
@@ -275,13 +276,3 @@ export default function ImageGrid(props) {
275
276
  </div>
276
277
  );
277
278
  }
278
-
279
- ImageGrid.propTypes = {
280
- attribute: PropTypes.string,
281
- locale: PropTypes.string,
282
- locales: PropTypes.object,
283
- records: PropTypes.array,
284
- enablePrimary: PropTypes.bool,
285
- primaryAttribute: PropTypes.string,
286
- showEmbed: PropTypes.bool
287
- };
@@ -1,21 +1,17 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { MouseEvent, useEffect, useState } from "react";
2
+
2
3
  import copyToClipboard from "../../lib/copyToClipboard";
3
- import EditableImage from "../EditableImage";
4
4
  import useToastStore from "../../stores/useToastStore";
5
- import { ImageResource, Locale } from "../../types";
6
- import Placeholder from "./Placeholder";
7
-
5
+ import * as Drag from "../../types/Drag";
6
+ import * as Images from "../../types/Images";
7
+ import { Locale } from "../../types";
8
8
  import { useDraggable } from "../drag";
9
9
 
10
- interface Record {
11
- id: number | null;
12
- image: ImageResource;
13
- src: string | null;
14
- file: File | null;
15
- }
10
+ import EditableImage from "../EditableImage";
11
+ import Placeholder from "./Placeholder";
16
12
 
17
- interface GridImageProps {
18
- draggable: { handle: string; record: Record };
13
+ interface Props {
14
+ draggable: Drag.Draggable<Images.Record>;
19
15
  attributeName: string;
20
16
  locale: string;
21
17
  locales: { [index: string]: Locale };
@@ -25,36 +21,39 @@ interface GridImageProps {
25
21
  primary: boolean;
26
22
  position: number;
27
23
  deleteImage: () => void;
28
- startDrag: (evt: Event, draggable: Draggable) => void;
29
- onUpdate: (newImage: ImageResource, src: string) => void;
24
+ startDrag: (
25
+ evt: MouseEvent,
26
+ draggable: Drag.Draggable<Images.Record>
27
+ ) => void;
28
+ onUpdate: (newImage: Images.Resource, src: string) => void;
30
29
  }
31
30
 
32
- export default function GridImage(props: GridImageProps) {
31
+ export default function GridImage(props: Props) {
33
32
  const { attributeName, draggable } = props;
34
33
  const record = draggable.record;
35
34
  const image = record.image;
36
35
 
37
36
  const notice = useToastStore((state) => state.notice);
38
37
 
39
- const [src, setSrc] = useState(record.src || null);
38
+ const [src, setSrc] = useState<string>(record.src || null);
40
39
 
41
40
  const dragAttrs = useDraggable(draggable, props.startDrag);
42
41
 
43
42
  useEffect(() => {
44
- if (record.file) {
43
+ if ("file" in record && record.file) {
45
44
  const reader = new FileReader();
46
- reader.onload = () => setSrc(reader.result);
45
+ reader.onload = () => setSrc(reader.result as string);
47
46
  reader.readAsDataURL(record.file);
48
47
  }
49
48
  }, []);
50
49
 
51
- const copyEmbed = (evt: Event) => {
50
+ const copyEmbed = (evt: MouseEvent) => {
52
51
  evt.preventDefault();
53
52
  copyToClipboard(`[image:${image.id}]`);
54
53
  notice("Embed code copied to clipboard");
55
54
  };
56
55
 
57
- const deleteImage = (evt: Event) => {
56
+ const deleteImage = (evt: MouseEvent) => {
58
57
  evt.preventDefault();
59
58
  if (props.deleteImage) {
60
59
  props.deleteImage();
@@ -65,7 +64,7 @@ export default function GridImage(props: GridImageProps) {
65
64
  if (props.placeholder) {
66
65
  classes.push("placeholder");
67
66
  }
68
- if (record.file) {
67
+ if ("file" in record) {
69
68
  classes.push("uploading");
70
69
  }
71
70
 
@@ -90,7 +89,7 @@ export default function GridImage(props: GridImageProps) {
90
89
  <input
91
90
  name={`${attributeName}[primary]`}
92
91
  type="hidden"
93
- value={props.primary}
92
+ value={props.primary ? "true" : "false"}
94
93
  />
95
94
  )}
96
95
  {!image && <Placeholder src={src} />}
@@ -1,10 +1,10 @@
1
1
  import React from "react";
2
2
 
3
- interface PlaceholderProps {
3
+ interface Props {
4
4
  src: string;
5
5
  }
6
6
 
7
- export default function Placeholder(props: PlaceholderProps) {
7
+ export default function Placeholder(props: Props) {
8
8
  if (props.src) {
9
9
  return (
10
10
  <div className="temp-image">
@@ -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
+ }