Dropdown Menu — Confluence Cloud MacroNew
This Confluence user macro provides a permission-aware status selector that saves the selected value as a page content property via the Confluence Cloud REST API.
Server-side, it checks whether the user can edit: if onlyEdit=true, it validates update restrictions; otherwise, it optionally verifies membership in specified allowedGroups.
Client-side, it renders a styled dropdown built with AUI (AJS.toInit) and predefined status sets (e.g., Traffic Light, Sprint, Issue, Email).
On load, it fetches the existing property from /api/v2/pages/{id}/properties and applies the saved status if present.
If a selection changes, it creates or updates the property using proper version incrementing.
User Parameters
Set
Choose a set of options
Only Edit (higher priority over group permissions)
Allow status changes for users with edit rights only
Allowed Groups
Enter groups (comma-separated) with status change permission
Template
<script>
window.AP.theming.initializeTheming();
</script>
#set ($contentId = $page.id)
#set ($propertyKey = $macroId)
#set ($canEdit = true)
#if ($parameters.get("onlyEdit") == "true")
## onlyEdit = true: check page edit restrictions ONLY, allowedGroups is ignored entirely
#set ($resp = $ConfluenceManager.get("/wiki/rest/api/content/${page.id}/restriction/byOperation/update"))
#if ($resp && $resp.restrictions)
#set ($userResults = $resp.restrictions.user.results)
#set ($groupResults = $resp.restrictions.group.results)
#if ($userResults.size() > 0 || $groupResults.size() > 0)
#set ($canEdit = false)
#foreach ($u in $userResults)
#if ($u.accountId == $user.accountId)
#set ($canEdit = true)
#end
#end
#end
#end
#else
## onlyEdit = false: check allowedGroups if provided
#if ($parameters.get("allowedGroups") && $parameters.get("allowedGroups") != "")
#set ($canEdit = false)
#set ($memberResp = $ConfluenceManager.get("/wiki/rest/api/user/memberof?accountId=${user.accountId}"))
#if ($memberResp && $memberResp.results)
#set ($allowedList = $StringUtils.split($parameters.get("allowedGroups"), ","))
#foreach ($group in $memberResp.results)
#foreach ($allowed in $allowedList)
#if ($group.name == $StringUtils.trim($allowed))
#set ($canEdit = true)
#end
#end
#end
#end
#end
#end
<style>
.sw-${macroId} {
position: relative;
display: inline-block;
font-weight: 600;
min-width: fit-content;
visibility: hidden;
}
.sw-${macroId} .status-box {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
border-radius: 6px;
cursor: pointer;
transition: 0.15s ease;
user-select: none;
}
.sw-${macroId} .status-box.readonly {
cursor: default;
}
.sw-${macroId} .status-arrow {
margin-left: 10px;
transition: transform 0.15s ease;
}
.sw-${macroId} .status-arrow.open {
transform: rotate(180deg);
}
.sw-${macroId} .status-menu {
position: absolute;
top: 110%;
left: 0;
right: 0;
background: white;
border-radius: 6px;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
overflow: hidden;
display: none;
z-index: 100;
}
.sw-${macroId} .status-option {
padding: 10px 14px;
cursor: pointer;
}
.sw-${macroId} .status-option:hover {
background: #f4f5f7;
}
html[data-color-mode="dark"] .status-option {
color: grey !important;
}
.status-default {
background: #f4f5f7;
color: #42526e;
}
.status-stop {
background: #ffebe6;
color: #bf2600;
}
.status-go_if_possible {
background: #fff7d6;
color: #7a5d00;
}
.status-go {
background: #e3fcef;
color: #006644;
}
.status-ready {
background: #deebff;
color: #0747a6;
}
.status-postponed {
background: #f4f5f7;
color: #42526e;
}
.status-inprogress {
background: #deebff;
color: #0065ff;
}
.status-review {
background: #eae6ff;
color: #403294;
}
.status-done {
background: #e3fcef;
color: #006644;
}
.status-waiting_for_customer {
background: #fff7d6;
color: #7a5d00;
}
.status-investigating {
background: #eae6ff;
color: #403294;
}
.status-escalated {
background: #ffebe6;
color: #bf2600;
}
.status-cancelled {
background: #f4f5f7;
color: #42526e;
}
.status-resolved {
background: #e3fcef;
color: #006644;
}
.status-waiting_for_email {
background: #fff7d6;
color: #7a5d00;
}
.status-reply_to_email {
background: #deebff;
color: #0065ff;
}
.status-email_sent {
background: #e3fcef;
color: #006644;
}
.status-created_draft_email {
background: #eae6ff;
color: #403294;
}
.status-email_deleted {
background: #ffebe6;
color: #bf2600;
}
.status-junk_email {
background: #f4f5f7;
color: #42526e;
}
</style>
<div class="sw-${macroId}" id="sw-${macroId}">
<div id="box-${macroId}" class="status-box status-default">
<span id="txt-${macroId}">Select status</span>
<span id="arr-${macroId}" class="status-arrow" style="#if(!$canEdit) display:none; #end">▼</span>
</div>
<div id="menu-${macroId}" class="status-menu"></div>
</div>
<script>
AJS.toInit(() => {
const contentId = "$contentId";
const propertyKey = "$propertyKey";
const statusSet = "$!parameters.get('set')" || "Traffic Light";
const canEdit = $canEdit;
const mid = "$macroId";
const STATUS_SETS = {
"Traffic Light": [{
value: "go", label: "GO"
},
{
value: "go_if_possible", label: "GO IF POSSIBLE"
},
{
value: "stop", label: "STOP"
}],
"Active Sprint": [{
value: "ready", label: "READY"
},
{
value: "postponed", label: "POSTPONED"
},
{
value: "inprogress", label: "IN PROGRESS"
},
{
value: "review", label: "REVIEW"
},
{
value: "done", label: "DONE"
}],
"Customer Issue": [{
value: "waiting_for_customer", label: "WAITING FOR CUSTOMER"
},
{
value: "inprogress", label: "IN PROGRESS"
},
{
value: "investigating", label: "INVESTIGATING"
},
{
value: "escalated", label: "ESCALATED"
},
{
value: "cancelled", label: "CANCELLED"
},
{
value: "resolved", label: "RESOLVED"
}],
"Email Status": [{
value: "waiting_for_email", label: "WAITING FOR EMAIL"
},
{
value: "reply_to_email", label: "REPLY TO EMAIL"
},
{
value: "email_sent", label: "EMAIL SENT"
},
{
value: "created_draft_email", label: "CREATED DRAFT EMAIL"
},
{
value: "email_deleted", label: "EMAIL DELETED"
},
{
value: "junk_email", label: "JUNK EMAIL"
}]
};
const options = STATUS_SETS[statusSet] || STATUS_SETS["Traffic Light"];
const wrapper = document.getElementById("sw-" + mid);
const box = document.getElementById("box-" + mid);
const text = document.getElementById("txt-" + mid);
const menu = document.getElementById("menu-" + mid);
const arrow = document.getElementById("arr-" + mid);
let propertyId = null;
let propertyVersion = null;
let isOpen = false;
let resizeTimer = null;
if (canEdit) {
options.forEach(opt => {
const div = document.createElement("div");
div.className = "status-option";
div.dataset.value = opt.value;
div.textContent = opt.label;
div.addEventListener("click", () => {
applyStatus(opt.value);
toggleMenu(false);
propertyId ? updateProperty(opt.value): createProperty(opt.value);
});
menu.appendChild(div);
});
}
const resize = () => {
requestAnimationFrame(() => {
const boxH = box.offsetHeight;
const menuH = isOpen ? menu.offsetHeight: 0;
const total = boxH + menuH + 24;
AP.resize("100%", total);
});
};
const toggleMenu = (forceState) => {
if (!canEdit) return;
isOpen = forceState !== undefined ? forceState: !isOpen;
menu.style.display = isOpen ? "block": "none";
arrow.classList.toggle("open", isOpen);
if (isOpen) {
const rect = box.getBoundingClientRect();
menu.style.position = "fixed";
menu.style.top = rect.bottom + "px";
menu.style.left = rect.left + "px";
menu.style.width = rect.width + "px";
}
resize();
};
const applyStatus = (value) => {
box.className = "status-box";
if (!canEdit) box.classList.add("readonly");
if (!value) {
box.classList.add("status-default");
text.textContent = "Select status";
return;
}
box.classList.add("status-" + value);
const match = options.find(o => o.value === value);
text.textContent = match ? match.label: value.replace(/_/g, " ").toUpperCase();
};
const loadProperty = () => {
AP.request({
url: "/api/v2/pages/" + contentId + "/properties?key=" + propertyKey,
type: "GET",
success: (respText) => {
const resp = JSON.parse(respText);
if (resp.results?.length > 0) {
const prop = resp.results[0];
propertyId = prop.id;
propertyVersion = prop.version.number;
applyStatus(prop.value.status);
}
wrapper.style.visibility = "visible";
resize();
},
error: () => {
wrapper.style.visibility = "visible";
resize();
}
});
};
const createProperty = (value) => {
AP.request({
url: "/api/v2/pages/" + ${contentId} + "/properties",
type: "POST",
contentType: "application/json",
data: JSON.stringify({
key: propertyKey, value: {
status: value
}
}),
error: (statusText) => {
const flag = AP.flag.create({
title: 'An error has occurred',
body: statusText,
close: 'auto',
type: 'error',
});
},
success: (respText) => {
const resp = JSON.parse(respText);
propertyId = resp.id;
propertyVersion = resp.version.number;
const flag = AP.flag.create({
title: 'Successfully set an option',
close: 'auto',
type: 'success',
});
}
});
};
const updateProperty = (value) => {
AP.request({
url: "/api/v2/pages/" + contentId + "/properties/" + propertyId,
type: "PUT",
contentType: "application/json",
data: JSON.stringify({
key: propertyKey,
value: {
status: value
},
version: {
number: propertyVersion + 1, message: "Status updated"
}
}),
error: (statusText) => {
const flag = AP.flag.create({
title: 'An error has occurred',
body: statusText,
close: 'auto',
type: 'error',
});
},
success: (respText) => {
const resp = JSON.parse(respText);
propertyVersion = resp.version.number;
const flag = AP.flag.create({
title: 'Successfully set an option',
close: 'auto',
type: 'success',
});
}
});
};
if (canEdit) box.addEventListener("click", (e) => {
e.stopPropagation();
toggleMenu();
});
document.addEventListener("click", (e) => {
if (!wrapper.contains(e.target)) toggleMenu(false);
});
loadProperty();
const measure = document.createElement("span");
measure.style.cssText =
"position:absolute;visibility:hidden;white-space:nowrap;" +
"font-weight:600;font-size:14px;padding:0;";
document.body.appendChild(measure);
let maxTextW = 0;
options.forEach(opt => {
measure.textContent = opt.label;
maxTextW = Math.max(maxTextW, measure.offsetWidth);
});
measure.textContent = "Select status";
maxTextW = Math.max(maxTextW,
measure.offsetWidth);
document.body.removeChild(measure);
const pad = 28 + (canEdit ? 22: 0);
const natural = maxTextW + pad;
wrapper.style.width = Math.min(natural,
150) + "px";
});
</script>Recommended Macros
Create dropdown menus in Confluence Cloud with custom sets of options, multiselect features, and permission gates
Create dropdown menus in Confluence Cloud with predefined sets of options, multiselect features, and permission gates
Displays a list of pages in specific space with certain title or label
Shows H1 headings directly to children's topics for easy navigation.
Shows page creation date
Basic greeting for user
Convert a specified amount of cryptocurrency to fiat currency.
User macro for displaying issues using custom JQL filter