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.
Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/assets/builds/pages_core/admin-dist.js +1 -1
  4. data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
  5. data/app/assets/builds/pages_core/admin.css +378 -253
  6. data/app/assets/builds/pages_core/mailer.css +41 -6
  7. data/app/assets/builds/pages_core_fonts/121b837e.woff2 +0 -0
  8. data/app/assets/builds/pages_core_fonts/216e5c23.woff2 +0 -0
  9. data/app/assets/builds/pages_core_fonts/3017b52f.woff2 +0 -0
  10. data/app/assets/builds/pages_core_fonts/489746b9.woff2 +0 -0
  11. data/app/assets/builds/pages_core_fonts/49775483.woff2 +0 -0
  12. data/app/assets/builds/pages_core_fonts/49c9e472.woff2 +0 -0
  13. data/app/assets/builds/pages_core_fonts/4a119645.woff2 +0 -0
  14. data/app/assets/builds/pages_core_fonts/5d56d7a8.woff2 +0 -0
  15. data/app/assets/builds/pages_core_fonts/61ea75a6.woff2 +0 -0
  16. data/app/assets/builds/pages_core_fonts/62cbb778.woff2 +0 -0
  17. data/app/assets/builds/pages_core_fonts/647d26c.woff2 +0 -0
  18. data/app/assets/builds/pages_core_fonts/67764053.woff2 +0 -0
  19. data/app/assets/builds/pages_core_fonts/6bb0fd00.woff2 +0 -0
  20. data/app/assets/builds/pages_core_fonts/6c0194a2.woff2 +0 -0
  21. data/app/assets/builds/pages_core_fonts/71423409.woff2 +0 -0
  22. data/app/assets/builds/pages_core_fonts/7584e61d.woff2 +0 -0
  23. data/app/assets/builds/pages_core_fonts/77bcfa1c.woff2 +0 -0
  24. data/app/assets/builds/pages_core_fonts/7aca0cc5.woff2 +0 -0
  25. data/app/assets/builds/pages_core_fonts/9a09533f.woff2 +0 -0
  26. data/app/assets/builds/pages_core_fonts/a51f5bc8.woff2 +0 -0
  27. data/app/assets/builds/pages_core_fonts/a80b2975.woff2 +0 -0
  28. data/app/assets/builds/pages_core_fonts/a891f617.woff2 +0 -0
  29. data/app/assets/builds/pages_core_fonts/ad6083f3.woff2 +0 -0
  30. data/app/assets/builds/pages_core_fonts/b29a61ff.woff2 +0 -0
  31. data/app/assets/builds/{fonts/6569749d.ttf → pages_core_fonts/b30b0656.ttf} +0 -0
  32. data/app/assets/builds/pages_core_fonts/b3a5f48c.woff2 +0 -0
  33. data/app/assets/builds/pages_core_fonts/bc73ee06.woff2 +0 -0
  34. data/app/assets/builds/pages_core_fonts/c38c6d45.woff2 +0 -0
  35. data/app/assets/builds/pages_core_fonts/c5ce0b1f.woff2 +0 -0
  36. data/app/assets/builds/pages_core_fonts/c8d53904.woff2 +0 -0
  37. data/app/assets/builds/pages_core_fonts/ce13c169.woff2 +0 -0
  38. data/app/assets/builds/pages_core_fonts/d43bd0d5.woff2 +0 -0
  39. data/app/assets/builds/pages_core_fonts/e1c7d368.woff2 +0 -0
  40. data/app/assets/builds/pages_core_fonts/e1e8175d.woff2 +0 -0
  41. data/app/assets/builds/pages_core_fonts/e318f796.woff2 +0 -0
  42. data/app/assets/builds/{fonts/ee32bc60.ttf → pages_core_fonts/e7acb7d9.ttf} +0 -0
  43. data/app/assets/builds/pages_core_fonts/ee5514c6.woff2 +0 -0
  44. data/app/assets/builds/pages_core_fonts/f4e495e2.woff2 +0 -0
  45. data/app/assets/builds/pages_core_fonts/f736ec65.woff2 +0 -0
  46. data/app/assets/builds/pages_core_fonts/f741c7ba.woff2 +0 -0
  47. data/app/assets/builds/pages_core_fonts/f7767345.woff2 +0 -0
  48. data/app/assets/builds/pages_core_fonts/fe9eb751.woff2 +0 -0
  49. data/app/assets/stylesheets/pages_core/admin/components/forms.css +2 -2
  50. data/app/assets/stylesheets/pages_core/admin/components/header.css +1 -1
  51. data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +1 -1
  52. data/app/assets/stylesheets/pages_core/admin/global/fonts.css +38 -38
  53. data/app/controllers/{pages_core → admin}/admin_controller.rb +1 -3
  54. data/app/controllers/attachments_controller.rb +40 -0
  55. data/app/controllers/concerns/pages_core/document_title_controller.rb +16 -0
  56. data/app/controllers/concerns/pages_core/page_parameters.rb +1 -1
  57. data/app/controllers/concerns/pages_core/pages/preview_controller.rb +49 -0
  58. data/app/controllers/concerns/pages_core/pages/rss_controller.rb +43 -0
  59. data/app/controllers/errors_controller.rb +2 -0
  60. data/app/controllers/images_controller.rb +13 -0
  61. data/app/controllers/pages_core/frontend/pages_controller.rb +3 -4
  62. data/app/controllers/pages_core/frontend_controller.rb +6 -1
  63. data/app/controllers/pages_core/sitemaps_controller.rb +21 -52
  64. data/app/helpers/pages_core/admin/image_uploads_helper.rb +1 -1
  65. data/app/helpers/pages_core/application_helper.rb +0 -3
  66. data/app/helpers/pages_core/attachments_helper.rb +0 -10
  67. data/app/helpers/pages_core/feed_tags_helper.rb +31 -0
  68. data/app/helpers/pages_core/frontend_helper.rb +3 -0
  69. data/app/helpers/pages_core/head_tags_helper.rb +80 -70
  70. data/app/helpers/pages_core/page_path_helper.rb +1 -12
  71. data/app/javascript/components/Attachments/Attachment.tsx +3 -3
  72. data/app/javascript/components/Attachments/AttachmentEditor.tsx +5 -5
  73. data/app/javascript/components/Attachments/Deleted.tsx +28 -0
  74. data/app/javascript/components/Attachments/List.tsx +11 -24
  75. data/app/javascript/components/Attachments/Placeholder.tsx +0 -2
  76. data/app/javascript/components/Attachments.tsx +2 -3
  77. data/app/javascript/components/DateRangeSelect.tsx +13 -10
  78. data/app/javascript/components/DateTimeSelect.tsx +11 -11
  79. data/app/javascript/components/EditableImage.tsx +3 -3
  80. data/app/javascript/components/FileUploadButton.tsx +3 -3
  81. data/app/javascript/components/ImageCropper/FocalPoint.tsx +10 -14
  82. data/app/javascript/components/ImageCropper/Image.tsx +19 -25
  83. data/app/javascript/components/ImageCropper/Toolbar.tsx +27 -26
  84. data/app/javascript/components/ImageCropper/useContainerSize.ts +25 -0
  85. data/app/javascript/components/ImageCropper/useCrop.ts +28 -13
  86. data/app/javascript/components/ImageCropper/useImageCropperContext.ts +13 -0
  87. data/app/javascript/components/ImageCropper.tsx +24 -83
  88. data/app/javascript/components/ImageEditor/Form.tsx +25 -28
  89. data/app/javascript/components/ImageEditor/useImageEditor.ts +63 -0
  90. data/app/javascript/components/ImageEditor/useImageEditorContext.ts +14 -0
  91. data/app/javascript/components/ImageEditor.tsx +28 -42
  92. data/app/javascript/components/ImageGrid/Deleted.tsx +28 -0
  93. data/app/javascript/components/ImageGrid/DragElement.tsx +5 -5
  94. data/app/javascript/components/ImageGrid/FilePlaceholder.tsx +0 -2
  95. data/app/javascript/components/ImageGrid/Grid.tsx +15 -24
  96. data/app/javascript/components/ImageGrid/GridImage.tsx +4 -4
  97. data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -4
  98. data/app/javascript/components/ImageGrid.tsx +2 -4
  99. data/app/javascript/components/ImageUploader.tsx +5 -5
  100. data/app/javascript/components/LabelledField.tsx +6 -6
  101. data/app/javascript/components/Modal.tsx +16 -13
  102. data/app/javascript/components/PageForm/Block.tsx +3 -3
  103. data/app/javascript/components/PageForm/Content.tsx +11 -15
  104. data/app/javascript/components/PageForm/Dates.tsx +3 -11
  105. data/app/javascript/components/PageForm/Files.tsx +2 -4
  106. data/app/javascript/components/PageForm/Form.tsx +3 -9
  107. data/app/javascript/components/PageForm/Images.tsx +2 -4
  108. data/app/javascript/components/PageForm/LocaleLinks.tsx +4 -11
  109. data/app/javascript/components/PageForm/Metadata.tsx +8 -13
  110. data/app/javascript/components/PageForm/Options.tsx +28 -11
  111. data/app/javascript/components/PageForm/PageDescription.tsx +7 -14
  112. data/app/javascript/components/PageForm/PathSegment.tsx +5 -10
  113. data/app/javascript/components/PageForm/TabPanel.tsx +3 -6
  114. data/app/javascript/components/PageForm/Tabs.tsx +2 -4
  115. data/app/javascript/components/PageForm/UnconfiguredContent.tsx +7 -12
  116. data/app/javascript/components/PageForm/pageParams.ts +3 -2
  117. data/app/javascript/components/PageForm/usePage.ts +1 -46
  118. data/app/javascript/components/PageForm/usePageFormContext.ts +8 -0
  119. data/app/javascript/components/PageForm/useTabs.ts +1 -1
  120. data/app/javascript/components/PageForm/utils.ts +49 -0
  121. data/app/javascript/components/PageForm.tsx +52 -48
  122. data/app/javascript/components/PageImages.tsx +1 -3
  123. data/app/javascript/components/PageTree/Button.tsx +25 -0
  124. data/app/javascript/components/PageTree/CollapseArrow.tsx +34 -0
  125. data/app/javascript/components/PageTree/CollapsedLabel.tsx +21 -0
  126. data/app/javascript/components/PageTree/EditPageName.tsx +68 -0
  127. data/app/javascript/components/PageTree/Node.tsx +143 -413
  128. data/app/javascript/components/PageTree/PageName.tsx +6 -4
  129. data/app/javascript/components/PageTree/StatusLabel.tsx +10 -0
  130. data/app/javascript/components/PageTree/tree.ts +268 -0
  131. data/app/javascript/components/PageTree/usePageTree.ts +268 -0
  132. data/app/javascript/components/PageTree/usePageTreeContext.ts +13 -0
  133. data/app/javascript/components/PageTree.tsx +194 -214
  134. data/app/javascript/components/{RichTextToolbarButton.tsx → RichTextArea/ToolbarButton.tsx} +3 -5
  135. data/app/javascript/components/RichTextArea/actions.ts +106 -0
  136. data/app/javascript/components/RichTextArea/useMaybeControlledValue.ts +14 -0
  137. data/app/javascript/components/RichTextArea.tsx +91 -209
  138. data/app/javascript/components/TagEditor/AddTagForm.tsx +2 -2
  139. data/app/javascript/components/TagEditor/Editor.tsx +3 -5
  140. data/app/javascript/components/TagEditor/Tag.tsx +3 -5
  141. data/app/javascript/components/TagEditor/useTags.ts +7 -4
  142. data/app/javascript/components/TagEditor.tsx +2 -4
  143. data/app/javascript/components/Toast.tsx +5 -5
  144. data/app/javascript/components/drag/draggedOrder.ts +6 -6
  145. data/app/javascript/components/drag/useDragCollection.ts +21 -25
  146. data/app/javascript/components/drag/useDragUploader.ts +20 -18
  147. data/app/javascript/components/drag/useDraggable.ts +3 -3
  148. data/app/javascript/features/RichText.tsx +0 -1
  149. data/app/javascript/features/contentTabs.ts +2 -2
  150. data/app/javascript/stores/useModalStore.ts +1 -1
  151. data/app/javascript/stores/useToastStore.ts +2 -2
  152. data/app/javascript/types/Attachments.ts +11 -11
  153. data/app/javascript/types/Crop.ts +16 -12
  154. data/app/javascript/types/Drag.ts +21 -23
  155. data/app/javascript/types/Images.ts +8 -8
  156. data/app/javascript/types/PageEditor.ts +11 -4
  157. data/app/javascript/types/Pages.ts +22 -27
  158. data/app/javascript/types/Tags.ts +5 -6
  159. data/app/javascript/types/Template.ts +4 -4
  160. data/app/javascript/types.ts +2 -2
  161. data/app/models/attachment.rb +5 -9
  162. data/app/models/autopublisher.rb +1 -1
  163. data/app/models/concerns/pages_core/page_model/redirectable.rb +1 -2
  164. data/app/models/concerns/pages_core/page_model/searchable.rb +1 -1
  165. data/app/models/concerns/pages_core/page_model/status.rb +2 -4
  166. data/app/models/concerns/pages_core/searchable_document.rb +2 -4
  167. data/app/models/image.rb +0 -15
  168. data/app/models/page_builder.rb +4 -6
  169. data/app/resources/admin/page_resource.rb +2 -2
  170. data/app/resources/export/page_resource.rb +1 -1
  171. data/app/services/pages_core/invite_service.rb +1 -2
  172. data/app/views/layouts/admin.html.erb +1 -0
  173. data/app/views/pages_core/sitemaps/index.xml.builder +10 -0
  174. data/config/routes.rb +4 -3
  175. data/db/migrate/20240917142300_add_skip_index_to_pages.rb +7 -0
  176. data/lib/pages_core/engine.rb +15 -17
  177. data/lib/pages_core/sitemap.rb +58 -0
  178. data/lib/pages_core/templates/configuration_proxy.rb +3 -3
  179. data/lib/pages_core.rb +7 -4
  180. data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +2 -2
  181. data/lib/rails/generators/pages_core/frontend/templates/application.html.erb +13 -5
  182. data/lib/rails/generators/pages_core/frontend/templates/postcss.config.js +2 -6
  183. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.css +3 -1
  184. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.css +2 -3
  185. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/animation.css +1 -1
  186. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.css +6 -5
  187. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/fonts.css +1 -1
  188. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/grid.css +9 -6
  189. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.css +42 -26
  190. data/lib/rails/generators/pages_core/install/templates/application_controller.rb +0 -6
  191. data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +1 -1
  192. metadata +81 -49
  193. data/app/assets/builds/fonts/7b7db107.woff2 +0 -0
  194. data/app/assets/builds/fonts/921961e9.woff2 +0 -0
  195. data/app/controller_dummies/admin/admin_controller.rb +0 -6
  196. data/app/controller_dummies/application_controller.rb +0 -6
  197. data/app/controller_dummies/attachments_controller.rb +0 -4
  198. data/app/controller_dummies/frontend_controller.rb +0 -4
  199. data/app/controller_dummies/images_controller.rb +0 -4
  200. data/app/controller_dummies/page_files_controller.rb +0 -4
  201. data/app/controller_dummies/pages_controller.rb +0 -4
  202. data/app/controller_dummies/sitemaps_controller.rb +0 -4
  203. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +0 -47
  204. data/app/controllers/concerns/pages_core/rss_controller.rb +0 -41
  205. data/app/controllers/pages_core/attachments_controller.rb +0 -42
  206. data/app/controllers/pages_core/frontend/page_files_controller.rb +0 -25
  207. data/app/controllers/pages_core/images_controller.rb +0 -15
  208. data/app/helpers/pages_core/meta_tags_helper.rb +0 -96
  209. data/app/helpers/pages_core/open_graph_tags_helper.rb +0 -49
  210. data/app/javascript/components/PageTree/Draggable.tsx +0 -338
  211. data/app/javascript/lib/Tree.ts +0 -305
  212. data/app/javascript/types/Trees.ts +0 -19
  213. 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
+ }