Introduction
Well, it finally happened. I finally had to start learning JavaScript.
It’s actually not that bad, I probably should have learned a while ago. My use case for it is writing Confluence Macros and plugins for both Confluence and Jira. I started with the plugins, for simplicity’s sake.
My inspiration came from a post on the Atlassian Community Forums. Someone had requested a way to essentially mirror the setup of a macro. But they wanted to mirror the most recent child page, of a parent page.
I think that without pretty strong knowledge of Confluence and the REST API, I’d have struggled to complete this. It enough work to learn JavaScript’s basic tenets as I went.
Digging Into The Problem
Okay so what do we actually need the script to do? We need it to:
- Figure out the most recently updated child page of a parent page
- Fetch the macro setup of the child page
- Update the parent page accordingly
These are the three high-level functions that the macro needs to accomplish.
Figuring out the most recently updated child page wasn’t hard. You can make a call to baseURL + pageID + “/child/page?limit=1000&expand=history.lastUpdated. This returns a list of the most recently updated child pages for the given parent. The base URL is easy, since it’s simply the instance that we’re on. The pageID is simply the page ID of the parent page, which we can return by calling AJS.params.pageId.
So we have a URL that’ll give us a list of pages. If we sort that page by date and return the most recent one, we have the page ID of the most recently updated child page.
We can then use that ID to return the page object of the child page itself, by calling /pages/viewpage.action?pageId=${childPageID}. Here’s what the function looks like:
const baseURL = "/rest/api/content/";
const childrenURL = baseURL + pageID + "/child/page?limit=1000&expand=history.lastUpdated";
//Get the API endpoint with which we will fetch the most recently update child pages
fetch(childrenURL)
.then(response => response.json())
.then(data => {
const sortedChildren = data.results.sort((a, b) => {
const aDate = new Date(a.history.lastUpdated.when);
const bDate = new Date(b.history.lastUpdated.when);
return bDate - aDate;
});
//Return and sort the most recently update child pages
const mostRecentChildID = sortedChildren[0].id;
console.log("The ID of the most recently updated child page is: " + mostRecentChildID);
}).catch(error => console.log("Error fetching child page ID:", error));
The variable we end up with, mostRecentChildID, is the value we need to work with next. But there’s a problem: fetch doesn’t return a value. It returns a promise. So let’s explore that a little bit.
Making Promises
Briefly, a promise is the result of an asynchronous JavaScript function. In other words, it’s basically a placeholder. The function will run in the background when it’s called, and the rest of the script will continue processing. When you’re ready, you can call upon the results of that promise. So what’s the problem?
The problem is that we can’t simply refer to the variable that we declared within the fetch function. It’s not available, because the promise hasn’t been fulfilled yet. If we use it within the fetch function, the function knows to wait for the promise to be resolved or rejected.
So that’s option 1. We can just do everything inside the fetch function, treating it like a giant closure.
Option 2 is to use a callback function, wherein we pass the value of the resolved promise to another function. In this way the fetch still knows that we want to do something with the results, but the value is made available outside of the context of the promise. Example:
<script type="text/javascript">
//let newVar;
const p = fetch('/rest/api/content')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch data');
}
return response.json();
})
.then(data => {
newVar = data;
myFunction(newVar); // Call the function that needs to access the value of newVar
})
.catch(error => {
console.error("Encountered an error fetching data:", error);
});
function myFunction(data) {
console.log("This is the value of newVar: ", data);
}
</script>
This successfully passes the value of newVar outside of the fetch to another function. Worth noting is that I left a “let” statement commented out at the top so that we could touch on that possibility: simply declaring the variable outside of the promise does NOT fix the issue. We must either pass it to a function or use it within the confines of the promise.
With all that in mind, the actual macro isn’t terribly complicated. Let’s look at the whole thing.
A Macro Level View
So here’s the macro. It has three nested fetch statements.
The first fetch statement grabs the ID of the most recently updated child page.
The second-level fetch statement uses that ID to grab the settings of the first excerpt-extract macro on the child page.
The third-level fetch statement uses those macro settings to update the parent page.
The result is a macro that mirrors, on a parent page, the macro setup of the most recently update child page. Neat!
## @noparams
<script type="text/javascript">
let pageID = AJS.params.pageId
//Get the ID of the current (parent) page
const baseURL = "/rest/api/content/";
const childrenURL = baseURL + pageID + "/child/page?limit=1000&expand=history.lastUpdated";
//Get the API endpoint with which we will fetch the most recently update child pages
fetch(childrenURL)
.then(response => response.json())
.then(data => {
const sortedChildren = data.results.sort((a, b) => {
const aDate = new Date(a.history.lastUpdated.when);
const bDate = new Date(b.history.lastUpdated.when);
return bDate - aDate;
});
//Return and sort the most recently update child pages
const mostRecentChildID = sortedChildren[0].id;
console.log("The ID of the most recently updated child page is: " + mostRecentChildID);
//Turn the ID of the most recently update child page into a variable
//Start second-level loop
const url = `/pages/viewpage.action?pageId=` + mostRecentChildID;
//Define the URL of the target child page
fetch(url)
.then(response => response.text())
.then(html => {
const div = document.createElement('div');
div.innerHTML = html;
const macroElements = Array.from(div.querySelectorAll(".conf-macro"));
const matchtestMacros = macroElements.filter(macro => macro.getAttribute("data-macro-name") === "excerpt-include");
const macroData = [];
matchtestMacros.forEach(macro => {
const divs = macro.querySelectorAll("div");
const res = divs[0].innerHTML.replace(/<\/?b>/g, "");
macroData.push(res);
});
const ChildMacroSource = macroData[0];
//Get the first excerpt-include macro from the page
//We're assuming that we're only interested in the first result
//Start third-level loop
const pageURL = baseURL + pageID + '?expand=body.storage,version';
// Retrieve the page content
fetch(pageURL)
.then(response => response.json())
.then(data => {
const pageBody = data.body.storage.value;
// Replace "SourcePage" with "NewPage" in the page body
const modifiedPageBody = pageBody.replace(/ri:content-title="([^"]*)"/g, `ri:content-title="${ChildMacroSource}"`);
//Replace the excerpt-include source with the source from the child page
// Update the page with the modified content
const updateURL = baseURL + pageID;
const bodyData = JSON.stringify({
"id": pageID,
"type": "page",
"title": data.title,
"version": {
"number": data.version.number + 1,
"minorEdit": false
},
"body": {
"storage": {
"value": modifiedPageBody,
"representation": "storage"
}
}
});
fetch(updateURL, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: bodyData
})
.then(response => {
console.log(response);
if (!response.ok) {
throw new Error('Failed to update page content: ' + response);
}
alert('Page content updated successfully');
})
.catch(error => {
alert('Error: ' + error.message);
});
})
.catch(error => alert("Encoutered an error updating the page content: " + error));
//End third-level loop
}).catch(error => console.error("Encoutered an error getting the excerpt-include source from the child page: " + error));
//Second level "then" loop end
}).catch(error => console.log("Error fetching child page ID:", error));
//First level "then" loop end
</script>
Leave a Reply