@aaronsb/jira-cloud-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/build/client/jira-client.js +721 -0
- package/build/handlers/board-handlers.js +326 -0
- package/build/handlers/filter-handlers.js +427 -0
- package/build/handlers/issue-handlers.js +311 -0
- package/build/handlers/project-handlers.js +368 -0
- package/build/handlers/resource-handlers.js +320 -0
- package/build/handlers/search-handlers.js +103 -0
- package/build/handlers/sprint-handlers.js +433 -0
- package/build/handlers/tool-resource-handlers.js +1185 -0
- package/build/health-check.js +67 -0
- package/build/index.js +141 -0
- package/build/schemas/request-schemas.js +187 -0
- package/build/schemas/tool-schemas.js +450 -0
- package/build/types/index.js +1 -0
- package/build/utils/formatters/base-formatter.js +58 -0
- package/build/utils/formatters/board-formatter.js +63 -0
- package/build/utils/formatters/filter-formatter.js +66 -0
- package/build/utils/formatters/index.js +7 -0
- package/build/utils/formatters/issue-formatter.js +84 -0
- package/build/utils/formatters/project-formatter.js +55 -0
- package/build/utils/formatters/search-formatter.js +62 -0
- package/build/utils/formatters/sprint-formatter.js +111 -0
- package/build/utils/text-processing.js +343 -0
- package/package.json +65 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
+
export class IssueFormatter {
|
|
3
|
+
/**
|
|
4
|
+
* Format an issue response with the standard structure and optional expansions
|
|
5
|
+
* @param issue The issue data
|
|
6
|
+
* @param options Expansion options
|
|
7
|
+
* @param transitions Optional transitions data (if requested)
|
|
8
|
+
* @returns A formatted issue response
|
|
9
|
+
*/
|
|
10
|
+
static formatIssue(issue, options = {}, transitions) {
|
|
11
|
+
// Create metadata with available expansions
|
|
12
|
+
const metadata = this.createIssueMetadata(issue, options, transitions);
|
|
13
|
+
// Create summary with status and suggested actions
|
|
14
|
+
const summary = this.createIssueSummary(issue, transitions);
|
|
15
|
+
return BaseFormatter.formatResponse(issue, metadata, summary);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create metadata for an issue response
|
|
19
|
+
*/
|
|
20
|
+
static createIssueMetadata(issue, options, transitions) {
|
|
21
|
+
// Determine which expansions are available but not included
|
|
22
|
+
const availableExpansions = [];
|
|
23
|
+
if (!options.comments && issue.comments === undefined) {
|
|
24
|
+
availableExpansions.push('comments');
|
|
25
|
+
}
|
|
26
|
+
if (!options.transitions && transitions === undefined) {
|
|
27
|
+
availableExpansions.push('transitions');
|
|
28
|
+
}
|
|
29
|
+
if (!options.attachments && issue.attachments === undefined) {
|
|
30
|
+
availableExpansions.push('attachments');
|
|
31
|
+
}
|
|
32
|
+
if (!options.related_issues) {
|
|
33
|
+
availableExpansions.push('related_issues');
|
|
34
|
+
}
|
|
35
|
+
if (!options.history) {
|
|
36
|
+
availableExpansions.push('history');
|
|
37
|
+
}
|
|
38
|
+
// Create related entities map
|
|
39
|
+
const related = {};
|
|
40
|
+
if (issue.parent) {
|
|
41
|
+
related.parent = issue.parent;
|
|
42
|
+
}
|
|
43
|
+
// Extract related issues from issue links
|
|
44
|
+
const relatedIssues = issue.issueLinks
|
|
45
|
+
.map(link => link.outward || link.inward)
|
|
46
|
+
.filter((key) => key !== null);
|
|
47
|
+
if (relatedIssues.length > 0) {
|
|
48
|
+
related.linked_issues = relatedIssues;
|
|
49
|
+
}
|
|
50
|
+
return BaseFormatter.createMetadata({
|
|
51
|
+
expansions: availableExpansions,
|
|
52
|
+
related
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create a summary for an issue response
|
|
57
|
+
*/
|
|
58
|
+
static createIssueSummary(issue, transitions) {
|
|
59
|
+
const suggestedActions = [];
|
|
60
|
+
// Add suggested actions based on available transitions
|
|
61
|
+
if (transitions && transitions.length > 0) {
|
|
62
|
+
// Common transitions to suggest
|
|
63
|
+
const commonTransitions = ['Done', 'In Progress', 'To Do', 'Closed', 'Resolved'];
|
|
64
|
+
for (const transitionName of commonTransitions) {
|
|
65
|
+
const transition = transitions.find(t => t.name === transitionName);
|
|
66
|
+
if (transition) {
|
|
67
|
+
suggestedActions.push({
|
|
68
|
+
text: `Move to ${transitionName}`,
|
|
69
|
+
action_id: transition.id
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Add assignment suggestion if not assigned
|
|
75
|
+
if (!issue.assignee) {
|
|
76
|
+
suggestedActions.push({
|
|
77
|
+
text: 'Assign to team member'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return BaseFormatter.createSummary({
|
|
81
|
+
suggested_actions: suggestedActions
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
+
export class ProjectFormatter {
|
|
3
|
+
/**
|
|
4
|
+
* Format a project response with the standard structure and optional expansions
|
|
5
|
+
* @param project The project data
|
|
6
|
+
* @param options Expansion options
|
|
7
|
+
* @returns A formatted project response
|
|
8
|
+
*/
|
|
9
|
+
static formatProject(project, options = {}) {
|
|
10
|
+
// Create metadata with available expansions
|
|
11
|
+
const metadata = this.createProjectMetadata(project, options);
|
|
12
|
+
// Create summary with status counts
|
|
13
|
+
const summary = this.createProjectSummary(project);
|
|
14
|
+
return BaseFormatter.formatResponse(project, metadata, summary);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create metadata for a project response
|
|
18
|
+
*/
|
|
19
|
+
static createProjectMetadata(project, options) {
|
|
20
|
+
// Determine which expansions are available but not included
|
|
21
|
+
const availableExpansions = [];
|
|
22
|
+
if (!options.boards) {
|
|
23
|
+
availableExpansions.push('boards');
|
|
24
|
+
}
|
|
25
|
+
if (!options.components) {
|
|
26
|
+
availableExpansions.push('components');
|
|
27
|
+
}
|
|
28
|
+
if (!options.versions) {
|
|
29
|
+
availableExpansions.push('versions');
|
|
30
|
+
}
|
|
31
|
+
if (!options.recent_issues) {
|
|
32
|
+
availableExpansions.push('recent_issues');
|
|
33
|
+
}
|
|
34
|
+
return BaseFormatter.createMetadata({
|
|
35
|
+
expansions: availableExpansions
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a summary for a project response
|
|
40
|
+
*/
|
|
41
|
+
static createProjectSummary(project) {
|
|
42
|
+
const suggestedActions = [
|
|
43
|
+
{
|
|
44
|
+
text: `View all issues in ${project.key}`
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
text: `Create issue in ${project.key}`
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
return BaseFormatter.createSummary({
|
|
51
|
+
status_counts: project.status_counts,
|
|
52
|
+
suggested_actions: suggestedActions
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
+
export class SearchFormatter {
|
|
3
|
+
/**
|
|
4
|
+
* Format a search response with the standard structure and optional expansions
|
|
5
|
+
* @param searchResult The search result data
|
|
6
|
+
* @param options Expansion options
|
|
7
|
+
* @returns A formatted search response
|
|
8
|
+
*/
|
|
9
|
+
static formatSearchResult(searchResult, options = {}) {
|
|
10
|
+
// Create metadata with available expansions and pagination
|
|
11
|
+
const metadata = this.createSearchMetadata(searchResult, options);
|
|
12
|
+
// Create summary with status counts
|
|
13
|
+
const summary = this.createSearchSummary(searchResult);
|
|
14
|
+
return BaseFormatter.formatResponse(searchResult, metadata, summary);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create metadata for a search response
|
|
18
|
+
*/
|
|
19
|
+
static createSearchMetadata(searchResult, options) {
|
|
20
|
+
// Determine which expansions are available but not included
|
|
21
|
+
const availableExpansions = [];
|
|
22
|
+
if (!options.issue_details) {
|
|
23
|
+
availableExpansions.push('issue_details');
|
|
24
|
+
}
|
|
25
|
+
if (!options.transitions) {
|
|
26
|
+
availableExpansions.push('transitions');
|
|
27
|
+
}
|
|
28
|
+
if (!options.comments_preview) {
|
|
29
|
+
availableExpansions.push('comments_preview');
|
|
30
|
+
}
|
|
31
|
+
return BaseFormatter.createMetadata({
|
|
32
|
+
expansions: availableExpansions,
|
|
33
|
+
pagination: {
|
|
34
|
+
startAt: searchResult.pagination.startAt,
|
|
35
|
+
maxResults: searchResult.pagination.maxResults,
|
|
36
|
+
total: searchResult.pagination.total
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a summary for a search response
|
|
42
|
+
*/
|
|
43
|
+
static createSearchSummary(searchResult) {
|
|
44
|
+
// Count issues by status
|
|
45
|
+
const statusCounts = {};
|
|
46
|
+
for (const issue of searchResult.issues) {
|
|
47
|
+
const status = issue.status;
|
|
48
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
49
|
+
}
|
|
50
|
+
const suggestedActions = [];
|
|
51
|
+
// Add pagination actions if there are more results
|
|
52
|
+
if (searchResult.pagination.hasMore) {
|
|
53
|
+
suggestedActions.push({
|
|
54
|
+
text: 'Load more results'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return BaseFormatter.createSummary({
|
|
58
|
+
status_counts: statusCounts,
|
|
59
|
+
suggested_actions: suggestedActions
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
+
export class SprintFormatter {
|
|
3
|
+
/**
|
|
4
|
+
* Format a sprint response with the standard structure and optional expansions
|
|
5
|
+
* @param sprint The sprint data
|
|
6
|
+
* @param options Expansion options
|
|
7
|
+
* @returns A formatted sprint response
|
|
8
|
+
*/
|
|
9
|
+
static formatSprint(sprint, options = {}) {
|
|
10
|
+
// Create metadata with available expansions
|
|
11
|
+
const metadata = this.createSprintMetadata(sprint, options);
|
|
12
|
+
// Create summary
|
|
13
|
+
const summary = this.createSprintSummary(sprint);
|
|
14
|
+
return BaseFormatter.formatResponse(sprint, metadata, summary);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Format a list of sprints
|
|
18
|
+
* @param sprints Array of sprint data
|
|
19
|
+
* @param pagination Pagination information
|
|
20
|
+
* @returns A formatted response with sprints array
|
|
21
|
+
*/
|
|
22
|
+
static formatSprintList(sprints, pagination) {
|
|
23
|
+
// Create metadata
|
|
24
|
+
const metadata = {};
|
|
25
|
+
if (pagination) {
|
|
26
|
+
metadata.pagination = {
|
|
27
|
+
startAt: pagination.startAt,
|
|
28
|
+
maxResults: pagination.maxResults,
|
|
29
|
+
total: pagination.total,
|
|
30
|
+
hasMore: pagination.startAt + pagination.maxResults < pagination.total,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Create summary with status counts
|
|
34
|
+
const statusCounts = {
|
|
35
|
+
future: 0,
|
|
36
|
+
active: 0,
|
|
37
|
+
closed: 0,
|
|
38
|
+
};
|
|
39
|
+
sprints.forEach(sprint => {
|
|
40
|
+
if (sprint.state && statusCounts[sprint.state] !== undefined) {
|
|
41
|
+
statusCounts[sprint.state]++;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const summary = {
|
|
45
|
+
status_counts: statusCounts,
|
|
46
|
+
suggested_actions: [
|
|
47
|
+
{ text: 'Create new sprint', action_id: 'create_sprint' },
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
return BaseFormatter.formatResponse(sprints, metadata, summary);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create metadata for a sprint response
|
|
54
|
+
*/
|
|
55
|
+
static createSprintMetadata(sprint, options) {
|
|
56
|
+
// Determine which expansions are available but not included
|
|
57
|
+
const availableExpansions = [];
|
|
58
|
+
if (!options.issues && !sprint.issues) {
|
|
59
|
+
availableExpansions.push('issues');
|
|
60
|
+
}
|
|
61
|
+
if (!options.report && !sprint.report) {
|
|
62
|
+
availableExpansions.push('report');
|
|
63
|
+
}
|
|
64
|
+
// Create related entities map
|
|
65
|
+
const related = {
|
|
66
|
+
board: sprint.boardId.toString(),
|
|
67
|
+
};
|
|
68
|
+
return BaseFormatter.createMetadata({
|
|
69
|
+
expansions: availableExpansions,
|
|
70
|
+
related
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a summary for a sprint response
|
|
75
|
+
*/
|
|
76
|
+
static createSprintSummary(sprint) {
|
|
77
|
+
const suggestedActions = [];
|
|
78
|
+
// Add suggested actions based on sprint state
|
|
79
|
+
switch (sprint.state) {
|
|
80
|
+
case 'future':
|
|
81
|
+
suggestedActions.push({ text: 'Start Sprint', action_id: 'start_sprint' });
|
|
82
|
+
suggestedActions.push({ text: 'Add Issues to Sprint', action_id: 'add_issues' });
|
|
83
|
+
suggestedActions.push({ text: 'Edit Sprint', action_id: 'update_sprint' });
|
|
84
|
+
break;
|
|
85
|
+
case 'active':
|
|
86
|
+
suggestedActions.push({ text: 'Complete Sprint', action_id: 'complete_sprint' });
|
|
87
|
+
suggestedActions.push({ text: 'Add Issues to Sprint', action_id: 'add_issues' });
|
|
88
|
+
suggestedActions.push({ text: 'Remove Issues from Sprint', action_id: 'remove_issues' });
|
|
89
|
+
suggestedActions.push({ text: 'Edit Sprint', action_id: 'update_sprint' });
|
|
90
|
+
break;
|
|
91
|
+
case 'closed':
|
|
92
|
+
suggestedActions.push({ text: 'View Sprint Report', action_id: 'view_report' });
|
|
93
|
+
suggestedActions.push({ text: 'Create New Sprint', action_id: 'create_sprint' });
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
// Add status counts if issues are available
|
|
97
|
+
const statusCounts = {};
|
|
98
|
+
if (sprint.issues && sprint.issues.length > 0) {
|
|
99
|
+
sprint.issues.forEach(issue => {
|
|
100
|
+
if (!statusCounts[issue.status]) {
|
|
101
|
+
statusCounts[issue.status] = 0;
|
|
102
|
+
}
|
|
103
|
+
statusCounts[issue.status]++;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return BaseFormatter.createSummary({
|
|
107
|
+
status_counts: Object.keys(statusCounts).length > 0 ? statusCounts : undefined,
|
|
108
|
+
suggested_actions: suggestedActions
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it';
|
|
2
|
+
export class TextProcessor {
|
|
3
|
+
static md = new MarkdownIt();
|
|
4
|
+
static markdownToAdf(markdown) {
|
|
5
|
+
const tokens = TextProcessor.md.parse(markdown, {});
|
|
6
|
+
const content = [];
|
|
7
|
+
let currentListItems = [];
|
|
8
|
+
let isInList = false;
|
|
9
|
+
let listType = null;
|
|
10
|
+
for (const token of tokens) {
|
|
11
|
+
switch (token.type) {
|
|
12
|
+
case 'heading_open':
|
|
13
|
+
// Start a new heading block
|
|
14
|
+
content.push({
|
|
15
|
+
type: 'heading',
|
|
16
|
+
attrs: { level: parseInt(token.tag.slice(1)) },
|
|
17
|
+
content: []
|
|
18
|
+
});
|
|
19
|
+
break;
|
|
20
|
+
case 'heading_close':
|
|
21
|
+
break;
|
|
22
|
+
case 'paragraph_open':
|
|
23
|
+
if (!isInList) {
|
|
24
|
+
content.push({
|
|
25
|
+
type: 'paragraph',
|
|
26
|
+
content: []
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case 'paragraph_close':
|
|
31
|
+
break;
|
|
32
|
+
case 'bullet_list_open':
|
|
33
|
+
case 'ordered_list_open':
|
|
34
|
+
isInList = true;
|
|
35
|
+
listType = token.type === 'bullet_list_open' ? 'bulletList' : 'orderedList';
|
|
36
|
+
currentListItems = [];
|
|
37
|
+
break;
|
|
38
|
+
case 'bullet_list_close':
|
|
39
|
+
case 'ordered_list_close':
|
|
40
|
+
if (currentListItems.length > 0) {
|
|
41
|
+
content.push({
|
|
42
|
+
type: listType,
|
|
43
|
+
content: currentListItems
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
isInList = false;
|
|
47
|
+
listType = null;
|
|
48
|
+
currentListItems = [];
|
|
49
|
+
break;
|
|
50
|
+
case 'list_item_open':
|
|
51
|
+
currentListItems.push({
|
|
52
|
+
type: 'listItem',
|
|
53
|
+
content: [{
|
|
54
|
+
type: 'paragraph',
|
|
55
|
+
content: []
|
|
56
|
+
}]
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
case 'list_item_close':
|
|
60
|
+
break;
|
|
61
|
+
case 'inline':
|
|
62
|
+
const lastBlock = isInList
|
|
63
|
+
? currentListItems[currentListItems.length - 1].content[0]
|
|
64
|
+
: content[content.length - 1];
|
|
65
|
+
if (!lastBlock)
|
|
66
|
+
continue;
|
|
67
|
+
let currentText = '';
|
|
68
|
+
let marks = [];
|
|
69
|
+
for (let i = 0; i < token.children.length; i++) {
|
|
70
|
+
const child = token.children[i];
|
|
71
|
+
if (child.type === 'text') {
|
|
72
|
+
if (currentText && marks.length > 0) {
|
|
73
|
+
lastBlock.content.push({
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: currentText,
|
|
76
|
+
marks
|
|
77
|
+
});
|
|
78
|
+
currentText = '';
|
|
79
|
+
marks = [];
|
|
80
|
+
}
|
|
81
|
+
currentText = child.content;
|
|
82
|
+
}
|
|
83
|
+
else if (child.type === 'strong_open') {
|
|
84
|
+
if (currentText) {
|
|
85
|
+
lastBlock.content.push({
|
|
86
|
+
type: 'text',
|
|
87
|
+
text: currentText
|
|
88
|
+
});
|
|
89
|
+
currentText = '';
|
|
90
|
+
}
|
|
91
|
+
marks.push({ type: 'strong' });
|
|
92
|
+
}
|
|
93
|
+
else if (child.type === 'em_open') {
|
|
94
|
+
if (currentText) {
|
|
95
|
+
lastBlock.content.push({
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: currentText
|
|
98
|
+
});
|
|
99
|
+
currentText = '';
|
|
100
|
+
}
|
|
101
|
+
marks.push({ type: 'em' });
|
|
102
|
+
}
|
|
103
|
+
else if (child.type === 'link_open') {
|
|
104
|
+
if (currentText) {
|
|
105
|
+
lastBlock.content.push({
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: currentText
|
|
108
|
+
});
|
|
109
|
+
currentText = '';
|
|
110
|
+
}
|
|
111
|
+
marks.push({
|
|
112
|
+
type: 'link',
|
|
113
|
+
attrs: {
|
|
114
|
+
href: child.attrs[0][1]
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else if (child.type === 'code_inline') {
|
|
119
|
+
if (currentText) {
|
|
120
|
+
lastBlock.content.push({
|
|
121
|
+
type: 'text',
|
|
122
|
+
text: currentText
|
|
123
|
+
});
|
|
124
|
+
currentText = '';
|
|
125
|
+
}
|
|
126
|
+
lastBlock.content.push({
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: child.content,
|
|
129
|
+
marks: [{ type: 'code' }]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (currentText) {
|
|
134
|
+
lastBlock.content.push({
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: currentText,
|
|
137
|
+
...(marks.length > 0 && { marks })
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case 'hr':
|
|
142
|
+
content.push({
|
|
143
|
+
type: 'rule'
|
|
144
|
+
});
|
|
145
|
+
break;
|
|
146
|
+
case 'hardbreak':
|
|
147
|
+
const lastContent = content[content.length - 1];
|
|
148
|
+
if (lastContent && lastContent.content) {
|
|
149
|
+
lastContent.content.push({
|
|
150
|
+
type: 'hardBreak'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
type: 'doc',
|
|
158
|
+
version: 1,
|
|
159
|
+
content
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
static extractTextFromAdf(node) {
|
|
163
|
+
if (!node)
|
|
164
|
+
return '';
|
|
165
|
+
if (node.type === 'text') {
|
|
166
|
+
return node.text || '';
|
|
167
|
+
}
|
|
168
|
+
if (node.type === 'mention') {
|
|
169
|
+
return `@${node.attrs?.text?.replace('@', '') || ''}`;
|
|
170
|
+
}
|
|
171
|
+
if (node.type === 'hardBreak' || node.type === 'paragraph') {
|
|
172
|
+
return '\n';
|
|
173
|
+
}
|
|
174
|
+
if (node.content) {
|
|
175
|
+
return node.content
|
|
176
|
+
.map((child) => TextProcessor.extractTextFromAdf(child))
|
|
177
|
+
.join('')
|
|
178
|
+
.replace(/\n{3,}/g, '\n\n'); // Normalize multiple newlines
|
|
179
|
+
}
|
|
180
|
+
return '';
|
|
181
|
+
}
|
|
182
|
+
static isFieldPopulated(value) {
|
|
183
|
+
if (value === null || value === undefined)
|
|
184
|
+
return false;
|
|
185
|
+
if (typeof value === 'string' && value.trim() === '')
|
|
186
|
+
return false;
|
|
187
|
+
if (Array.isArray(value) && value.length === 0)
|
|
188
|
+
return false;
|
|
189
|
+
if (typeof value === 'object' && Object.keys(value).length === 0)
|
|
190
|
+
return false;
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
static shouldExcludeField(fieldId, fieldValue) {
|
|
194
|
+
// Exclude system metadata and UI-specific fields
|
|
195
|
+
const excludePatterns = [
|
|
196
|
+
'avatar', 'icon', 'self', 'thumbnail', 'timetracking', 'worklog',
|
|
197
|
+
'watches', 'subtasks', 'attachment', 'aggregateprogress', 'progress',
|
|
198
|
+
'votes', '_links', 'accountId', 'emailAddress', 'active', 'timeZone',
|
|
199
|
+
'accountType', '_expands', 'groupIds', 'portalId', 'serviceDeskId',
|
|
200
|
+
'issueTypeId', 'renderedFields', 'names', 'id', 'expand', 'schema',
|
|
201
|
+
'operations', 'editmeta', 'changelog', 'versionedRepresentations',
|
|
202
|
+
'fieldsToInclude', 'properties', 'updateAuthor', 'jsdPublic', 'mediaType',
|
|
203
|
+
'maxResults', 'total', 'startAt', 'iconUrls', 'issuerestrictions',
|
|
204
|
+
'shouldDisplay', 'nonEditableReason', 'hasEpicLinkFieldDependency',
|
|
205
|
+
'showField', 'statusDate', 'statusCategory', 'collection', 'localId',
|
|
206
|
+
'attrs', 'marks', 'layout', 'version', 'type', 'content', 'table',
|
|
207
|
+
'tableRow', 'tableCell', 'mediaSingle', 'media', 'heading', 'paragraph',
|
|
208
|
+
'bulletList', 'listItem', 'orderedList', 'rule', 'inlineCard', 'hardBreak',
|
|
209
|
+
'workRatio', 'parentLink', 'restrictTo', 'timeToResolution',
|
|
210
|
+
'timeToFirstResponse', 'slaForInitialResponse'
|
|
211
|
+
];
|
|
212
|
+
// Also exclude email signature related fields and meaningless values
|
|
213
|
+
if (typeof fieldValue === 'string') {
|
|
214
|
+
// Email signature patterns
|
|
215
|
+
const emailPatterns = [
|
|
216
|
+
'CAUTION:', 'From:', 'Sent:', 'To:', 'Subject:',
|
|
217
|
+
'Book time to meet with me', 'Best-', 'Best regards',
|
|
218
|
+
'Kind regards', 'Regards,', 'Mobile', 'Phone', 'Tel:',
|
|
219
|
+
'www.', 'http://', 'https://', '@.*\\.com$', '^M:',
|
|
220
|
+
'LLC', 'Inc.', 'Ltd.', 'ForefrontDermatology.com',
|
|
221
|
+
'Mobile:', 'Office:', 'Direct:'
|
|
222
|
+
];
|
|
223
|
+
// Check for email patterns
|
|
224
|
+
if (emailPatterns.some(pattern => pattern.startsWith('^') || pattern.endsWith('$')
|
|
225
|
+
? new RegExp(pattern).test(fieldValue)
|
|
226
|
+
: fieldValue.includes(pattern))) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
// Exclude meaningless values
|
|
230
|
+
if (fieldValue === '-1' ||
|
|
231
|
+
fieldValue === 'false false' ||
|
|
232
|
+
fieldValue === '0' ||
|
|
233
|
+
fieldValue === 'true, ' ||
|
|
234
|
+
fieldValue === '.' ||
|
|
235
|
+
/^\s*$/.test(fieldValue)) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Exclude fields that are just punctuation or very short text
|
|
240
|
+
if (typeof fieldValue === 'string' &&
|
|
241
|
+
(fieldValue.trim().length <= 1 ||
|
|
242
|
+
fieldValue.trim() === '.' ||
|
|
243
|
+
fieldValue.trim() === '-' ||
|
|
244
|
+
fieldValue.trim() === '_')) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
return excludePatterns.some(pattern => fieldId.toLowerCase().includes(pattern.toLowerCase()));
|
|
248
|
+
}
|
|
249
|
+
static formatFieldValue(value, fieldName) {
|
|
250
|
+
if (value === null || value === undefined)
|
|
251
|
+
return '';
|
|
252
|
+
// Handle arrays
|
|
253
|
+
if (Array.isArray(value)) {
|
|
254
|
+
// Special handling for comments
|
|
255
|
+
if (fieldName === 'Comment' || fieldName === 'comments') {
|
|
256
|
+
return value
|
|
257
|
+
.map(comment => {
|
|
258
|
+
const author = comment.author?.displayName || 'Unknown';
|
|
259
|
+
let body = '';
|
|
260
|
+
// Handle rich text content
|
|
261
|
+
if (comment.body?.content) {
|
|
262
|
+
body = TextProcessor.extractTextFromAdf(comment.body);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
body = String(comment.body || '');
|
|
266
|
+
}
|
|
267
|
+
// Clean up email signatures and formatting from body
|
|
268
|
+
body = body
|
|
269
|
+
.replace(/^[\s\S]*?From:[\s\S]*?Sent:[\s\S]*?To:[\s\S]*?Subject:[\s\S]*?\n/gm, '')
|
|
270
|
+
.replace(/^>.*$/gm, '')
|
|
271
|
+
.replace(/_{3,}|-{3,}|={3,}/g, '')
|
|
272
|
+
.replace(/(?:(?:https?|ftp):\/\/|\b(?:[a-z\d]+\.))(?:(?:[^\s()<>]+|\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))?\))+(?:\((?:[^\s()<>]+|(?:\(?:[^\s()<>]+\)))?\)|[^\s`!()[\]{};:'".,<>?«»""'']))?/g, '')
|
|
273
|
+
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '')
|
|
274
|
+
.replace(/(?:^|\s)(?:Best regards|Kind regards|Regards|Best|Thanks|Thank you|Cheers),.*/gs, '')
|
|
275
|
+
.replace(/(?:Mobile|Tel|Phone|Office|Direct):\s*[\d\s.+-]+/g, '')
|
|
276
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
277
|
+
.trim();
|
|
278
|
+
if (!body)
|
|
279
|
+
return '';
|
|
280
|
+
const created = new Date(comment.created).toLocaleString();
|
|
281
|
+
return `${author} (${created}):\n${body}`;
|
|
282
|
+
})
|
|
283
|
+
.filter(comment => comment)
|
|
284
|
+
.join('\n\n');
|
|
285
|
+
}
|
|
286
|
+
return value
|
|
287
|
+
.map(item => TextProcessor.formatFieldValue(item))
|
|
288
|
+
.filter(item => item)
|
|
289
|
+
.join(', ');
|
|
290
|
+
}
|
|
291
|
+
// Handle objects
|
|
292
|
+
if (typeof value === 'object') {
|
|
293
|
+
// Handle user objects
|
|
294
|
+
if (value.displayName) {
|
|
295
|
+
return value.displayName;
|
|
296
|
+
}
|
|
297
|
+
// Handle request type
|
|
298
|
+
if (value.requestType?.name) {
|
|
299
|
+
const desc = value.requestType.description ?
|
|
300
|
+
': ' + value.requestType.description.split('.')[0] + '.' : '';
|
|
301
|
+
return `${value.requestType.name}${desc}`;
|
|
302
|
+
}
|
|
303
|
+
// Handle status objects
|
|
304
|
+
if (value.status && value.statusCategory) {
|
|
305
|
+
return `${value.status} (${value.statusCategory})`;
|
|
306
|
+
}
|
|
307
|
+
// Handle rich text content
|
|
308
|
+
if (value.content) {
|
|
309
|
+
return TextProcessor.extractTextFromAdf(value);
|
|
310
|
+
}
|
|
311
|
+
// Handle simple name/value objects
|
|
312
|
+
if (value.name) {
|
|
313
|
+
return value.name;
|
|
314
|
+
}
|
|
315
|
+
if (value.value) {
|
|
316
|
+
return value.value;
|
|
317
|
+
}
|
|
318
|
+
// For other objects, try to extract meaningful values
|
|
319
|
+
const meaningful = Object.entries(value)
|
|
320
|
+
.filter(([_k, v]) => !TextProcessor.shouldExcludeField(_k, v) &&
|
|
321
|
+
v !== null &&
|
|
322
|
+
v !== undefined &&
|
|
323
|
+
!_k.startsWith('_'))
|
|
324
|
+
.map(([_k, v]) => TextProcessor.formatFieldValue(v))
|
|
325
|
+
.filter(v => v)
|
|
326
|
+
.join(' ');
|
|
327
|
+
return meaningful || '';
|
|
328
|
+
}
|
|
329
|
+
// Format dates
|
|
330
|
+
if (fieldName && (fieldName.toLowerCase().includes('date') ||
|
|
331
|
+
fieldName.toLowerCase().includes('created') ||
|
|
332
|
+
fieldName.toLowerCase().includes('updated'))) {
|
|
333
|
+
try {
|
|
334
|
+
return new Date(value).toLocaleString();
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return String(value);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Handle primitive values
|
|
341
|
+
return String(value);
|
|
342
|
+
}
|
|
343
|
+
}
|