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
@@ -28,7 +28,8 @@ module PagesCore
28
28
  # * <tt>:ratio</tt>: Ratio to constrain image by.
29
29
  # * <tt>:size</tt>: Max size for image.
30
30
  def image_figure(image, opts = {})
31
- class_name = ["image", image_class_name(image), opts[:class_name]].compact
31
+ class_name = ["image", image_class_name(image, ratio: opts[:ratio]),
32
+ opts[:class_name]].compact
32
33
  image_tag = image_figure_image_tag(image,
33
34
  size: opts[:size],
34
35
  ratio: opts[:ratio])
@@ -47,7 +48,8 @@ module PagesCore
47
48
  # * <tt>:ratio</tt>: Ratio to constrain image by.
48
49
  # * <tt>:sizes</tt>: Sizes attribute for image tag, default: "100vw".
49
50
  def picture(image, opts = {})
50
- class_name = ["image", image_class_name(image), opts[:class_name]].compact
51
+ class_name = ["image", image_class_name(image, ratio: opts[:ratio]),
52
+ opts[:class_name]].compact
51
53
  pict = picture_tag(image, ratio: opts[:ratio], sizes: opts[:sizes])
52
54
  content = opts[:link] ? image_link_to(pict, opts[:link]) : pict
53
55
  tag.figure(content + image_caption(image, caption: opts[:caption]),
@@ -95,9 +97,11 @@ module PagesCore
95
97
  Vector2d.new(v.y * ratio, v.y).fit(v)
96
98
  end
97
99
 
98
- def image_class_name(image)
99
- return "square" if image.size.x == image.size.y
100
- return "landscape" if image.size.x > image.size.y
100
+ def image_class_name(image, ratio: nil)
101
+ size = ratio ? fit_ratio(image.size, ratio) : image.size
102
+
103
+ return "square" if size.x == size.y
104
+ return "landscape" if size.x > size.y
101
105
 
102
106
  "portrait"
103
107
  end
@@ -120,9 +124,7 @@ module PagesCore
120
124
  end
121
125
 
122
126
  def image_widths(image)
123
- [233, 350, 700, 1050, 1400, 2100, 2800].select do |w|
124
- image.size.x >= w
125
- end
127
+ [233, 350, 700, 1050, 1400, 2100, 2800].select { |w| image.size.x >= w }
126
128
  end
127
129
 
128
130
  def srcset(image, ratio: nil, format: nil)
@@ -30,15 +30,10 @@ module PagesCore
30
30
  end
31
31
 
32
32
  def labelled_country_select(
33
- attr, label = nil, priority = {}, opts = {}, html_opts = {}
33
+ attr, label = nil, opts = {}, html_opts = {}
34
34
  )
35
- if priority.is_a?(Hash)
36
- return labelled_field(attr, label, priority) do |options|
37
- country_select(attr, options, opts, html_opts)
38
- end
39
- end
40
35
  labelled_field(attr, label, opts) do |options|
41
- country_select(attr, priority, options, html_opts)
36
+ country_select(attr, options, html_opts)
42
37
  end
43
38
  end
44
39
 
@@ -31,7 +31,7 @@ module PagesCore
31
31
 
32
32
  def page_redirect_url(locale, page)
33
33
  redirect = page.redirect_path(locale:)
34
- return redirect if redirect =~ %r{^https?://}
34
+ return redirect if %r{^https?://}.match?(redirect)
35
35
 
36
36
  base_page_url + redirect
37
37
  end
@@ -1,32 +1,31 @@
1
- import React from "react";
1
+ import React, { MouseEvent } from "react";
2
2
  import copyToClipboard from "../../lib/copyToClipboard";
3
3
  import AttachmentEditor from "./AttachmentEditor";
4
4
  import useModalStore from "../../stores/useModalStore";
5
5
  import useToastStore from "../../stores/useToastStore";
6
- import { AttachmentResource, Locale } from "../../types";
6
+ import * as Attachments from "../../types/Attachments";
7
+ import * as Drag from "../../types/Drag";
8
+ import { Locale } from "../../types";
7
9
 
8
- import { useDraggable, Draggable } from "../drag";
10
+ import { useDraggable } from "../drag";
9
11
 
10
- interface Record {
11
- id: number | null;
12
- attachment: AttachmentResource;
13
- uploading: boolean;
14
- }
15
-
16
- interface AttachmentProps {
12
+ interface Props {
17
13
  attributeName: string;
18
14
  placeholder: boolean;
19
- draggable: { record: Record };
15
+ draggable: Drag.Draggable<Attachments.Record>;
20
16
  locale: string;
21
17
  locales: { [index: string]: Locale };
22
18
  deleteRecord: () => void;
23
19
  showEmbed: boolean;
24
20
  position: number;
25
- onUpdate: (localizations: Record<string, Record<string, string>>) => void;
26
- startDrag: (evt: Event, draggable: Draggable) => void;
21
+ onUpdate: (attachment: Partial<Attachments.Resource>) => void;
22
+ startDrag: (
23
+ evt: MouseEvent,
24
+ draggable: Drag.Draggable<Attachments.Record>
25
+ ) => void;
27
26
  }
28
27
 
29
- export default function Attachment(props: AttachmentProps) {
28
+ export default function Attachment(props: Props) {
30
29
  const { attributeName, draggable, locales, locale } = props;
31
30
  const { record } = draggable;
32
31
  const { attachment, uploading } = record;
@@ -34,15 +33,18 @@ export default function Attachment(props: AttachmentProps) {
34
33
  const openModal = useModalStore((state) => state.open);
35
34
  const notice = useToastStore((state) => state.notice);
36
35
 
37
- const listeners = useDraggable(draggable, props.startDrag);
36
+ const listeners = useDraggable<Attachments.Record>(
37
+ draggable,
38
+ props.startDrag
39
+ );
38
40
 
39
- const copyEmbed = (evt: Event) => {
41
+ const copyEmbed = (evt: MouseEvent) => {
40
42
  evt.preventDefault();
41
43
  copyToClipboard(`[attachment:${attachment.id}]`);
42
44
  notice("Embed code copied to clipboard");
43
45
  };
44
46
 
45
- const deleteRecord = (evt: Event) => {
47
+ const deleteRecord = (evt: MouseEvent) => {
46
48
  evt.preventDefault();
47
49
  if (props.deleteRecord) {
48
50
  props.deleteRecord();
@@ -63,7 +65,7 @@ export default function Attachment(props: AttachmentProps) {
63
65
  return null;
64
66
  };
65
67
 
66
- const editAttachment = (evt: Event) => {
68
+ const editAttachment = (evt: MouseEvent) => {
67
69
  evt.preventDefault();
68
70
  openModal(
69
71
  <AttachmentEditor
@@ -1,18 +1,19 @@
1
- import React, { ChangeEvent, useState } from "react";
1
+ import React, { ChangeEvent, MouseEvent, useState } from "react";
2
2
  import copyToClipboard, { copySupported } from "../../lib/copyToClipboard";
3
3
  import useModalStore from "../../stores/useModalStore";
4
4
  import useToastStore from "../../stores/useToastStore";
5
- import { AttachmentResource, Locale } from "../../types";
6
5
  import { putJson } from "../../lib/request";
6
+ import * as Attachments from "../../types/Attachments";
7
+ import { Locale } from "../../types";
7
8
 
8
- interface AttachmentEditorProps {
9
- attachment: AttachmentResource;
9
+ interface Props {
10
+ attachment: Attachments.Resource;
10
11
  locale: string;
11
12
  locales: { [index: string]: Locale };
12
- onUpdate: (localizations: Record<string, Record<string, string>>) => void;
13
+ onUpdate: (attachment: Partial<Attachments.Resource>) => void;
13
14
  }
14
15
 
15
- export default function AttachmentEditor(props: AttachmentEditorProps) {
16
+ export default function AttachmentEditor(props: Props) {
16
17
  const { attachment, locales } = props;
17
18
 
18
19
  const [locale, setLocale] = useState(props.locale);
@@ -25,20 +26,21 @@ export default function AttachmentEditor(props: AttachmentEditorProps) {
25
26
  const closeModal = useModalStore((state) => state.close);
26
27
 
27
28
  const updateLocalization =
28
- (name: "name" | "description") => (evt: ChangeEvent<HTMLInputElement>) => {
29
+ (name: "name" | "description") =>
30
+ (evt: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement>) => {
29
31
  setLocalizations({
30
32
  ...localizations,
31
33
  [name]: { ...localizations[name], [locale]: evt.target.value }
32
34
  });
33
35
  };
34
36
 
35
- const copyEmbedCode = (evt: Event) => {
37
+ const copyEmbedCode = (evt: MouseEvent) => {
36
38
  evt.preventDefault();
37
39
  copyToClipboard(`[attachment:${attachment.id}]`);
38
40
  notice("Embed code copied to clipboard");
39
41
  };
40
42
 
41
- const save = (evt: Event) => {
43
+ const save = (evt: MouseEvent) => {
42
44
  evt.preventDefault();
43
45
  evt.stopPropagation();
44
46
 
@@ -1,31 +1,27 @@
1
- import React, { useState } from "react";
2
- import PropTypes from "prop-types";
3
- import Attachment from "./Attachments/Attachment";
4
- import Placeholder from "./Attachments/Placeholder";
5
- import FileUploadButton from "./FileUploadButton";
6
- import { post } from "../lib/request";
7
-
8
- import {
9
- createDraggable,
10
- draggedOrder,
11
- useDragCollection,
12
- useDragUploader
13
- } from "./drag";
14
-
15
- function filenameToName(str) {
1
+ import React from "react";
2
+ import Attachment from "./Attachment";
3
+ import Placeholder from "./Placeholder";
4
+ import FileUploadButton from "../FileUploadButton";
5
+ import { post } from "../../lib/request";
6
+ import * as Attachments from "../../types/Attachments";
7
+ import * as Drag from "../../types/Drag";
8
+
9
+ import { createDraggable, draggedOrder, useDragUploader } from "../drag";
10
+
11
+ interface Props extends Attachments.Options {
12
+ state: Attachments.State;
13
+ }
14
+
15
+ function filenameToName(str: string): string {
16
16
  return str.replace(/\.[\w\d]+$/, "").replace(/_/g, " ");
17
17
  }
18
18
 
19
- export default function Attachments(props) {
20
- const collection = useDragCollection(props.records);
21
- const locales =
22
- props.locales && props.locales.length > 0
23
- ? Object.keys(props.locales)
24
- : [props.locale];
25
- const [deleted, setDeleted] = useState([]);
19
+ export default function List(props: Props) {
20
+ const { collection, deleted, setDeleted } = props.state;
21
+ const locales = props.locales ? Object.keys(props.locales) : [props.locale];
26
22
 
27
- const uploadAttachment = (file) => {
28
- let name = {};
23
+ const uploadAttachment = (file: File) => {
24
+ const name = {};
29
25
  locales.forEach((l) => (name[l] = file.name));
30
26
 
31
27
  const draggable = createDraggable({
@@ -33,34 +29,36 @@ export default function Attachments(props) {
33
29
  uploading: true
34
30
  });
35
31
 
36
- let data = new FormData();
32
+ const data = new FormData();
37
33
 
38
34
  data.append("attachment[file]", file);
39
35
  locales.forEach((l) => {
40
36
  data.append(`attachment[name][${l}]`, filenameToName(file.name));
41
37
  });
42
38
 
43
- post("/admin/attachments.json", data).then((json) => {
44
- collection.dispatch({
45
- type: "update",
46
- payload: {
47
- ...draggable,
48
- record: { attachment: json, uploading: false }
49
- }
50
- });
51
- });
39
+ void post("/admin/attachments.json", data).then(
40
+ (json: Attachments.Resource) => {
41
+ collection.dispatch({
42
+ type: "update",
43
+ payload: {
44
+ ...draggable,
45
+ record: { attachment: json, uploading: false }
46
+ }
47
+ });
48
+ }
49
+ );
52
50
 
53
51
  return draggable;
54
52
  };
55
53
 
56
- const receiveFiles = (files) => {
54
+ const receiveFiles = (files: File[]) => {
57
55
  collection.dispatch({
58
56
  type: "append",
59
57
  payload: files.map((f) => uploadAttachment(f))
60
58
  });
61
59
  };
62
60
 
63
- const dragEnd = (dragState, files) => {
61
+ const dragEnd = (dragState: Drag.State, files: File[]) => {
64
62
  collection.dispatch({
65
63
  type: "reorder",
66
64
  payload: draggedOrder(collection, dragState)
@@ -71,43 +69,48 @@ export default function Attachments(props) {
71
69
  });
72
70
  };
73
71
 
74
- const [dragState, dragStart, listeners] = useDragUploader(
72
+ const [dragState, dragStart, listeners] = useDragUploader<Attachments.Record>(
75
73
  [collection],
76
74
  dragEnd
77
75
  );
78
76
 
79
- const position = (record) => {
77
+ const position = (record: Attachments.Record) => {
80
78
  return (
81
- [...collection.draggables.map((d) => d.record), ...deleted].indexOf(
82
- record
83
- ) + 1
79
+ [
80
+ ...collection.draggables.map(
81
+ (d: Drag.Draggable<Attachments.Record>) => d.record
82
+ ),
83
+ ...deleted
84
+ ].indexOf(record) + 1
84
85
  );
85
86
  };
86
87
 
87
- const attrName = (record) => {
88
+ const attrName = (record: Attachments.Record) => {
88
89
  return `${props.attribute}[${position(record)}]`;
89
90
  };
90
91
 
91
- const update = (draggable) => (attachment) => {
92
- const { record } = draggable;
93
- const updated = {
94
- ...draggable,
95
- record: {
96
- ...record,
97
- attachment: { ...record.attachment, ...attachment }
98
- }
92
+ const update =
93
+ (draggable: Drag.Draggable<Attachments.Record>) =>
94
+ (attachment: Partial<Attachments.Resource>) => {
95
+ const { record } = draggable;
96
+ const updated = {
97
+ ...draggable,
98
+ record: {
99
+ ...record,
100
+ attachment: { ...record.attachment, ...attachment }
101
+ }
102
+ };
103
+ collection.dispatch({ type: "update", payload: updated });
99
104
  };
100
- collection.dispatch({ type: "update", payload: updated });
101
- };
102
105
 
103
- const remove = (draggable) => () => {
106
+ const remove = (draggable: Drag.Draggable<Attachments.Record>) => () => {
104
107
  collection.dispatch({ type: "remove", payload: draggable });
105
108
  if (draggable.record.id) {
106
109
  setDeleted([...deleted, draggable.record]);
107
110
  }
108
111
  };
109
112
 
110
- const attachment = (draggable) => {
113
+ const attachment = (draggable: Drag.Item<Attachments.Record>) => {
111
114
  const { dragging } = dragState;
112
115
 
113
116
  if (draggable === "Files") {
@@ -153,7 +156,7 @@ export default function Attachments(props) {
153
156
  <input
154
157
  name={`${attrName(r)}[_destroy]`}
155
158
  type="hidden"
156
- value={true}
159
+ value="true"
157
160
  />
158
161
  </span>
159
162
  ))}
@@ -168,11 +171,3 @@ export default function Attachments(props) {
168
171
  </div>
169
172
  );
170
173
  }
171
-
172
- Attachments.propTypes = {
173
- attribute: PropTypes.string,
174
- locale: PropTypes.string,
175
- locales: PropTypes.object,
176
- records: PropTypes.array,
177
- showEmbed: PropTypes.bool
178
- };
@@ -0,0 +1,15 @@
1
+ import { useState } from "react";
2
+ import { useDragCollection } from "../drag";
3
+ import * as Attachments from "../../types/Attachments";
4
+
5
+ export default function useAttachments(
6
+ records: Attachments.Record[]
7
+ ): Attachments.State {
8
+ const [deleted, setDeleted] = useState<Attachments.Record[]>([]);
9
+
10
+ return {
11
+ collection: useDragCollection(records),
12
+ deleted: deleted,
13
+ setDeleted: setDeleted
14
+ };
15
+ }
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import useAttachments from "./Attachments/useAttachments";
3
+ import List from "./Attachments/List";
4
+ import * as Attachment from "../types/Attachments";
5
+
6
+ interface Props extends Attachment.Options {
7
+ records: Attachment.Record[];
8
+ }
9
+
10
+ export default function Attachments(props: Props) {
11
+ const state = useAttachments(props.records);
12
+
13
+ return <List state={state} {...props} />;
14
+ }
@@ -0,0 +1,105 @@
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ import DateTimeSelect from "./DateTimeSelect";
4
+
5
+ interface Props {
6
+ objectName: string;
7
+ startsAt: Date | string;
8
+ endsAt: Date | string;
9
+ setStartsAt?: (date: Date) => void;
10
+ setEndsAt?: (date: Date) => void;
11
+ disabled?: boolean;
12
+ disableTime?: boolean;
13
+ }
14
+
15
+ function defaultDate(offset = 0): Date {
16
+ const coeff = 1000 * 60 * 60;
17
+ return new Date(
18
+ Math.round(new Date().getTime() / coeff) * coeff +
19
+ coeff +
20
+ 1000 * 60 * offset
21
+ );
22
+ }
23
+
24
+ function parseDate(str: Date | string): Date {
25
+ if (!str) {
26
+ return null;
27
+ } else if (typeof str === "string") {
28
+ return new Date(str);
29
+ } else {
30
+ return str;
31
+ }
32
+ }
33
+
34
+ export default function DateRangeSelect(props: Props) {
35
+ const { disabled, disableTime, objectName } = props;
36
+
37
+ const [uncontrolledStartsAt, setUncontrolledStartsAt] = useState(
38
+ parseDate(props.startsAt)
39
+ );
40
+
41
+ const [uncontrolledEndsAt, setUncontrolledEndsAt] = useState(
42
+ parseDate(props.endsAt) || defaultDate(60)
43
+ );
44
+
45
+ const startsAt = parseDate(
46
+ props.setStartsAt ? props.startsAt : uncontrolledStartsAt
47
+ );
48
+ const setStartsAt = props.setStartsAt || setUncontrolledStartsAt;
49
+
50
+ const endsAt = parseDate(props.setEndsAt ? props.endsAt : uncontrolledEndsAt);
51
+ const setEndsAt = props.setEndsAt || setUncontrolledEndsAt;
52
+
53
+ const setDates = (start: Date, end: Date) => {
54
+ if (end < start) {
55
+ end = start;
56
+ }
57
+ setStartsAt(start);
58
+ setEndsAt(end);
59
+ };
60
+
61
+ const changeStartsAt = (newDate: Date) => {
62
+ setDates(
63
+ newDate,
64
+ new Date(endsAt.getTime() + (newDate.getTime() - startsAt.getTime()))
65
+ );
66
+ };
67
+
68
+ const changeEndsAt = (newDate: Date) => {
69
+ setDates(startsAt, newDate);
70
+ };
71
+
72
+ useEffect(() => {
73
+ if (!startsAt || !endsAt) {
74
+ setDates(startsAt || defaultDate(), endsAt || defaultDate(60));
75
+ }
76
+ }, [startsAt, endsAt]);
77
+
78
+ return (
79
+ <div className="date-range-select">
80
+ {startsAt && (
81
+ <div className="date">
82
+ <DateTimeSelect
83
+ name={objectName + "[starts_at]"}
84
+ disabled={disabled}
85
+ disableTime={disableTime}
86
+ onChange={changeStartsAt}
87
+ value={startsAt}
88
+ />
89
+ </div>
90
+ )}
91
+ <span className="to">to</span>
92
+ {endsAt && (
93
+ <div className="date">
94
+ <DateTimeSelect
95
+ name={objectName + "[ends_at]"}
96
+ disabled={disabled}
97
+ disableTime={disableTime}
98
+ onChange={changeEndsAt}
99
+ value={endsAt}
100
+ />
101
+ </div>
102
+ )}
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,136 @@
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ interface DateTimeSelectProps {
4
+ name: string;
5
+ onChange: (date: Date) => void;
6
+ value: Date;
7
+ disabled?: boolean;
8
+ disableTime?: boolean;
9
+ }
10
+
11
+ interface ModifyOptions {
12
+ year?: number;
13
+ month?: number;
14
+ date?: number;
15
+ time?: string;
16
+ }
17
+
18
+ function modifyDate(original: Date, options: ModifyOptions = {}): Date {
19
+ const newDate = new Date(original);
20
+ if ("year" in options) {
21
+ newDate.setFullYear(options.year);
22
+ }
23
+ if ("month" in options) {
24
+ newDate.setMonth(options.month);
25
+ }
26
+ if ("date" in options) {
27
+ newDate.setDate(options.date);
28
+ }
29
+ if ("time" in options && options.time.match(/^[\d]{1,2}(:[\d]{1,2})?$/)) {
30
+ newDate.setHours(parseInt(options.time.split(":")[0], 10));
31
+ newDate.setMinutes(parseInt(options.time.split(":")[1], 10) || 0);
32
+ }
33
+ return newDate;
34
+ }
35
+
36
+ function timeToString(time: Date): string {
37
+ return time.toTimeString().slice(0, 5);
38
+ }
39
+
40
+ // Returns an array with years from 2000 to 10 years from now.
41
+ function yearOptions(): number[] {
42
+ const start = 2000;
43
+ const years: number[] = [];
44
+ for (let i = start; i <= new Date().getFullYear() + 11; i++) {
45
+ years.push(i);
46
+ }
47
+ return years;
48
+ }
49
+
50
+ function monthOptions(): string[] {
51
+ return [
52
+ "January",
53
+ "February",
54
+ "March",
55
+ "April",
56
+ "May",
57
+ "June",
58
+ "July",
59
+ "August",
60
+ "September",
61
+ "October",
62
+ "November",
63
+ "December"
64
+ ];
65
+ }
66
+
67
+ function dayOptions(): number[] {
68
+ const numbers: number[] = [];
69
+ for (let i = 1; i <= 31; i++) {
70
+ numbers.push(i);
71
+ }
72
+ return numbers;
73
+ }
74
+
75
+ export default function DateTimeSelect(props: DateTimeSelectProps) {
76
+ const { name, disabled, disableTime, onChange, value } = props;
77
+
78
+ const [timeString, setTimeString] = useState(timeToString(value));
79
+
80
+ useEffect(() => {
81
+ setTimeString(timeToString(value));
82
+ }, [value]);
83
+
84
+ const handleChange = (options = {}) => {
85
+ onChange(modifyDate(value, options));
86
+ };
87
+
88
+ return (
89
+ <div className="date-select">
90
+ {name && (
91
+ <input type="hidden" name={name} value={!disabled && value.toJSON()} />
92
+ )}
93
+ <select
94
+ value={value.getMonth()}
95
+ onChange={(e) => handleChange({ month: e.target.value })}
96
+ disabled={disabled}>
97
+ {monthOptions().map((m, i) => (
98
+ <option key={i} value={i}>
99
+ {m}
100
+ </option>
101
+ ))}
102
+ </select>
103
+ <select
104
+ value={value.getDate()}
105
+ onChange={(e) => handleChange({ date: e.target.value })}
106
+ disabled={disabled}>
107
+ {dayOptions().map((d) => (
108
+ <option key={d} value={d}>
109
+ {d}
110
+ </option>
111
+ ))}
112
+ </select>
113
+ <select
114
+ value={value.getFullYear()}
115
+ onChange={(e) => handleChange({ year: e.target.value })}
116
+ disabled={disabled}>
117
+ {yearOptions().map((y) => (
118
+ <option key={y} value={y}>
119
+ {y}
120
+ </option>
121
+ ))}
122
+ </select>
123
+ {!disableTime && (
124
+ <input
125
+ className="time"
126
+ type="text"
127
+ size={5}
128
+ disabled={disabled}
129
+ value={timeString}
130
+ onChange={(e) => setTimeString(e.target.value)}
131
+ onBlur={(e) => handleChange({ time: e.target.value })}
132
+ />
133
+ )}
134
+ </div>
135
+ );
136
+ }