buddy-workbench 0.1.85 → 0.1.87
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/package.json +12 -1
- package/server/config.js +2 -0
- package/server/repositories/mind-maps.js +87 -0
- package/server/routes/mind-maps.js +158 -0
- package/server/routes/pr-conflicts.js +79 -7
- package/server.js +2 -0
- package/ui/dist/assets/index-B1yWhudx.css +1 -0
- package/ui/dist/assets/index-DgxtZMu4.js +590 -0
- package/ui/dist/assets/{index-Dty-56mC.js → index-_vMuByUV.js} +3 -3
- package/ui/dist/assets/xmindAdapter-uxrJ6A4P.js +16 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-BGlY7rb1.css +0 -1
- package/ui/dist/assets/index-DqqDpL-U.js +0 -586
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.87",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,17 @@
|
|
|
24
24
|
"version": "node -e \"const v=process.env.npm_package_version; const fs=require('fs'); ['ui/package.json', 'ui/package-lock.json'].forEach(p=>{if(fs.existsSync(p)){const j=JSON.parse(fs.readFileSync(p)); j.version=v; if(j.packages&&j.packages['']){j.packages[''].version=v;} fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');}});\" && git add ui/package.json ui/package-lock.json"
|
|
25
25
|
},
|
|
26
26
|
"devbuddyChangelog": [
|
|
27
|
+
{
|
|
28
|
+
"version": "0.1.87",
|
|
29
|
+
"name": "Mind maps and test case workflow",
|
|
30
|
+
"notes": [
|
|
31
|
+
"Added editable Mind maps list and detail routes with XMind, JSON, and Markdown import/export, plus one JSON data file per map.",
|
|
32
|
+
"Added a left-to-right canvas with centered map positioning, expanded pan space, stable zoom controls, canvas panning, and expand-all/collapse-all actions.",
|
|
33
|
+
"Added Jira issue settings with key or link import, summary lookup, key/summary search, clickable issue cards, node tagging and untagging, and tagged-state styling.",
|
|
34
|
+
"Added node workflow controls for tested status cascading, moving a node subtree with an Ant Design Cascader, and confirmation before deleting maps or ideas.",
|
|
35
|
+
"Refined Mind maps headers, inspector controls, node spacing, Jira presentation, and canvas layout to support test case planning more clearly."
|
|
36
|
+
]
|
|
37
|
+
},
|
|
27
38
|
{
|
|
28
39
|
"version": "0.1.42",
|
|
29
40
|
"name": "Jira Workspace and navigation improvements",
|
package/server/config.js
CHANGED
|
@@ -36,6 +36,8 @@ export const paths = {
|
|
|
36
36
|
branchSync: join(userDataDir, 'branch-sync.json'),
|
|
37
37
|
packageUpgrade: join(userDataDir, 'package-upgrade.json'),
|
|
38
38
|
presentations: join(userDataDir, 'presentations.json'),
|
|
39
|
+
mindMaps: join(userDataDir, 'mind-maps'),
|
|
40
|
+
legacyMindMaps: join(userDataDir, 'mind-maps.json'),
|
|
39
41
|
shutdownLog: join(userDataDir, 'shutdown.log'),
|
|
40
42
|
clipboardDir: join(userDataDir, 'clipboard'),
|
|
41
43
|
plugins: join(root, 'plugins'),
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { paths } from '../config.js';
|
|
11
|
+
|
|
12
|
+
const getMapFilename = (id) => `${encodeURIComponent(String(id))}.json`;
|
|
13
|
+
|
|
14
|
+
function ensureMindMapsDir() {
|
|
15
|
+
mkdirSync(paths.mindMaps, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mapFiles() {
|
|
19
|
+
if (!existsSync(paths.mindMaps)) return [];
|
|
20
|
+
|
|
21
|
+
return readdirSync(paths.mindMaps, { withFileTypes: true })
|
|
22
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
23
|
+
.map((entry) => entry.name)
|
|
24
|
+
.sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeMindMapFile(map) {
|
|
28
|
+
if (!map?.id) return;
|
|
29
|
+
writeFileSync(
|
|
30
|
+
join(paths.mindMaps, getMapFilename(map.id)),
|
|
31
|
+
JSON.stringify(map, null, 2),
|
|
32
|
+
'utf8'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function migrateLegacyMindMaps() {
|
|
37
|
+
if (!existsSync(paths.legacyMindMaps)) return;
|
|
38
|
+
|
|
39
|
+
ensureMindMapsDir();
|
|
40
|
+
const existingFiles = mapFiles();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(readFileSync(paths.legacyMindMaps, 'utf8'));
|
|
44
|
+
if (!existingFiles.length && Array.isArray(parsed)) {
|
|
45
|
+
parsed.forEach(writeMindMapFile);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Remove an invalid legacy file so it cannot block the new storage format.
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
unlinkSync(paths.legacyMindMaps);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function listMindMaps() {
|
|
55
|
+
try {
|
|
56
|
+
migrateLegacyMindMaps();
|
|
57
|
+
return mapFiles().flatMap((filename) => {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(readFileSync(join(paths.mindMaps, filename), 'utf8'));
|
|
60
|
+
return parsed && typeof parsed === 'object' ? [parsed] : [];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveMindMaps(mindMaps) {
|
|
71
|
+
migrateLegacyMindMaps();
|
|
72
|
+
ensureMindMapsDir();
|
|
73
|
+
|
|
74
|
+
const expectedFiles = new Set();
|
|
75
|
+
for (const map of Array.isArray(mindMaps) ? mindMaps : []) {
|
|
76
|
+
if (!map?.id) continue;
|
|
77
|
+
const filename = getMapFilename(map.id);
|
|
78
|
+
expectedFiles.add(filename);
|
|
79
|
+
writeMindMapFile(map);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const filename of mapFiles()) {
|
|
83
|
+
if (!expectedFiles.has(filename)) {
|
|
84
|
+
unlinkSync(join(paths.mindMaps, filename));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import { readSettings } from '../repositories/settings.js';
|
|
6
|
+
import { listMindMaps, saveMindMaps } from '../repositories/mind-maps.js';
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
const httpClient = axios.create({
|
|
10
|
+
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
|
11
|
+
validateStatus: () => true,
|
|
12
|
+
timeout: 15000
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const makeId = () => crypto.randomUUID();
|
|
16
|
+
|
|
17
|
+
function getJiraHost(domain) {
|
|
18
|
+
const clean = String(domain || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '');
|
|
19
|
+
if (!clean) return '';
|
|
20
|
+
return clean.includes('jira') || clean.includes('.atlassian.net') ? clean : `jira.${clean}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractJiraKey(value) {
|
|
24
|
+
const match = String(value || '').match(/(?:\/browse\/|\b)([A-Z][A-Z0-9_]*-\d+)\b/i);
|
|
25
|
+
return match?.[1]?.toUpperCase() || '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function resolveJiraIssue(value) {
|
|
29
|
+
const input = String(value || '').trim();
|
|
30
|
+
const key = extractJiraKey(input);
|
|
31
|
+
if (!key) throw new Error('Enter a valid Jira No or Jira issue link.');
|
|
32
|
+
|
|
33
|
+
const settings = readSettings();
|
|
34
|
+
const host = getJiraHost(settings.domain);
|
|
35
|
+
if (!host) throw new Error('Jira domain is not configured. Please set Domain in Settings.');
|
|
36
|
+
|
|
37
|
+
const headers = { Accept: 'application/json' };
|
|
38
|
+
if (settings.jiraAccessToken) {
|
|
39
|
+
headers.Authorization = settings.jiraAccessToken.startsWith('Bearer ')
|
|
40
|
+
? settings.jiraAccessToken
|
|
41
|
+
: `Bearer ${settings.jiraAccessToken}`;
|
|
42
|
+
}
|
|
43
|
+
const response = await httpClient.get(
|
|
44
|
+
`https://${host}/rest/api/2/issue/${encodeURIComponent(key)}?fields=summary`,
|
|
45
|
+
{ headers }
|
|
46
|
+
);
|
|
47
|
+
if (response.status < 200 || response.status >= 300) {
|
|
48
|
+
const detail = response.data?.errorMessages?.[0] || response.data?.message || response.statusText;
|
|
49
|
+
throw new Error(detail || `Jira returned status ${response.status}.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const summary = String(response.data?.fields?.summary || '').trim();
|
|
53
|
+
if (!summary) throw new Error(`Jira issue ${key} has no summary.`);
|
|
54
|
+
return {
|
|
55
|
+
id: String(response.data?.id || key),
|
|
56
|
+
key: String(response.data?.key || key).toUpperCase(),
|
|
57
|
+
summary,
|
|
58
|
+
url: /^https?:\/\//i.test(input) ? input : `https://${host}/browse/${encodeURIComponent(key)}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeNode(node, fallbackText = 'Untitled idea') {
|
|
63
|
+
const text = String(node?.text ?? node?.label ?? fallbackText).trim() || fallbackText;
|
|
64
|
+
const x = Number(node?.position?.x);
|
|
65
|
+
const y = Number(node?.position?.y);
|
|
66
|
+
return {
|
|
67
|
+
id: String(node?.id || makeId()),
|
|
68
|
+
text,
|
|
69
|
+
...(typeof node?.color === 'string' && node.color.trim() ? { color: node.color.trim() } : {}),
|
|
70
|
+
collapsed: Boolean(node?.collapsed),
|
|
71
|
+
tested: Boolean(node?.tested),
|
|
72
|
+
...(typeof node?.notes === 'string' && node.notes ? { notes: node.notes } : {}),
|
|
73
|
+
...(Array.isArray(node?.jiraKeys)
|
|
74
|
+
? { jiraKeys: [...new Set(node.jiraKeys.map((key) => String(key).trim().toUpperCase()).filter(Boolean))] }
|
|
75
|
+
: {}),
|
|
76
|
+
...(Number.isFinite(x) && Number.isFinite(y) ? { position: { x, y } } : {}),
|
|
77
|
+
children: Array.isArray(node?.children)
|
|
78
|
+
? node.children.map((child) => normalizeNode(child))
|
|
79
|
+
: []
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeMindMap(input, { id = makeId(), createdAt = new Date().toISOString() } = {}) {
|
|
84
|
+
const rootInput = input?.root || input?.data?.root;
|
|
85
|
+
if (!rootInput || typeof rootInput !== 'object') return null;
|
|
86
|
+
const title = String(input?.title || rootInput.text || 'Untitled mind map').trim() || 'Untitled mind map';
|
|
87
|
+
const root = normalizeNode(rootInput, title);
|
|
88
|
+
return {
|
|
89
|
+
id: String(id),
|
|
90
|
+
title,
|
|
91
|
+
layout: 'left-to-right',
|
|
92
|
+
createdAt,
|
|
93
|
+
updatedAt: new Date().toISOString(),
|
|
94
|
+
jiraIssues: Array.isArray(input?.jiraIssues)
|
|
95
|
+
? input.jiraIssues
|
|
96
|
+
.map((issue) => ({
|
|
97
|
+
id: String(issue?.id || issue?.key || '').trim(),
|
|
98
|
+
key: String(issue?.key || '').trim().toUpperCase(),
|
|
99
|
+
summary: String(issue?.summary || '').trim(),
|
|
100
|
+
url: typeof issue?.url === 'string' ? issue.url.trim() : ''
|
|
101
|
+
}))
|
|
102
|
+
.filter((issue) => issue.id && issue.key)
|
|
103
|
+
: [],
|
|
104
|
+
root: { ...root, text: root.text || title }
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
router.get('/', (_req, res) => {
|
|
109
|
+
res.json(listMindMaps());
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
router.get('/:id', (req, res) => {
|
|
113
|
+
const mindMap = listMindMaps().find((item) => String(item.id) === String(req.params.id));
|
|
114
|
+
if (!mindMap) return res.status(404).json({ error: 'Mind map not found.' });
|
|
115
|
+
res.json(mindMap);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
router.post('/jira-issues/resolve', async (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
res.json(await resolveJiraIssue(req.body?.input));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
res.status(400).json({ error: error.message || 'Unable to load Jira issue summary.' });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
router.post('/', (req, res) => {
|
|
127
|
+
const mindMap = normalizeMindMap(req.body);
|
|
128
|
+
if (!mindMap) return res.status(400).json({ error: 'A root node is required.' });
|
|
129
|
+
const mindMaps = listMindMaps();
|
|
130
|
+
mindMaps.unshift(mindMap);
|
|
131
|
+
saveMindMaps(mindMaps);
|
|
132
|
+
res.status(201).json(mindMap);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
router.put('/:id', (req, res) => {
|
|
136
|
+
const mindMaps = listMindMaps();
|
|
137
|
+
const index = mindMaps.findIndex((item) => String(item.id) === String(req.params.id));
|
|
138
|
+
if (index < 0) return res.status(404).json({ error: 'Mind map not found.' });
|
|
139
|
+
|
|
140
|
+
const updated = normalizeMindMap(req.body, {
|
|
141
|
+
id: mindMaps[index].id,
|
|
142
|
+
createdAt: mindMaps[index].createdAt || new Date().toISOString()
|
|
143
|
+
});
|
|
144
|
+
if (!updated) return res.status(400).json({ error: 'A root node is required.' });
|
|
145
|
+
mindMaps[index] = updated;
|
|
146
|
+
saveMindMaps(mindMaps);
|
|
147
|
+
res.json(updated);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
router.delete('/:id', (req, res) => {
|
|
151
|
+
const mindMaps = listMindMaps();
|
|
152
|
+
const next = mindMaps.filter((item) => String(item.id) !== String(req.params.id));
|
|
153
|
+
if (next.length === mindMaps.length) return res.status(404).json({ error: 'Mind map not found.' });
|
|
154
|
+
saveMindMaps(next);
|
|
155
|
+
res.status(204).end();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export default router;
|
|
@@ -39,6 +39,10 @@ function webPullRequestUrl(host, pr) {
|
|
|
39
39
|
return `https://${host}/${projectPath}/repos/${repository}/pull-requests/${pr.id}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function pullRequestKey(project, repository, id) {
|
|
43
|
+
return `${project}/${repository}/${id}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
async function git(folder, args, options = {}) {
|
|
43
47
|
const result = await execFileAsync('git', args, {
|
|
44
48
|
cwd: folder,
|
|
@@ -49,6 +53,31 @@ async function git(folder, args, options = {}) {
|
|
|
49
53
|
return result.stdout;
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
async function detectConflictedPaths(folder, sourceRef, targetRef) {
|
|
57
|
+
try {
|
|
58
|
+
const output = await git(folder, [
|
|
59
|
+
'merge-tree',
|
|
60
|
+
'--write-tree',
|
|
61
|
+
'--name-only',
|
|
62
|
+
'-z',
|
|
63
|
+
'--no-messages',
|
|
64
|
+
targetRef,
|
|
65
|
+
sourceRef
|
|
66
|
+
]);
|
|
67
|
+
return String(output)
|
|
68
|
+
.split('\0')
|
|
69
|
+
.map((path) => path.trim())
|
|
70
|
+
.filter((path) => path && !/^[0-9a-f]{40}$/i.test(path));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const output = String(error.stdout || '');
|
|
73
|
+
if (!output) throw error;
|
|
74
|
+
return output
|
|
75
|
+
.split('\0')
|
|
76
|
+
.map((path) => path.trim())
|
|
77
|
+
.filter((path) => path && !/^[0-9a-f]{40}$/i.test(path));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
function remoteIdentity(remoteUrl) {
|
|
53
82
|
const raw = String(remoteUrl || '').replace(/\.git$/i, '');
|
|
54
83
|
let pathname = '';
|
|
@@ -226,6 +255,12 @@ export function UserProfile({ userId }: { userId: string }) {
|
|
|
226
255
|
sourceContent: `{\n "name": "enterprise-workbench",\n "version": "2.4.0",\n "endpoints": {\n "auth": "https://auth.internal.company.com/oauth2/token",\n "storage": "https://storage.internal.company.com/data"\n }\n}`,
|
|
227
256
|
targetContent: `{\n "name": "enterprise-workbench",\n "version": "2.3.1",\n "endpoints": {\n "auth": "https://auth-legacy.internal.company.com/token",\n "storage": "https://storage.internal.company.com/data"\n }\n}`,
|
|
228
257
|
defaultChoice: 'target'
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
path: 'README.md',
|
|
261
|
+
sourceContent: '# Enterprise Workbench\n\nUpdated setup instructions for the new release.\n',
|
|
262
|
+
targetContent: '# Enterprise Workbench\n\nSetup instructions for the current release.\n',
|
|
263
|
+
conflicted: false
|
|
229
264
|
}
|
|
230
265
|
]
|
|
231
266
|
});
|
|
@@ -247,15 +282,26 @@ export function UserProfile({ userId }: { userId: string }) {
|
|
|
247
282
|
const changes = changesResponse.data?.values || [];
|
|
248
283
|
const sourceCommit = pr.fromRef?.latestCommit || pr.fromRef?.id;
|
|
249
284
|
const targetCommit = pr.toRef?.latestCommit || pr.toRef?.id;
|
|
285
|
+
let conflictedPaths = null;
|
|
286
|
+
if (mergeResponse.data?.conflicted) {
|
|
287
|
+
try {
|
|
288
|
+
const folder = await findWorkspaceRepository(parsed);
|
|
289
|
+
await git(folder, ['fetch', 'origin', '--prune']);
|
|
290
|
+
const sourceRef = `origin/${pr.fromRef?.displayId}`;
|
|
291
|
+
const targetRef = `origin/${pr.toRef?.displayId}`;
|
|
292
|
+
conflictedPaths = new Set(await detectConflictedPaths(folder, sourceRef, targetRef));
|
|
293
|
+
} catch {}
|
|
294
|
+
}
|
|
250
295
|
const files = await Promise.all(changes.map(async (change) => {
|
|
251
296
|
const path = pathOf(change);
|
|
297
|
+
if (conflictedPaths && !conflictedPaths.has(path)) return null;
|
|
252
298
|
const [sourceContent, targetContent] = await Promise.all([
|
|
253
299
|
readFile({ ...parsed, commit: sourceCommit, path }),
|
|
254
300
|
readFile({ ...parsed, commit: targetCommit, path })
|
|
255
301
|
]);
|
|
256
302
|
return { path, sourceContent, targetContent, defaultChoice: 'source' };
|
|
257
303
|
}));
|
|
258
|
-
res.json({ conflicted: Boolean(mergeResponse.data.conflicted), pr: { title: pr.title, sourceBranch: pr.fromRef?.displayId, targetBranch: pr.toRef?.displayId }, files: files.filter((file) => file.path) });
|
|
304
|
+
res.json({ conflicted: Boolean(mergeResponse.data.conflicted), pr: { title: pr.title, sourceBranch: pr.fromRef?.displayId, targetBranch: pr.toRef?.displayId }, files: files.filter((file) => file?.path).map((file) => ({ ...file, conflicted: conflictedPaths ? conflictedPaths.has(file.path) : true })) });
|
|
259
305
|
} catch (error) {
|
|
260
306
|
res.status(500).json({ error: `Unable to load conflict data: ${error.message}` });
|
|
261
307
|
}
|
|
@@ -264,28 +310,54 @@ export function UserProfile({ userId }: { userId: string }) {
|
|
|
264
310
|
router.get('/my-conflicts', async (_req, res) => {
|
|
265
311
|
const host = configuredBitbucketHost();
|
|
266
312
|
if (!host) return res.json({ values: [] });
|
|
267
|
-
const dashboardUrl = `https://${host}/rest/api/latest/dashboard/pull-requests?role=author&state=
|
|
313
|
+
const dashboardUrl = `https://${host}/rest/api/latest/dashboard/pull-requests?role=author&state=ALL&limit=1000`;
|
|
268
314
|
try {
|
|
269
315
|
const response = await client.get(dashboardUrl, { headers: authHeaders() });
|
|
270
316
|
if (response.status < 200 || response.status >= 300) return res.status(response.status).json({ error: `Unable to load authored Pull Requests (${response.status}).` });
|
|
271
317
|
const values = Array.isArray(response.data?.values) ? response.data.values : [];
|
|
318
|
+
const resolutionPrs = new Map();
|
|
319
|
+
values.forEach((pr) => {
|
|
320
|
+
const project = pr.toRef?.repository?.project?.key;
|
|
321
|
+
const repository = pr.toRef?.repository?.slug;
|
|
322
|
+
const originalId = String(pr.description || '').match(/This branch resolves conflicts for Pull Request #(\d+)/i)?.[1];
|
|
323
|
+
if (project && repository && originalId && pr.id) {
|
|
324
|
+
resolutionPrs.set(pullRequestKey(project, repository, originalId), pr);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
272
328
|
const conflicts = await Promise.all(values.map(async (pr) => {
|
|
273
329
|
const project = pr.toRef?.repository?.project?.key;
|
|
274
330
|
const repository = pr.toRef?.repository?.slug;
|
|
275
331
|
if (!project || !repository || !pr.id) return null;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
332
|
+
if (String(pr.description || '').match(/This branch resolves conflicts for Pull Request #\d+/i)) return null;
|
|
333
|
+
|
|
334
|
+
const resolutionPr = resolutionPrs.get(pullRequestKey(project, repository, pr.id));
|
|
335
|
+
const baseConflict = {
|
|
280
336
|
id: pr.id,
|
|
281
337
|
title: pr.title || `Pull Request #${pr.id}`,
|
|
338
|
+
repositoryName: pr.toRef?.repository?.name || repository,
|
|
282
339
|
sourceBranch: pr.fromRef?.displayId || pr.fromRef?.id,
|
|
283
340
|
targetBranch: pr.toRef?.displayId || pr.toRef?.id,
|
|
284
341
|
updatedDate: pr.updatedDate,
|
|
285
342
|
url: webPullRequestUrl(host, pr)
|
|
286
343
|
};
|
|
344
|
+
if (resolutionPr) {
|
|
345
|
+
return {
|
|
346
|
+
...baseConflict,
|
|
347
|
+
resolutionPrUrl: webPullRequestUrl(host, resolutionPr),
|
|
348
|
+
resolutionPrId: resolutionPr.id
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const mergeUrl = `https://${host}/rest/api/latest/projects/${encodeURIComponent(project)}/repos/${encodeURIComponent(repository)}/pull-requests/${pr.id}/merge`;
|
|
353
|
+
const mergeResponse = await client.get(mergeUrl, { headers: authHeaders() });
|
|
354
|
+
if (mergeResponse.status < 200 || mergeResponse.status >= 300 || !mergeResponse.data?.conflicted) return null;
|
|
355
|
+
return baseConflict;
|
|
287
356
|
}));
|
|
288
|
-
|
|
357
|
+
const sorted = conflicts.filter(Boolean).sort((a, b) => (b.updatedDate || 0) - (a.updatedDate || 0));
|
|
358
|
+
const unresolved = sorted.filter((pr) => !pr.resolutionPrUrl);
|
|
359
|
+
const resolved = sorted.filter((pr) => pr.resolutionPrUrl);
|
|
360
|
+
res.json({ values: unresolved, unresolved, resolved });
|
|
289
361
|
} catch (error) {
|
|
290
362
|
res.status(500).json({ error: `Unable to load authored Pull Requests: ${error.message}` });
|
|
291
363
|
}
|
package/server.js
CHANGED
|
@@ -26,6 +26,7 @@ import branchSyncRoutes from './server/routes/branch-sync.js';
|
|
|
26
26
|
import updateRoutes from './server/routes/updates.js';
|
|
27
27
|
import dataBackupRoutes from './server/routes/data-backup.js';
|
|
28
28
|
import presentationsRoutes from './server/routes/presentations.js';
|
|
29
|
+
import mindMapsRoutes from './server/routes/mind-maps.js';
|
|
29
30
|
import packageUpgradeRoutes from './server/routes/package-upgrade.js';
|
|
30
31
|
import overviewRoutes from './server/routes/overview.js';
|
|
31
32
|
import { addErrorRecord } from './server/repositories/errors.js';
|
|
@@ -103,6 +104,7 @@ app.use('/api/branch-sync', branchSyncRoutes);
|
|
|
103
104
|
app.use('/api/updates', updateRoutes);
|
|
104
105
|
app.use('/api/data-backup', dataBackupRoutes);
|
|
105
106
|
app.use('/api/presentations', presentationsRoutes);
|
|
107
|
+
app.use('/api/mind-maps', mindMapsRoutes);
|
|
106
108
|
app.use('/api/package-upgrade', packageUpgradeRoutes);
|
|
107
109
|
app.use('/api/overview', overviewRoutes);
|
|
108
110
|
|