pages_core 3.12.1 → 3.12.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/assets/builds/pages_core/admin-dist.js +135 -50
  4. data/app/assets/builds/pages_core/admin-dist.js.map +7 -0
  5. data/app/assets/builds/pages_core/admin.css +72 -20
  6. data/app/assets/stylesheets/pages_core/admin/components/attachments.css +1 -1
  7. data/app/assets/stylesheets/pages_core/admin/components/image_editor.css +2 -2
  8. data/app/assets/stylesheets/pages_core/admin/components/image_grid.css +8 -8
  9. data/app/assets/stylesheets/pages_core/admin/components/image_uploader.css +2 -2
  10. data/app/assets/stylesheets/pages_core/admin/components/layout.css +2 -2
  11. data/app/assets/stylesheets/pages_core/admin/components/modal.css +2 -2
  12. data/app/assets/stylesheets/pages_core/admin/components/search.css +27 -0
  13. data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +6 -0
  14. data/app/controllers/admin/pages_controller.rb +12 -11
  15. data/app/controllers/concerns/pages_core/rss_controller.rb +17 -1
  16. data/app/controllers/pages_core/admin_controller.rb +6 -0
  17. data/app/controllers/pages_core/frontend/pages_controller.rb +9 -5
  18. data/app/controllers/pages_core/sitemaps_controller.rb +3 -5
  19. data/app/helpers/admin/pages_helper.rb +32 -0
  20. data/app/helpers/pages_core/images_helper.rb +28 -7
  21. data/app/javascript/admin-dist.ts +2 -0
  22. data/app/javascript/components/Attachments/{Attachment.jsx → Attachment.tsx} +42 -33
  23. data/app/javascript/components/Attachments/{AttachmentEditor.jsx → AttachmentEditor.tsx} +23 -23
  24. data/app/javascript/components/{EditableImage.jsx → EditableImage.tsx} +27 -24
  25. data/app/javascript/components/{FileUploadButton.jsx → FileUploadButton.tsx} +15 -16
  26. data/app/javascript/components/ImageCropper/FocalPoint.tsx +94 -0
  27. data/app/javascript/components/ImageCropper/{Image.jsx → Image.tsx} +13 -14
  28. data/app/javascript/components/ImageCropper/{Toolbar.jsx → Toolbar.tsx} +16 -12
  29. data/app/javascript/components/ImageCropper/{useCrop.js → useCrop.ts} +80 -37
  30. data/app/javascript/components/{ImageCropper.jsx → ImageCropper.tsx} +17 -15
  31. data/app/javascript/components/ImageEditor/{Form.jsx → Form.tsx} +24 -23
  32. data/app/javascript/components/{ImageEditor.jsx → ImageEditor.tsx} +17 -15
  33. data/app/javascript/components/ImageGrid/{DragElement.jsx → DragElement.tsx} +12 -10
  34. data/app/javascript/components/ImageGrid/{GridImage.jsx → GridImage.tsx} +40 -30
  35. data/app/javascript/components/ImageGrid/{Placeholder.jsx → Placeholder.tsx} +5 -6
  36. data/app/javascript/components/ImageGrid.jsx +3 -4
  37. data/app/javascript/components/{ImageUploader.jsx → ImageUploader.tsx} +46 -41
  38. data/app/javascript/components/Modal.tsx +48 -0
  39. data/app/javascript/components/PageImages.tsx +28 -0
  40. data/app/javascript/components/{PageTreeDraggable.jsx → PageTree/Draggable.tsx} +79 -57
  41. data/app/javascript/components/{PageTreeNode.jsx → PageTree/Node.tsx} +79 -70
  42. data/app/javascript/components/PageTree/types.ts +15 -0
  43. data/app/javascript/components/PageTree.tsx +206 -0
  44. data/app/javascript/components/RichTextToolbarButton.tsx +17 -0
  45. data/app/javascript/components/TagEditor/{AddTagForm.jsx → AddTagForm.tsx} +9 -10
  46. data/app/javascript/components/TagEditor/{Tag.jsx → Tag.tsx} +8 -9
  47. data/app/javascript/components/{TagEditor.jsx → TagEditor.tsx} +12 -13
  48. data/app/javascript/components/Toast.tsx +61 -0
  49. data/app/javascript/components/drag/{draggedOrder.js → draggedOrder.ts} +22 -12
  50. data/app/javascript/components/drag/types.ts +28 -0
  51. data/app/javascript/components/drag/{useDragCollection.js → useDragCollection.ts} +40 -22
  52. data/app/javascript/components/drag/{useDragUploader.js → useDragUploader.ts} +34 -25
  53. data/app/javascript/components/drag/useDraggable.ts +21 -0
  54. data/app/javascript/components/{drag.js → drag.ts} +1 -0
  55. data/app/javascript/controllers/{EditPageController.js → EditPageController.ts} +3 -1
  56. data/app/javascript/controllers/{LoginController.js → LoginController.ts} +7 -3
  57. data/app/javascript/controllers/{MainController.js → MainController.ts} +19 -14
  58. data/app/javascript/features/{RichText.jsx → RichText.tsx} +3 -3
  59. data/app/javascript/{index.js → index.ts} +8 -7
  60. data/app/javascript/lib/{Tree.js → Tree.ts} +106 -85
  61. data/app/javascript/lib/{copyToClipboard.js → copyToClipboard.ts} +1 -1
  62. data/app/javascript/lib/{readyHandler.js → readyHandler.ts} +4 -2
  63. data/app/javascript/lib/{request.js → request.ts} +11 -5
  64. data/app/javascript/stores/useModalStore.ts +15 -0
  65. data/app/javascript/stores/useToastStore.ts +26 -0
  66. data/app/javascript/stores.ts +2 -0
  67. data/app/javascript/types.ts +30 -0
  68. data/app/policies/page_policy.rb +4 -0
  69. data/app/views/admin/calendars/_sidebar.html.erb +3 -0
  70. data/app/views/admin/news/_sidebar.html.erb +3 -0
  71. data/app/views/admin/pages/_list_item.html.erb +4 -22
  72. data/app/views/admin/pages/_search_bar.html.erb +12 -0
  73. data/app/views/admin/pages/index.html.erb +3 -0
  74. data/app/views/admin/pages/search.html.erb +54 -0
  75. data/app/views/feeds/pages.rss.builder +3 -9
  76. data/config/routes.rb +1 -0
  77. data/lib/pages_core/configuration/pages.rb +0 -1
  78. data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +33 -17
  79. data/lib/rails/generators/pages_core/frontend/templates/application.html.erb +0 -1
  80. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +40 -0
  81. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +68 -0
  82. data/lib/rails/generators/pages_core/frontend/templates/postcss.config.js +17 -0
  83. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/application.postcss.css +4 -0
  84. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.css +24 -0
  85. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/layout.css +21 -0
  86. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.css +5 -0
  87. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/animation.css +5 -0
  88. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.css +18 -0
  89. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/fonts.css +6 -0
  90. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/grid.css +65 -0
  91. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.css +131 -0
  92. data/lib/rails/generators/pages_core/install/templates/pages_initializer.rb +0 -3
  93. metadata +69 -63
  94. data/app/javascript/admin-dist.js +0 -2
  95. data/app/javascript/components/ImageCropper/FocalPoint.jsx +0 -93
  96. data/app/javascript/components/Modal.jsx +0 -59
  97. data/app/javascript/components/PageImages.jsx +0 -25
  98. data/app/javascript/components/PageTree.jsx +0 -196
  99. data/app/javascript/components/RichTextToolbarButton.jsx +0 -20
  100. data/app/javascript/components/Toast.jsx +0 -72
  101. data/app/javascript/components/drag/useDraggable.js +0 -17
  102. data/app/javascript/stores/ModalStore.jsx +0 -12
  103. data/app/javascript/stores/ToastStore.jsx +0 -14
  104. data/app/javascript/stores.js +0 -2
  105. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/GridOverlay.js +0 -66
  106. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/ResponsiveEmbeds.js +0 -72
  107. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/application.sass.scss +0 -15
  108. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.scss +0 -12
  109. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.scss +0 -26
  110. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/breakpoints.scss +0 -42
  111. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/clearfix.scss +0 -7
  112. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/fonts.scss +0 -32
  113. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/grid.scss +0 -168
  114. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/grid_overlay.scss +0 -44
  115. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.scss +0 -8
  116. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.scss +0 -90
  117. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/vendor/normalize.css +0 -349
  118. /data/app/javascript/components/Attachments/{Placeholder.jsx → Placeholder.tsx} +0 -0
  119. /data/app/javascript/components/ImageGrid/{FilePlaceholder.jsx → FilePlaceholder.tsx} +0 -0
  120. /data/app/javascript/{components.js → components.ts} +0 -0
  121. /data/app/javascript/{hooks.js → hooks.ts} +0 -0
