Skip to content

Commit c343fe9

Browse files
committed
- Convert PageMap webform block to obsidian block
1 parent e69d8a6 commit c343fe9

File tree

15 files changed

+1377
-804
lines changed

15 files changed

+1377
-804
lines changed

Rock.Blocks/Cms/PageMap.cs

Lines changed: 581 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
<!-- Copyright by the Spark Development Network; Licensed under the Rock Community License -->
2+
<template>
3+
<NotificationBox v-if="blockError" alertType="danger" v-html="blockError"></NotificationBox>
4+
<Panel :title="panelName"
5+
type="block"
6+
:isCollapsible="false"
7+
:isInitiallyCollapsed="false">
8+
<template #headerActions>
9+
<DropDownMenu title="Add Page"
10+
:items="dropDownMenuItems"
11+
:align="'left'">
12+
<i class="ti ti-plus"></i>
13+
</DropDownMenu>
14+
</template>
15+
<TreeList v-if="isTreeListDataInitialized"
16+
v-model="treeListSelectedItems"
17+
:items="treeListItems"
18+
:initiallyExpanded="treeListInitiallyExpandedGuids"
19+
@update:modelValue="onSelect"
20+
@update:items="onUpdateItems"
21+
@treeitem-expanded="onTreeItemExpanded"
22+
@treeitem-collapsed="onTreeItemCollapsed"
23+
autoExpand />
24+
</Panel>
25+
</template>
26+
27+
<style scoped>
28+
:deep(.panel-body .rocktree span:hover) {
29+
background-color: var(--base-interface-softer);
30+
}
31+
32+
:deep(.panel-body .rocktree .rocktree-name.selected) {
33+
background-color: var(--color-info-soft);
34+
}
35+
</style>
36+
37+
<script setup lang="ts">
38+
import { ref, computed, onBeforeMount } from "vue";
39+
import NotificationBox from "@Obsidian/Controls/notificationBox.obs";
40+
import Panel from "@Obsidian/Controls/panel.obs";
41+
import DropDownMenu from "@Obsidian/Controls/dropDownMenu.obs";
42+
import TreeList from "@Obsidian/Controls/treeList.obs";
43+
import { Guid } from "@Obsidian/Types";
44+
import { SiteType } from "@Obsidian/Enums/Cms/siteType";
45+
import { areEqual, emptyGuid, toGuidOrNull } from "@Obsidian/Utility/guid";
46+
import { MenuAction } from "@Obsidian/Types/Controls/dropDownMenu";
47+
import { TreeItemBag } from "@Obsidian/ViewModels/Utility/treeItemBag";
48+
import { CustomBlockBox } from "@Obsidian/ViewModels/Blocks/customBlockBox";
49+
import { PageMapBag } from "@Obsidian/ViewModels/Blocks/Cms/PageMap/pageMapBag";
50+
import { PageMapOptionsBag } from "@Obsidian/ViewModels/Blocks/Cms/PageMap/pageMapOptionsBag";
51+
import { PageMapTreeListBag } from "@Obsidian/ViewModels/Blocks/Cms/PageMap/pageMapTreeListBag";
52+
import { PageTreeItemProvider } from "@Obsidian/Utility/treeItemProviders";
53+
import { onConfigurationValuesChanged, useConfigurationValues, useInvokeBlockAction, useReloadBlock } from "@Obsidian/Utility/block";
54+
55+
const invokeBlockAction = useInvokeBlockAction();
56+
57+
const box = useConfigurationValues<CustomBlockBox<PageMapBag, PageMapOptionsBag>>();
58+
const bag = box.bag ?? {};
59+
const options = box.options ?? getEmptyOptionsBag();
60+
61+
// #region Values
62+
63+
const panelName = "Pages";
64+
65+
const itemProvider = new PageTreeItemProvider();
66+
itemProvider.rootPageGuid = toGuidOrNull(options.blockProperties?.rootPage) ?? undefined;
67+
itemProvider.siteType = options.blockProperties?.siteType !== null ? options.blockProperties?.siteType : undefined;
68+
itemProvider.selectedPageGuids = [];
69+
70+
const blockError = ref("");
71+
72+
const isTreeListDataInitialized = ref<boolean>(false);
73+
74+
const treeList = ref<PageMapTreeListBag>(bag.treeList ?? getEmptyTreeListBag());
75+
const treeListSelectedItems = ref<Guid[]>(treeList.value.selectedItems ?? []);
76+
const treeListItems = ref<TreeItemBag[]>(treeList.value.items ?? []);
77+
const treeListExpandedItems = ref<Guid[]>(treeList.value.expandedItems ?? []);
78+
const treeListInitiallyExpandedGuids = ref<Guid[]>(treeList.value.initiallyExpandedItems ?? []);
79+
80+
// #endregion Values
81+
82+
// #region Computed Values
83+
84+
const dropDownMenuItems = computed<MenuAction[]>(() => [
85+
{
86+
title: "Add Top-Level",
87+
type: "default",
88+
handler: onAddRootPage
89+
},
90+
{
91+
title: "Add Child To Selected",
92+
type: "default",
93+
handler: onAddChildPage,
94+
disabled: !treeListSelectedItems || treeListSelectedItems.value.length !== 1
95+
}
96+
]);
97+
98+
// #endregion Computed Values
99+
100+
// #region Functions
101+
102+
/**
103+
* Creates and returns an empty PageMapTreeListBag.
104+
*
105+
* @returns An empty PageMapTreeListBag.
106+
*/
107+
function getEmptyTreeListBag(): PageMapTreeListBag {
108+
return {
109+
selectedItems: [],
110+
items: [],
111+
expandedItems: [],
112+
initiallyExpandedItems: [],
113+
};
114+
}
115+
116+
/**
117+
* Creates and returns an empty PageMapOptionsBag.
118+
*/
119+
function getEmptyOptionsBag(): PageMapOptionsBag {
120+
return {
121+
blockProperties: {
122+
rootPage: null,
123+
siteType: SiteType.Web,
124+
}
125+
};
126+
}
127+
128+
/**
129+
* Sets the tree list items by retrieving and formatting them.
130+
*/
131+
async function setTreeListItems(): Promise<void> {
132+
treeListItems.value = await getFormattedTreeListItems();
133+
}
134+
135+
/**
136+
* Recursively retrieves and formats tree list items, setting default icons and loading children for expanded/selected items.
137+
*
138+
* @param items Optional array of TreeItemBag to format; if not provided, root items will be fetched.
139+
* @returns A Promise that resolves to an array of formatted TreeItemBag.
140+
*/
141+
async function getFormattedTreeListItems(items: TreeItemBag[] | null = null): Promise<TreeItemBag[]> {
142+
/** Recursively sets default icons for items that don't have one and populates children for expanded/selected items */
143+
async function processTreeItems(items: TreeItemBag[]): Promise<void> {
144+
for (const item of items) {
145+
if (!item.value) {
146+
continue;
147+
}
148+
149+
if (!item.iconCssClass || item.iconCssClass === "") {
150+
item.iconCssClass = "ti ti-file";
151+
}
152+
153+
// Load children for expanded or selected items
154+
const isItemSelected = treeListSelectedItems.value.includes(item.value);
155+
const isItemExpanded = treeListExpandedItems.value.includes(item.value);
156+
const isItemExpandedOrSelected = isItemExpanded || isItemSelected;
157+
158+
if (item.hasChildren && isItemExpandedOrSelected) {
159+
if (!item.children || item.children.length === 0) {
160+
item.children = await itemProvider.getChildItems(item);
161+
}
162+
if (item.children && item.children.length > 0) {
163+
await processTreeItems(item.children);
164+
item.childCount = item.children.length;
165+
item.isFolder = true;
166+
}
167+
}
168+
}
169+
}
170+
171+
// Ensure selected items are considered expanded
172+
treeListSelectedItems.value.forEach(selectedItem => {
173+
if (!treeListExpandedItems.value.includes(selectedItem)) {
174+
treeListExpandedItems.value = [...treeListExpandedItems.value, selectedItem];
175+
}
176+
if (!treeListInitiallyExpandedGuids.value.includes(selectedItem)) {
177+
treeListInitiallyExpandedGuids.value = [...treeListInitiallyExpandedGuids.value, selectedItem];
178+
}
179+
});
180+
181+
let formattedItems = items !== null ? JSON.parse(JSON.stringify(items)) as TreeItemBag[] : await itemProvider.getRootItems(treeListInitiallyExpandedGuids.value);
182+
183+
// Recursively process all items
184+
await processTreeItems(formattedItems);
185+
186+
return formattedItems;
187+
}
188+
189+
// #endregion Functions
190+
191+
// #region Event Handlers
192+
193+
/**
194+
* Handles the expanding of a tree item by tracking its value and loading children if necessary.
195+
*
196+
* @param item The tree item that was expanded.
197+
*/
198+
async function onTreeItemExpanded(item: TreeItemBag): Promise<void> {
199+
if (item.value) {
200+
// Load child items if they haven't been loaded yet
201+
if (!item.children || item.children.length === 0) {
202+
const itemChildren = await itemProvider.getChildItems(item);
203+
204+
if (itemChildren && itemChildren.length > 0) {
205+
// Set default icons for newly loaded children
206+
const setDefaultIconsRecursively = (items: TreeItemBag[]): void => {
207+
items.forEach(childItem => {
208+
if (!childItem.iconCssClass || childItem.iconCssClass === "") {
209+
childItem.iconCssClass = "ti ti-file";
210+
}
211+
if (childItem.children && childItem.children.length > 0) {
212+
setDefaultIconsRecursively(childItem.children);
213+
}
214+
});
215+
};
216+
setDefaultIconsRecursively(itemChildren);
217+
item.children = itemChildren;
218+
item.hasChildren = true;
219+
item.childCount = itemChildren.length;
220+
item.isFolder = true;
221+
}
222+
}
223+
224+
treeListExpandedItems.value = [...treeListExpandedItems.value, item.value];
225+
}
226+
}
227+
228+
/**
229+
* Handles the collapsing of a tree item by removing its value from the tracked expanded items.
230+
*
231+
* @param item The tree item that was collapsed.
232+
*/
233+
function onTreeItemCollapsed(item: TreeItemBag): void {
234+
if (item.value) {
235+
treeListExpandedItems.value = treeListExpandedItems.value.filter(id => id !== item.value);
236+
}
237+
}
238+
239+
/**
240+
* Handles the selection of a page and navigates to the corresponding URL.
241+
*
242+
* @param value The array of selected page values.
243+
*/
244+
async function onSelect(value: string[]): Promise<void> {
245+
if (isTreeListDataInitialized.value === false) {
246+
return;
247+
}
248+
249+
if (value.length > 1 || value.length < 0) {
250+
treeListSelectedItems.value = [];
251+
return;
252+
}
253+
254+
if (treeListItems.value) {
255+
// Recursively search for the matching item in treeListItems and their children
256+
const findMatchingTreeListItem = (items: TreeItemBag[], targetValue: string): TreeItemBag | undefined => {
257+
for (const item of items) {
258+
if (item.value === targetValue) {
259+
return item;
260+
}
261+
if (item.children && item.children.length > 0) {
262+
const found = findMatchingTreeListItem(item.children, targetValue);
263+
if (found) {
264+
return found;
265+
}
266+
}
267+
}
268+
return undefined;
269+
};
270+
271+
const matchingTreeListItem = findMatchingTreeListItem(treeListItems.value, value[0]);
272+
273+
if (matchingTreeListItem && matchingTreeListItem.value) {
274+
275+
window.location.href = await getNavigationUrl(matchingTreeListItem.value, emptyGuid, treeListExpandedItems.value);
276+
}
277+
}
278+
}
279+
280+
/**
281+
* Event handler for when the list of items in the tree list has been
282+
* updated outside of this component.
283+
*
284+
* @param newItems The new root items being used by the tree list.
285+
*/
286+
async function onUpdateItems(newItems: TreeItemBag[]): Promise<void> {
287+
treeListItems.value = await getFormattedTreeListItems(newItems);
288+
}
289+
290+
/**
291+
* Handles adding a new root page by navigating to the appropriate URL.
292+
*/
293+
async function onAddRootPage(): Promise<void> {
294+
const rootPageGuid = toGuidOrNull(options.blockProperties?.rootPage);
295+
296+
if (rootPageGuid && !areEqual(rootPageGuid, emptyGuid)) {
297+
window.location.href = await getNavigationUrl(
298+
emptyGuid,
299+
rootPageGuid,
300+
treeListExpandedItems.value,
301+
);
302+
}
303+
else {
304+
window.location.href = await getNavigationUrl(
305+
emptyGuid,
306+
emptyGuid,
307+
treeListExpandedItems.value,
308+
);
309+
}
310+
}
311+
312+
/**
313+
* Handles adding a new child page to the selected page by navigating to the appropriate URL.
314+
*/
315+
async function onAddChildPage(): Promise<void> {
316+
window.location.href = await getNavigationUrl(
317+
emptyGuid,
318+
treeListSelectedItems.value[0],
319+
treeListExpandedItems.value,
320+
);
321+
}
322+
323+
// #endregion Event Handlers
324+
325+
// #region Block Actions
326+
327+
/**
328+
* Invokes the GetNavigationUrl block action to retrieve the navigation URL for a page.
329+
*
330+
* @param pageGuid The GUID of the page for the Page ID.
331+
* @param parentGuid The GUID of the parent page.
332+
* @param expandedGuids An array of GUIDs representing the currently expanded pages.
333+
*
334+
* @returns A Promise that resolves to the navigation URL as a string.
335+
*/
336+
async function getNavigationUrl(pageGuid: Guid | null, parentGuid: Guid | null, expandedGuids: Guid[] | null): Promise<string> {
337+
if (!options.blockProperties) {
338+
return "";
339+
}
340+
341+
if (pageGuid === null) {
342+
pageGuid = emptyGuid;
343+
}
344+
if (parentGuid === null) {
345+
parentGuid = emptyGuid;
346+
}
347+
if (!expandedGuids) {
348+
expandedGuids = [];
349+
}
350+
351+
const result = await invokeBlockAction<string>("GetNavigationUrl", {
352+
entityGuid: pageGuid,
353+
parentGuid: parentGuid,
354+
expandedGuids: expandedGuids,
355+
});
356+
357+
if (result.isSuccess && result.data) {
358+
return result.data;
359+
}
360+
else if (result.errorMessage) {
361+
blockError.value = result.errorMessage;
362+
}
363+
364+
return "";
365+
}
366+
367+
// #endregion Block Actions
368+
369+
if (box.errorMessage) {
370+
blockError.value = box.errorMessage;
371+
}
372+
373+
onBeforeMount(async () => {
374+
await setTreeListItems();
375+
isTreeListDataInitialized.value = true;
376+
});
377+
378+
onConfigurationValuesChanged(useReloadBlock());
379+
</script>

0 commit comments

Comments
 (0)