pages_core 3.12.1 → 3.12.2
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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/assets/builds/pages_core/admin-dist.js +59 -14
- data/app/assets/builds/pages_core/admin-dist.js.map +7 -0
- data/app/assets/builds/pages_core/admin.css +39 -0
- data/app/assets/stylesheets/pages_core/admin/components/search.css +27 -0
- data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +6 -0
- data/app/controllers/admin/pages_controller.rb +12 -11
- data/app/controllers/concerns/pages_core/rss_controller.rb +17 -1
- data/app/controllers/pages_core/admin_controller.rb +6 -0
- data/app/controllers/pages_core/frontend/pages_controller.rb +9 -5
- data/app/controllers/pages_core/sitemaps_controller.rb +3 -5
- data/app/helpers/admin/pages_helper.rb +32 -0
- data/app/javascript/admin-dist.ts +2 -0
- data/app/javascript/components/Attachments/{Attachment.jsx → Attachment.tsx} +42 -33
- data/app/javascript/components/Attachments/{AttachmentEditor.jsx → AttachmentEditor.tsx} +23 -23
- data/app/javascript/components/{EditableImage.jsx → EditableImage.tsx} +27 -24
- data/app/javascript/components/{FileUploadButton.jsx → FileUploadButton.tsx} +15 -16
- data/app/javascript/components/ImageCropper/FocalPoint.tsx +94 -0
- data/app/javascript/components/ImageCropper/{Image.jsx → Image.tsx} +13 -14
- data/app/javascript/components/ImageCropper/{Toolbar.jsx → Toolbar.tsx} +16 -12
- data/app/javascript/components/ImageCropper/{useCrop.js → useCrop.ts} +80 -37
- data/app/javascript/components/{ImageCropper.jsx → ImageCropper.tsx} +17 -15
- data/app/javascript/components/ImageEditor/{Form.jsx → Form.tsx} +24 -23
- data/app/javascript/components/{ImageEditor.jsx → ImageEditor.tsx} +17 -15
- data/app/javascript/components/ImageGrid/{DragElement.jsx → DragElement.tsx} +12 -10
- data/app/javascript/components/ImageGrid/{GridImage.jsx → GridImage.tsx} +40 -30
- data/app/javascript/components/ImageGrid/{Placeholder.jsx → Placeholder.tsx} +5 -6
- data/app/javascript/components/ImageGrid.jsx +3 -4
- data/app/javascript/components/{ImageUploader.jsx → ImageUploader.tsx} +46 -41
- data/app/javascript/components/Modal.tsx +48 -0
- data/app/javascript/components/PageImages.tsx +28 -0
- data/app/javascript/components/{PageTreeDraggable.jsx → PageTree/Draggable.tsx} +79 -57
- data/app/javascript/components/{PageTreeNode.jsx → PageTree/Node.tsx} +79 -70
- data/app/javascript/components/PageTree/types.ts +15 -0
- data/app/javascript/components/PageTree.tsx +206 -0
- data/app/javascript/components/RichTextToolbarButton.tsx +17 -0
- data/app/javascript/components/TagEditor/{AddTagForm.jsx → AddTagForm.tsx} +9 -10
- data/app/javascript/components/TagEditor/{Tag.jsx → Tag.tsx} +8 -9
- data/app/javascript/components/{TagEditor.jsx → TagEditor.tsx} +12 -13
- data/app/javascript/components/Toast.tsx +61 -0
- data/app/javascript/components/drag/{draggedOrder.js → draggedOrder.ts} +22 -12
- data/app/javascript/components/drag/types.ts +28 -0
- data/app/javascript/components/drag/{useDragCollection.js → useDragCollection.ts} +40 -22
- data/app/javascript/components/drag/{useDragUploader.js → useDragUploader.ts} +34 -25
- data/app/javascript/components/drag/useDraggable.ts +21 -0
- data/app/javascript/components/{drag.js → drag.ts} +1 -0
- data/app/javascript/controllers/{EditPageController.js → EditPageController.ts} +3 -1
- data/app/javascript/controllers/{LoginController.js → LoginController.ts} +7 -3
- data/app/javascript/controllers/{MainController.js → MainController.ts} +19 -14
- data/app/javascript/{index.js → index.ts} +8 -7
- data/app/javascript/lib/{Tree.js → Tree.ts} +106 -85
- data/app/javascript/lib/{copyToClipboard.js → copyToClipboard.ts} +1 -1
- data/app/javascript/lib/{readyHandler.js → readyHandler.ts} +4 -2
- data/app/javascript/lib/{request.js → request.ts} +11 -5
- data/app/javascript/stores/useModalStore.ts +15 -0
- data/app/javascript/stores/useToastStore.ts +26 -0
- data/app/javascript/stores.ts +2 -0
- data/app/javascript/types.ts +30 -0
- data/app/policies/page_policy.rb +4 -0
- data/app/views/admin/calendars/_sidebar.html.erb +3 -0
- data/app/views/admin/news/_sidebar.html.erb +3 -0
- data/app/views/admin/pages/_list_item.html.erb +4 -22
- data/app/views/admin/pages/_search_bar.html.erb +12 -0
- data/app/views/admin/pages/index.html.erb +3 -0
- data/app/views/admin/pages/search.html.erb +54 -0
- data/app/views/feeds/pages.rss.builder +3 -9
- data/config/routes.rb +1 -0
- data/lib/pages_core/configuration/pages.rb +0 -1
- data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +33 -17
- data/lib/rails/generators/pages_core/frontend/templates/application.html.erb +0 -1
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +40 -0
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +68 -0
- data/lib/rails/generators/pages_core/frontend/templates/postcss.config.js +17 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/application.postcss.css +4 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.css +24 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/layout.css +21 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.css +5 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/animation.css +5 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.css +18 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/fonts.css +6 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/grid.css +65 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.css +131 -0
- data/lib/rails/generators/pages_core/install/templates/pages_initializer.rb +0 -3
- metadata +68 -62
- data/app/javascript/admin-dist.js +0 -2
- data/app/javascript/components/ImageCropper/FocalPoint.jsx +0 -93
- data/app/javascript/components/Modal.jsx +0 -59
- data/app/javascript/components/PageImages.jsx +0 -25
- data/app/javascript/components/PageTree.jsx +0 -196
- data/app/javascript/components/RichTextToolbarButton.jsx +0 -20
- data/app/javascript/components/Toast.jsx +0 -72
- data/app/javascript/components/drag/useDraggable.js +0 -17
- data/app/javascript/stores/ModalStore.jsx +0 -12
- data/app/javascript/stores/ToastStore.jsx +0 -14
- data/app/javascript/stores.js +0 -2
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/GridOverlay.js +0 -66
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/ResponsiveEmbeds.js +0 -72
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/application.sass.scss +0 -15
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.scss +0 -12
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.scss +0 -26
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/breakpoints.scss +0 -42
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/clearfix.scss +0 -7
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/fonts.scss +0 -32
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/grid.scss +0 -168
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/grid_overlay.scss +0 -44
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.scss +0 -8
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.scss +0 -90
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/vendor/normalize.css +0 -349
- /data/app/javascript/components/Attachments/{Placeholder.jsx → Placeholder.tsx} +0 -0
- /data/app/javascript/components/ImageGrid/{FilePlaceholder.jsx → FilePlaceholder.tsx} +0 -0
- /data/app/javascript/{components.js → components.ts} +0 -0
- /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
|
|
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
|
-
|
|
32
|
-
|
|
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 =
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
const childrenStyles = {};
|
|
132
157
|
if (index.node.collapsed) {
|
|
133
158
|
childrenStyles.display = "none";
|
|
134
159
|
}
|
|
135
|
-
childrenStyles["paddingLeft"] = this.props.paddingLeft
|
|
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
|
-
|
|
165
|
+
const childIndex = tree.getIndex(child);
|
|
141
166
|
return (
|
|
142
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
196
|
+
const handleCollapse = (e: Event) => {
|
|
173
197
|
e.stopPropagation();
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
if (
|
|
260
|
-
props.onDragStart(props.index.id,
|
|
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
|
-
|
|
287
|
-
|
|
308
|
+
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
309
|
+
this.setState({ newName: event.target.value });
|
|
288
310
|
};
|
|
289
311
|
|
|
290
|
-
|
|
312
|
+
const performEdit = (event: Event) => {
|
|
291
313
|
event.preventDefault();
|
|
292
|
-
|
|
293
|
-
name:
|
|
314
|
+
this.updatePage({
|
|
315
|
+
name: this.state.newName,
|
|
294
316
|
editing: false
|
|
295
317
|
});
|
|
296
318
|
};
|
|
297
319
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
329
|
-
|
|
350
|
+
const index = this.props.index;
|
|
351
|
+
const node = index.node;
|
|
330
352
|
|
|
331
|
-
|
|
332
|
-
|
|
353
|
+
let pageName = <span className="name">{this.pageName()}</span>;
|
|
354
|
+
let className = "page";
|
|
333
355
|
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
};
|