We’ve recently migrated our Confluence to the cloud and are rewriting our trusty old Velocity user macros into Forge. I’ll admit that I’m a React novice but I’m dismayed to see how slow the Forge apps are vs. the old templates. Pages are painful to load, sometimes time out, and the PDF generation (which we must do often because these are official policy docs) doesn’t render the macros at all.
Am I missing something obvious?
Old macro that was very quick
## @param ExcludeLabels:title=Exclude pages with these labels|type=string|desc=Pages with these labels will be excluded from the table. Separate multiple labels with comma
## @param FeaturedLabels:title=Labels matching this regex will be displayed in the table column|type=string|desc=Enter labels to be displayed in the column.|default=.*
#set ( $excludedLabelsString = $paramExcludeLabels + "," )
#set ( $excludedLabelsString = $excludedLabelsString.replace(" ","") )
##set ( $descendantPages = $pageManager.getDescendents($content) )
#set ( $descendantPages = $pageManager.getPage($content.id).getChildren() )
#set ( $compareDate = $action.dateFormatter.getCalendar() )
#set ( $spaceKey = $space.getKey() )
$compareDate.setTime($content.getCurrentDate())
$compareDate.add(6, -365)
<table>
<tr>
<th>Page</th>
<th>Author</th>
<th>Last Modifier</th>
<th>Modified Date</th>
<th>Modified < 365d?</th>
<th>Labels</th>
</tr>
#foreach ( $page in $descendantPages )
#set ( $notContainsExcludedLabel = true )
#if ( $excludedLabelsString != ",")
#foreach ( $labelling in $page.getLabellings() )
#set ( $label = $labelling.getLabel() + "," )
#if ( $excludedLabelsString.contains($label) )
#set ( $notContainsExcludedLabel = false )
#end
#end
#end
#if ( $notContainsExcludedLabel )
<tr>
<td><a href="${req.contextPath}$page.urlPath"> $page.title </a></td>
<td> $page.Creator.fullName </td>
<td> $page.lastModifier.fullName </td>
<td>
<div style="text-align: center;">
$action.dateFormatter.format($page.lastModificationDate)
</div>
</td>
<td>
<div style="text-align: center;">
#set ($pageDate = $action.dateFormatter.getCalendar())
$pageDate.setTime($page.getLastModificationDate())
#if ( $pageDate.before($compareDate) )
<span class="aui-lozenge aui-lozenge-error">NO</span>
#else
<span class="aui-lozenge aui-lozenge-success">YES</span>
#end
</div>
</td>
<td>
#foreach ( $labelling in $page.getLabellings() )
#set( $label = $labelling.getLabel().getName())
#if( $label.matches($paramFeaturedLabels) )
<a href="/label/$spaceKey/$label">$label</a>
#end
#end
</td>
</tr>
#end
#end
</table>
Forge
import ForgeUI, { render, Macro, MacroConfig, TextField, Fragment, Text, DateLozenge, Badge, User, Table, Head, Row, Cell, Link, Strong, Heading, useProductContext, useAction, useConfig } from "@forge/ui";
import api, { route } from "@forge/api"
const getPageChildren = async (contentId) =>{
const response = await api.asUser().requestConfluence(route`/wiki/api/v2/pages/${contentId}/children?limit=100`)
if(!response.ok){
const err = `Error while /wiki/api/v2/pages/${contentId}/children: ${response.status} ${response.statusText}`;
console.error(err);
throw new Error(err);
}
const result = await response.json();
return result;
};
const getPageLabels = async (contentId) =>{
const response = await api.asUser().requestConfluence(route`/wiki/api/v2/pages/${contentId}/labels`)
if(!response.ok){
const err = `Error while /wiki/api/v2/pages/${contentId}/labels: ${response.status} ${response.statusText}`;
console.error(err);
throw new Error(err);
}
const result = await response.json();
return result;
};
const getPageDetails = async (contentId) =>{
const response = await api.asUser().requestConfluence(route`/wiki/api/v2/pages/${contentId}`)
if(!response.ok){
const err = `Error while /wiki/api/v2/pages/${contentId}: ${response.status} ${response.statusText}`;
console.error(err);
throw new Error(err);
}
const result = await response.json();
return result;
};
const defaultConfig = {
pageListRegex: ".+" ,
labelLinksRegex: ".+" ,
maxDaysSinceEdit: "365"
};
const App = () => {
const localConfig = useConfig() || defaultConfig;
const pageContext= useProductContext();
let pageListFilter = null;
if(localConfig.pageListRegex.length > 0) {
pageListFilter = new RegExp(localConfig.pageListRegex, "i");
}
const labelLinksFilter = new RegExp(localConfig.labelLinksRegex, "i");
const maxDaysPageAge = parseInt(localConfig.maxDaysSinceEdit);
let minPageEditDate = new Date()
minPageEditDate.setDate(minPageEditDate.getDate() - maxDaysPageAge)
const [children]= useAction(
() => null,
getPageChildren(pageContext.contentId)
);
const {results} = children;
// Build a map of child page details and labels that can be accessed synchronously during renderTableRows
let pageMap = new Map();
for (let i = 0; i < results.length; i++) {
const [labels] = useAction(
() => null,
getPageLabels(results[i].id)
);
let labelNames = [];
let includePage = false;
if(pageListFilter == null) {
includePage = true;
}
// Process labels
for(let j=0; j < labels.results.length; j++){
let labelName = labels.results[j].name
// Test the label to determine whether we should include this page in the list
if(pageListFilter && pageListFilter.test(labelName)) {
includePage = true;
}
// Test the label to determine whether we should display the label in the table column
if(labelLinksFilter.test(labelName)) {
labelNames.push(labelName)
}
}
if(includePage) {
const details = useAction(
() => null,
getPageDetails(results[i].id)
);
let pageInfo = {labels: labelNames, details: details[0], lastUpdated: (new Date(details[0].version.createdAt))}
pageMap.set(results[i].id, pageInfo);
}
}
// generate the table rows listing the child pages
const renderTableRows = (results) => {
return(
results.map((entry) => (
<Row>
<Cell>
<Text><Link appearance="link" href={`/wiki${entry.details._links.webui}`}>{entry.details.title}</Link></Text>
</Cell>
<Cell>
<Text><User accountId={`${entry.details.authorId}`}/></Text>
</Cell>
<Cell>
<Text><User accountId={`${entry.details.version.authorId}`}/></Text>
</Cell>
<Cell>
<Text>
{entry.lastUpdated.toISOString().substring(0, 10)}
</Text>
</Cell>
<Cell>
<Text>
{entry.lastUpdated >= minPageEditDate && <Badge appearance="added" text="Y" /> }
{entry.lastUpdated < minPageEditDate && <Badge appearance="primary" text="N" /> }
</Text>
</Cell>
<Cell>
<Text>
{entry.labels.map((labelName) => (
<Link appearance="link" href={`/wiki/label/${pageContext.spaceKey}/${labelName}`}>{labelName}{" "}</Link>
))
}
</Text>
</Cell>
</Row>
))
)
};
return (
<Fragment>
<Table>
<Head>
<Cell><Text>Page</Text></Cell>
<Cell><Text>Author</Text></Cell>
<Cell><Text>Last Updated By</Text></Cell>
<Cell><Text>Last Updated</Text></Cell>
<Cell><Text>Updated < {maxDaysPageAge}d</Text></Cell>
<Cell><Text>Labels</Text></Cell>
</Head>
{renderTableRows(Array.from(pageMap.values()))}
</Table>
</Fragment>
);
};
export const run = render(
<Macro
app={<App />}
/>
);
const Config = () => {
return (
<MacroConfig>
<TextField name="pageListRegex" label="Regex of page labels to match for inclusion in a table row. Leave blank for all pages, even those without labels." defaultValue={defaultConfig.pageListRegex} />
<TextField name="labelLinksRegex" label="Regex of page labels to show in-row as hyperlinks" defaultValue={defaultConfig.labelLinksRegex} />
<TextField name="maxDaysSinceEdit" label="Max days since edit (to be considered current) " defaultValue={defaultConfig.maxDaysSinceEdit} />
</MacroConfig>
);
};
export const config = render(<Config />);