github-issue-tower-defence-management 1.196.1 → 1.197.1
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/CHANGELOG.md +14 -0
- package/README.md +1 -0
- package/bin/adapter/entry-points/cli/index.js +66 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +6 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleDataDelivery.js +1 -20
- package/bin/adapter/entry-points/console/consoleDataDelivery.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-CYwLv9pf.js → index-lT5OKx7e.js} +2 -2
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/domain/usecases/CliErrorReportUseCase.js +50 -0
- package/bin/domain/usecases/CliErrorReportUseCase.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +85 -0
- package/src/adapter/entry-points/cli/projectConfig.ts +14 -0
- package/src/adapter/entry-points/console/consoleDataDelivery.test.ts +12 -6
- package/src/adapter/entry-points/console/consoleDataDelivery.ts +1 -31
- package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +30 -18
- package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.ts +12 -3
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +3 -1
- package/src/adapter/entry-points/console/ui-dist/assets/{index-CYwLv9pf.js → index-lT5OKx7e.js} +2 -2
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +10 -4
- package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +2 -2
- package/src/domain/usecases/CliErrorReportUseCase.test.ts +151 -0
- package/src/domain/usecases/CliErrorReportUseCase.ts +72 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleDataDelivery.d.ts.map +1 -1
- package/types/domain/usecases/CliErrorReportUseCase.d.ts +14 -0
- package/types/domain/usecases/CliErrorReportUseCase.d.ts.map +1 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-lT5OKx7e.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-CLHvrafO.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CliErrorReportUseCase = void 0;
|
|
4
|
+
class CliErrorReportUseCase {
|
|
5
|
+
constructor(issueRepository) {
|
|
6
|
+
this.issueRepository = issueRepository;
|
|
7
|
+
this.run = async (params) => {
|
|
8
|
+
const { error, owner, repo, commandLine } = params;
|
|
9
|
+
const errorName = error instanceof Error ? (error.name ?? 'Error') : 'Error';
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
const stack = error instanceof Error && error.stack ? error.stack : String(error);
|
|
12
|
+
const title = `CLI error: ${errorName}: ${message.slice(0, 80)}`;
|
|
13
|
+
const occurredAt = new Date().toISOString();
|
|
14
|
+
const buildBody = (prefix) => [
|
|
15
|
+
`${prefix}`,
|
|
16
|
+
``,
|
|
17
|
+
`Error name: ${errorName}`,
|
|
18
|
+
`Message: ${message}`,
|
|
19
|
+
`Command: \`${commandLine}\``,
|
|
20
|
+
`Occurred at: ${occurredAt}`,
|
|
21
|
+
``,
|
|
22
|
+
`Stack trace:`,
|
|
23
|
+
`\`\`\``,
|
|
24
|
+
stack,
|
|
25
|
+
`\`\`\``,
|
|
26
|
+
].join('\n');
|
|
27
|
+
try {
|
|
28
|
+
const results = await this.issueRepository.searchIssue({
|
|
29
|
+
owner,
|
|
30
|
+
repositoryName: repo,
|
|
31
|
+
type: 'issue',
|
|
32
|
+
state: 'open',
|
|
33
|
+
title,
|
|
34
|
+
});
|
|
35
|
+
const existing = results.find((r) => r.title === title);
|
|
36
|
+
if (existing) {
|
|
37
|
+
await this.issueRepository.createCommentByUrl(existing.url, buildBody('CLI error recurrence'));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
await this.issueRepository.createNewIssue(owner, repo, title, buildBody('CLI error report'), [], []);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (reportError) {
|
|
44
|
+
console.error('CliErrorReportUseCase: failed to report error:', reportError);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.CliErrorReportUseCase = CliErrorReportUseCase;
|
|
50
|
+
//# sourceMappingURL=CliErrorReportUseCase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CliErrorReportUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/CliErrorReportUseCase.ts"],"names":[],"mappings":";;;AAOA,MAAa,qBAAqB;IAChC,YAA6B,eAAyC;QAAzC,oBAAe,GAAf,eAAe,CAA0B;QAEtE,QAAG,GAAG,KAAK,EAAE,MAKZ,EAAiB,EAAE;YAClB,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;YACnD,MAAM,SAAS,GACb,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YAC7D,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,MAAM,KAAK,GACT,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,MAAM,KAAK,GAAG,cAAc,SAAS,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;YACjE,MAAM,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAE5C,MAAM,SAAS,GAAG,CAAC,MAAc,EAAU,EAAE,CAC3C;gBACE,GAAG,MAAM,EAAE;gBACX,EAAE;gBACF,eAAe,SAAS,EAAE;gBAC1B,YAAY,OAAO,EAAE;gBACrB,cAAc,WAAW,IAAI;gBAC7B,gBAAgB,UAAU,EAAE;gBAC5B,EAAE;gBACF,cAAc;gBACd,QAAQ;gBACR,KAAK;gBACL,QAAQ;aACT,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEf,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC;oBACrD,KAAK;oBACL,cAAc,EAAE,IAAI;oBACpB,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,MAAM;oBACb,KAAK;iBACN,CAAC,CAAC;gBACH,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;gBACxD,IAAI,QAAQ,EAAE,CAAC;oBACb,MAAM,IAAI,CAAC,eAAe,CAAC,kBAAkB,CAC3C,QAAQ,CAAC,GAAG,EACZ,SAAS,CAAC,sBAAsB,CAAC,CAClC,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,CAAC,eAAe,CAAC,cAAc,CACvC,KAAK,EACL,IAAI,EACJ,KAAK,EACL,SAAS,CAAC,kBAAkB,CAAC,EAC7B,EAAE,EACF,EAAE,CACH,CAAC;gBACJ,CAAC;YACH,CAAC;YAAC,OAAO,WAAW,EAAE,CAAC;gBACrB,OAAO,CAAC,KAAK,CACX,gDAAgD,EAChD,WAAW,CACZ,CAAC;YACJ,CAAC;QACH,CAAC,CAAC;IA9DuE,CAAC;CA+D3E;AAhED,sDAgEC"}
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import type { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
15
15
|
import { CheckIssueReviewReadinessUseCase } from '../../../domain/usecases/CheckIssueReviewReadinessUseCase';
|
|
16
|
+
import { CliErrorReportUseCase } from '../../../domain/usecases/CliErrorReportUseCase';
|
|
16
17
|
import { assertDashboardDisplayLabelsUnique } from '../../../domain/usecases/dashboard/DashboardProjectCode';
|
|
17
18
|
import { isOwnerCallCalledAtValid } from '../../../domain/usecases/intmux/OwnerCallFile';
|
|
18
19
|
import { NotifyFinishedIssuePreparationUseCase } from '../../../domain/usecases/NotifyFinishedIssuePreparationUseCase';
|
|
@@ -362,6 +363,11 @@ program
|
|
|
362
363
|
readmeOverrides,
|
|
363
364
|
);
|
|
364
365
|
|
|
366
|
+
if (config.errorReportingRepository) {
|
|
367
|
+
process.env.TDPM_ERROR_REPORT_REPOSITORY =
|
|
368
|
+
config.errorReportingRepository;
|
|
369
|
+
}
|
|
370
|
+
|
|
365
371
|
const projectUrl = config.projectUrl;
|
|
366
372
|
const defaultAgentName = config.defaultAgentName;
|
|
367
373
|
const manager = config.manager;
|
|
@@ -605,6 +611,11 @@ program
|
|
|
605
611
|
readmeOverrides,
|
|
606
612
|
);
|
|
607
613
|
|
|
614
|
+
if (config.errorReportingRepository) {
|
|
615
|
+
process.env.TDPM_ERROR_REPORT_REPOSITORY =
|
|
616
|
+
config.errorReportingRepository;
|
|
617
|
+
}
|
|
618
|
+
|
|
608
619
|
const projectUrl = config.projectUrl;
|
|
609
620
|
|
|
610
621
|
if (!projectUrl) {
|
|
@@ -750,6 +761,11 @@ program
|
|
|
750
761
|
readmeOverrides,
|
|
751
762
|
);
|
|
752
763
|
|
|
764
|
+
if (config.errorReportingRepository) {
|
|
765
|
+
process.env.TDPM_ERROR_REPORT_REPOSITORY =
|
|
766
|
+
config.errorReportingRepository;
|
|
767
|
+
}
|
|
768
|
+
|
|
753
769
|
const projectName = config.projectName ?? 'default';
|
|
754
770
|
const localStorageRepository = new LocalStorageRepository();
|
|
755
771
|
const cachePath = projectCacheDirectory(projectName);
|
|
@@ -1350,13 +1366,82 @@ export const reportFatalErrorAndExit = (error: unknown): void => {
|
|
|
1350
1366
|
process.exit(1);
|
|
1351
1367
|
};
|
|
1352
1368
|
|
|
1369
|
+
const buildCliErrorReporter = (): {
|
|
1370
|
+
useCase: CliErrorReportUseCase;
|
|
1371
|
+
owner: string;
|
|
1372
|
+
repo: string;
|
|
1373
|
+
} | null => {
|
|
1374
|
+
const token = process.env.GH_TOKEN;
|
|
1375
|
+
const targetRepo = process.env.TDPM_ERROR_REPORT_REPOSITORY;
|
|
1376
|
+
if (!token || !targetRepo) {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
const slashIndex = targetRepo.indexOf('/');
|
|
1380
|
+
if (slashIndex <= 0 || slashIndex === targetRepo.length - 1) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
const owner = targetRepo.slice(0, slashIndex);
|
|
1384
|
+
const repo = targetRepo.slice(slashIndex + 1);
|
|
1385
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
1386
|
+
const githubRepositoryParams = buildGithubRepositoryParams(
|
|
1387
|
+
localStorageRepository,
|
|
1388
|
+
token,
|
|
1389
|
+
);
|
|
1390
|
+
const apiV3IssueRepository = new ApiV3IssueRepository(
|
|
1391
|
+
...githubRepositoryParams,
|
|
1392
|
+
);
|
|
1393
|
+
const restIssueRepository = new RestIssueRepository(
|
|
1394
|
+
...githubRepositoryParams,
|
|
1395
|
+
);
|
|
1396
|
+
const useCase = new CliErrorReportUseCase({
|
|
1397
|
+
searchIssue: apiV3IssueRepository.searchIssue.bind(apiV3IssueRepository),
|
|
1398
|
+
createNewIssue:
|
|
1399
|
+
restIssueRepository.createNewIssue.bind(restIssueRepository),
|
|
1400
|
+
createCommentByUrl: async (issueOrPrUrl: string, commentBody: string) => {
|
|
1401
|
+
await restIssueRepository.createComment(issueOrPrUrl, commentBody);
|
|
1402
|
+
},
|
|
1403
|
+
});
|
|
1404
|
+
return { useCase, owner, repo };
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1353
1407
|
export const runCliProgram = async (
|
|
1354
1408
|
argv: string[],
|
|
1355
1409
|
handleFatalError: (error: unknown) => void,
|
|
1356
1410
|
): Promise<void> => {
|
|
1411
|
+
let reported = false;
|
|
1412
|
+
|
|
1413
|
+
const safeReport = async (error: unknown): Promise<void> => {
|
|
1414
|
+
if (reported) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
reported = true;
|
|
1418
|
+
const reporter = buildCliErrorReporter();
|
|
1419
|
+
if (!reporter) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
await reporter.useCase.run({
|
|
1423
|
+
error,
|
|
1424
|
+
owner: reporter.owner,
|
|
1425
|
+
repo: reporter.repo,
|
|
1426
|
+
commandLine: argv.join(' '),
|
|
1427
|
+
});
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
process.on('uncaughtException', (error: Error) => {
|
|
1431
|
+
void safeReport(error).then(() => {
|
|
1432
|
+
reportFatalErrorAndExit(error);
|
|
1433
|
+
});
|
|
1434
|
+
});
|
|
1435
|
+
process.on('unhandledRejection', (reason: unknown) => {
|
|
1436
|
+
void safeReport(reason).then(() => {
|
|
1437
|
+
reportFatalErrorAndExit(reason);
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1357
1441
|
try {
|
|
1358
1442
|
await program.parseAsync(argv);
|
|
1359
1443
|
} catch (error) {
|
|
1444
|
+
await safeReport(error);
|
|
1360
1445
|
handleFatalError(error);
|
|
1361
1446
|
}
|
|
1362
1447
|
};
|
|
@@ -33,6 +33,7 @@ export type ConfigFile = {
|
|
|
33
33
|
consoleGithubTokenFileDir?: string;
|
|
34
34
|
disks?: DiskConfig[];
|
|
35
35
|
ownerApprovalTimeoutCycles?: number;
|
|
36
|
+
errorReportingRepository?: string;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
export type DiskConfig = {
|
|
@@ -139,6 +140,7 @@ const knownProjectReadmeConfigKeys = [
|
|
|
139
140
|
'labelsNotRequiringPullRequest',
|
|
140
141
|
'changeTargetPathAliases',
|
|
141
142
|
'consoleDataOutputDir',
|
|
143
|
+
'errorReportingRepository',
|
|
142
144
|
] as const;
|
|
143
145
|
|
|
144
146
|
export const loadConfigFile = (configFilePath: string): ConfigFile => {
|
|
@@ -211,6 +213,10 @@ export const loadConfigFile = (configFilePath: string): ConfigFile => {
|
|
|
211
213
|
parsed,
|
|
212
214
|
'ownerApprovalTimeoutCycles',
|
|
213
215
|
),
|
|
216
|
+
errorReportingRepository: getStringValue(
|
|
217
|
+
parsed,
|
|
218
|
+
'errorReportingRepository',
|
|
219
|
+
),
|
|
214
220
|
};
|
|
215
221
|
} catch (error) {
|
|
216
222
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -297,6 +303,10 @@ export const parseProjectReadmeConfig = (
|
|
|
297
303
|
'changeTargetPathAliases',
|
|
298
304
|
),
|
|
299
305
|
consoleDataOutputDir: getStringValue(parsed, 'consoleDataOutputDir'),
|
|
306
|
+
errorReportingRepository: getStringValue(
|
|
307
|
+
parsed,
|
|
308
|
+
'errorReportingRepository',
|
|
309
|
+
),
|
|
300
310
|
};
|
|
301
311
|
} catch {
|
|
302
312
|
console.warn('Failed to parse YAML from project README config section');
|
|
@@ -403,6 +413,10 @@ export const mergeConfigs = (
|
|
|
403
413
|
ownerApprovalTimeoutCycles:
|
|
404
414
|
cliOverrides.ownerApprovalTimeoutCycles ??
|
|
405
415
|
configFile.ownerApprovalTimeoutCycles,
|
|
416
|
+
errorReportingRepository:
|
|
417
|
+
readmeOverrides.errorReportingRepository ??
|
|
418
|
+
cliOverrides.errorReportingRepository ??
|
|
419
|
+
configFile.errorReportingRepository,
|
|
406
420
|
});
|
|
407
421
|
|
|
408
422
|
type GraphqlProjectV2ReadmeResponse = {
|
|
@@ -111,12 +111,12 @@ describe('buildConsoleDataResponse', () => {
|
|
|
111
111
|
expect(response.statusCode).toBe(404);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
it('serves a list file
|
|
114
|
+
it('serves a list file without filtering items that have done records', () => {
|
|
115
115
|
writeJson('acme/prs/list.json', {
|
|
116
116
|
pjcode: 'acme',
|
|
117
117
|
items: [
|
|
118
118
|
{ projectItemId: 'PVTI_1', title: 'keep' },
|
|
119
|
-
{ projectItemId: 'PVTI_2', title: '
|
|
119
|
+
{ projectItemId: 'PVTI_2', title: 'also keep despite done record' },
|
|
120
120
|
],
|
|
121
121
|
});
|
|
122
122
|
recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_2');
|
|
@@ -129,16 +129,19 @@ describe('buildConsoleDataResponse', () => {
|
|
|
129
129
|
const parsed: unknown = JSON.parse(response.body);
|
|
130
130
|
expect(parsed).toEqual({
|
|
131
131
|
pjcode: 'acme',
|
|
132
|
-
items: [
|
|
132
|
+
items: [
|
|
133
|
+
{ projectItemId: 'PVTI_1', title: 'keep' },
|
|
134
|
+
{ projectItemId: 'PVTI_2', title: 'also keep despite done record' },
|
|
135
|
+
],
|
|
133
136
|
});
|
|
134
137
|
});
|
|
135
138
|
|
|
136
|
-
it('
|
|
139
|
+
it('serves the workflow-blocker list without filtering items that have done records', () => {
|
|
137
140
|
writeJson('acme/workflow-blocker/list.json', {
|
|
138
141
|
pjcode: 'acme',
|
|
139
142
|
items: [
|
|
140
143
|
{ projectItemId: 'PVTI_1', title: 'keep' },
|
|
141
|
-
{ projectItemId: 'PVTI_2', title: '
|
|
144
|
+
{ projectItemId: 'PVTI_2', title: 'also keep despite done record' },
|
|
142
145
|
],
|
|
143
146
|
});
|
|
144
147
|
recordDoneProjectItemId(baseDir, 'acme', 'workflow-blocker', 'PVTI_2');
|
|
@@ -151,7 +154,10 @@ describe('buildConsoleDataResponse', () => {
|
|
|
151
154
|
const parsed: unknown = JSON.parse(response.body);
|
|
152
155
|
expect(parsed).toEqual({
|
|
153
156
|
pjcode: 'acme',
|
|
154
|
-
items: [
|
|
157
|
+
items: [
|
|
158
|
+
{ projectItemId: 'PVTI_1', title: 'keep' },
|
|
159
|
+
{ projectItemId: 'PVTI_2', title: 'also keep despite done record' },
|
|
160
|
+
],
|
|
155
161
|
});
|
|
156
162
|
});
|
|
157
163
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { readDoneProjectItemIds } from './consoleDoneStore';
|
|
4
3
|
import { CONSOLE_LIST_TAB_NAMES } from './consoleTabNames';
|
|
5
4
|
|
|
6
5
|
export { CONSOLE_LIST_TAB_NAMES };
|
|
@@ -55,9 +54,6 @@ export const parseConsoleDataRoute = (
|
|
|
55
54
|
return null;
|
|
56
55
|
};
|
|
57
56
|
|
|
58
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
59
|
-
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
60
|
-
|
|
61
57
|
type ReadJsonResult = { found: false } | { found: true; data: unknown };
|
|
62
58
|
|
|
63
59
|
const readJsonFile = (filePath: string): ReadJsonResult => {
|
|
@@ -75,26 +71,6 @@ const readJsonFile = (filePath: string): ReadJsonResult => {
|
|
|
75
71
|
return { found: true, data };
|
|
76
72
|
};
|
|
77
73
|
|
|
78
|
-
const isExcludedItem = (item: unknown, doneSet: Set<string>): boolean => {
|
|
79
|
-
if (!isRecord(item)) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
const projectItemId = item.projectItemId;
|
|
83
|
-
return typeof projectItemId === 'string' && doneSet.has(projectItemId);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const applyDoneExclusion = (
|
|
87
|
-
listData: unknown,
|
|
88
|
-
doneProjectItemIds: string[],
|
|
89
|
-
): unknown => {
|
|
90
|
-
if (!isRecord(listData) || !Array.isArray(listData.items)) {
|
|
91
|
-
return listData;
|
|
92
|
-
}
|
|
93
|
-
const doneSet = new Set(doneProjectItemIds);
|
|
94
|
-
const items = listData.items.filter((item) => !isExcludedItem(item, doneSet));
|
|
95
|
-
return { ...listData, items };
|
|
96
|
-
};
|
|
97
|
-
|
|
98
74
|
export type ConsoleDataResponse = {
|
|
99
75
|
statusCode: number;
|
|
100
76
|
contentType: string;
|
|
@@ -116,13 +92,7 @@ export const buildConsoleDataResponse = (
|
|
|
116
92
|
if (!listResult.found) {
|
|
117
93
|
return notFoundJson();
|
|
118
94
|
}
|
|
119
|
-
|
|
120
|
-
consoleDataOutputDir,
|
|
121
|
-
route.pjcode,
|
|
122
|
-
route.tab,
|
|
123
|
-
);
|
|
124
|
-
const filtered = applyDoneExclusion(listResult.data, doneProjectItemIds);
|
|
125
|
-
return okJson(filtered);
|
|
95
|
+
return okJson(listResult.data);
|
|
126
96
|
}
|
|
127
97
|
if (route.kind === 'detail') {
|
|
128
98
|
const filePath = path.join(
|
|
@@ -81,33 +81,33 @@ describe('counts driven to zero do not revive on tab switch', () => {
|
|
|
81
81
|
const overlay: ConsoleOverlay = {
|
|
82
82
|
PVTI_1: { ts: 500, mode: 'prs', done: true },
|
|
83
83
|
};
|
|
84
|
-
expect(countPendingItems([item(1)], overlay)).toBe(0);
|
|
84
|
+
expect(countPendingItems([item(1)], overlay, 'prs')).toBe(0);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
it('
|
|
87
|
+
it('does not subtract a done item from a tab other than the one it was processed in', () => {
|
|
88
88
|
const overlay: ConsoleOverlay = {
|
|
89
89
|
PVTI_1: { ts: 100, mode: 'triage', done: true },
|
|
90
90
|
};
|
|
91
|
-
expect(countPendingItems([item(1)], overlay)).toBe(
|
|
91
|
+
expect(countPendingItems([item(1)], overlay, 'prs')).toBe(1);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
it('does not revive a done item even when an entry was processed before the snapshot it still appears in', () => {
|
|
95
95
|
const overlay: ConsoleOverlay = {
|
|
96
96
|
PVTI_1: { ts: 100, mode: 'prs', done: true },
|
|
97
97
|
};
|
|
98
|
-
expect(countPendingItems([item(1)], overlay)).toBe(0);
|
|
98
|
+
expect(countPendingItems([item(1)], overlay, 'prs')).toBe(0);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
it('
|
|
101
|
+
it('shows a done item in a tab other than the one it was processed in', () => {
|
|
102
102
|
const overlay: ConsoleOverlay = {
|
|
103
103
|
PVTI_1: { ts: 100, mode: 'prs', done: true },
|
|
104
104
|
};
|
|
105
|
-
expect(countPendingItems([item(1)], overlay)).toBe(
|
|
106
|
-
expect(filterPendingItems([item(1)], overlay)).toEqual([]);
|
|
105
|
+
expect(countPendingItems([item(1)], overlay, 'triage')).toBe(1);
|
|
106
|
+
expect(filterPendingItems([item(1)], overlay, 'triage')).toEqual([item(1)]);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
it('counts an item that has no done entry', () => {
|
|
110
|
-
expect(countPendingItems([item(1)], {})).toBe(1);
|
|
110
|
+
expect(countPendingItems([item(1)], {}, 'prs')).toBe(1);
|
|
111
111
|
});
|
|
112
112
|
});
|
|
113
113
|
|
|
@@ -116,7 +116,7 @@ describe('filterPendingItems', () => {
|
|
|
116
116
|
const overlay: ConsoleOverlay = {
|
|
117
117
|
PVTI_1: { ts: 500, mode: 'prs', done: true },
|
|
118
118
|
};
|
|
119
|
-
const result = filterPendingItems([item(1), item(2)], overlay);
|
|
119
|
+
const result = filterPendingItems([item(1), item(2)], overlay, 'prs');
|
|
120
120
|
expect(result.map((entry) => entry.number)).toEqual([2]);
|
|
121
121
|
});
|
|
122
122
|
|
|
@@ -125,8 +125,8 @@ describe('filterPendingItems', () => {
|
|
|
125
125
|
PVTI_1: { ts: 500, mode: 'prs', done: true },
|
|
126
126
|
};
|
|
127
127
|
const items = [item(1), item(2)];
|
|
128
|
-
expect(countPendingItems(items, overlay)).toBe(
|
|
129
|
-
filterPendingItems(items, overlay).length,
|
|
128
|
+
expect(countPendingItems(items, overlay, 'prs')).toBe(
|
|
129
|
+
filterPendingItems(items, overlay, 'prs').length,
|
|
130
130
|
);
|
|
131
131
|
});
|
|
132
132
|
});
|
|
@@ -136,14 +136,20 @@ describe('workflow-blocker items are filtered by the done overlay like every oth
|
|
|
136
136
|
const overlay: ConsoleOverlay = {
|
|
137
137
|
PVTI_1: { ts: 100, mode: 'workflow-blocker', done: true },
|
|
138
138
|
};
|
|
139
|
-
expect(
|
|
139
|
+
expect(
|
|
140
|
+
countPendingItems([item(1), item(2)], overlay, 'workflow-blocker'),
|
|
141
|
+
).toBe(1);
|
|
140
142
|
});
|
|
141
143
|
|
|
142
144
|
it('removes a processed workflow-blocker item from the list', () => {
|
|
143
145
|
const overlay: ConsoleOverlay = {
|
|
144
146
|
PVTI_1: { ts: 100, mode: 'workflow-blocker', done: true },
|
|
145
147
|
};
|
|
146
|
-
const result = filterPendingItems(
|
|
148
|
+
const result = filterPendingItems(
|
|
149
|
+
[item(1), item(2)],
|
|
150
|
+
overlay,
|
|
151
|
+
'workflow-blocker',
|
|
152
|
+
);
|
|
147
153
|
expect(result.map((entry) => entry.number)).toEqual([2]);
|
|
148
154
|
});
|
|
149
155
|
|
|
@@ -152,8 +158,12 @@ describe('workflow-blocker items are filtered by the done overlay like every oth
|
|
|
152
158
|
PVTI_1: { ts: 100, mode: 'workflow-blocker', done: true },
|
|
153
159
|
PVTI_2: { ts: 100, mode: 'workflow-blocker', done: true },
|
|
154
160
|
};
|
|
155
|
-
expect(
|
|
156
|
-
|
|
161
|
+
expect(
|
|
162
|
+
countPendingItems([item(1), item(2)], overlay, 'workflow-blocker'),
|
|
163
|
+
).toBe(0);
|
|
164
|
+
expect(
|
|
165
|
+
filterPendingItems([item(1), item(2)], overlay, 'workflow-blocker'),
|
|
166
|
+
).toEqual([]);
|
|
157
167
|
});
|
|
158
168
|
});
|
|
159
169
|
|
|
@@ -171,8 +181,10 @@ describe('overlayEntriesActedSinceSnapshot', () => {
|
|
|
171
181
|
'2026-08-04T00:26:06Z',
|
|
172
182
|
);
|
|
173
183
|
expect(acted).toEqual({});
|
|
174
|
-
expect(countPendingItems([item(1)], acted)).toBe(1);
|
|
175
|
-
expect(filterPendingItems([item(1)], acted)).toEqual([
|
|
184
|
+
expect(countPendingItems([item(1)], acted, 'todo-by-human')).toBe(1);
|
|
185
|
+
expect(filterPendingItems([item(1)], acted, 'todo-by-human')).toEqual([
|
|
186
|
+
item(1),
|
|
187
|
+
]);
|
|
176
188
|
});
|
|
177
189
|
|
|
178
190
|
it('keeps an entry written after the snapshot was generated so the item stays hidden', () => {
|
|
@@ -187,7 +199,7 @@ describe('overlayEntriesActedSinceSnapshot', () => {
|
|
|
187
199
|
overlay,
|
|
188
200
|
'2026-08-04T00:26:06Z',
|
|
189
201
|
);
|
|
190
|
-
expect(countPendingItems([item(1)], acted)).toBe(0);
|
|
202
|
+
expect(countPendingItems([item(1)], acted, 'todo-by-human')).toBe(0);
|
|
191
203
|
});
|
|
192
204
|
|
|
193
205
|
it('keeps an entry written at the exact snapshot generation time', () => {
|
|
@@ -16,19 +16,28 @@ export const isOverlayEntryActed = (
|
|
|
16
16
|
entry: ConsoleOverlayEntry | undefined,
|
|
17
17
|
): boolean => entry !== undefined && entry.done === true;
|
|
18
18
|
|
|
19
|
+
const isActedInTab = (
|
|
20
|
+
entry: ConsoleOverlayEntry | undefined,
|
|
21
|
+
currentTab: ConsoleTabName,
|
|
22
|
+
): boolean =>
|
|
23
|
+
entry !== undefined && entry.done === true && entry.mode === currentTab;
|
|
24
|
+
|
|
19
25
|
export const countPendingItems = (
|
|
20
26
|
items: ConsoleListItem[],
|
|
21
27
|
overlay: ConsoleOverlay,
|
|
28
|
+
currentTab: ConsoleTabName,
|
|
22
29
|
): number =>
|
|
23
|
-
items.filter(
|
|
24
|
-
|
|
30
|
+
items.filter(
|
|
31
|
+
(item) => !isActedInTab(overlay[overlayKeyForItem(item)], currentTab),
|
|
32
|
+
).length;
|
|
25
33
|
|
|
26
34
|
export const filterPendingItems = (
|
|
27
35
|
items: ConsoleListItem[],
|
|
28
36
|
overlay: ConsoleOverlay,
|
|
37
|
+
currentTab: ConsoleTabName,
|
|
29
38
|
): ConsoleListItem[] =>
|
|
30
39
|
items.filter(
|
|
31
|
-
(item) => !
|
|
40
|
+
(item) => !isActedInTab(overlay[overlayKeyForItem(item)], currentTab),
|
|
32
41
|
);
|
|
33
42
|
|
|
34
43
|
export const overlayEntriesActedSinceSnapshot = (
|
|
@@ -107,6 +107,7 @@ export const ConsolePage = () => {
|
|
|
107
107
|
overlayState.overlay,
|
|
108
108
|
snapshot.generatedAt,
|
|
109
109
|
),
|
|
110
|
+
tab.name,
|
|
110
111
|
);
|
|
111
112
|
}
|
|
112
113
|
}
|
|
@@ -151,8 +152,9 @@ export const ConsolePage = () => {
|
|
|
151
152
|
overlayState.overlay,
|
|
152
153
|
activeSnapshot.generatedAt,
|
|
153
154
|
),
|
|
155
|
+
activeTab,
|
|
154
156
|
);
|
|
155
|
-
}, [activeSnapshot, overlayState.overlay]);
|
|
157
|
+
}, [activeSnapshot, overlayState.overlay, activeTab]);
|
|
156
158
|
|
|
157
159
|
const orderedPendingKeys = useMemo(
|
|
158
160
|
() => pendingItems.map((item) => overlayKeyForItem(item)),
|