pages_core 3.15.3 → 3.15.5
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 +1 -1
- data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
- data/app/assets/builds/pages_core/admin.css +378 -253
- data/app/assets/builds/pages_core/mailer.css +41 -6
- data/app/assets/builds/pages_core_fonts/121b837e.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/216e5c23.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/3017b52f.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/489746b9.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/49775483.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/49c9e472.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/4a119645.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/5d56d7a8.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/61ea75a6.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/62cbb778.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/647d26c.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/67764053.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/6bb0fd00.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/6c0194a2.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/71423409.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/7584e61d.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/77bcfa1c.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/7aca0cc5.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/9a09533f.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/a51f5bc8.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/a80b2975.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/a891f617.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/ad6083f3.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/b29a61ff.woff2 +0 -0
- data/app/assets/builds/{fonts/6569749d.ttf → pages_core_fonts/b30b0656.ttf} +0 -0
- data/app/assets/builds/pages_core_fonts/b3a5f48c.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/bc73ee06.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/c38c6d45.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/c5ce0b1f.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/c8d53904.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/ce13c169.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/d43bd0d5.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/e1c7d368.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/e1e8175d.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/e318f796.woff2 +0 -0
- data/app/assets/builds/{fonts/ee32bc60.ttf → pages_core_fonts/e7acb7d9.ttf} +0 -0
- data/app/assets/builds/pages_core_fonts/ee5514c6.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/f4e495e2.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/f736ec65.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/f741c7ba.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/f7767345.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/fe9eb751.woff2 +0 -0
- data/app/assets/stylesheets/pages_core/admin/components/forms.css +2 -2
- data/app/assets/stylesheets/pages_core/admin/components/header.css +1 -1
- data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +1 -1
- data/app/assets/stylesheets/pages_core/admin/global/fonts.css +38 -38
- data/app/controllers/{pages_core → admin}/admin_controller.rb +1 -3
- data/app/controllers/attachments_controller.rb +40 -0
- data/app/controllers/concerns/pages_core/document_title_controller.rb +16 -0
- data/app/controllers/concerns/pages_core/page_parameters.rb +1 -1
- data/app/controllers/concerns/pages_core/pages/preview_controller.rb +49 -0
- data/app/controllers/concerns/pages_core/pages/rss_controller.rb +43 -0
- data/app/controllers/errors_controller.rb +2 -0
- data/app/controllers/images_controller.rb +13 -0
- data/app/controllers/pages_core/frontend/pages_controller.rb +3 -4
- data/app/controllers/pages_core/frontend_controller.rb +6 -1
- data/app/controllers/pages_core/sitemaps_controller.rb +21 -52
- data/app/helpers/pages_core/admin/image_uploads_helper.rb +1 -1
- data/app/helpers/pages_core/application_helper.rb +0 -3
- data/app/helpers/pages_core/attachments_helper.rb +0 -10
- data/app/helpers/pages_core/feed_tags_helper.rb +31 -0
- data/app/helpers/pages_core/frontend_helper.rb +3 -0
- data/app/helpers/pages_core/head_tags_helper.rb +80 -70
- data/app/helpers/pages_core/page_path_helper.rb +1 -12
- data/app/javascript/components/Attachments/Attachment.tsx +3 -3
- data/app/javascript/components/Attachments/AttachmentEditor.tsx +5 -5
- data/app/javascript/components/Attachments/Deleted.tsx +28 -0
- data/app/javascript/components/Attachments/List.tsx +11 -24
- data/app/javascript/components/Attachments/Placeholder.tsx +0 -2
- data/app/javascript/components/Attachments.tsx +2 -3
- data/app/javascript/components/DateRangeSelect.tsx +13 -10
- data/app/javascript/components/DateTimeSelect.tsx +11 -11
- data/app/javascript/components/EditableImage.tsx +3 -3
- data/app/javascript/components/FileUploadButton.tsx +3 -3
- data/app/javascript/components/ImageCropper/FocalPoint.tsx +10 -14
- data/app/javascript/components/ImageCropper/Image.tsx +19 -25
- data/app/javascript/components/ImageCropper/Toolbar.tsx +27 -26
- data/app/javascript/components/ImageCropper/useContainerSize.ts +25 -0
- data/app/javascript/components/ImageCropper/useCrop.ts +28 -13
- data/app/javascript/components/ImageCropper/useImageCropperContext.ts +13 -0
- data/app/javascript/components/ImageCropper.tsx +24 -83
- data/app/javascript/components/ImageEditor/Form.tsx +25 -28
- data/app/javascript/components/ImageEditor/useImageEditor.ts +63 -0
- data/app/javascript/components/ImageEditor/useImageEditorContext.ts +14 -0
- data/app/javascript/components/ImageEditor.tsx +28 -42
- data/app/javascript/components/ImageGrid/Deleted.tsx +28 -0
- data/app/javascript/components/ImageGrid/DragElement.tsx +5 -5
- data/app/javascript/components/ImageGrid/FilePlaceholder.tsx +0 -2
- data/app/javascript/components/ImageGrid/Grid.tsx +15 -24
- data/app/javascript/components/ImageGrid/GridImage.tsx +4 -4
- data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -4
- data/app/javascript/components/ImageGrid.tsx +2 -4
- data/app/javascript/components/ImageUploader.tsx +5 -5
- data/app/javascript/components/LabelledField.tsx +6 -6
- data/app/javascript/components/Modal.tsx +16 -13
- data/app/javascript/components/PageForm/Block.tsx +3 -3
- data/app/javascript/components/PageForm/Content.tsx +11 -15
- data/app/javascript/components/PageForm/Dates.tsx +3 -11
- data/app/javascript/components/PageForm/Files.tsx +2 -4
- data/app/javascript/components/PageForm/Form.tsx +3 -9
- data/app/javascript/components/PageForm/Images.tsx +2 -4
- data/app/javascript/components/PageForm/LocaleLinks.tsx +4 -11
- data/app/javascript/components/PageForm/Metadata.tsx +8 -13
- data/app/javascript/components/PageForm/Options.tsx +28 -11
- data/app/javascript/components/PageForm/PageDescription.tsx +7 -14
- data/app/javascript/components/PageForm/PathSegment.tsx +5 -10
- data/app/javascript/components/PageForm/TabPanel.tsx +3 -6
- data/app/javascript/components/PageForm/Tabs.tsx +2 -4
- data/app/javascript/components/PageForm/UnconfiguredContent.tsx +7 -12
- data/app/javascript/components/PageForm/pageParams.ts +3 -2
- data/app/javascript/components/PageForm/usePage.ts +1 -46
- data/app/javascript/components/PageForm/usePageFormContext.ts +8 -0
- data/app/javascript/components/PageForm/useTabs.ts +1 -1
- data/app/javascript/components/PageForm/utils.ts +49 -0
- data/app/javascript/components/PageForm.tsx +52 -48
- data/app/javascript/components/PageImages.tsx +1 -3
- data/app/javascript/components/PageTree/Button.tsx +25 -0
- data/app/javascript/components/PageTree/CollapseArrow.tsx +34 -0
- data/app/javascript/components/PageTree/CollapsedLabel.tsx +21 -0
- data/app/javascript/components/PageTree/EditPageName.tsx +68 -0
- data/app/javascript/components/PageTree/Node.tsx +143 -413
- data/app/javascript/components/PageTree/PageName.tsx +6 -4
- data/app/javascript/components/PageTree/StatusLabel.tsx +10 -0
- data/app/javascript/components/PageTree/tree.ts +268 -0
- data/app/javascript/components/PageTree/usePageTree.ts +268 -0
- data/app/javascript/components/PageTree/usePageTreeContext.ts +13 -0
- data/app/javascript/components/PageTree.tsx +194 -214
- data/app/javascript/components/{RichTextToolbarButton.tsx → RichTextArea/ToolbarButton.tsx} +3 -5
- data/app/javascript/components/RichTextArea/actions.ts +106 -0
- data/app/javascript/components/RichTextArea/useMaybeControlledValue.ts +14 -0
- data/app/javascript/components/RichTextArea.tsx +91 -209
- data/app/javascript/components/TagEditor/AddTagForm.tsx +2 -2
- data/app/javascript/components/TagEditor/Editor.tsx +3 -5
- data/app/javascript/components/TagEditor/Tag.tsx +3 -5
- data/app/javascript/components/TagEditor/useTags.ts +7 -4
- data/app/javascript/components/TagEditor.tsx +2 -4
- data/app/javascript/components/Toast.tsx +5 -5
- data/app/javascript/components/drag/draggedOrder.ts +6 -6
- data/app/javascript/components/drag/useDragCollection.ts +21 -25
- data/app/javascript/components/drag/useDragUploader.ts +20 -18
- data/app/javascript/components/drag/useDraggable.ts +3 -3
- data/app/javascript/features/RichText.tsx +0 -1
- data/app/javascript/features/contentTabs.ts +2 -2
- data/app/javascript/stores/useModalStore.ts +1 -1
- data/app/javascript/stores/useToastStore.ts +2 -2
- data/app/javascript/types/Attachments.ts +11 -11
- data/app/javascript/types/Crop.ts +16 -12
- data/app/javascript/types/Drag.ts +21 -23
- data/app/javascript/types/Images.ts +8 -8
- data/app/javascript/types/PageEditor.ts +11 -4
- data/app/javascript/types/Pages.ts +22 -27
- data/app/javascript/types/Tags.ts +5 -6
- data/app/javascript/types/Template.ts +4 -4
- data/app/javascript/types.ts +2 -2
- data/app/models/attachment.rb +5 -9
- data/app/models/autopublisher.rb +1 -1
- data/app/models/concerns/pages_core/page_model/redirectable.rb +1 -2
- data/app/models/concerns/pages_core/page_model/searchable.rb +1 -1
- data/app/models/concerns/pages_core/page_model/status.rb +2 -4
- data/app/models/concerns/pages_core/searchable_document.rb +2 -4
- data/app/models/image.rb +0 -15
- data/app/models/page_builder.rb +4 -6
- data/app/resources/admin/page_resource.rb +2 -2
- data/app/resources/export/page_resource.rb +1 -1
- data/app/services/pages_core/invite_service.rb +1 -2
- data/app/views/layouts/admin.html.erb +1 -0
- data/app/views/pages_core/sitemaps/index.xml.builder +10 -0
- data/config/routes.rb +4 -3
- data/db/migrate/20240917142300_add_skip_index_to_pages.rb +7 -0
- data/lib/pages_core/engine.rb +15 -17
- data/lib/pages_core/sitemap.rb +58 -0
- data/lib/pages_core/templates/configuration_proxy.rb +3 -3
- data/lib/pages_core.rb +7 -4
- data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +2 -2
- data/lib/rails/generators/pages_core/frontend/templates/application.html.erb +13 -5
- data/lib/rails/generators/pages_core/frontend/templates/postcss.config.js +2 -6
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.css +3 -1
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.css +2 -3
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/animation.css +1 -1
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.css +6 -5
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/fonts.css +1 -1
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/grid.css +9 -6
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.css +42 -26
- data/lib/rails/generators/pages_core/install/templates/application_controller.rb +0 -6
- data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +1 -1
- metadata +81 -49
- data/app/assets/builds/fonts/7b7db107.woff2 +0 -0
- data/app/assets/builds/fonts/921961e9.woff2 +0 -0
- data/app/controller_dummies/admin/admin_controller.rb +0 -6
- data/app/controller_dummies/application_controller.rb +0 -6
- data/app/controller_dummies/attachments_controller.rb +0 -4
- data/app/controller_dummies/frontend_controller.rb +0 -4
- data/app/controller_dummies/images_controller.rb +0 -4
- data/app/controller_dummies/page_files_controller.rb +0 -4
- data/app/controller_dummies/pages_controller.rb +0 -4
- data/app/controller_dummies/sitemaps_controller.rb +0 -4
- data/app/controllers/concerns/pages_core/preview_pages_controller.rb +0 -47
- data/app/controllers/concerns/pages_core/rss_controller.rb +0 -41
- data/app/controllers/pages_core/attachments_controller.rb +0 -42
- data/app/controllers/pages_core/frontend/page_files_controller.rb +0 -25
- data/app/controllers/pages_core/images_controller.rb +0 -15
- data/app/helpers/pages_core/meta_tags_helper.rb +0 -96
- data/app/helpers/pages_core/open_graph_tags_helper.rb +0 -49
- data/app/javascript/components/PageTree/Draggable.tsx +0 -338
- data/app/javascript/lib/Tree.ts +0 -305
- data/app/javascript/types/Trees.ts +0 -19
- data/app/views/sitemaps/show.xml.builder +0 -11
@@ -0,0 +1,268 @@
|
|
1
|
+
export type Id = number;
|
2
|
+
export type Node<T> = {
|
3
|
+
id: Id;
|
4
|
+
collapsed: boolean;
|
5
|
+
record: T;
|
6
|
+
childNodes: Id[];
|
7
|
+
parent?: Id;
|
8
|
+
height?: number;
|
9
|
+
top?: number;
|
10
|
+
left?: number;
|
11
|
+
};
|
12
|
+
|
13
|
+
export type MovePlacement = "before" | "after" | "prepend" | "append";
|
14
|
+
|
15
|
+
type InitNodeFn<T> = (node: T) => { children: Array<T>; collapsed?: boolean };
|
16
|
+
type SortFn<T> = (a: Node<T>, b: Node<T>) => number;
|
17
|
+
type Index<T> = Record<Id, Node<T>>;
|
18
|
+
|
19
|
+
export type State<T> = {
|
20
|
+
rootId: Id;
|
21
|
+
nodes: Index<T>;
|
22
|
+
initNode: InitNodeFn<T>;
|
23
|
+
};
|
24
|
+
|
25
|
+
const uniqueId = (() => {
|
26
|
+
let id = 1;
|
27
|
+
return (): Id => {
|
28
|
+
return id++;
|
29
|
+
};
|
30
|
+
})();
|
31
|
+
|
32
|
+
export function getNodeByTop<T>(state: State<T>, top: number): Node<T> | null {
|
33
|
+
return Object.values(state.nodes).find((n) => n.top === top) || null;
|
34
|
+
}
|
35
|
+
|
36
|
+
export function sibling<T>(state: State<T>, id: Id, offset: number) {
|
37
|
+
const parent = state.nodes[state.nodes[id].parent];
|
38
|
+
if (!parent) {
|
39
|
+
return null;
|
40
|
+
}
|
41
|
+
const index = parent.childNodes.indexOf(id) + offset;
|
42
|
+
if (index >= 0 && index < parent.childNodes.length) {
|
43
|
+
return state.nodes[parent.childNodes[index]];
|
44
|
+
}
|
45
|
+
return null;
|
46
|
+
}
|
47
|
+
|
48
|
+
export function parents<T>(state: State<T>, id: Id) {
|
49
|
+
const node = state.nodes[id];
|
50
|
+
if (node.parent) {
|
51
|
+
return [...parents(state, node.parent), node.parent];
|
52
|
+
} else {
|
53
|
+
return [];
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
export function nextSibling<T>(state: State<T>, id: Id) {
|
58
|
+
return sibling(state, id, 1);
|
59
|
+
}
|
60
|
+
|
61
|
+
export function prevSibling<T>(state: State<T>, id: Id) {
|
62
|
+
return sibling(state, id, -1);
|
63
|
+
}
|
64
|
+
|
65
|
+
export function sortChildNodes<T>(state: State<T>, id: Id, sortFn: SortFn<T>) {
|
66
|
+
return updateNode(state, id, {
|
67
|
+
childNodes: state.nodes[id].childNodes
|
68
|
+
.map((i) => state.nodes[i])
|
69
|
+
.sort(sortFn)
|
70
|
+
.map((n) => n.id)
|
71
|
+
});
|
72
|
+
}
|
73
|
+
|
74
|
+
export function move<T>(
|
75
|
+
prevState: State<T>,
|
76
|
+
id: Id,
|
77
|
+
target: Id,
|
78
|
+
position: number
|
79
|
+
) {
|
80
|
+
if (id === target) {
|
81
|
+
return prevState;
|
82
|
+
}
|
83
|
+
const node = prevState.nodes[id];
|
84
|
+
const state = removeNode(prevState, id);
|
85
|
+
return insertNode(state, target, node, position);
|
86
|
+
}
|
87
|
+
|
88
|
+
export function moveRelative<T>(
|
89
|
+
prevState: State<T>,
|
90
|
+
id: Id,
|
91
|
+
target: Id,
|
92
|
+
placement: MovePlacement
|
93
|
+
): State<T> {
|
94
|
+
if (id === target) {
|
95
|
+
return prevState;
|
96
|
+
}
|
97
|
+
const node = prevState.nodes[id];
|
98
|
+
const state = removeNode(prevState, id);
|
99
|
+
|
100
|
+
switch (placement) {
|
101
|
+
case "before":
|
102
|
+
return insertAdjacentNode(state, target, node);
|
103
|
+
case "after":
|
104
|
+
return insertAdjacentNode(state, target, node, 1);
|
105
|
+
case "prepend":
|
106
|
+
return insertNode(state, target, node);
|
107
|
+
case "append":
|
108
|
+
return insertNode(state, target, node, -1);
|
109
|
+
default:
|
110
|
+
return state;
|
111
|
+
}
|
112
|
+
}
|
113
|
+
|
114
|
+
export function insertAdjacent<T>(
|
115
|
+
state: State<T>,
|
116
|
+
sibling: Id,
|
117
|
+
record: T,
|
118
|
+
offset: number = 0
|
119
|
+
): State<T> {
|
120
|
+
const target = state.nodes[sibling];
|
121
|
+
const index = state.nodes[target.parent].childNodes.indexOf(target.id);
|
122
|
+
return insert(state, target.parent, record, index + offset);
|
123
|
+
}
|
124
|
+
|
125
|
+
function insertAdjacentNode<T>(
|
126
|
+
state: State<T>,
|
127
|
+
sibling: Id,
|
128
|
+
node: Node<T>,
|
129
|
+
offset: number = 0
|
130
|
+
): State<T> {
|
131
|
+
const target = state.nodes[sibling];
|
132
|
+
const index = state.nodes[target.parent].childNodes.indexOf(target.id);
|
133
|
+
return insertNode(state, target.parent, node, index + offset);
|
134
|
+
}
|
135
|
+
|
136
|
+
function insertNode<T>(
|
137
|
+
state: State<T>,
|
138
|
+
parent: Id,
|
139
|
+
node: Node<T>,
|
140
|
+
position: number = 0
|
141
|
+
) {
|
142
|
+
const parentNode = state.nodes[parent];
|
143
|
+
const childNodes = [...parentNode.childNodes];
|
144
|
+
if (position < 0) {
|
145
|
+
position += childNodes.length + 1;
|
146
|
+
}
|
147
|
+
childNodes.splice(position, 0, node.id);
|
148
|
+
|
149
|
+
return updateNode(updateNode(state, node.id, { parent: parent }), parent, {
|
150
|
+
childNodes: childNodes
|
151
|
+
});
|
152
|
+
}
|
153
|
+
|
154
|
+
export function insert<T>(
|
155
|
+
state: State<T>,
|
156
|
+
parent: Id,
|
157
|
+
record: T,
|
158
|
+
position: number = 0
|
159
|
+
): State<T> {
|
160
|
+
const [node, newNodes] = makeNode(record, state.initNode, parent);
|
161
|
+
const nextState = { ...state, nodes: { ...state.nodes, ...newNodes } };
|
162
|
+
return insertNode(nextState, parent, node, position);
|
163
|
+
}
|
164
|
+
|
165
|
+
function makeNode<T>(
|
166
|
+
record: T,
|
167
|
+
initNode: InitNodeFn<T>,
|
168
|
+
parent?: Id
|
169
|
+
): [Node<T>, Index<T>] {
|
170
|
+
const id = uniqueId();
|
171
|
+
const childNodes = [];
|
172
|
+
let index = {};
|
173
|
+
|
174
|
+
const { children, collapsed } = initNode(record);
|
175
|
+
|
176
|
+
if (children) {
|
177
|
+
children.forEach((r) => {
|
178
|
+
const [childNode, childIndex] = makeNode(r, initNode, id);
|
179
|
+
childNodes.push(childNode.id);
|
180
|
+
index = { ...index, ...childIndex };
|
181
|
+
});
|
182
|
+
}
|
183
|
+
|
184
|
+
const node = {
|
185
|
+
id: id,
|
186
|
+
collapsed: collapsed || false,
|
187
|
+
record: record,
|
188
|
+
childNodes: childNodes,
|
189
|
+
parent: parent
|
190
|
+
};
|
191
|
+
|
192
|
+
return [node, { ...index, [node.id]: node }];
|
193
|
+
}
|
194
|
+
|
195
|
+
function removeNode<T>(state: State<T>, id: Id) {
|
196
|
+
const node = state.nodes[id];
|
197
|
+
if (node.parent) {
|
198
|
+
const childNodes = [...state.nodes[node.parent].childNodes];
|
199
|
+
childNodes.splice(childNodes.indexOf(id), 1);
|
200
|
+
return updateNode(state, node.parent, { childNodes: childNodes });
|
201
|
+
} else {
|
202
|
+
return state;
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
export function remove<T>(prevState: State<T>, id: Id) {
|
207
|
+
const state = removeNode(prevState, id);
|
208
|
+
|
209
|
+
const removeIndex = (id: Id) => {
|
210
|
+
state.nodes[id].childNodes.forEach((c) => removeIndex(c));
|
211
|
+
delete state.nodes[id];
|
212
|
+
};
|
213
|
+
removeIndex(id);
|
214
|
+
|
215
|
+
return state;
|
216
|
+
}
|
217
|
+
|
218
|
+
export function update<T>(state: State<T>, id: Id, updated: Partial<T>) {
|
219
|
+
const record = { ...state.nodes[id].record, ...updated };
|
220
|
+
return updateNode(state, id, { record: record });
|
221
|
+
}
|
222
|
+
|
223
|
+
export function updateNode<T>(
|
224
|
+
state: State<T>,
|
225
|
+
id: Id,
|
226
|
+
updated: Partial<Node<T>>
|
227
|
+
) {
|
228
|
+
const node = state.nodes[id];
|
229
|
+
return { ...state, nodes: { ...state.nodes, [id]: { ...node, ...updated } } };
|
230
|
+
}
|
231
|
+
|
232
|
+
export function indexPositions<T>(prevState: State<T>): State<T> {
|
233
|
+
let top = 1;
|
234
|
+
let state = { ...prevState };
|
235
|
+
const walk = (id: Id, left: number = 1, parentCollapsed?: boolean) => {
|
236
|
+
const position = { height: 1, top: null, left: null };
|
237
|
+
const node = state.nodes[id];
|
238
|
+
|
239
|
+
if (!parentCollapsed) {
|
240
|
+
position.top = top++;
|
241
|
+
position.left = left;
|
242
|
+
}
|
243
|
+
|
244
|
+
node.childNodes.forEach((i) => {
|
245
|
+
position.height += walk(i, left + 1, parentCollapsed || node.collapsed);
|
246
|
+
});
|
247
|
+
|
248
|
+
if (node.collapsed) {
|
249
|
+
position.height = 1;
|
250
|
+
}
|
251
|
+
|
252
|
+
state = updateNode(state, id, position);
|
253
|
+
return position.height;
|
254
|
+
};
|
255
|
+
|
256
|
+
walk(state.rootId);
|
257
|
+
|
258
|
+
return state;
|
259
|
+
}
|
260
|
+
|
261
|
+
export function build<T>(root: T, initNode: InitNodeFn<T>): State<T> {
|
262
|
+
const [rootNode, nodes] = makeNode(root, initNode);
|
263
|
+
return {
|
264
|
+
rootId: rootNode.id,
|
265
|
+
initNode: initNode,
|
266
|
+
nodes: nodes
|
267
|
+
};
|
268
|
+
}
|
@@ -0,0 +1,268 @@
|
|
1
|
+
import { useMemo, useReducer } from "react";
|
2
|
+
|
3
|
+
import * as Pages from "../../types/Pages";
|
4
|
+
import { postJson, putJson } from "../../lib/request";
|
5
|
+
import * as Tree from "./tree";
|
6
|
+
|
7
|
+
type CollapsedState = Record<number, boolean>;
|
8
|
+
type RootRecord = {
|
9
|
+
blocks: Pages.Blocks;
|
10
|
+
permissions: string[];
|
11
|
+
root: true;
|
12
|
+
editing: false;
|
13
|
+
};
|
14
|
+
type PageRecord = Pages.TreeResource;
|
15
|
+
type TreeRecord = RootRecord | PageRecord;
|
16
|
+
|
17
|
+
export type State = Tree.State<TreeRecord> & {
|
18
|
+
locale: string;
|
19
|
+
dir: string;
|
20
|
+
};
|
21
|
+
|
22
|
+
export type Action =
|
23
|
+
| {
|
24
|
+
type: "move";
|
25
|
+
id: Tree.Id;
|
26
|
+
payload: { parent: Tree.Id; position: number };
|
27
|
+
}
|
28
|
+
| { type: "append" | "addChild"; id: Tree.Id; payload: TreeRecord }
|
29
|
+
| { type: "remove" | "sortNewsPage"; id: Tree.Id }
|
30
|
+
| { type: "setCollapsed"; id: Tree.Id; payload: boolean }
|
31
|
+
| { type: "update"; id: Tree.Id; payload: Partial<TreeRecord> };
|
32
|
+
|
33
|
+
const permittedAttributes = [
|
34
|
+
"status",
|
35
|
+
"news_page",
|
36
|
+
"published_at",
|
37
|
+
"pinned",
|
38
|
+
"parent_page_id"
|
39
|
+
];
|
40
|
+
|
41
|
+
export function movePage(
|
42
|
+
prevState: State,
|
43
|
+
id: Tree.Id,
|
44
|
+
dispatch: React.Dispatch<Action>
|
45
|
+
) {
|
46
|
+
const state = sortNewsPage(prevState, prevState.nodes[id].parent);
|
47
|
+
const node = state.nodes[id];
|
48
|
+
const parentNode = state.nodes[node.parent];
|
49
|
+
const position = parentNode.childNodes.indexOf(id);
|
50
|
+
|
51
|
+
if ("id" in node.record && node.record.id) {
|
52
|
+
const data = {
|
53
|
+
parent_id: !("root" in parentNode.record) && parentNode.record.id,
|
54
|
+
position: position + 1
|
55
|
+
};
|
56
|
+
|
57
|
+
dispatch({
|
58
|
+
type: "move",
|
59
|
+
id: id,
|
60
|
+
payload: { parent: parentNode.id, position: position }
|
61
|
+
});
|
62
|
+
|
63
|
+
putJson(
|
64
|
+
`/admin/${state.locale}/pages/${node.record.id}/move.json`,
|
65
|
+
data
|
66
|
+
).then((response) => {
|
67
|
+
dispatch({ type: "update", id: id, payload: response });
|
68
|
+
});
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
export function updatePage(
|
73
|
+
state: State,
|
74
|
+
id: Tree.Id,
|
75
|
+
dispatch: React.Dispatch<Action>,
|
76
|
+
attributes: Partial<PageRecord>
|
77
|
+
) {
|
78
|
+
const node = state.nodes[id];
|
79
|
+
const page = node.record;
|
80
|
+
const updateState = (updated: Partial<PageRecord>) => {
|
81
|
+
dispatch({
|
82
|
+
type: "update",
|
83
|
+
id: id,
|
84
|
+
payload: { ...attributes, ...updated }
|
85
|
+
});
|
86
|
+
};
|
87
|
+
|
88
|
+
let data = {};
|
89
|
+
if (attributes.blocks) {
|
90
|
+
data = { ...attributes.blocks };
|
91
|
+
}
|
92
|
+
permittedAttributes.forEach((a) => {
|
93
|
+
if (Object.prototype.hasOwnProperty.call(attributes, a)) {
|
94
|
+
data[a] = attributes[a];
|
95
|
+
}
|
96
|
+
});
|
97
|
+
|
98
|
+
if ("id" in page) {
|
99
|
+
putJson(`/admin/${state.locale}/pages/${page.id}.json`, {
|
100
|
+
page: data
|
101
|
+
}).then(updateState);
|
102
|
+
} else {
|
103
|
+
const parent = state.nodes[node.parent];
|
104
|
+
if (parent && "id" in parent.record) {
|
105
|
+
data = { parent_page_id: parent.record.id, ...data };
|
106
|
+
}
|
107
|
+
postJson(`/admin/${state.locale}/pages.json`, { page: data }).then(
|
108
|
+
updateState
|
109
|
+
);
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
export function visibleChildNodes(state: State, id: Tree.Id) {
|
114
|
+
return state.nodes[id].childNodes
|
115
|
+
.map((i) => state.nodes[i])
|
116
|
+
.filter((n) => "status" in n.record && n.record.status !== 4)
|
117
|
+
.map((n) => n.id);
|
118
|
+
}
|
119
|
+
|
120
|
+
export function addChild(
|
121
|
+
state: State,
|
122
|
+
id: Tree.Id,
|
123
|
+
dispatch: React.Dispatch<Action>
|
124
|
+
) {
|
125
|
+
const parentNode = state.nodes[id];
|
126
|
+
|
127
|
+
const record: PageRecord = {
|
128
|
+
blocks: { name: { [state.locale]: "" } },
|
129
|
+
status: 0,
|
130
|
+
editing: true,
|
131
|
+
news_page: false,
|
132
|
+
published_at: new Date(),
|
133
|
+
pinned: false,
|
134
|
+
parent_page_id: "id" in parentNode.record && parentNode.record.id,
|
135
|
+
permissions: parentNode.record.permissions
|
136
|
+
};
|
137
|
+
|
138
|
+
dispatch({ type: "addChild", id: id, payload: record });
|
139
|
+
}
|
140
|
+
|
141
|
+
function setCollapsed(state: State, id: Tree.Id, value: boolean): State {
|
142
|
+
const node = state.nodes[id];
|
143
|
+
if ("id" in node.record) {
|
144
|
+
storeCollapsed(node.record.id, value);
|
145
|
+
}
|
146
|
+
return { ...state, ...Tree.updateNode(state, id, { collapsed: value }) };
|
147
|
+
}
|
148
|
+
|
149
|
+
function sortNewsPage(state: State, id: Tree.Id) {
|
150
|
+
const record = state.nodes[id].record;
|
151
|
+
if ("news_page" in record && record.news_page) {
|
152
|
+
return { ...state, ...Tree.sortChildNodes(state, id, sortChildren) };
|
153
|
+
} else {
|
154
|
+
return state;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
function reducer(state: State, action: Action): State {
|
159
|
+
const { id, type } = action;
|
160
|
+
|
161
|
+
const chain = (operations: Array<Partial<Action>>) => {
|
162
|
+
return operations.reduce((s, o) => {
|
163
|
+
return reducer(s, { id: id, ...o } as Action);
|
164
|
+
}, state);
|
165
|
+
};
|
166
|
+
|
167
|
+
switch (type) {
|
168
|
+
case "addChild": {
|
169
|
+
return chain([
|
170
|
+
{ type: "setCollapsed", payload: false },
|
171
|
+
{ type: "append", payload: action.payload },
|
172
|
+
{ type: "sortNewsPage" }
|
173
|
+
]);
|
174
|
+
}
|
175
|
+
case "append":
|
176
|
+
return { ...state, ...Tree.insert(state, id, action.payload, -1) };
|
177
|
+
case "move":
|
178
|
+
return {
|
179
|
+
...state,
|
180
|
+
...Tree.move(state, id, action.payload.parent, action.payload.position)
|
181
|
+
};
|
182
|
+
case "remove":
|
183
|
+
return { ...state, ...Tree.remove(state, id) };
|
184
|
+
case "setCollapsed":
|
185
|
+
return { ...state, ...setCollapsed(state, id, action.payload) };
|
186
|
+
case "sortNewsPage":
|
187
|
+
return { ...state, ...sortNewsPage(state, id) };
|
188
|
+
case "update":
|
189
|
+
return { ...state, ...Tree.update(state, id, action.payload) };
|
190
|
+
default:
|
191
|
+
return state;
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
function collapsedState(): CollapsedState {
|
196
|
+
return JSON.parse(window?.localStorage?.getItem("collapsedPages") || "{}");
|
197
|
+
}
|
198
|
+
|
199
|
+
function storeCollapsed(id: number, value: boolean) {
|
200
|
+
const state = { ...collapsedState(), [id]: value };
|
201
|
+
window.localStorage.setItem("collapsedPages", JSON.stringify(state));
|
202
|
+
}
|
203
|
+
|
204
|
+
function sortChildren(a: Tree.Node<PageRecord>, b: Tree.Node<PageRecord>) {
|
205
|
+
if (a.record.pinned == b.record.pinned) {
|
206
|
+
return (
|
207
|
+
new Date(b.record.published_at).getTime() -
|
208
|
+
new Date(a.record.published_at).getTime()
|
209
|
+
);
|
210
|
+
} else {
|
211
|
+
return a.record.pinned ? -1 : 1;
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
function indexingReducer(state: State, action: Action): State {
|
216
|
+
return { ...state, ...Tree.indexPositions(reducer(state, action)) };
|
217
|
+
}
|
218
|
+
|
219
|
+
export default function usePageTree(
|
220
|
+
pages: PageRecord[],
|
221
|
+
locale: string,
|
222
|
+
dir: string,
|
223
|
+
permissions: string[]
|
224
|
+
): [State, React.Dispatch<Action>] {
|
225
|
+
const root: RootRecord = {
|
226
|
+
blocks: { name: { [locale]: "All Pages" } },
|
227
|
+
permissions: permissions,
|
228
|
+
root: true,
|
229
|
+
editing: false
|
230
|
+
};
|
231
|
+
|
232
|
+
const parentMap = useMemo(() => {
|
233
|
+
return pages.reduce((m, p) => {
|
234
|
+
const id = p.parent_page_id || 0;
|
235
|
+
m[id] = [...(m[id] || []), p];
|
236
|
+
return m;
|
237
|
+
}, {});
|
238
|
+
}, [pages]);
|
239
|
+
|
240
|
+
const isCollapsed = (page: PageRecord) => {
|
241
|
+
const state = collapsedState();
|
242
|
+
if (page.id && page.id in state) {
|
243
|
+
return state[page.id];
|
244
|
+
} else if (page.news_page || page.parent_page_id) {
|
245
|
+
return true;
|
246
|
+
}
|
247
|
+
return false;
|
248
|
+
};
|
249
|
+
|
250
|
+
const initNode = (page: TreeRecord) => {
|
251
|
+
if ("root" in page) {
|
252
|
+
return { children: parentMap[0], collapsed: false };
|
253
|
+
} else if (page.id) {
|
254
|
+
return { children: parentMap[page.id], collapsed: isCollapsed(page) };
|
255
|
+
} else {
|
256
|
+
return { children: [], collapsed: false };
|
257
|
+
}
|
258
|
+
};
|
259
|
+
|
260
|
+
const [state, dispatch] = useReducer(indexingReducer, {}, () => {
|
261
|
+
return {
|
262
|
+
...Tree.indexPositions(Tree.build(root, initNode)),
|
263
|
+
dir: dir,
|
264
|
+
locale: locale
|
265
|
+
};
|
266
|
+
});
|
267
|
+
return [state, dispatch];
|
268
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { createContext, useContext } from "react";
|
2
|
+
import { State, Action } from "./usePageTree";
|
3
|
+
|
4
|
+
type Context = {
|
5
|
+
state: State;
|
6
|
+
dispatch: React.Dispatch<Action>;
|
7
|
+
};
|
8
|
+
|
9
|
+
export const PageTreeContext = createContext<Context>(null);
|
10
|
+
|
11
|
+
export default function usePageTreeContext() {
|
12
|
+
return useContext(PageTreeContext);
|
13
|
+
}
|