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
@@ -1,46 +1,9 @@
1
1
  import { useEffect, useReducer, useState } from "react";
2
2
 
3
- import { ImageResource } from "../../types";
3
+ import * as Crop from "../../types/Crop";
4
+ import * as Images from "../../types/Images";
4
5
 
5
- export interface Position {
6
- x: number;
7
- y: number;
8
- }
9
-
10
- export interface Size {
11
- width: number;
12
- height: number;
13
- }
14
-
15
- interface CropParams {
16
- crop_start_x: number;
17
- crop_start_y: number;
18
- crop_width: number;
19
- crop_height: number;
20
- crop_gravity_x: number;
21
- crop_gravity_y: number;
22
- }
23
-
24
- export interface CropState extends CropParams {
25
- aspect: number | null;
26
- cropping: boolean;
27
- image: ImageResource;
28
- }
29
-
30
- export interface CropSize {
31
- x: number;
32
- y: number;
33
- width: number;
34
- height: number;
35
- aspect?: number;
36
- }
37
-
38
- export interface CropAction {
39
- type: string;
40
- payload?: CropSize | Position;
41
- }
42
-
43
- function applyAspect(state: CropState, aspect: number | null) {
6
+ function applyAspect(state: Crop.State, aspect: number | null) {
44
7
  const crop = cropSize(state);
45
8
  const image = state.image;
46
9
  const imageAspect = image.real_width / image.real_height;
@@ -65,7 +28,7 @@ function applyAspect(state: CropState, aspect: number | null) {
65
28
  return applyCrop(state, crop);
66
29
  }
67
30
 
68
- function applyCrop(state: CropState, crop: CropSize) {
31
+ function applyCrop(state: Crop.State, crop: Crop.CropSize) {
69
32
  const { image } = state;
70
33
 
71
34
  // Don't crop if dimensions are below the threshold
@@ -86,7 +49,14 @@ function applyCrop(state: CropState, crop: CropSize) {
86
49
  };
87
50
  }
88
51
 
89
- function cropReducer(state: CropState, action: CropAction): CropState {
52
+ function setFocal(state: Crop.State, position: Crop.Position) {
53
+ return {
54
+ crop_gravity_x: state.crop_width * (position.x / 100) + state.crop_start_x,
55
+ crop_gravity_y: state.crop_height * (position.y / 100) + state.crop_start_y
56
+ };
57
+ }
58
+
59
+ function cropReducer(state: Crop.State, action: Crop.Action): Crop.State {
90
60
  const {
91
61
  crop_start_x,
92
62
  crop_start_y,
@@ -119,11 +89,7 @@ function cropReducer(state: CropState, action: CropAction): CropState {
119
89
  case "setAspect":
120
90
  return { ...state, ...applyAspect(state, action.payload) };
121
91
  case "setFocal":
122
- return {
123
- ...state,
124
- crop_gravity_x: crop_width * (action.payload.x / 100) + crop_start_x,
125
- crop_gravity_y: crop_height * (action.payload.y / 100) + crop_start_y
126
- };
92
+ return { ...state, ...setFocal(state, action.payload) };
127
93
  case "startCrop":
128
94
  return { ...state, cropping: true };
129
95
  case "toggleFocal":
@@ -140,7 +106,10 @@ function cropReducer(state: CropState, action: CropAction): CropState {
140
106
  }
141
107
  }
142
108
 
143
- function croppedImageCanvas(img: HTMLImageElement, crop: CropSize) {
109
+ function croppedImageCanvas(
110
+ img: HTMLImageElement,
111
+ crop: Crop.CropSize
112
+ ): [HTMLCanvasElement, CanvasRenderingContext2D] {
144
113
  const canvas = document.createElement("canvas");
145
114
  canvas.width = img.naturalWidth * (crop.width / 100);
146
115
  canvas.height = img.naturalHeight * (crop.height / 100);
@@ -172,13 +141,13 @@ function imageDataUrl(
172
141
  return canvas.toDataURL("image/jpeg");
173
142
  }
174
143
 
175
- export function cropParams(state: CropState): CropParams {
144
+ export function cropParams(state: Crop.State): Crop.Params {
176
145
  const maybe = (func: (number) => number) => (val: number | null) =>
177
146
  val === null ? val : func(val);
178
147
  const maybeRound = maybe(Math.round);
179
148
  const maybeCeil = maybe(Math.ceil);
180
149
 
181
- const crop: CropParams = {
150
+ const crop: Crop.Params = {
182
151
  crop_start_x: maybeRound(state.crop_start_x),
183
152
  crop_start_y: maybeRound(state.crop_start_y),
184
153
  crop_width: maybeCeil(state.crop_width),
@@ -198,7 +167,7 @@ export function cropParams(state: CropState): CropParams {
198
167
  return crop;
199
168
  }
200
169
 
201
- export function cropSize(state: CropState): CropSize {
170
+ export function cropSize(state: Crop.State): Crop.CropSize {
202
171
  const { image, aspect, crop_start_x, crop_start_y, crop_width, crop_height } =
203
172
  state;
204
173
  const imageAspect = image.real_width / image.real_height;
@@ -221,8 +190,10 @@ export function cropSize(state: CropState): CropSize {
221
190
  }
222
191
  }
223
192
 
224
- export default function useCrop(image: ImageResource) {
225
- const initialState: CropState = {
193
+ export default function useCrop(
194
+ image: Images.Resource
195
+ ): [Crop.State, (action: Crop.Action) => void, string] {
196
+ const initialState: Crop.State = {
226
197
  aspect: null,
227
198
  cropping: false,
228
199
  crop_start_x: image.crop_start_x || 0,
@@ -1,24 +1,19 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
2
 
3
+ import * as Crop from "../types/Crop";
4
+
3
5
  import Image from "./ImageCropper/Image";
4
6
  import Toolbar from "./ImageCropper/Toolbar";
5
7
 
6
- import {
7
- CropAction,
8
- CropSize,
9
- CropState,
10
- Position
11
- } from "./ImageCropper/useCrop";
12
-
13
8
  export { default as useCrop, cropParams } from "./ImageCropper/useCrop";
14
9
 
15
- interface ImageCropperProps {
10
+ interface Props {
16
11
  croppedImage: string;
17
- cropState: CropState;
18
- dispatch: (action: CropAction) => void;
12
+ cropState: Crop.State;
13
+ dispatch: (action: Crop.Action) => void;
19
14
  }
20
15
 
21
- function focalPoint(state: CropState): Position {
16
+ function focalPoint(state: Crop.State): Crop.Position {
22
17
  if (state.crop_gravity_x === null || state.crop_gravity_y === null) {
23
18
  return null;
24
19
  } else {
@@ -29,9 +24,9 @@ function focalPoint(state: CropState): Position {
29
24
  }
30
25
  }
31
26
 
32
- export default function ImageCropper(props: ImageCropperProps) {
27
+ export default function ImageCropper(props: Props) {
33
28
  const containerRef = useRef<HTMLDivElement>();
34
- const [containerSize, setContainerSize] = useState();
29
+ const [containerSize, setContainerSize] = useState<Crop.Size>();
35
30
 
36
31
  const handleResize = () => {
37
32
  const elem = containerRef.current;
@@ -56,11 +51,11 @@ export default function ImageCropper(props: ImageCropperProps) {
56
51
  props.dispatch({ type: "setAspect", payload: aspect });
57
52
  };
58
53
 
59
- const setCrop = (crop: CropSize) => {
54
+ const setCrop = (crop: Crop.CropSize) => {
60
55
  props.dispatch({ type: "setCrop", payload: crop });
61
56
  };
62
57
 
63
- const setFocal = (focal: Position) => {
58
+ const setFocal = (focal: Crop.Position) => {
64
59
  props.dispatch({ type: "setFocal", payload: focal });
65
60
  };
66
61
 
@@ -1,28 +1,30 @@
1
- import React, { ChangeEvent } from "react";
1
+ import React, { ChangeEvent, MouseEvent } from "react";
2
+
2
3
  import useModalStore from "../../stores/useModalStore";
3
4
  import useToastStore from "../../stores/useToastStore";
4
- import { Locale, ImageResource } from "../../types";
5
5
  import copyToClipboard, { copySupported } from "../../lib/copyToClipboard";
6
+ import * as Images from "../../types/Images";
7
+ import { Locale } from "../../types";
6
8
 
7
- interface FormProps {
9
+ interface Props {
8
10
  alternative: Record<string, string>;
9
11
  caption: Record<string, string>;
10
- image: ImageResource;
12
+ image: Images.Resource;
11
13
  locale: string;
12
14
  locales: Record<string, Locale>;
13
15
  setLocale: (locale: string) => void;
14
- save: (evt: Event) => void;
16
+ save: (evt: MouseEvent) => void;
15
17
  showCaption: boolean;
16
18
  updateLocalization: (name: "alternative" | "caption", value: string) => void;
17
19
  }
18
20
 
19
- export default function Form(props: FormProps) {
21
+ export default function Form(props: Props) {
20
22
  const { alternative, caption, image, locale, locales } = props;
21
23
 
22
24
  const closeModal = useModalStore((state) => state.close);
23
25
  const notice = useToastStore((state) => state.notice);
24
26
 
25
- const copyEmbedCode = (evt: Event) => {
27
+ const copyEmbedCode = (evt: MouseEvent) => {
26
28
  evt.preventDefault();
27
29
  copyToClipboard(`[image:${image.id}]`);
28
30
  notice("Embed code copied to clipboard");
@@ -86,7 +88,9 @@ export default function Form(props: FormProps) {
86
88
  </div>
87
89
  )}
88
90
  <div className="buttons">
89
- <button onClick={props.save}>Save</button>
91
+ <button className="primary" onClick={props.save}>
92
+ Save
93
+ </button>
90
94
  <button onClick={closeModal}>Cancel</button>
91
95
  </div>
92
96
  </form>
@@ -1,20 +1,25 @@
1
- import React, { useState } from "react";
1
+ import React, { MouseEvent, useState } from "react";
2
+
2
3
  import useModalStore from "../stores/useModalStore";
3
4
  import { putJson } from "../lib/request";
5
+ import * as Images from "../types/Images";
6
+ import { Locale } from "../types";
4
7
 
5
- import { Locale, ImageResource } from "../types";
6
8
  import ImageCropper, { useCrop, cropParams } from "./ImageCropper";
7
9
  import Form from "./ImageEditor/Form";
8
10
 
9
- interface ImageEditorProps {
10
- image: ImageResource;
11
+ interface Props {
12
+ image: Images.Resource;
11
13
  caption: boolean;
12
14
  locale: string;
13
15
  locales: Record<string, Locale>;
14
- onUpdate?: (data: ImageResource, croppedImage: string | null) => void;
16
+ onUpdate?: (
17
+ data: Partial<Images.Resource>,
18
+ croppedImage: string | null
19
+ ) => void;
15
20
  }
16
21
 
17
- export default function ImageEditor(props: ImageEditorProps) {
22
+ export default function ImageEditor(props: Props) {
18
23
  const [cropState, dispatch, croppedImage] = useCrop(props.image);
19
24
  const [locale, setLocale] = useState(props.locale);
20
25
  const [localizations, setLocalizations] = useState({
@@ -34,7 +39,7 @@ export default function ImageEditor(props: ImageEditorProps) {
34
39
  });
35
40
  };
36
41
 
37
- const save = (evt: Event) => {
42
+ const save = (evt: MouseEvent) => {
38
43
  evt.preventDefault();
39
44
  evt.stopPropagation();
40
45
 
@@ -1,20 +1,21 @@
1
1
  import React, { RefObject } from "react";
2
2
 
3
- import { ImageResource } from "../../types";
4
- import { DragState } from "../drag";
3
+ import * as Drag from "../../types/Drag";
4
+ import * as Images from "../../types/Images";
5
5
 
6
- interface DragElementProps {
6
+ interface Props {
7
7
  container: RefObject<HTMLDivElement>;
8
- draggable: string | { record: { image: ImageResource; src?: string } };
9
- dragState: DragState;
8
+ draggable: Drag.Item<Images.Record>;
9
+ dragState: Drag.State;
10
10
  }
11
11
 
12
- export default function DragElement(props: DragElementProps) {
12
+ export default function DragElement(props: Props) {
13
13
  const { draggable, dragState, container } = props;
14
14
 
15
15
  if (draggable === "Files") {
16
16
  return "";
17
- } else {
17
+ } else if (typeof draggable !== "string") {
18
+ const record = draggable.record;
18
19
  const containerSize = container.current.getBoundingClientRect();
19
20
  const x = dragState.x - (containerSize.x || containerSize.left);
20
21
  const y = dragState.y - (containerSize.y || containerSize.top);
@@ -23,11 +24,7 @@ export default function DragElement(props: DragElementProps) {
23
24
  };
24
25
  return (
25
26
  <div className="drag-image" style={translateStyle}>
26
- {"record" in draggable && draggable.record.image && (
27
- <img
28
- src={draggable.record.src || draggable.record.image.thumbnail_url}
29
- />
30
- )}
27
+ {record.image && <img src={record.src || record.image.thumbnail_url} />}
31
28
  </div>
32
29
  );
33
30
  }
@@ -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">