formatters.ts•8.94 kB
export class JiraFormatters {
/**
* Extract text from Atlassian Document Format (ADF)
* Jira uses ADF for rich text fields like description and comments
* Returns readable text with mentions, links, etc. or raw JSON on parse failure
*/
private static extractTextFromADF(adf: any): string {
if (!adf) return '';
// If it's already a string, return it
if (typeof adf === 'string') return adf;
try {
return this.parseADFNode(adf);
} catch (error) {
// Fallback: return raw ADF JSON if parsing fails
return JSON.stringify(adf, null, 2);
}
}
/**
* Recursively parse ADF nodes
*/
private static parseADFNode(adf: any): string {
if (!adf) return '';
if (typeof adf === 'string') return adf;
let text = '';
// Handle mentions
if (adf.type === 'mention') {
const mentionText = adf.attrs?.text || `@${adf.attrs?.id || 'unknown'}`;
return mentionText;
}
// Handle inline cards (links)
if (adf.type === 'inlineCard') {
const url = adf.attrs?.url || '';
return url;
}
// Handle text nodes with marks (bold, italic, links, etc.)
if (adf.type === 'text' && adf.text) {
text = adf.text;
// Check for link marks
if (adf.marks && Array.isArray(adf.marks)) {
for (const mark of adf.marks) {
if (mark.type === 'link' && mark.attrs?.href) {
text = `[${text}](${mark.attrs.href})`;
}
// You can add more mark types here (bold, italic, etc.)
}
}
return text;
}
// Handle content arrays (recursive)
if (adf.content && Array.isArray(adf.content)) {
for (const node of adf.content) {
text += this.parseADFNode(node);
}
}
// Handle simple text property
if (adf.text && !adf.marks) {
text += adf.text;
}
// Add formatting based on node type
if (adf.type === 'paragraph' && text) {
text += '\n\n';
}
if (adf.type === 'listItem') {
text = '- ' + text.trim() + '\n';
}
if (adf.type === 'heading') {
const level = adf.attrs?.level || 1;
text = '#'.repeat(level) + ' ' + text.trim() + '\n\n';
}
if (adf.type === 'codeBlock') {
text = '```\n' + text.trim() + '\n```\n\n';
}
// Handle hard breaks
if (adf.type === 'hardBreak') {
text = '\n';
}
return text;
}
static formatIssue(issue: any, fieldMetadata?: Map<string, string>): string {
const fields = issue.fields || {};
const key = issue.key;
const summary = fields.summary || 'No summary';
// Handle description (can be ADF object or string)
const description = fields.description
? this.extractTextFromADF(fields.description).trim() || 'No description'
: 'No description';
const status = fields.status?.name || 'Unknown';
const priority = fields.priority?.name || 'None';
const issueType = fields.issuetype?.name || 'Unknown';
const assignee = fields.assignee?.displayName || 'Unassigned';
const reporter = fields.reporter?.displayName || 'Unknown';
const created = fields.created ? new Date(fields.created).toLocaleString() : 'Unknown';
const updated = fields.updated ? new Date(fields.updated).toLocaleString() : 'Unknown';
let formatted = `# ${key}: ${summary}\n\n`;
formatted += `**Type**: ${issueType} | **Status**: ${status} | **Priority**: ${priority}\n`;
formatted += `**Assignee**: ${assignee} | **Reporter**: ${reporter}\n`;
formatted += `**Created**: ${created} | **Updated**: ${updated}\n\n`;
formatted += `## Description\n${description}\n`;
// Add labels if present
if (fields.labels && fields.labels.length > 0) {
formatted += `\n**Labels**: ${fields.labels.join(', ')}\n`;
}
// Add components if present
if (fields.components && fields.components.length > 0) {
const components = fields.components.map((c: any) => c.name).join(', ');
formatted += `**Components**: ${components}\n`;
}
// Add subtasks if present
if (fields.subtasks && fields.subtasks.length > 0) {
formatted += `\n## Subtasks\n`;
fields.subtasks.forEach((subtask: any) => {
formatted += `- ${subtask.key}: ${subtask.fields?.summary || 'No summary'} (${subtask.fields?.status?.name || 'Unknown'})\n`;
});
}
// Add custom fields
const customFields = Object.keys(fields).filter(key => key.startsWith('customfield_'));
if (customFields.length > 0) {
formatted += `\n## Custom Fields\n\n`;
customFields.forEach((fieldKey) => {
const fieldValue = fields[fieldKey];
if (fieldValue !== null && fieldValue !== undefined) {
const formattedValue = this.formatCustomFieldValue(fieldValue);
// Get display name if available
const displayName = fieldMetadata?.get(fieldKey) || fieldKey;
const fieldLabel = displayName !== fieldKey
? `${displayName} (${fieldKey})`
: fieldKey;
formatted += `- **${fieldLabel}**: ${formattedValue}\n`;
}
});
}
return formatted;
}
/**
* Format custom field value based on its type
*/
private static formatCustomFieldValue(value: any): string {
if (value === null || value === undefined) {
return 'N/A';
}
// Handle arrays
if (Array.isArray(value)) {
if (value.length === 0) return 'None';
return value.map(item => {
if (typeof item === 'object' && item !== null) {
// Extract name, value, or displayName
return item.name || item.value || item.displayName || JSON.stringify(item);
}
return String(item);
}).join(', ');
}
// Handle objects
if (typeof value === 'object') {
// Common Jira object patterns
if (value.name) return value.name;
if (value.value) return value.value;
if (value.displayName) return value.displayName;
if (value.emailAddress) return value.emailAddress;
// For complex objects, stringify
return JSON.stringify(value);
}
// Handle primitives
return String(value);
}
static formatIssueList(issues: any[]): string {
if (!issues || issues.length === 0) {
return 'No issues found.';
}
let formatted = `# Found ${issues.length} issue(s)\n\n`;
issues.forEach((issue) => {
const fields = issue.fields || {};
const key = issue.key;
const summary = fields.summary || 'No summary';
const status = fields.status?.name || 'Unknown';
const assignee = fields.assignee?.displayName || 'Unassigned';
formatted += `## ${key}: ${summary}\n`;
formatted += `**Status**: ${status} | **Assignee**: ${assignee}\n\n`;
});
return formatted;
}
static formatComments(comments: any): string {
if (!comments || !comments.comments || comments.comments.length === 0) {
return 'No comments found.';
}
let formatted = `# Comments (${comments.total || comments.comments.length})\n\n`;
comments.comments.forEach((comment: any, index: number) => {
const author = comment.author?.displayName || 'Unknown';
const created = comment.created ? new Date(comment.created).toLocaleString() : 'Unknown';
// Handle comment body (can be ADF object or string)
const body = comment.body
? this.extractTextFromADF(comment.body).trim() || 'No content'
: 'No content';
formatted += `## Comment ${index + 1} - ${author} (${created})\n`;
formatted += `${body}\n\n`;
});
return formatted;
}
static formatTransitions(transitions: any[]): string {
if (!transitions || transitions.length === 0) {
return 'No transitions available.';
}
let formatted = `# Available Transitions\n\n`;
transitions.forEach((transition) => {
const name = transition.name || 'Unknown';
const id = transition.id || 'Unknown';
const to = transition.to?.name || 'Unknown';
formatted += `- **${name}** (ID: ${id}) → ${to}\n`;
});
return formatted;
}
static formatProjects(projects: any[]): string {
if (!projects || projects.length === 0) {
return 'No projects found.';
}
let formatted = `# Found ${projects.length} project(s)\n\n`;
projects.forEach((project) => {
const key = project.key || 'Unknown';
const name = project.name || 'No name';
const projectType = project.projectTypeKey || 'Unknown';
const lead = project.lead?.displayName || 'Unknown';
formatted += `## ${key}: ${name}\n`;
formatted += `**Type**: ${projectType} | **Lead**: ${lead}\n\n`;
});
return formatted;
}
static formatError(error: any): string {
if (typeof error === 'string') {
return `Error: ${error}`;
}
const message = error.message || error.errorMessages?.[0] || 'Unknown error';
return `Error: ${message}`;
}
}