github-issue-tower-defence-management 1.187.3 → 1.188.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 +15 -0
- package/README.md +3 -0
- package/bin/adapter/entry-points/cli/index.js +20 -12
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CA6c7Pfz.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-D1xvwesF.css +2 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +3 -4
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +3 -3
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +13 -1
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -1
- package/bin/domain/usecases/isHumanComment.js +21 -0
- package/bin/domain/usecases/isHumanComment.js.map +1 -0
- package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js +5 -0
- package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js.map +1 -0
- package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js +62 -12
- package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +2 -0
- package/src/adapter/entry-points/cli/index.ts +39 -16
- package/src/adapter/entry-points/cli/projectConfig.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +39 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +61 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +92 -2
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +2 -0
- package/src/adapter/entry-points/console/ui/src/index.css +50 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-CA6c7Pfz.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-D1xvwesF.css +2 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +98 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +9 -5
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +9 -4
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +88 -0
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +17 -2
- package/src/domain/usecases/isHumanComment.test.ts +90 -0
- package/src/domain/usecases/isHumanComment.ts +23 -0
- package/src/domain/usecases/nextStepAgentDispatchRepeatedMessage.ts +2 -0
- package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.test.ts +312 -72
- package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts +103 -19
- 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/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -0
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +1 -0
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -1
- package/types/domain/usecases/isHumanComment.d.ts +5 -0
- package/types/domain/usecases/isHumanComment.d.ts.map +1 -0
- package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts +2 -0
- package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts.map +1 -0
- package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts +5 -2
- package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DfKDTGzS.js +0 -86
- package/bin/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +0 -2
- package/src/adapter/entry-points/console/ui-dist/assets/index-DfKDTGzS.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +0 -2
|
@@ -1,31 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveNextStepAgentDispatchRepetition = exports.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD = void 0;
|
|
3
|
+
exports.resolveNextStepAgentDispatchRepetition = exports.DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP = exports.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD = void 0;
|
|
4
4
|
const ProjectFieldName_1 = require("../entities/ProjectFieldName");
|
|
5
|
-
|
|
6
|
-
const
|
|
5
|
+
const extractNextStepAgent_1 = require("./extractNextStepAgent");
|
|
6
|
+
const findLastAgentReport_1 = require("./findLastAgentReport");
|
|
7
|
+
const isAgentReportBody_1 = require("./isAgentReportBody");
|
|
8
|
+
const isHumanComment_1 = require("./isHumanComment");
|
|
9
|
+
const nextStepAgentDispatchRepeatedMessage_1 = require("./nextStepAgentDispatchRepeatedMessage");
|
|
10
|
+
Object.defineProperty(exports, "NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD", { enumerable: true, get: function () { return nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD; } });
|
|
11
|
+
exports.DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP = 6;
|
|
12
|
+
const countSilentRedispatches = (params) => {
|
|
7
13
|
if (params.agentFieldValue === null ||
|
|
8
14
|
(0, ProjectFieldName_1.normalizeProjectFieldName)(params.agentFieldValue) !==
|
|
9
15
|
(0, ProjectFieldName_1.normalizeProjectFieldName)(params.nextStepAgent)) {
|
|
10
|
-
return
|
|
16
|
+
return null;
|
|
11
17
|
}
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
const lastAgentReport = (0, findLastAgentReport_1.findLastAgentReport)(params.comments, params.isTrustedAuthor);
|
|
19
|
+
const commentsAfterLastAgentReport = lastAgentReport
|
|
20
|
+
? params.comments.slice(params.comments.indexOf(lastAgentReport) + 1)
|
|
21
|
+
: [];
|
|
22
|
+
return (commentsAfterLastAgentReport.filter((comment) => params.isTrustedAuthor(comment.author) &&
|
|
23
|
+
comment.content.startsWith(nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD)).length + 1);
|
|
24
|
+
};
|
|
25
|
+
const countDispatchesInCurrentCycle = (params) => {
|
|
26
|
+
const lastHumanCommentIndex = params.comments.reduce((found, comment, index) => (0, isHumanComment_1.isHumanComment)(comment, params.isTrustedAuthor) ? index : found, -1);
|
|
27
|
+
const reportsInCurrentCycle = params.comments
|
|
28
|
+
.slice(lastHumanCommentIndex + 1)
|
|
29
|
+
.filter((comment) => params.isTrustedAuthor(comment.author) &&
|
|
30
|
+
(0, isAgentReportBody_1.isAgentReportBody)(comment.content));
|
|
31
|
+
return (reportsInCurrentCycle.slice(0, -1).filter((comment) => {
|
|
32
|
+
const declared = (0, extractNextStepAgent_1.extractNextStepAgent)(comment.content);
|
|
33
|
+
return (declared !== null &&
|
|
34
|
+
(0, ProjectFieldName_1.normalizeProjectFieldName)(declared) ===
|
|
35
|
+
(0, ProjectFieldName_1.normalizeProjectFieldName)(params.nextStepAgent));
|
|
36
|
+
}).length + 1);
|
|
37
|
+
};
|
|
38
|
+
const resolveNextStepAgentDispatchRepetition = (params) => {
|
|
39
|
+
const silentRedispatches = countSilentRedispatches(params);
|
|
40
|
+
if (silentRedispatches !== null &&
|
|
41
|
+
silentRedispatches >= params.thresholdForAutoReject) {
|
|
16
42
|
return {
|
|
17
43
|
type: 'escalateToFailedPreparation',
|
|
18
|
-
comment: `${
|
|
44
|
+
comment: `${nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
|
|
19
45
|
|
|
20
46
|
Failed to receive a report from the dispatched agent for ${params.thresholdForAutoReject} times`,
|
|
21
47
|
};
|
|
22
48
|
}
|
|
23
|
-
|
|
49
|
+
const dispatchesInCycle = countDispatchesInCurrentCycle(params);
|
|
50
|
+
if (dispatchesInCycle >= params.thresholdForDispatchLoop) {
|
|
51
|
+
return {
|
|
52
|
+
type: 'escalateToFailedPreparation',
|
|
53
|
+
comment: `${nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
|
|
54
|
+
|
|
55
|
+
This agent has been dispatched ${params.thresholdForDispatchLoop} times since the last human comment on this issue and the task has not moved past it, so the issue is escalated for a decision instead of being dispatched again.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const silentRedispatchMessage = {
|
|
24
59
|
type: 'dispatchAgain',
|
|
25
|
-
comment: `${
|
|
60
|
+
comment: `${nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
|
|
26
61
|
|
|
27
|
-
The latest agent report names this agent as the next step and the agent field already holds it, so the previous dispatch to it ended without a report. Dispatching it again (${
|
|
62
|
+
The latest agent report names this agent as the next step and the agent field already holds it, so the previous dispatch to it ended without a report. Dispatching it again (${silentRedispatches}/${params.thresholdForAutoReject}).`,
|
|
28
63
|
};
|
|
64
|
+
if (silentRedispatches !== null && silentRedispatches > 1) {
|
|
65
|
+
return silentRedispatchMessage;
|
|
66
|
+
}
|
|
67
|
+
if (dispatchesInCycle > 1) {
|
|
68
|
+
return {
|
|
69
|
+
type: 'dispatchAgain',
|
|
70
|
+
comment: `${nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
|
|
71
|
+
|
|
72
|
+
The latest agent report names this agent as the next step and it has already been dispatched on this issue since the last human comment. Dispatching it again (${dispatchesInCycle}/${params.thresholdForDispatchLoop}).`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (silentRedispatches !== null) {
|
|
76
|
+
return silentRedispatchMessage;
|
|
77
|
+
}
|
|
78
|
+
return { type: 'notRepeated' };
|
|
29
79
|
};
|
|
30
80
|
exports.resolveNextStepAgentDispatchRepetition = resolveNextStepAgentDispatchRepetition;
|
|
31
81
|
//# sourceMappingURL=resolveNextStepAgentDispatchRepetition.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveNextStepAgentDispatchRepetition.js","sourceRoot":"","sources":["../../../src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts"],"names":[],"mappings":";;;AAAA,mEAAyE;
|
|
1
|
+
{"version":3,"file":"resolveNextStepAgentDispatchRepetition.js","sourceRoot":"","sources":["../../../src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1510,6 +1510,7 @@ mysteryKey: 'value'
|
|
|
1510
1510
|
projectUrl: 'https://github.com/orgs/test/projects/1',
|
|
1511
1511
|
issueUrl: 'https://github.com/test/repo/issues/1',
|
|
1512
1512
|
thresholdForAutoReject: 3,
|
|
1513
|
+
thresholdForDispatchLoop: 6,
|
|
1513
1514
|
workflowBlockerResolvedWebhookUrl: null,
|
|
1514
1515
|
allowedIssueAuthors: null,
|
|
1515
1516
|
labelsAsLlmAgentName: null,
|
|
@@ -1553,6 +1554,7 @@ mysteryKey: 'value'
|
|
|
1553
1554
|
projectUrl: 'https://github.com/orgs/override/projects/2',
|
|
1554
1555
|
issueUrl: 'https://github.com/test/repo/issues/1',
|
|
1555
1556
|
thresholdForAutoReject: 3,
|
|
1557
|
+
thresholdForDispatchLoop: 6,
|
|
1556
1558
|
workflowBlockerResolvedWebhookUrl: null,
|
|
1557
1559
|
allowedIssueAuthors: null,
|
|
1558
1560
|
labelsAsLlmAgentName: null,
|
|
@@ -33,6 +33,7 @@ import { projectCacheDirectory } from '../../repositories/localStorageCacheDirec
|
|
|
33
33
|
import { NodeLocalCommandRunner } from '../../repositories/NodeLocalCommandRunner';
|
|
34
34
|
import { NodeTmuxSessionRepository } from '../../repositories/NodeTmuxSessionRepository';
|
|
35
35
|
import { ProcTakeOwnershipSpawnRepository } from '../../repositories/ProcTakeOwnershipSpawnRepository';
|
|
36
|
+
import { DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP } from '../../../domain/usecases/resolveNextStepAgentDispatchRepetition';
|
|
36
37
|
import { ProxyClaudeTokenUsageRepository } from '../../repositories/ProxyClaudeTokenUsageRepository';
|
|
37
38
|
import { SystemDateRepository } from '../../repositories/SystemDateRepository';
|
|
38
39
|
import {
|
|
@@ -89,10 +90,29 @@ type StartDaemonOptions = {
|
|
|
89
90
|
configFilePath: string;
|
|
90
91
|
};
|
|
91
92
|
|
|
93
|
+
const resolvePositiveIntegerOption = (
|
|
94
|
+
rawValue: number | string | undefined,
|
|
95
|
+
optionName: string,
|
|
96
|
+
fallback: number,
|
|
97
|
+
): number => {
|
|
98
|
+
if (rawValue === undefined) {
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
const parsed = Number(rawValue);
|
|
102
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
|
|
103
|
+
console.error(
|
|
104
|
+
`Invalid value for --${optionName}. It must be a positive integer.`,
|
|
105
|
+
);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
};
|
|
110
|
+
|
|
92
111
|
type NotifyFinishedOptions = {
|
|
93
112
|
issueUrl: string;
|
|
94
113
|
projectUrl?: string;
|
|
95
114
|
thresholdForAutoReject?: string;
|
|
115
|
+
thresholdForDispatchLoop?: string;
|
|
96
116
|
workflowBlockerResolvedWebhookUrl?: string;
|
|
97
117
|
configFilePath: string;
|
|
98
118
|
missingAgentName?: string;
|
|
@@ -414,6 +434,7 @@ program
|
|
|
414
434
|
projectUrl,
|
|
415
435
|
preparationProcessCheckCommand,
|
|
416
436
|
thresholdForAutoReject: config.thresholdForAutoReject ?? 3,
|
|
437
|
+
thresholdForDispatchLoop: config.thresholdForDispatchLoop,
|
|
417
438
|
awLogDirectoryPath: config.awLogDirectoryPath,
|
|
418
439
|
awLogStaleThresholdMinutes: config.awLogStaleThresholdMinutes,
|
|
419
440
|
labelsAsLlmAgentName: config.labelsAsLlmAgentName ?? null,
|
|
@@ -498,6 +519,10 @@ program
|
|
|
498
519
|
'--thresholdForAutoReject <count>',
|
|
499
520
|
'Threshold for auto-escalation after consecutive rejections (default: 3)',
|
|
500
521
|
)
|
|
522
|
+
.option(
|
|
523
|
+
'--thresholdForDispatchLoop <count>',
|
|
524
|
+
'Threshold for auto-escalation after one agent is dispatched this many times since the last human comment, whether it names itself or two agents name each other in turn (default: 6)',
|
|
525
|
+
)
|
|
501
526
|
.option(
|
|
502
527
|
'--workflowBlockerResolvedWebhookUrl <url>',
|
|
503
528
|
'Webhook URL to notify when a workflow blocker issue status changes to awaiting quality check. Supports {URL} and {MESSAGE} placeholders.',
|
|
@@ -528,6 +553,9 @@ program
|
|
|
528
553
|
thresholdForAutoReject: options.thresholdForAutoReject
|
|
529
554
|
? Number(options.thresholdForAutoReject)
|
|
530
555
|
: undefined,
|
|
556
|
+
thresholdForDispatchLoop: options.thresholdForDispatchLoop
|
|
557
|
+
? Number(options.thresholdForDispatchLoop)
|
|
558
|
+
: undefined,
|
|
531
559
|
workflowBlockerResolvedWebhookUrl:
|
|
532
560
|
options.workflowBlockerResolvedWebhookUrl,
|
|
533
561
|
};
|
|
@@ -558,22 +586,16 @@ program
|
|
|
558
586
|
process.exit(1);
|
|
559
587
|
}
|
|
560
588
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
'Invalid value for --thresholdForAutoReject. It must be a positive integer.',
|
|
572
|
-
);
|
|
573
|
-
process.exit(1);
|
|
574
|
-
}
|
|
575
|
-
thresholdForAutoReject = parsed;
|
|
576
|
-
}
|
|
589
|
+
const thresholdForAutoReject = resolvePositiveIntegerOption(
|
|
590
|
+
config.thresholdForAutoReject,
|
|
591
|
+
'thresholdForAutoReject',
|
|
592
|
+
3,
|
|
593
|
+
);
|
|
594
|
+
const thresholdForDispatchLoop = resolvePositiveIntegerOption(
|
|
595
|
+
config.thresholdForDispatchLoop,
|
|
596
|
+
'thresholdForDispatchLoop',
|
|
597
|
+
DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
|
|
598
|
+
);
|
|
577
599
|
|
|
578
600
|
const workflowBlockerResolvedWebhookUrl: string | null =
|
|
579
601
|
config.workflowBlockerResolvedWebhookUrl ?? null;
|
|
@@ -640,6 +662,7 @@ program
|
|
|
640
662
|
projectUrl,
|
|
641
663
|
issueUrl: options.issueUrl,
|
|
642
664
|
thresholdForAutoReject,
|
|
665
|
+
thresholdForDispatchLoop,
|
|
643
666
|
workflowBlockerResolvedWebhookUrl,
|
|
644
667
|
allowedIssueAuthors,
|
|
645
668
|
labelsAsLlmAgentName: config.labelsAsLlmAgentName ?? null,
|
|
@@ -15,6 +15,7 @@ export type ConfigFile = {
|
|
|
15
15
|
allowedIssueAuthors?: string;
|
|
16
16
|
autoAssignManagerAuthors?: string;
|
|
17
17
|
thresholdForAutoReject?: number;
|
|
18
|
+
thresholdForDispatchLoop?: number;
|
|
18
19
|
workflowBlockerResolvedWebhookUrl?: string;
|
|
19
20
|
projectName?: string;
|
|
20
21
|
preparationProcessCheckCommand?: string;
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { type ComponentProps, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import type { ConsoleTabName } from '../../logic/types';
|
|
4
4
|
import { ConsoleTabList } from './ConsoleTabList';
|
|
5
5
|
|
|
6
|
+
const allPjcodes = ['utage3', 'cmg', 'xmile', 'xcare', 'umino'];
|
|
7
|
+
|
|
6
8
|
const meta: Meta<typeof ConsoleTabList> = {
|
|
7
9
|
title: 'Console/ConsoleTabList',
|
|
8
10
|
component: ConsoleTabList,
|
|
9
11
|
args: {
|
|
10
12
|
pjcode: 'acme',
|
|
13
|
+
pjcodes: allPjcodes,
|
|
11
14
|
generatedAt: '2026-06-19T08:42:11.000Z',
|
|
12
15
|
fromCache: false,
|
|
13
16
|
tabHref: (tab: ConsoleTabName) => `/projects/acme/${tab}`,
|
|
14
17
|
onSelectTab: () => {},
|
|
18
|
+
onSelectProject: () => {},
|
|
15
19
|
},
|
|
16
20
|
};
|
|
17
21
|
|
|
@@ -126,3 +130,37 @@ export const WithSettingsButton: Story = {
|
|
|
126
130
|
),
|
|
127
131
|
},
|
|
128
132
|
};
|
|
133
|
+
|
|
134
|
+
export const WithProjectSwitcherClosed: Story = {
|
|
135
|
+
args: {
|
|
136
|
+
activeTab: 'prs',
|
|
137
|
+
counts,
|
|
138
|
+
pjcode: 'umino',
|
|
139
|
+
pjcodes: allPjcodes,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const DropdownOpen = (args: ComponentProps<typeof ConsoleTabList>) => {
|
|
144
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const btn = containerRef.current?.querySelector<HTMLButtonElement>(
|
|
147
|
+
'.console-tab-pjname-button',
|
|
148
|
+
);
|
|
149
|
+
btn?.click();
|
|
150
|
+
}, []);
|
|
151
|
+
return (
|
|
152
|
+
<div ref={containerRef}>
|
|
153
|
+
<ConsoleTabList {...args} />
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const WithProjectSwitcherOpen: Story = {
|
|
159
|
+
args: {
|
|
160
|
+
activeTab: 'prs',
|
|
161
|
+
counts,
|
|
162
|
+
pjcode: 'umino',
|
|
163
|
+
pjcodes: allPjcodes,
|
|
164
|
+
},
|
|
165
|
+
render: (args) => <DropdownOpen {...args} />,
|
|
166
|
+
};
|
|
@@ -14,10 +14,12 @@ const counts: Record<ConsoleTabName, number> = {
|
|
|
14
14
|
|
|
15
15
|
const baseProps = {
|
|
16
16
|
pjcode: 'acme',
|
|
17
|
+
pjcodes: ['utage3', 'cmg', 'xmile', 'xcare', 'umino'],
|
|
17
18
|
generatedAt: '2026-06-19T08:42:11.000Z',
|
|
18
19
|
fromCache: false,
|
|
19
20
|
tabHref: (tab: ConsoleTabName) => `/projects/acme/${tab}`,
|
|
20
21
|
onSelectTab: () => {},
|
|
22
|
+
onSelectProject: () => {},
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
describe('ConsoleTabList', () => {
|
|
@@ -170,4 +172,63 @@ describe('ConsoleTabList', () => {
|
|
|
170
172
|
);
|
|
171
173
|
expect(container.querySelector('.console-tab-settings')).toBeNull();
|
|
172
174
|
});
|
|
175
|
+
|
|
176
|
+
it('opens the project dropdown when the pjcode button is clicked', () => {
|
|
177
|
+
const { getByRole, baseElement } = render(
|
|
178
|
+
<ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
|
|
179
|
+
);
|
|
180
|
+
expect(
|
|
181
|
+
baseElement.querySelector('.console-tab-pjname-dropdown'),
|
|
182
|
+
).toBeNull();
|
|
183
|
+
fireEvent.click(getByRole('button', { name: /acme/i }));
|
|
184
|
+
expect(
|
|
185
|
+
baseElement.querySelector('.console-tab-pjname-dropdown'),
|
|
186
|
+
).toBeInTheDocument();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('lists all pjcodes in the dropdown', () => {
|
|
190
|
+
const { getByRole, baseElement } = render(
|
|
191
|
+
<ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
|
|
192
|
+
);
|
|
193
|
+
fireEvent.click(getByRole('button', { name: /acme/i }));
|
|
194
|
+
const options = Array.from(
|
|
195
|
+
baseElement.querySelectorAll('.console-tab-pjname-option'),
|
|
196
|
+
);
|
|
197
|
+
expect(options.map((o) => o.textContent)).toEqual([
|
|
198
|
+
'utage3',
|
|
199
|
+
'cmg',
|
|
200
|
+
'xmile',
|
|
201
|
+
'xcare',
|
|
202
|
+
'umino',
|
|
203
|
+
]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('calls onSelectProject with the chosen pjcode when a dropdown option is clicked', () => {
|
|
207
|
+
const onSelectProject = jest.fn();
|
|
208
|
+
const { getByRole, getByText } = render(
|
|
209
|
+
<ConsoleTabList
|
|
210
|
+
{...baseProps}
|
|
211
|
+
activeTab="prs"
|
|
212
|
+
counts={counts}
|
|
213
|
+
onSelectProject={onSelectProject}
|
|
214
|
+
/>,
|
|
215
|
+
);
|
|
216
|
+
fireEvent.click(getByRole('button', { name: /acme/i }));
|
|
217
|
+
fireEvent.click(getByText('xmile'));
|
|
218
|
+
expect(onSelectProject).toHaveBeenCalledWith('xmile');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('closes the dropdown after a project option is selected', () => {
|
|
222
|
+
const { getByRole, getByText, baseElement } = render(
|
|
223
|
+
<ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
|
|
224
|
+
);
|
|
225
|
+
fireEvent.click(getByRole('button', { name: /acme/i }));
|
|
226
|
+
expect(
|
|
227
|
+
baseElement.querySelector('.console-tab-pjname-dropdown'),
|
|
228
|
+
).toBeInTheDocument();
|
|
229
|
+
fireEvent.click(getByText('xmile'));
|
|
230
|
+
expect(
|
|
231
|
+
baseElement.querySelector('.console-tab-pjname-dropdown'),
|
|
232
|
+
).toBeNull();
|
|
233
|
+
});
|
|
173
234
|
});
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import { CONSOLE_TABS, type ConsoleTabName } from '../../logic/types';
|
|
3
4
|
|
|
4
5
|
export type ConsoleTabBarProps = {
|
|
5
6
|
activeTab: ConsoleTabName;
|
|
6
7
|
counts: Record<ConsoleTabName, number>;
|
|
7
8
|
pjcode: string | null;
|
|
9
|
+
pjcodes: string[];
|
|
8
10
|
generatedAt: string | null;
|
|
9
11
|
fromCache: boolean;
|
|
10
12
|
tabHref: (tab: ConsoleTabName) => string;
|
|
11
13
|
onSelectTab: (tab: ConsoleTabName) => void;
|
|
14
|
+
onSelectProject: (pjcode: string) => void;
|
|
12
15
|
settingsButton?: ReactNode;
|
|
13
16
|
};
|
|
14
17
|
|
|
@@ -16,12 +19,51 @@ export const ConsoleTabList = ({
|
|
|
16
19
|
activeTab,
|
|
17
20
|
counts,
|
|
18
21
|
pjcode,
|
|
22
|
+
pjcodes,
|
|
19
23
|
generatedAt,
|
|
20
24
|
fromCache,
|
|
21
25
|
tabHref,
|
|
22
26
|
onSelectTab,
|
|
27
|
+
onSelectProject,
|
|
23
28
|
settingsButton,
|
|
24
29
|
}: ConsoleTabBarProps) => {
|
|
30
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
31
|
+
const [dropdownPos, setDropdownPos] = useState<{
|
|
32
|
+
top: number;
|
|
33
|
+
right: number;
|
|
34
|
+
} | null>(null);
|
|
35
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
36
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isDropdownOpen) return;
|
|
40
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
41
|
+
if (
|
|
42
|
+
dropdownRef.current &&
|
|
43
|
+
!dropdownRef.current.contains(event.target as Node) &&
|
|
44
|
+
buttonRef.current &&
|
|
45
|
+
!buttonRef.current.contains(event.target as Node)
|
|
46
|
+
) {
|
|
47
|
+
setIsDropdownOpen(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
51
|
+
return () => {
|
|
52
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
53
|
+
};
|
|
54
|
+
}, [isDropdownOpen]);
|
|
55
|
+
|
|
56
|
+
const handleButtonClick = () => {
|
|
57
|
+
if (!isDropdownOpen && buttonRef.current) {
|
|
58
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
59
|
+
setDropdownPos({
|
|
60
|
+
top: rect.bottom + 4,
|
|
61
|
+
right: window.innerWidth - rect.right,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
setIsDropdownOpen((prev) => !prev);
|
|
65
|
+
};
|
|
66
|
+
|
|
25
67
|
return (
|
|
26
68
|
<nav aria-label="Console tabs" className="console-tabbar">
|
|
27
69
|
{CONSOLE_TABS.filter((tab) => {
|
|
@@ -62,7 +104,55 @@ export const ConsoleTabList = ({
|
|
|
62
104
|
</a>
|
|
63
105
|
);
|
|
64
106
|
})}
|
|
65
|
-
{pjcode !== null &&
|
|
107
|
+
{pjcode !== null && (
|
|
108
|
+
<div className="console-tab-pjname">
|
|
109
|
+
<button
|
|
110
|
+
ref={buttonRef}
|
|
111
|
+
type="button"
|
|
112
|
+
className="console-tab-pjname-button"
|
|
113
|
+
aria-expanded={isDropdownOpen}
|
|
114
|
+
aria-haspopup="menu"
|
|
115
|
+
onClick={handleButtonClick}
|
|
116
|
+
>
|
|
117
|
+
{pjcode}
|
|
118
|
+
<span className="console-tab-pjname-arrow" aria-hidden="true">
|
|
119
|
+
▾
|
|
120
|
+
</span>
|
|
121
|
+
</button>
|
|
122
|
+
{isDropdownOpen &&
|
|
123
|
+
dropdownPos !== null &&
|
|
124
|
+
createPortal(
|
|
125
|
+
<div
|
|
126
|
+
ref={dropdownRef}
|
|
127
|
+
role="menu"
|
|
128
|
+
className="console-tab-pjname-dropdown"
|
|
129
|
+
aria-label="Select project"
|
|
130
|
+
style={{
|
|
131
|
+
position: 'fixed',
|
|
132
|
+
top: dropdownPos.top,
|
|
133
|
+
right: dropdownPos.right,
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{pjcodes.map((code) => (
|
|
137
|
+
<button
|
|
138
|
+
key={code}
|
|
139
|
+
type="button"
|
|
140
|
+
role="menuitem"
|
|
141
|
+
className="console-tab-pjname-option"
|
|
142
|
+
data-active={code === pjcode ? 'true' : undefined}
|
|
143
|
+
onClick={() => {
|
|
144
|
+
onSelectProject(code);
|
|
145
|
+
setIsDropdownOpen(false);
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{code}
|
|
149
|
+
</button>
|
|
150
|
+
))}
|
|
151
|
+
</div>,
|
|
152
|
+
document.body,
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
66
156
|
{generatedAt !== null && (
|
|
67
157
|
<span
|
|
68
158
|
className="console-tab-geninfo"
|
|
@@ -476,10 +476,12 @@ export const ConsolePage = () => {
|
|
|
476
476
|
activeTab={activeTab}
|
|
477
477
|
counts={counts}
|
|
478
478
|
pjcode={pjcode}
|
|
479
|
+
pjcodes={pjcodes}
|
|
479
480
|
generatedAt={generatedAt}
|
|
480
481
|
fromCache={fromCache}
|
|
481
482
|
tabHref={navigation.tabHref}
|
|
482
483
|
onSelectTab={navigation.selectTab}
|
|
484
|
+
onSelectProject={(code) => navigateAssign(`/projects/${code}`)}
|
|
483
485
|
settingsButton={
|
|
484
486
|
<ConsoleTimerSettingsModalDialog
|
|
485
487
|
isOpen={isSettingsOpen}
|
|
@@ -103,8 +103,58 @@ body {
|
|
|
103
103
|
margin-left: auto;
|
|
104
104
|
align-self: center;
|
|
105
105
|
padding: 0 8px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.console-tab-pjname-button {
|
|
109
|
+
background: transparent;
|
|
110
|
+
border: none;
|
|
106
111
|
color: #8b949e;
|
|
112
|
+
cursor: pointer;
|
|
107
113
|
font-size: 0.7188rem;
|
|
114
|
+
padding: 2px 4px;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
gap: 3px;
|
|
119
|
+
line-height: 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.console-tab-pjname-button:hover {
|
|
123
|
+
color: #e6edf3;
|
|
124
|
+
background: #21262d;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.console-tab-pjname-arrow {
|
|
128
|
+
font-size: 0.625rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.console-tab-pjname-dropdown {
|
|
132
|
+
background: #161b22;
|
|
133
|
+
border: 1px solid #30363d;
|
|
134
|
+
border-radius: 6px;
|
|
135
|
+
list-style: none;
|
|
136
|
+
margin: 0;
|
|
137
|
+
padding: 4px 0;
|
|
138
|
+
min-width: 120px;
|
|
139
|
+
z-index: 100;
|
|
140
|
+
box-shadow: 0 8px 24px rgba(1, 4, 9, 0.5);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.console-tab-pjname-option {
|
|
144
|
+
display: block;
|
|
145
|
+
width: 100%;
|
|
146
|
+
background: transparent;
|
|
147
|
+
border: none;
|
|
148
|
+
color: #e6edf3;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
font-size: 0.7188rem;
|
|
151
|
+
padding: 4px 12px;
|
|
152
|
+
text-align: left;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.console-tab-pjname-option:hover,
|
|
156
|
+
.console-tab-pjname-option[data-active="true"] {
|
|
157
|
+
background: #21262d;
|
|
108
158
|
}
|
|
109
159
|
|
|
110
160
|
.console-tab-geninfo {
|