Clean up Jira rich text in Webex approval card details.

Convert wiki markup to readable text and render each detail field in its own card block.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jmcqueen 2026-07-28 12:26:28 -04:00
parent d0e1e20843
commit 98dd33a20e
5 changed files with 281 additions and 51 deletions

View file

@ -55,6 +55,7 @@
{ "fieldId": "customfield_10287", "label": "Change category", "format": "option" },
{ "fieldId": "customfield_10259", "label": "Impact level", "format": "option" },
{ "fieldId": "customfield_10257", "label": "Implementation date", "format": "datetime" },
{ "fieldId": "description", "label": "Implementation steps", "format": "textarea" },
{ "fieldId": "customfield_10252", "label": "Business justification", "format": "textarea" },
{ "fieldId": "customfield_10256", "label": "Business impact / risk", "format": "textarea" },
{ "fieldId": "customfield_10253", "label": "Testing completed", "format": "textarea" },
@ -89,6 +90,7 @@
"includeCommon": true,
"displayFields": [
{ "fieldId": "customfield_10257", "label": "Production deploy date", "format": "datetime" },
{ "fieldId": "description", "label": "Implementation steps", "format": "textarea" },
{ "fieldId": "customfield_10252", "label": "Business justification", "format": "textarea" },
{ "fieldId": "customfield_10256", "label": "Impacted services", "format": "textarea" },
{ "fieldId": "customfield_10253", "label": "Testing completed", "format": "textarea" },
@ -104,6 +106,7 @@
"serviceDeskId": "135",
"includeCommon": true,
"displayFields": [
{ "fieldId": "description", "label": "Implementation steps", "format": "textarea" },
{ "fieldId": "customfield_10309", "label": "Firewall request details", "format": "textarea" },
{ "fieldId": "customfield_10252", "label": "End result / justification", "format": "textarea" },
{ "fieldId": "customfield_10242", "label": "Application(s)", "format": "cmdb" },

101
lib/jiraRichText.js Normal file
View file

@ -0,0 +1,101 @@
function adfNodeToText(node) {
if (!node) {
return '';
}
if (Array.isArray(node)) {
return node.map(adfNodeToText).join('');
}
if (typeof node === 'string') {
return node;
}
switch (node.type) {
case 'text':
return node.text || '';
case 'hardBreak':
return '\n';
case 'paragraph':
case 'heading':
return `${(node.content || []).map(adfNodeToText).join('')}\n`;
case 'bulletList':
case 'orderedList':
return (node.content || [])
.map(item => adfNodeToText(item))
.join('');
case 'listItem':
return `${(node.content || []).map(adfNodeToText).join('').trim()}\n`;
case 'rule':
return '\n';
case 'codeBlock':
return `${(node.content || []).map(adfNodeToText).join('')}\n`;
case 'inlineCard':
case 'blockCard':
return node.attrs?.url || '';
default:
if (node.content) {
return node.content.map(adfNodeToText).join('');
}
return '';
}
}
export function adfToPlainText(value) {
if (!value || typeof value !== 'object') {
return '';
}
if (value.type !== 'doc' || !Array.isArray(value.content)) {
return '';
}
return value.content.map(adfNodeToText).join('').trim();
}
export function formatJiraWikiMarkup(text) {
if (text == null || text === '') {
return '';
}
let output = String(text).replace(/\r\n/g, '\n');
output = output.replace(/\{noformat\}([\s\S]*?)\{noformat\}/gi, (_, body) => `\n${body.trim()}\n`);
output = output.replace(/\{code(?::[^}]*)?\}([\s\S]*?)\{code\}/gi, (_, body) => `\n${body.trim()}\n`);
output = output.replace(/\{color(?::[^}]*)?\}([\s\S]*?)\{color\}/gi, '$1');
output = output.replace(/\{quote\}([\s\S]*?)\{quote\}/gi, '$1');
output = output.replace(/![^!\n]+!/g, '');
output = output.replace(/\[~accountid:[^\]]+\]/gi, '');
output = output.replace(/\[([^\]|]+)\|([^\]]+?)(?:\|smart-link)?\]/gi, (_, label, url) => {
const cleanLabel = label.trim();
const cleanUrl = url.trim();
if (!cleanLabel || cleanLabel === cleanUrl) {
return cleanUrl;
}
return `${cleanLabel} (${cleanUrl})`;
});
output = output.replace(/\*\*([^*\n]+?)\*\*/g, '$1');
output = output.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '$1');
output = output.replace(/^h([1-6])\.\s*(.+)$/gim, (_, __, title) => `${title.trim()}\n`);
output = output.replace(/^#{1,6}\s+(.+)$/gm, '$1\n');
output = output.replace(/^\s*[-*#]\s+/gm, '• ');
output = output.replace(/^-{4,}\s*$/gm, '\n');
output = output.replace(/[ \t]+\n/g, '\n');
output = output.replace(/\n{3,}/g, '\n\n');
return output.trim();
}
export function formatJiraRichText(value) {
if (value == null || value === '') {
return '';
}
if (typeof value === 'object') {
const adfText = adfToPlainText(value);
if (adfText) {
return formatJiraWikiMarkup(adfText);
}
return value.displayName || value.value || value.name || '';
}
return formatJiraWikiMarkup(value);
}

View file

@ -1,4 +1,5 @@
import fs from 'fs';
import { formatJiraRichText } from './jiraRichText.js';
const SKIP_FIELD_IDS = new Set(['attachment']);
@ -89,9 +90,10 @@ export function formatFieldValue(value, format) {
case 'text':
default:
if (typeof value === 'object') {
return value.displayName || value.value || value.name || JSON.stringify(value);
const richText = formatJiraRichText(value);
return richText || value.displayName || value.value || value.name || null;
}
return String(value);
return formatJiraRichText(value) || null;
}
}

View file

@ -2,13 +2,6 @@ import { buildDetailLinesForRequestType } from '../lib/requestTypeFields.js';
const PORTAL_BASE_URL = 'https://aeo.atlassian.net/servicedesk/customer/portal/135';
function appendDetailLines(detailText, lines) {
for (const line of lines) {
detailText += ` - **${line.label}:** ${line.value}\n`;
}
return detailText;
}
function getRequestTypeId(jiraTicket) {
return jiraTicket.issue.fields?.customfield_10010?.requestType?.id;
}
@ -78,19 +71,84 @@ function buildPlainTextHeader(jiraTicket) {
return text;
}
function buildCardPayload({ jiraTicket, detailText, assignmentText = '' }) {
const body = [...buildHeaderBlocks(jiraTicket)];
function buildDetailBlocks(detailLines) {
if (!detailLines.length) {
return [];
}
if (detailText.trim()) {
body.push({
const blocks = [
{
type: 'TextBlock',
wrap: true,
text: detailText.trim(),
text: 'Details',
weight: 'Bolder',
size: 'Medium',
separator: true,
spacing: 'Medium',
},
];
for (const line of detailLines) {
blocks.push({
type: 'TextBlock',
text: line.label,
weight: 'Bolder',
spacing: 'Medium',
wrap: true,
});
blocks.push({
type: 'TextBlock',
text: line.value,
spacing: 'Small',
wrap: true,
});
}
return blocks;
}
function formatDetailLinesPlain(detailLines) {
let text = '**Details:**\n';
for (const line of detailLines) {
text += `\n**${line.label}:**\n${line.value}\n`;
}
return text;
}
function buildFallbackChangeDetailLines(jiraTicket) {
const lines = [];
const fields = jiraTicket.issue.fields;
if (fields.customfield_10080) {
lines.push({ label: 'Risk', value: fields.customfield_10080.value });
}
if (fields.customfield_10004) {
lines.push({ label: 'Impact', value: fields.customfield_10004.value });
}
if (fields.customfield_10256) {
lines.push({ label: 'Business impact', value: fields.customfield_10256 });
}
if (fields.customfield_10252) {
lines.push({ label: 'Business justification', value: fields.customfield_10252 });
}
if (fields.customfield_10257) {
const changeDate = new Date(fields.customfield_10257).toLocaleDateString();
const changeTime = new Date(fields.customfield_10257).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: true,
});
lines.push({ label: 'Implementation date', value: `${changeDate} ${changeTime}` });
}
return lines;
}
function buildCardPayload({ jiraTicket, detailLines = [], assignmentText = '' }) {
const body = [
...buildHeaderBlocks(jiraTicket),
...buildDetailBlocks(detailLines),
];
if (assignmentText.trim()) {
body.push({
type: 'TextBlock',
@ -122,8 +180,8 @@ function buildCardPayload({ jiraTicket, detailText, assignmentText = '' }) {
};
let requestText = buildPlainTextHeader(jiraTicket);
if (detailText.trim()) {
requestText += `---\n${detailText}`;
if (detailLines.length) {
requestText += `---\n${formatDetailLinesPlain(detailLines)}`;
}
if (assignmentText.trim()) {
requestText += `---\n${assignmentText}`;
@ -134,49 +192,36 @@ function buildCardPayload({ jiraTicket, detailText, assignmentText = '' }) {
export function buildChangeApproval(jiraTicket, log) {
return new Promise(function (resolve) {
let detailText = '**Details:**\n';
if (jiraTicket.issue.fields.description) {
detailText += `**Description:** ${jiraTicket.issue.fields.description}\n`;
}
const requestTypeId = getRequestTypeId(jiraTicket);
if (requestTypeId) {
detailText = appendDetailLines(detailText, buildDetailLinesForRequestType(jiraTicket, requestTypeId));
} else {
if (jiraTicket.issue.fields.customfield_10080) { detailText += ` - **Risk:** ${jiraTicket.issue.fields.customfield_10080.value}\n`; }
if (jiraTicket.issue.fields.customfield_10004) { detailText += ` - **Impact:** ${jiraTicket.issue.fields.customfield_10004.value}\n`; }
if (jiraTicket.issue.fields.customfield_10256) { detailText += ` - **Business Impact:** ${jiraTicket.issue.fields.customfield_10256}\n`; }
if (jiraTicket.issue.fields.customfield_10252) { detailText += ` - **Business Justification:** ${jiraTicket.issue.fields.customfield_10252}\n`; }
if (jiraTicket.issue.fields.customfield_10257) {
const changeDate = new Date(jiraTicket.issue.fields.customfield_10257).toLocaleDateString();
const changeTime = new Date(jiraTicket.issue.fields.customfield_10257).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true });
detailText += ` - **Implementation Date:** ${changeDate} ${changeTime}\n`;
}
}
const detailLines = requestTypeId
? buildDetailLinesForRequestType(jiraTicket, requestTypeId)
: buildFallbackChangeDetailLines(jiraTicket);
const hasDetails = detailText.replace('**Details:**\n', '').trim().length > 0;
log.logDebug('buildChangeApproval', `Built card for ${jiraTicket.issue.key}`);
resolve(buildCardPayload({
jiraTicket,
detailText: hasDetails ? detailText : '',
}));
resolve(buildCardPayload({ jiraTicket, detailLines }));
});
}
export function buildRequestApproval(jiraTicket) {
return new Promise(function (resolve) {
let detailText = '**Details:**\n';
const requestTypeId = getRequestTypeId(jiraTicket);
if (requestTypeId) {
detailText = appendDetailLines(detailText, buildDetailLinesForRequestType(jiraTicket, requestTypeId));
} else if (jiraTicket.issue.fields.customfield_10314) {
detailText += ` - **Person:** ${jiraTicket.issue.fields.customfield_10346}, ${jiraTicket.issue.fields.customfield_10314}`;
if (jiraTicket.issue.fields.customfield_10310) { detailText += ` (${jiraTicket.issue.fields.customfield_10310.value})`; }
detailText += '\n';
if (jiraTicket.issue.fields.customfield_10318) { detailText += ` - **Position:** ${jiraTicket.issue.fields.customfield_10318}\n`; }
}
let detailLines = [];
const hasDetails = detailText.replace('**Details:**\n', '').trim().length > 0;
if (requestTypeId) {
detailLines = buildDetailLinesForRequestType(jiraTicket, requestTypeId);
} else if (jiraTicket.issue.fields.customfield_10314) {
let person = `${jiraTicket.issue.fields.customfield_10346}, ${jiraTicket.issue.fields.customfield_10314}`;
if (jiraTicket.issue.fields.customfield_10310) {
person += ` (${jiraTicket.issue.fields.customfield_10310.value})`;
}
detailLines.push({ label: 'Person', value: person });
if (jiraTicket.issue.fields.customfield_10318) {
detailLines.push({
label: 'Position',
value: jiraTicket.issue.fields.customfield_10318,
});
}
}
let assignmentText = '';
if (jiraTicket.issue.fields.reporter) {
@ -185,7 +230,7 @@ export function buildRequestApproval(jiraTicket) {
resolve(buildCardPayload({
jiraTicket,
detailText: hasDetails ? detailText : '',
detailLines,
assignmentText,
}));
});

79
test/jiraRichText.test.js Normal file
View file

@ -0,0 +1,79 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { adfToPlainText, formatJiraRichText, formatJiraWikiMarkup } from '../lib/jiraRichText.js';
describe('formatJiraWikiMarkup', () => {
it('converts Jira wiki links to readable text', () => {
const input = '[Repo|https://code.ae.com/projects/SCL/repos/web-apps/browse]';
assert.equal(
formatJiraWikiMarkup(input),
'Repo (https://code.ae.com/projects/SCL/repos/web-apps/browse)'
);
});
it('converts duplicate-url smart links to a single URL', () => {
const input = '[https://aeo.atlassian.net/browse/CHANGE-1|https://aeo.atlassian.net/browse/CHANGE-1|smart-link]';
assert.equal(
formatJiraWikiMarkup(input),
'https://aeo.atlassian.net/browse/CHANGE-1'
);
});
it('normalizes bold markers and bullets', () => {
const input = '* Expected behavior\n** Current behavior';
assert.match(formatJiraWikiMarkup(input), /• Expected behavior/);
assert.match(formatJiraWikiMarkup(input), /Current behavior/);
assert.equal(formatJiraWikiMarkup('**Rollback** plan'), 'Rollback plan');
});
});
describe('adfToPlainText', () => {
it('extracts text from ADF documents', () => {
const adf = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Start approx 4am est' }],
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Deploy banner update' }],
},
],
},
],
},
],
};
assert.match(adfToPlainText(adf), /Start approx 4am est/);
assert.match(adfToPlainText(adf), /Deploy banner update/);
});
});
describe('formatJiraRichText', () => {
it('formats wiki strings and ADF objects', () => {
assert.equal(formatJiraRichText('**Rollback** plan'), 'Rollback plan');
assert.equal(
formatJiraRichText({
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Ready for approval' }],
},
],
}),
'Ready for approval'
);
});
});