@@ -25,24 +25,50 @@
25
25
  SOFTWARE.
26
26
  */
27
27
 
28
- import React from "react";
29
- import PropTypes from "prop-types";
28
+ import React, { createRef, ChangeEvent, Component, RefObject } from "react";
29
+ import Tree, { TreeId, TreeIndex } from "../../lib/Tree";
30
+ import { Attributes, PageNode } from "./types";
31
+
32
+ interface NodeProps {
33
+ addChild: (index: TreeIndex) => void,
34
+ dir: string,
35
+ dragging: number,
36
+ index: TreeIndex<PageNode>,
37
+ locale: string,
38
+ onCollapse: (id: TreeId) => void,
39
+ onDragStart: (id: number, element: HTMLDivElement, evt: Event) => void,
40
+ paddingLeft: number,
41
+ tree: Tree<PageNode>,
42
+ updatePage: (index: TreeIndex, attributes: Attributes) => void
43
+ }
44
+
45
+ interface NodeState {
46
+ newName: string
47
+ }
30
48
 
31
- export default class PageTreeNode extends React.Component {
32
- constructor(props) {
49
+ interface ButtonOptions {
50
+ icon: string,
51
+ className: string,
52
+ onClick: (evt: Event) => void
53
+ }
54
+
55
+ export default class Node extends Component<NodeProps, NodeState> {
56
+ innerRef: RefObject<HTMLDivElement>;
57
+
58
+ constructor(props: NodeProps) {
33
59
  super(props);
34
60
  this.state = { newName: props.index.node.name };
35
- this.innerRef = React.createRef();
61
+ this.innerRef = createRef<HTMLDivElement>();
36
62
  }
37
63
 
38
- permitted(action) {
64
+ permitted(action: string): boolean {
39
65
  return this.node().permissions &&
40
66
  this.node().permissions.indexOf(action) != -1;
41
67
  }
42
68
 
43
69
  actions() {
44
- let statusLabel = (this.node().status != 2) ? "Publish" : "Hide";
45
- let statusIcon = (this.node().status != 2) ? "check" : "ban";
70
+ const statusLabel = (this.node().status != 2) ? "Publish" : "Hide";
71
+ const statusIcon = (this.node().status != 2) ? "check" : "ban";
46
72
 
47
73
  if (this.node().editing) {
48
74
  return null;
@@ -91,11 +117,10 @@ export default class PageTreeNode extends React.Component {
91
117
  }
92
118
 
93
119
  addButton() {
94
- let self = this;
95
- let node = this.node();
96
- let handleClick = function () {
97
- if (self.props.addChild) {
98
- self.props.addChild(self.props.index);
120
+ const node = this.node();
121
+ const handleClick = () => {
122
+ if (this.props.addChild) {
123
+ this.props.addChild(this.props.index);
99
124
  }
100
125
  };
101
126
 
@@ -112,8 +137,8 @@ export default class PageTreeNode extends React.Component {
112
137
  }
113
138
  }
114
139
 
115
- button(label, options) {
116
- let icon = "fa-solid fa-" + options.icon + " icon";
140
+ button(label: string, options: ButtonOptions) {
141
+ const icon = "fa-solid fa-" + options.icon + " icon";
117
142
  return (
118
143
  <button type="button"
119
144
  className={options.className}
@@ -128,18 +153,18 @@ export default class PageTreeNode extends React.Component {
128
153
  const { index, tree, dragging, dir, locale } = this.props;
129
154
 
130
155
  if (index.children && index.children.length && !index.node.collapsed) {
131
- var childrenStyles = {};
156
+ const childrenStyles = {};
132
157
  if (index.node.collapsed) {
133
158
  childrenStyles.display = "none";
134
159
  }
135
- childrenStyles["paddingLeft"] = this.props.paddingLeft + "px";
160
+ childrenStyles["paddingLeft"] = `${this.props.paddingLeft}px`;
136
161
 
137
162
  return (
138
163
  <div className="children" style={childrenStyles}>
139
164
  {index.children.map((child) => {
140
- var childIndex = tree.getIndex(child);
165
+ const childIndex = tree.getIndex(child);
141
166
  return (
142
- <PageTreeNode
167
+ <Node
143
168
  tree={tree}
144
169
  index={childIndex}
145
170
  key={childIndex.id}
@@ -161,25 +186,24 @@ export default class PageTreeNode extends React.Component {
161
186
  }
162
187
 
163
188
  collapseArrow() {
164
- let index = this.props.index;
165
- let self = this;
189
+ const index = this.props.index;
166
190
 
167
191
  // Don't collapse the root node
168
192
  if (!index.parent) {
169
193
  return null;
170
194
  }
171
195
 
172
- let handleCollapse = function (e) {
196
+ const handleCollapse = (e: Event) => {
173
197
  e.stopPropagation();
174
- let nodeId = self.props.index.id;
175
- if (self.props.onCollapse) {
176
- self.props.onCollapse(nodeId);
198
+ const nodeId = this.props.index.id;
199
+ if (this.props.onCollapse) {
200
+ this.props.onCollapse(nodeId);
177
201
  }
178
202
  };
179
203
 
180
204
  if (this.visibleChildren().length > 0) {
181
- let collapsed = index.node.collapsed;
182
- var classnames = null;
205
+ const collapsed = index.node.collapsed;
206
+ let classnames = "";
183
207
 
184
208
  if (collapsed) {
185
209
  classnames = "collapse fa-solid fa-caret-right";
@@ -201,7 +225,7 @@ export default class PageTreeNode extends React.Component {
201
225
  if (this.node().collapsed &&
202
226
  this.node().children &&
203
227
  this.node().children.length > 0) {
204
- let pluralized = (this.node().children.length == 1) ? "item" : "items";
228
+ const pluralized = (this.node().children.length == 1) ? "item" : "items";
205
229
  return (
206
230
  <span className="collapsed-label">
207
231
  ({this.node().children.length} {pluralized})
@@ -222,11 +246,11 @@ export default class PageTreeNode extends React.Component {
222
246
  this.updatePage({editing: true});
223
247
  }
224
248
 
225
- editUrl(page) {
249
+ editUrl(page: PageNode) {
226
250
  return(`/admin/${page.locale}/pages/${page.param}/edit`);
227
251
  }
228
252
 
229
- node() {
253
+ node(): PageNode {
230
254
  return this.props.index.node;
231
255
  }
232
256
 
@@ -242,22 +266,21 @@ export default class PageTreeNode extends React.Component {
242
266
  }
243
267
 
244
268
  render() {
245
- let self = this;
246
- let props = this.props;
247
- let index = props.index;
248
- let dragging = props.dragging;
249
- let editing = this.node().editing;
250
- var classnames = "node";
269
+ const props = this.props;
270
+ const index = props.index;
271
+ const dragging = props.dragging;
272
+ const editing = this.node().editing;
273
+ let classnames = "node";
251
274
 
252
- var node = editing ? this.renderEditNode() : this.renderNode();
275
+ const node = editing ? this.renderEditNode() : this.renderNode();
253
276
 
254
277
  if (index.id === dragging) {
255
278
  classnames = "node placeholder";
256
279
  }
257
280
 
258
- let handleMouseDown = function (e) {
259
- if (self.permitted("edit") && !editing && props.onDragStart) {
260
- props.onDragStart(props.index.id, self.innerRef.current, e);
281
+ const handleMouseDown = (e: Event) => {
282
+ if (this.permitted("edit") && !editing && props.onDragStart) {
283
+ props.onDragStart(props.index.id, this.innerRef.current, e);
261
284
  }
262
285
  };
263
286
 
@@ -281,23 +304,22 @@ export default class PageTreeNode extends React.Component {
281
304
 
282
305
  renderEditNode() {
283
306
  const { dir, locale } = this.props;
284
- let self = this;
285
307
 
286
- let handleNameChange = function(event) {
287
- self.setState({newName: event.target.value});
308
+ const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
309
+ this.setState({ newName: event.target.value });
288
310
  };
289
311
 
290
- let performEdit = function(event) {
312
+ const performEdit = (event: Event) => {
291
313
  event.preventDefault();
292
- self.updatePage({
293
- name: self.state.newName,
314
+ this.updatePage({
315
+ name: this.state.newName,
294
316
  editing: false
295
317
  });
296
318
  };
297
319
 
298
- let cancelEdit = function() {
299
- self.setState({newName: self.node().name});
300
- self.updatePage({editing: false});
320
+ const cancelEdit = () => {
321
+ this.setState({ newName: this.node().name });
322
+ this.updatePage({ editing: false });
301
323
  };
302
324
 
303
325
  return (
@@ -325,13 +347,13 @@ export default class PageTreeNode extends React.Component {
325
347
  }
326
348
 
327
349
  renderNode() {
328
- let index = this.props.index;
329
- let node = index.node;
350
+ const index = this.props.index;
351
+ const node = index.node;
330
352
 
331
- var pageName = <span className="name">{this.pageName()}</span>;
332
- var className = "page";
353
+ let pageName = <span className="name">{this.pageName()}</span>;
354
+ let className = "page";
333
355
 
334
- var iconClass = "fa-regular fa-file icon";
356
+ let iconClass = "fa-regular fa-file icon";
335
357
 
336
358
  if (typeof(node.status) != "undefined") {
337
359
  className = `page status-${this.node().status}`;
@@ -361,7 +383,7 @@ export default class PageTreeNode extends React.Component {
361
383
  }
362
384
 
363
385
  statusLabel() {
364
- let labels = ["Draft", "Reviewed", "Published", "Hidden", "Deleted"];
386
+ const labels = ["Draft", "Reviewed", "Published", "Hidden", "Deleted"];
365
387
  if (typeof(this.node().status) != "undefined" && this.node().status != 2) {
366
388
  return (
367
389
  <span className="status-label">
@@ -381,13 +403,13 @@ export default class PageTreeNode extends React.Component {
381
403
  }
382
404
  }
383
405
 
384
- updatePage(attributes) {
406
+ updatePage(attributes: Attributes) {
385
407
  if (this.props.updatePage) {
386
408
  return this.props.updatePage(this.props.index, attributes);
387
409
  }
388
410
  }
389
411
 
390
- visibleChildren() {
412
+ visibleChildren(): PageNode[] {
391
413
  if (this.node().children) {
392
414
  return this.node().children.filter(p => p.status != 4);
393
415
  } else {
@@ -395,16 +417,3 @@ export default class PageTreeNode extends React.Component {
395
417
  }
396
418
  }
397
419
  }
398
-
399
- PageTreeNode.propTypes = {
400
- addChild: PropTypes.func,
401
- dragging: PropTypes.number,
402
- index: PropTypes.object,
403
- onCollapse: PropTypes.func,
404
- onDragStart: PropTypes.func,
405
- paddingLeft: PropTypes.number,
406
- tree: PropTypes.object,
407
- updatePage: PropTypes.func,
408
- locale: PropTypes.string,
409
- dir: PropTypes.string,
410
- };
@@ -0,0 +1,15 @@
1
+ import { TreeNode } from "../../lib/Tree";
2
+
3
+ export type Attributes = Record<string, unknown>;
4
+
5
+ export interface PageNode extends TreeNode {
6
+ id: number | null,
7
+ children: PageNode[],
8
+ editing: boolean,
9
+ locale: string,
10
+ name: string,
11
+ param: string,
12
+ permissions: string[],
13
+ published_at: string,
14
+ status: string
15
+ }
@@ -0,0 +1,206 @@
1
+ import React, { Component } from "react";
2
+ import Tree, { TreeId, TreeIndex } from "../lib/Tree";
3
+ import { postJson, putJson } from "../lib/request";
4
+ import { Attributes, PageNode } from "./PageTree/types";
5
+ import Draggable from "./PageTree/Draggable";
6
+
7
+ interface Page extends Record<string, unknown> {
8
+ parent_page_id: number | null
9
+ }
10
+
11
+ type CollapsedState = Record<number, boolean>;
12
+
13
+ interface ParentMap {
14
+ [index: number]: Page[]
15
+ }
16
+
17
+ interface PageTreeProps {
18
+ dir: string,
19
+ locale: string,
20
+ pages: Page[],
21
+ permissions: string[]
22
+ }
23
+
24
+ interface PageTreeState {
25
+ tree: Tree<PageNode>
26
+ }
27
+
28
+ function collapsedState(): CollapsedState {
29
+ if (window && window.localStorage &&
30
+ typeof(window.localStorage.collapsedPages) != "undefined") {
31
+ return JSON.parse(window.localStorage.getItem("collapsedPages")) as CollapsedState;
32
+ }
33
+ return {};
34
+ }
35
+
36
+ export default class PageTree extends Component<PageTreeProps, PageTreeState> {
37
+ constructor(props: PageTreeProps) {
38
+ super(props);
39
+
40
+ this.state = { tree: this.buildTree(props.pages) };
41
+ }
42
+
43
+ applyCollapsed(tree: Tree<PageNode>) {
44
+ const depth = (t: Tree, index: TreeIndex) => {
45
+ let depth = 0;
46
+ let pointer = t.getIndex(index.parent);
47
+ while (pointer) {
48
+ depth += 1;
49
+ pointer = t.getIndex(pointer.parent);
50
+ }
51
+ return depth;
52
+ };
53
+
54
+ const walk = (id: TreeId) => {
55
+ const index = tree.getIndex(id);
56
+ const node = index.node;
57
+ if (node.id && node.id in collapsedState()) {
58
+ node.collapsed = collapsedState()[node.id];
59
+ } else if (node.news_page) {
60
+ node.collapsed = true;
61
+ } else if (depth(tree, index) > 1) {
62
+ node.collapsed = true;
63
+ }
64
+ if (index.children && index.children.length) {
65
+ index.children.forEach(c => walk(c));
66
+ }
67
+ };
68
+ walk(1);
69
+ }
70
+
71
+ createPage(index: TreeIndex<PageNode>, attributes: Attributes) {
72
+ void postJson(`/admin/${index.node.locale}/pages.json`, { page: attributes })
73
+ .then((response: Attributes) => this.updateNode(index, response));
74
+ }
75
+
76
+ buildTree(pages: Page[]) {
77
+ // Build tree
78
+ const parentMap: ParentMap = pages.reduce((m: ParentMap, page: Page) => {
79
+ const id = page.parent_page_id || 0;
80
+ m[id] = [...(m[id] || []), page];
81
+ return m;
82
+ }, {});
83
+
84
+ pages.forEach((p: Page) => { p.children = parentMap[p.id] || []; });
85
+
86
+ const tree = new Tree({
87
+ name: "All Pages",
88
+ locale: this.props.locale,
89
+ permissions: this.props.permissions,
90
+ root: true,
91
+ children: parentMap[0]
92
+ });
93
+ this.applyCollapsed(tree);
94
+ tree.updateNodesPosition();
95
+ return tree;
96
+ }
97
+
98
+ movePage(index: TreeIndex<PageNode>, parent: TreeIndex<PageNode>, position: number) {
99
+ const data = {
100
+ parent_id: parent.node.id,
101
+ position: position
102
+ };
103
+ const url = `/admin/${index.node.locale}/pages/${index.node.id}/move.json`;
104
+ this.performUpdate(index, url, data);
105
+ }
106
+
107
+ performUpdate(index: TreeIndex, url: string, data: Attributes) {
108
+ void putJson(url, data)
109
+ .then((response: Page) => this.updateNode(index, response));
110
+ }
111
+
112
+ render() {
113
+ const addChild = (id: TreeId, attributes: Attributes) => {
114
+ const tree = this.state.tree;
115
+ const index = tree.append(attributes, id);
116
+ this.reorderChildren(id);
117
+ this.setCollapsed(id, false);
118
+ this.createPage(index, attributes);
119
+ this.setState({tree: tree});
120
+ };
121
+
122
+ const movedPage = (id: TreeId) => {
123
+ const tree = this.state.tree;
124
+ const index = tree.getIndex(id);
125
+ this.reorderChildren(index.parent);
126
+
127
+ const parent = tree.getIndex(index.parent);
128
+ const position = parent.children.indexOf(id) + 1;
129
+
130
+ this.movePage(index, parent, position);
131
+ this.setState({ tree: tree });
132
+ };
133
+
134
+ const toggleCollapsed = (id: TreeId) => {
135
+ const tree = this.state.tree;
136
+ const node = tree.getIndex(id).node;
137
+ this.setCollapsed(id, !node.collapsed);
138
+ this.setState({tree: tree});
139
+ };
140
+
141
+ const updatePage = (id: TreeId, attributes: Attributes) => {
142
+ const tree = this.state.tree;
143
+ const index = tree.getIndex(id);
144
+ const url = `/admin/${index.node.locale}/pages/${index.node.id}.json`;
145
+ this.updateNode(index, attributes);
146
+ this.performUpdate(index, url, { page: attributes });
147
+ };
148
+
149
+ const updateTree = (tree: Tree) => {
150
+ this.setState({ tree: tree });
151
+ };
152
+
153
+ return(
154
+ <Draggable tree={this.state.tree}
155
+ addChild={addChild}
156
+ movedPage={movedPage}
157
+ toggleCollapsed={toggleCollapsed}
158
+ updatePage={updatePage}
159
+ updateTree={updateTree}
160
+ locale={this.props.locale}
161
+ dir={this.props.dir} />
162
+ );
163
+ }
164
+
165
+ reorderChildren(id: TreeId) {
166
+ const tree = this.state.tree;
167
+ const index = this.state.tree.getIndex(id);
168
+ const node = index.node;
169
+ if (!node.news_page) {
170
+ return;
171
+ }
172
+ index.children = index.children.sort(function (a, b) {
173
+ const aNode = tree.getIndex(a).node;
174
+ const bNode = tree.getIndex(b).node;
175
+ if (aNode.pinned == bNode.pinned) {
176
+ return new Date(bNode.published_at) - new Date(aNode.published_at);
177
+ } else {
178
+ return aNode.pinned ? -1 : 1;
179
+ }
180
+ });
181
+ tree.updateNodesPosition();
182
+ }
183
+
184
+ setCollapsed(id: TreeId, value: boolean) {
185
+ const node = this.state.tree.getIndex(id).node;
186
+ node.collapsed = value;
187
+ this.storeCollapsed(id, node.collapsed);
188
+ this.state.tree.updateNodesPosition();
189
+ }
190
+
191
+ storeCollapsed(id: TreeId, newState: boolean) {
192
+ const node = this.state.tree.getIndex(id).node;
193
+ const store = collapsedState();
194
+ store[node.id] = newState;
195
+ window.localStorage.collapsedPages = JSON.stringify(store);
196
+ }
197
+
198
+ updateNode(index: TreeIndex, attributes: Attributes) {
199
+ for (const attr in attributes) {
200
+ if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
201
+ index.node[attr] = attributes[attr];
202
+ }
203
+ }
204
+ this.setState({ tree: this.state.tree });
205
+ }
206
+ }
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+
3
+ interface RichTextToolbarButtonProps {
4
+ className: string,
5
+ name: string,
6
+ onClick: (evt: Event) => void
7
+ }
8
+
9
+ export default function RichTextToolbarButton(props: RichTextToolbarButtonProps) {
10
+ return (
11
+ <a title={props.name}
12
+ className={"button " + props.className}
13
+ onClick={props.onClick}>
14
+ <i className={"fa-solid fa-" + props.className} />
15
+ </a>
16
+ );
17
+ }
@@ -1,22 +1,25 @@
1
- import React, { useState } from "react";
2
- import PropTypes from "prop-types";
1
+ import React, { ChangeEvent, useState } from "react";
3
2
 
4
- export default function AddTagForm(props) {
3
+ interface AddTagFormProps {
4
+ addTag: (string) => void
5
+ }
6
+
7
+ export default function AddTagForm(props: AddTagFormProps) {
5
8
  const [tag, setTag] = useState("");
6
9
 
7
- const submit = (evt) => {
10
+ const submit = (evt: Event) => {
8
11
  evt.preventDefault();
9
12
  props.addTag(tag);
10
13
  setTag("");
11
14
  };
12
15
 
13
- const handleKeyDown = (evt) => {
16
+ const handleKeyDown = (evt: Event) => {
14
17
  if (evt.which === 13) {
15
18
  submit(evt);
16
19
  }
17
20
  };
18
21
 
19
- const handleChange = (evt) => {
22
+ const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
20
23
  setTag(evt.target.value);
21
24
  };
22
25
 
@@ -36,7 +39,3 @@ export default function AddTagForm(props) {
36
39
  </div>
37
40
  );
38
41
  }
39
-
40
- AddTagForm.propTypes = {
41
- addTag: PropTypes.func
42
- };
@@ -1,12 +1,17 @@
1
1
  import React from "react";
2
- import PropTypes from "prop-types";
3
2
 
4
- export default function Tag(props) {
3
+ interface TagProps {
4
+ enabled: boolean,
5
+ tag: string,
6
+ toggleEnabled: (string) => void
7
+ }
8
+
9
+ export default function Tag(props: TagProps): JSX.Element {
5
10
  const handleChange = () => {
6
11
  props.toggleEnabled(props.tag);
7
12
  };
8
13
 
9
- let classes = ["tag"];
14
+ const classes = ["tag"];
10
15
  if (props.enabled) {
11
16
  classes.push("enabled");
12
17
  }
@@ -24,9 +29,3 @@ export default function Tag(props) {
24
29
  </span>
25
30
  );
26
31
  }
27
-
28
- Tag.propTypes = {
29
- enabled: PropTypes.bool,
30
- tag: PropTypes.string,
31
- toggleEnabled: PropTypes.func
32
- };
@@ -1,30 +1,35 @@
1
1
  import React, { useState } from "react";
2
- import PropTypes from "prop-types";
3
2
 
4
3
  import AddTagForm from "./TagEditor/AddTagForm";
5
4
  import Tag from "./TagEditor/Tag";
6
5
 
7
- function onlyUnique(value, index, self) {
6
+ interface TagEditorProps {
7
+ name: string,
8
+ enabled: string[],
9
+ tags: string[]
10
+ }
11
+
12
+ function onlyUnique(value: string, index: number, self: string[]): number {
8
13
  return self.indexOf(value) === index;
9
14
  }
10
15
 
11
- export default function TagEditor(props) {
16
+ export default function TagEditor(props: TagEditorProps) {
12
17
  const [tags, setTags] = useState(props.tags);
13
18
  const [enabled, setEnabled] = useState(props.enabled);
14
19
 
15
20
  const tagList = [...tags, ...enabled].filter(onlyUnique);
16
21
 
17
- const normalize = (tag) => {
22
+ const normalize = (tag: string): string => {
18
23
  return (
19
24
  tagList.filter(t => t.toLowerCase() == tag.toLowerCase())[0] || tag
20
25
  );
21
26
  };
22
27
 
23
- const tagEnabled = (tag) => {
28
+ const tagEnabled = (tag: string): boolean => {
24
29
  return enabled.map(t => t.toLowerCase()).indexOf(tag.toLowerCase()) !== -1;
25
30
  };
26
31
 
27
- const toggleEnabled = (tag) => {
32
+ const toggleEnabled = (tag: string) => {
28
33
  const normalized = normalize(tag);
29
34
 
30
35
  if (tagEnabled(normalized)) {
@@ -34,7 +39,7 @@ export default function TagEditor(props) {
34
39
  }
35
40
  };
36
41
 
37
- const addTag = (tag) => {
42
+ const addTag = (tag: string) => {
38
43
  const normalized = normalize(tag);
39
44
 
40
45
  setTags([...tags, normalized].filter(onlyUnique));
@@ -53,9 +58,3 @@ export default function TagEditor(props) {
53
58
  </div>
54
59
  );
55
60
  }
56
-
57
- TagEditor.propTypes = {
58
- name: PropTypes.string,
59
- enabled: PropTypes.array,
60
- tags: PropTypes.array
61
- };