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,28 +1,27 @@
1
1
  import React, { Component } from "react";
2
- import Tree, { TreeId, TreeIndex } from "../lib/Tree";
2
+
3
+ import Tree from "../lib/Tree";
3
4
  import { postJson, putJson } from "../lib/request";
4
- import { Attributes, PageNode } from "./PageTree/types";
5
- import Draggable from "./PageTree/Draggable";
5
+ import * as Trees from "../types/Trees";
6
+ import * as Pages from "../types/Pages";
6
7
 
7
- interface Page extends Record<string, unknown> {
8
- parent_page_id: number | null;
9
- }
8
+ import Draggable from "./PageTree/Draggable";
10
9
 
11
10
  type CollapsedState = Record<number, boolean>;
12
11
 
13
12
  interface ParentMap {
14
- [index: number]: Page[];
13
+ [index: number]: Pages.TreeNode[];
15
14
  }
16
15
 
17
- interface PageTreeProps {
16
+ interface Props {
18
17
  dir: string;
19
18
  locale: string;
20
- pages: Page[];
19
+ pages: Pages.TreeNode[];
21
20
  permissions: string[];
22
21
  }
23
22
 
24
- interface PageTreeState {
25
- tree: Tree<PageNode>;
23
+ interface State {
24
+ tree: Tree<Pages.TreeNode>;
26
25
  }
27
26
 
28
27
  function collapsedState(): CollapsedState {
@@ -38,15 +37,15 @@ function collapsedState(): CollapsedState {
38
37
  return {};
39
38
  }
40
39
 
41
- export default class PageTree extends Component<PageTreeProps, PageTreeState> {
42
- constructor(props: PageTreeProps) {
40
+ export default class PageTree extends Component<Props, State> {
41
+ constructor(props: Props) {
43
42
  super(props);
44
43
 
45
- this.state = { tree: this.buildTree(props.pages) };
44
+ this.state = { tree: this.buildTree(props.pages, props.locale) };
46
45
  }
47
46
 
48
- applyCollapsed(tree: Tree<PageNode>) {
49
- const depth = (t: Tree, index: TreeIndex) => {
47
+ applyCollapsed(tree: Tree<Pages.TreeNode>) {
48
+ const depth = (t: Tree, index: Trees.Index) => {
50
49
  let depth = 0;
51
50
  let pointer = t.getIndex(index.parent);
52
51
  while (pointer) {
@@ -56,7 +55,7 @@ export default class PageTree extends Component<PageTreeProps, PageTreeState> {
56
55
  return depth;
57
56
  };
58
57
 
59
- const walk = (id: TreeId) => {
58
+ const walk = (id: Trees.Id) => {
60
59
  const index = tree.getIndex(id);
61
60
  const node = index.node;
62
61
  if (node.id && node.id in collapsedState()) {
@@ -73,30 +72,35 @@ export default class PageTree extends Component<PageTreeProps, PageTreeState> {
73
72
  walk(1);
74
73
  }
75
74
 
76
- createPage(index: TreeIndex<PageNode>, attributes: Attributes) {
77
- void postJson(`/admin/${index.node.locale}/pages.json`, {
75
+ createPage(index: Trees.Index<Pages.TreeNode>, attributes: Pages.TreeItem) {
76
+ void postJson(`/admin/${this.props.locale}/pages.json`, {
78
77
  page: attributes
79
- }).then((response: Attributes) => this.updateNode(index, response));
78
+ }).then((response: Pages.TreeItem) => this.updateNode(index, response));
80
79
  }
81
80
 
82
- buildTree(pages: Page[]) {
81
+ buildTree(pages: Pages.TreeNode[], locale: string) {
83
82
  // Build tree
84
- const parentMap: ParentMap = pages.reduce((m: ParentMap, page: Page) => {
85
- const id = page.parent_page_id || 0;
86
- m[id] = [...(m[id] || []), page];
87
- return m;
88
- }, {});
83
+ const parentMap: ParentMap = pages.reduce(
84
+ (m: ParentMap, page: Pages.TreeNode) => {
85
+ const id = page.parent_page_id || 0;
86
+ m[id] = [...(m[id] || []), page];
87
+ return m;
88
+ },
89
+ {}
90
+ );
89
91
 
90
- pages.forEach((p: Page) => {
92
+ pages.forEach((p) => {
91
93
  p.children = parentMap[p.id] || [];
92
94
  });
93
95
 
94
- const tree = new Tree({
95
- name: "All Pages",
96
- locale: this.props.locale,
96
+ const tree = new Tree<Pages.TreeNode>({
97
+ blocks: {
98
+ name: { [locale]: "All Pages" }
99
+ },
97
100
  permissions: this.props.permissions,
98
101
  root: true,
99
- children: parentMap[0]
102
+ children: parentMap[0],
103
+ collapsed: false
100
104
  });
101
105
  this.applyCollapsed(tree);
102
106
  tree.updateNodesPosition();
@@ -104,26 +108,30 @@ export default class PageTree extends Component<PageTreeProps, PageTreeState> {
104
108
  }
105
109
 
106
110
  movePage(
107
- index: TreeIndex<PageNode>,
108
- parent: TreeIndex<PageNode>,
111
+ index: Trees.Index<Pages.TreeNode>,
112
+ parent: Trees.Index<Pages.TreeNode>,
109
113
  position: number
110
114
  ) {
111
115
  const data = {
112
116
  parent_id: parent.node.id,
113
117
  position: position
114
118
  };
115
- const url = `/admin/${index.node.locale}/pages/${index.node.id}/move.json`;
119
+ const url = `/admin/${this.props.locale}/pages/${index.node.id}/move.json`;
116
120
  this.performUpdate(index, url, data);
117
121
  }
118
122
 
119
- performUpdate(index: TreeIndex, url: string, data: Attributes) {
120
- void putJson(url, data).then((response: Page) =>
123
+ performUpdate(
124
+ index: Trees.Index<Pages.TreeNode>,
125
+ url: string,
126
+ data: Record<string, unknown>
127
+ ) {
128
+ void putJson(url, data).then((response: Pages.TreeItem) =>
121
129
  this.updateNode(index, response)
122
130
  );
123
131
  }
124
132
 
125
133
  render() {
126
- const addChild = (id: TreeId, attributes: Attributes) => {
134
+ const addChild = (id: Trees.Id, attributes: Pages.TreeNode) => {
127
135
  const tree = this.state.tree;
128
136
  const index = tree.append(attributes, id);
129
137
  this.reorderChildren(id);
@@ -132,7 +140,7 @@ export default class PageTree extends Component<PageTreeProps, PageTreeState> {
132
140
  this.setState({ tree: tree });
133
141
  };
134
142
 
135
- const movedPage = (id: TreeId) => {
143
+ const movedPage = (id: Trees.Id) => {
136
144
  const tree = this.state.tree;
137
145
  const index = tree.getIndex(id);
138
146
  this.reorderChildren(index.parent);
@@ -144,22 +152,27 @@ export default class PageTree extends Component<PageTreeProps, PageTreeState> {
144
152
  this.setState({ tree: tree });
145
153
  };
146
154
 
147
- const toggleCollapsed = (id: TreeId) => {
155
+ const toggleCollapsed = (id: Trees.Id) => {
148
156
  const tree = this.state.tree;
149
157
  const node = tree.getIndex(id).node;
150
158
  this.setCollapsed(id, !node.collapsed);
151
159
  this.setState({ tree: tree });
152
160
  };
153
161
 
154
- const updatePage = (id: TreeId, attributes: Attributes) => {
162
+ const updatePage = (id: Trees.Id, attributes: Pages.TreeItem) => {
155
163
  const tree = this.state.tree;
156
164
  const index = tree.getIndex(id);
157
- const url = `/admin/${index.node.locale}/pages/${index.node.id}.json`;
165
+ const url = `/admin/${this.props.locale}/pages/${index.node.id}.json`;
158
166
  this.updateNode(index, attributes);
159
- this.performUpdate(index, url, { page: attributes });
167
+
168
+ const data: Record<string, unknown> = { ...attributes };
169
+ if ("blocks" in attributes && "name" in attributes.blocks) {
170
+ data.name = attributes.blocks.name[this.props.locale];
171
+ }
172
+ this.performUpdate(index, url, { page: data });
160
173
  };
161
174
 
162
- const updateTree = (tree: Tree) => {
175
+ const updateTree = (tree: Tree<Pages.TreeNode>) => {
163
176
  this.setState({ tree: tree });
164
177
  };
165
178
 
@@ -177,7 +190,7 @@ export default class PageTree extends Component<PageTreeProps, PageTreeState> {
177
190
  );
178
191
  }
179
192
 
180
- reorderChildren(id: TreeId) {
193
+ reorderChildren(id: Trees.Id) {
181
194
  const tree = this.state.tree;
182
195
  const index = this.state.tree.getIndex(id);
183
196
  const node = index.node;
@@ -188,7 +201,10 @@ export default class PageTree extends Component<PageTreeProps, PageTreeState> {
188
201
  const aNode = tree.getIndex(a).node;
189
202
  const bNode = tree.getIndex(b).node;
190
203
  if (aNode.pinned == bNode.pinned) {
191
- return new Date(bNode.published_at) - new Date(aNode.published_at);
204
+ return (
205
+ new Date(bNode.published_at).getTime() -
206
+ new Date(aNode.published_at).getTime()
207
+ );
192
208
  } else {
193
209
  return aNode.pinned ? -1 : 1;
194
210
  }
@@ -196,26 +212,22 @@ export default class PageTree extends Component<PageTreeProps, PageTreeState> {
196
212
  tree.updateNodesPosition();
197
213
  }
198
214
 
199
- setCollapsed(id: TreeId, value: boolean) {
215
+ setCollapsed(id: Trees.Id, value: boolean) {
200
216
  const node = this.state.tree.getIndex(id).node;
201
217
  node.collapsed = value;
202
218
  this.storeCollapsed(id, node.collapsed);
203
219
  this.state.tree.updateNodesPosition();
204
220
  }
205
221
 
206
- storeCollapsed(id: TreeId, newState: boolean) {
222
+ storeCollapsed(id: Trees.Id, newState: boolean) {
207
223
  const node = this.state.tree.getIndex(id).node;
208
224
  const store = collapsedState();
209
225
  store[node.id] = newState;
210
226
  window.localStorage.collapsedPages = JSON.stringify(store);
211
227
  }
212
228
 
213
- updateNode(index: TreeIndex, attributes: Attributes) {
214
- for (const attr in attributes) {
215
- if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
216
- index.node[attr] = attributes[attr];
217
- }
218
- }
229
+ updateNode(index: Trees.Index<Pages.TreeNode>, attributes: Pages.TreeItem) {
230
+ index.node = { ...index.node, ...attributes };
219
231
  this.setState({ tree: this.state.tree });
220
232
  }
221
233
  }
@@ -1,140 +1,152 @@
1
- import React from "react";
2
- import PropTypes from "prop-types";
1
+ import React, { createRef, ChangeEvent, Component, RefObject } from "react";
3
2
  import RichTextToolbarButton from "./RichTextToolbarButton";
4
3
 
5
- export default class RichTextArea extends React.Component {
6
- constructor(props) {
4
+ interface Props {
5
+ id: string;
6
+ name: string;
7
+ value: string;
8
+ rows: number;
9
+ className?: string;
10
+ simple?: boolean;
11
+ lang?: string;
12
+ dir?: string;
13
+ onChange?: (str: string) => void;
14
+ }
15
+
16
+ interface State {
17
+ value: string;
18
+ rows: number;
19
+ }
20
+
21
+ type Replacement = [string, string, string];
22
+ type ActionFn = (str: string) => Replacement;
23
+
24
+ interface Action {
25
+ name: string;
26
+ className: string;
27
+ fn: ActionFn;
28
+ hotkey?: string;
29
+ }
30
+
31
+ export default class RichTextArea extends Component<Props, State> {
32
+ inputRef: RefObject<HTMLTextAreaElement>;
33
+
34
+ constructor(props: Props) {
7
35
  super(props);
8
36
  this.state = {
9
37
  value: props.value || "",
10
38
  rows: props.rows || 5
11
39
  };
12
- this.inputRef = React.createRef();
13
- this.handleChange = this.handleChange.bind(this);
14
- this.handleKeyPress = this.handleKeyPress.bind(this);
15
- this.getSelection = this.getSelection.bind(this);
16
- this.link = this.link.bind(this);
17
- this.replaceSelection = this.replaceSelection.bind(this);
40
+ this.inputRef = createRef<HTMLTextAreaElement>();
18
41
  }
19
42
 
20
- actions() {
21
- const simple = [
43
+ actions = () => {
44
+ const simple: Action[] = [
22
45
  {
23
46
  name: "bold",
24
47
  className: "bold",
25
48
  hotkey: "b",
26
- fn: (str) => ["<b>", str, "</b>"]
49
+ fn: (str: string) => ["<b>", str, "</b>"]
27
50
  },
28
51
  {
29
52
  name: "italic",
30
53
  className: "italic",
31
54
  hotkey: "i",
32
- fn: (str) => ["<i>", str, "</i>"]
55
+ fn: (str: string) => ["<i>", str, "</i>"]
33
56
  }
34
57
  ];
35
58
 
36
- const advanced = [
59
+ const advanced: Action[] = [
37
60
  {
38
61
  name: "Heading 2",
39
62
  className: "header h2",
40
- fn: (str) => ["h2. ", str, ""]
63
+ fn: (str: string) => ["h2. ", str, ""]
41
64
  },
42
65
  {
43
66
  name: "Heading 3",
44
67
  className: "header h3",
45
- fn: (str) => ["h3. ", str, ""]
68
+ fn: (str: string) => ["h3. ", str, ""]
46
69
  },
47
70
  {
48
71
  name: "Heading 4",
49
72
  className: "header h4",
50
- fn: (str) => ["h4. ", str, ""]
73
+ fn: (str: string) => ["h4. ", str, ""]
51
74
  },
52
75
  {
53
76
  name: "Blockquote",
54
77
  className: "quote-left",
55
- fn: (str) => ["bq. ", str, ""]
78
+ fn: (str: string) => ["bq. ", str, ""]
56
79
  },
57
80
  {
58
81
  name: "List",
59
82
  className: "list-ul",
60
- fn: (str) => ["", this.strToList(str, "*"), ""]
83
+ fn: (str: string) => ["", this.strToList(str, "*"), ""]
61
84
  },
62
85
  {
63
86
  name: "Ordered list",
64
87
  className: "list-ol",
65
- fn: (str) => ["", this.strToList(str, "#"), ""]
88
+ fn: (str: string) => ["", this.strToList(str, "#"), ""]
66
89
  },
67
- {
68
- name: "Link",
69
- className: "link",
70
- fn: this.link
71
- },
72
- {
73
- name: "Email link",
74
- className: "envelope",
75
- fn: this.emailLink
76
- }
90
+ { name: "Link", className: "link", fn: this.link },
91
+ { name: "Email link", className: "envelope", fn: this.emailLink }
77
92
  ];
78
93
 
79
94
  return this.props.simple ? simple : [...simple, ...advanced];
80
- }
95
+ };
81
96
 
82
- applyAction(fn) {
83
- let [prefix, replacement, postfix] = fn(this.getSelection());
97
+ applyAction(fn: ActionFn) {
98
+ const [prefix, replacement, postfix] = fn(this.getSelection());
84
99
  this.replaceSelection(prefix, replacement, postfix);
85
100
  }
86
101
 
87
- emailLink(selection) {
88
- var address = prompt("Enter email address", "");
89
- let name = selection.length > 0 ? selection : address;
102
+ emailLink = (selection: string): Replacement => {
103
+ const address = prompt("Enter email address", "");
104
+ const name = selection.length > 0 ? selection : address;
90
105
  return ['"', name, `":mailto:${address}`];
91
- }
106
+ };
92
107
 
93
- getSelection() {
94
- let { selectionStart, selectionEnd, value } = this.inputRef.current;
108
+ getSelection = (): string => {
109
+ const { selectionStart, selectionEnd, value } = this.inputRef.current;
95
110
  return value.substr(selectionStart, selectionEnd - selectionStart);
96
- }
111
+ };
97
112
 
98
- handleChange(evt) {
99
- this.setState({ value: evt.target.value });
100
- }
113
+ handleChange = (evt: ChangeEvent<HTMLTextAreaElement>) => {
114
+ this.updateValue(evt.target.value);
115
+ };
101
116
 
102
- handleKeyPress(evt) {
103
- let key;
117
+ handleKeyPress = (evt: React.KeyboardEvent) => {
118
+ let key: string;
104
119
  if (evt.which >= 65 && evt.which <= 90) {
105
120
  key = String.fromCharCode(evt.keyCode).toLowerCase();
106
121
  } else if (evt.keyCode === 13) {
107
122
  key = "enter";
108
123
  }
109
124
 
110
- let hotkeys = {};
125
+ const hotkeys: Record<string, ActionFn> = {};
111
126
  this.actions().forEach((a) => {
112
127
  if (a.hotkey) {
113
128
  hotkeys[a.hotkey] = a.fn;
114
129
  }
115
130
  });
116
131
 
117
- if (
118
- (evt.metaKey || evt.ctrlKey) &&
119
- Object.prototype.hasOwnProperty.call(hotkeys, key)
120
- ) {
132
+ if ((evt.metaKey || evt.ctrlKey) && key in hotkeys) {
121
133
  evt.preventDefault();
122
134
  this.applyAction(hotkeys[key]);
123
135
  }
124
- }
136
+ };
125
137
 
126
- link(selection) {
127
- let name = selection.length > 0 ? selection : "Link text";
128
- var url = prompt("Enter link URL", "");
138
+ link = (selection: string): Replacement => {
139
+ const name = selection.length > 0 ? selection : "Link text";
140
+ const url = prompt("Enter link URL", "");
129
141
  if (url) {
130
142
  return ['"', name, `":${this.relativeUrl(url)}`];
131
143
  } else {
132
144
  return ["", name, ""];
133
145
  }
134
- }
146
+ };
135
147
 
136
148
  localeOptions() {
137
- let opts = {};
149
+ const opts: React.HTMLProps<HTMLTextAreaElement> = {};
138
150
 
139
151
  if (this.props.lang) {
140
152
  opts.lang = this.props.lang;
@@ -147,8 +159,8 @@ export default class RichTextArea extends React.Component {
147
159
  return opts;
148
160
  }
149
161
 
150
- relativeUrl(str) {
151
- let url = null;
162
+ relativeUrl(str: string): string {
163
+ let url: URL = null;
152
164
 
153
165
  if (!str.match(/^https:\/\//) || !document || !document.location) {
154
166
  return str;
@@ -171,10 +183,11 @@ export default class RichTextArea extends React.Component {
171
183
  }
172
184
 
173
185
  render() {
174
- let { value, rows } = this.state;
175
- let { id, name } = this.props;
186
+ const { rows } = this.state;
187
+ const { id, name } = this.props;
188
+ const value = this.getValue();
176
189
 
177
- const clickHandler = (fn) => (evt) => {
190
+ const clickHandler = (fn: ActionFn) => (evt: React.MouseEvent) => {
178
191
  evt.preventDefault();
179
192
  this.applyAction(fn);
180
193
  };
@@ -192,7 +205,7 @@ export default class RichTextArea extends React.Component {
192
205
  ))}
193
206
  </div>
194
207
  <textarea
195
- className="rich"
208
+ className={this.props.className || "rich"}
196
209
  ref={this.inputRef}
197
210
  id={id}
198
211
  name={name}
@@ -206,9 +219,9 @@ export default class RichTextArea extends React.Component {
206
219
  );
207
220
  }
208
221
 
209
- replaceSelection(prefix, replacement, postfix) {
210
- let textarea = this.inputRef.current;
211
- let { selectionStart, selectionEnd, value } = textarea;
222
+ replaceSelection = (prefix: string, replacement: string, postfix: string) => {
223
+ const textarea = this.inputRef.current;
224
+ const { selectionStart, selectionEnd, value } = textarea;
212
225
 
213
226
  textarea.value =
214
227
  value.substr(0, selectionStart) +
@@ -222,23 +235,29 @@ export default class RichTextArea extends React.Component {
222
235
  selectionStart + prefix.length,
223
236
  selectionStart + prefix.length + replacement.length
224
237
  );
225
- this.setState({ value: textarea.value });
226
- }
238
+ this.updateValue(textarea.value);
239
+ };
227
240
 
228
- strToList(str, prefix) {
241
+ strToList(str: string, prefix: string) {
229
242
  return str
230
243
  .split("\n")
231
244
  .map((l) => prefix + " " + l)
232
245
  .join("\n");
233
246
  }
234
- }
235
247
 
236
- RichTextArea.propTypes = {
237
- id: PropTypes.string,
238
- name: PropTypes.string,
239
- value: PropTypes.string,
240
- rows: PropTypes.number,
241
- simple: PropTypes.bool,
242
- lang: PropTypes.string,
243
- dir: PropTypes.string
244
- };
248
+ getValue() {
249
+ if (this.props.onChange) {
250
+ return this.props.value;
251
+ } else {
252
+ return this.state.value;
253
+ }
254
+ }
255
+
256
+ updateValue(str: string) {
257
+ if (this.props.onChange) {
258
+ this.props.onChange(str);
259
+ } else {
260
+ this.setState({ value: str });
261
+ }
262
+ }
263
+ }
@@ -1,14 +1,12 @@
1
- import React from "react";
1
+ import React, { MouseEvent } from "react";
2
2
 
3
- interface RichTextToolbarButtonProps {
3
+ interface Props {
4
4
  className: string;
5
5
  name: string;
6
- onClick: (evt: Event) => void;
6
+ onClick: (evt: React.MouseEvent) => void;
7
7
  }
8
8
 
9
- export default function RichTextToolbarButton(
10
- props: RichTextToolbarButtonProps
11
- ) {
9
+ export default function RichTextToolbarButton(props: Props) {
12
10
  return (
13
11
  <a
14
12
  title={props.name}
@@ -1,21 +1,28 @@
1
- import React, { ChangeEvent, useState } from "react";
1
+ import React, { ChangeEvent, MouseEvent, KeyboardEvent, useState } from "react";
2
2
 
3
- interface AddTagFormProps {
4
- addTag: (string) => void;
3
+ import * as Tags from "../../types/Tags";
4
+
5
+ interface Props {
6
+ dispatch: (action: Tags.Action) => void;
5
7
  }
6
8
 
7
- export default function AddTagForm(props: AddTagFormProps) {
9
+ export default function AddTagForm(props: Props) {
8
10
  const [tag, setTag] = useState("");
9
11
 
10
- const submit = (evt: Event) => {
11
- evt.preventDefault();
12
- props.addTag(tag);
12
+ const submit = () => {
13
+ props.dispatch({ type: "addTag", payload: tag });
13
14
  setTag("");
14
15
  };
15
16
 
16
- const handleKeyDown = (evt: Event) => {
17
+ const handleSubmit = (evt: MouseEvent) => {
18
+ evt.preventDefault();
19
+ submit();
20
+ };
21
+
22
+ const handleKeyDown = (evt: KeyboardEvent<HTMLInputElement>) => {
17
23
  if (evt.which === 13) {
18
- submit(evt);
24
+ evt.preventDefault();
25
+ submit();
19
26
  }
20
27
  };
21
28
 
@@ -24,17 +31,17 @@ export default function AddTagForm(props: AddTagFormProps) {
24
31
  };
25
32
 
26
33
  return (
27
- <div className="add-tag-form">
34
+ <div className="add-tag-form inline-form">
28
35
  <input
29
36
  name="add-tag"
30
37
  type="text"
31
- className="add-tag"
38
+ className="add-tag tight"
32
39
  value={tag}
33
40
  onKeyDown={handleKeyDown}
34
41
  onChange={handleChange}
35
42
  placeholder="Add tag..."
36
43
  />
37
- <button onClick={submit} disabled={!tag}>
44
+ <button onClick={handleSubmit} disabled={!tag}>
38
45
  Add
39
46
  </button>
40
47
  </div>
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+
3
+ import { allTags, isEnabled } from "./useTags";
4
+
5
+ import AddTagForm from "./AddTagForm";
6
+ import Tag from "./Tag";
7
+ import * as Tags from "../../types/Tags";
8
+
9
+ interface Props {
10
+ name: string;
11
+ state: Tags.State;
12
+ dispatch: (action: Tags.Action) => void;
13
+ }
14
+
15
+ export default function Editor(props: Props) {
16
+ const { name, state, dispatch } = props;
17
+
18
+ return (
19
+ <div className="tag-editor">
20
+ <input type="hidden" name={name} value={JSON.stringify(state.enabled)} />
21
+ {allTags(state).map((t) => (
22
+ <Tag
23
+ key={t}
24
+ tag={t}
25
+ enabled={isEnabled(t, state)}
26
+ dispatch={dispatch}
27
+ />
28
+ ))}
29
+ <AddTagForm dispatch={dispatch} />
30
+ </div>
31
+ );
32
+ }
@@ -1,14 +1,16 @@
1
1
  import React from "react";
2
2
 
3
- interface TagProps {
3
+ import * as Tags from "../../types/Tags";
4
+
5
+ interface Props {
4
6
  enabled: boolean;
5
7
  tag: string;
6
- toggleEnabled: (string) => void;
8
+ dispatch: (action: Tags.Action) => void;
7
9
  }
8
10
 
9
- export default function Tag(props: TagProps): JSX.Element {
11
+ export default function Tag(props: Props) {
10
12
  const handleChange = () => {
11
- props.toggleEnabled(props.tag);
13
+ props.dispatch({ type: "toggleTag", payload: props.tag });
12
14
  };
13
15
 
14
16
  const classes = ["tag"];