github-issue-tower-defence-management 1.192.0 → 1.194.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/CHANGELOG.md +26 -0
- package/README.md +4 -0
- package/bin/adapter/entry-points/cli/index.js +19 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +9 -2
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-RaFqjB7u.js → index-CYwLv9pf.js} +2 -2
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
- package/bin/adapter/repositories/NodeLocalCommandRunner.js +5 -0
- package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
- package/bin/adapter/repositories/NodeTmuxSessionRepository.js +29 -0
- package/bin/adapter/repositories/NodeTmuxSessionRepository.js.map +1 -1
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +4 -2
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +70 -22
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +1 -3
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
- package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js +12 -4
- package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +102 -0
- package/src/adapter/entry-points/cli/index.ts +43 -0
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +19 -5
- package/src/adapter/entry-points/console/consoleOperationApi.ts +18 -1
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +45 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +7 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +31 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +96 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +20 -2
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +159 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +17 -1
- package/src/adapter/entry-points/console/ui-dist/assets/{index-RaFqjB7u.js → index-CYwLv9pf.js} +2 -2
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
- package/src/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.test.ts +4 -1
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +2 -0
- package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +1 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
- package/src/adapter/repositories/LocalCommandIssueAttachmentRepository.test.ts +4 -0
- package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.test.ts +1 -0
- package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +16 -0
- package/src/adapter/repositories/NodeLocalCommandRunner.ts +5 -1
- package/src/adapter/repositories/NodeSubAgentProcessLister.test.ts +1 -0
- package/src/adapter/repositories/NodeTmuxSessionRepository.test.ts +132 -0
- package/src/adapter/repositories/NodeTmuxSessionRepository.ts +37 -0
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +38 -0
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +6 -2
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +2 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +213 -2
- package/src/domain/usecases/StartPreparationUseCase.ts +155 -48
- package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/TmuxSessionRepository.ts +4 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +5 -4
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +9 -11
- package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.test.ts +63 -0
- package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.ts +13 -4
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
- package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -0
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
- package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts +1 -0
- package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +10 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts +1 -0
- package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts.map +1 -1
|
@@ -8,6 +8,7 @@ jest.mock('fs', () => {
|
|
|
8
8
|
return {
|
|
9
9
|
...actualFs,
|
|
10
10
|
readFileSync: jest.fn(actualFs.readFileSync),
|
|
11
|
+
existsSync: jest.fn(actualFs.existsSync),
|
|
11
12
|
};
|
|
12
13
|
});
|
|
13
14
|
import {
|
|
@@ -70,9 +71,11 @@ const mockRunCommand = jest.fn<
|
|
|
70
71
|
Promise<{ stdout: string; stderr: string; exitCode: number }>,
|
|
71
72
|
[string, string[]]
|
|
72
73
|
>();
|
|
74
|
+
const mockSpawnInteractive = jest.fn<void, [string, string[]]>();
|
|
73
75
|
jest.mock('../../repositories/NodeLocalCommandRunner', () => ({
|
|
74
76
|
NodeLocalCommandRunner: jest.fn().mockImplementation(() => ({
|
|
75
77
|
runCommand: mockRunCommand,
|
|
78
|
+
spawnInteractive: mockSpawnInteractive,
|
|
76
79
|
})),
|
|
77
80
|
}));
|
|
78
81
|
jest.mock('../../repositories/OauthAPIClaudeRepository', () => ({
|
|
@@ -2463,6 +2466,105 @@ mysteryKey: 'value'
|
|
|
2463
2466
|
});
|
|
2464
2467
|
});
|
|
2465
2468
|
|
|
2469
|
+
describe('attachOrCreate', () => {
|
|
2470
|
+
const issueUrl = 'https://github.com/owner/repo/issues/9';
|
|
2471
|
+
let scopeLibPath = '';
|
|
2472
|
+
|
|
2473
|
+
beforeEach(() => {
|
|
2474
|
+
scopeLibPath = path.join(
|
|
2475
|
+
os.tmpdir(),
|
|
2476
|
+
`test-cl-scope-lib-${Date.now()}.sh`,
|
|
2477
|
+
);
|
|
2478
|
+
fs.writeFileSync(scopeLibPath, '#!/bin/bash\n');
|
|
2479
|
+
process.env.CL_SCOPE_LIB_PATH = scopeLibPath;
|
|
2480
|
+
});
|
|
2481
|
+
|
|
2482
|
+
afterEach(() => {
|
|
2483
|
+
if (fs.existsSync(scopeLibPath)) {
|
|
2484
|
+
fs.unlinkSync(scopeLibPath);
|
|
2485
|
+
}
|
|
2486
|
+
delete process.env.CL_SCOPE_LIB_PATH;
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
it('attaches to the registered session when registry returns a live session', async () => {
|
|
2490
|
+
mockRunCommand
|
|
2491
|
+
.mockResolvedValueOnce({ stdout: 'wq7x3mk\n', stderr: '', exitCode: 0 })
|
|
2492
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 });
|
|
2493
|
+
|
|
2494
|
+
await program.parseAsync([
|
|
2495
|
+
'node',
|
|
2496
|
+
'test',
|
|
2497
|
+
'attachOrCreate',
|
|
2498
|
+
'--issueUrl',
|
|
2499
|
+
issueUrl,
|
|
2500
|
+
]);
|
|
2501
|
+
|
|
2502
|
+
expect(mockRunCommand.mock.calls[0]).toEqual([
|
|
2503
|
+
'bash',
|
|
2504
|
+
[scopeLibPath, 'registry-get-session', issueUrl],
|
|
2505
|
+
]);
|
|
2506
|
+
expect(mockRunCommand.mock.calls[1]).toEqual([
|
|
2507
|
+
'tmux',
|
|
2508
|
+
['has-session', '-t', '=wq7x3mk'],
|
|
2509
|
+
]);
|
|
2510
|
+
expect(mockSpawnInteractive).toHaveBeenCalledWith('tmux', [
|
|
2511
|
+
'attach-session',
|
|
2512
|
+
'-t',
|
|
2513
|
+
'=wq7x3mk',
|
|
2514
|
+
]);
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
it('creates a new session when no registered session is found', async () => {
|
|
2518
|
+
mockRunCommand.mockResolvedValueOnce({
|
|
2519
|
+
stdout: '',
|
|
2520
|
+
stderr: '',
|
|
2521
|
+
exitCode: 0,
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2524
|
+
await program.parseAsync([
|
|
2525
|
+
'node',
|
|
2526
|
+
'test',
|
|
2527
|
+
'attachOrCreate',
|
|
2528
|
+
'--issueUrl',
|
|
2529
|
+
issueUrl,
|
|
2530
|
+
]);
|
|
2531
|
+
|
|
2532
|
+
expect(mockSpawnInteractive).toHaveBeenCalledWith('tmux', [
|
|
2533
|
+
'new-session',
|
|
2534
|
+
'-A',
|
|
2535
|
+
'-s',
|
|
2536
|
+
issueUrl,
|
|
2537
|
+
'cl',
|
|
2538
|
+
issueUrl,
|
|
2539
|
+
]);
|
|
2540
|
+
});
|
|
2541
|
+
|
|
2542
|
+
it('creates a new session when no scope lib path is available', async () => {
|
|
2543
|
+
jest
|
|
2544
|
+
.mocked(fs.existsSync)
|
|
2545
|
+
.mockReturnValueOnce(false)
|
|
2546
|
+
.mockReturnValueOnce(false);
|
|
2547
|
+
|
|
2548
|
+
await program.parseAsync([
|
|
2549
|
+
'node',
|
|
2550
|
+
'test',
|
|
2551
|
+
'attachOrCreate',
|
|
2552
|
+
'--issueUrl',
|
|
2553
|
+
issueUrl,
|
|
2554
|
+
]);
|
|
2555
|
+
|
|
2556
|
+
expect(mockRunCommand.mock.calls).toHaveLength(0);
|
|
2557
|
+
expect(mockSpawnInteractive).toHaveBeenCalledWith('tmux', [
|
|
2558
|
+
'new-session',
|
|
2559
|
+
'-A',
|
|
2560
|
+
'-s',
|
|
2561
|
+
issueUrl,
|
|
2562
|
+
'cl',
|
|
2563
|
+
issueUrl,
|
|
2564
|
+
]);
|
|
2565
|
+
});
|
|
2566
|
+
});
|
|
2567
|
+
|
|
2466
2568
|
describe('ownerCallFileAppend and ownerCallFileDelete', () => {
|
|
2467
2569
|
const issueUrl = 'https://github.com/OWNER/REPO/issues/1';
|
|
2468
2570
|
const otherIssueUrl = 'https://github.com/OWNER/REPO/issues/2';
|
|
@@ -36,6 +36,7 @@ import { ProcTakeOwnershipSpawnRepository } from '../../repositories/ProcTakeOwn
|
|
|
36
36
|
import { DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP } from '../../../domain/usecases/resolveNextStepAgentDispatchRepetition';
|
|
37
37
|
import { ProxyClaudeTokenUsageRepository } from '../../repositories/ProxyClaudeTokenUsageRepository';
|
|
38
38
|
import { SystemDateRepository } from '../../repositories/SystemDateRepository';
|
|
39
|
+
import * as os from 'os';
|
|
39
40
|
import {
|
|
40
41
|
createConsoleGithubTokenResolver,
|
|
41
42
|
createConsoleGithubTokenResolverByItemUrl,
|
|
@@ -198,6 +199,27 @@ type KillTmuxSessionOptions = {
|
|
|
198
199
|
self?: boolean;
|
|
199
200
|
};
|
|
200
201
|
|
|
202
|
+
type AttachOrCreateOptions = {
|
|
203
|
+
issueUrl: string;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const resolveScopeLibPath = (): string | null => {
|
|
207
|
+
const explicitPath = process.env.CL_SCOPE_LIB_PATH;
|
|
208
|
+
if (explicitPath !== undefined && fs.existsSync(explicitPath)) {
|
|
209
|
+
return explicitPath;
|
|
210
|
+
}
|
|
211
|
+
const defaultPath = path.join(
|
|
212
|
+
os.homedir(),
|
|
213
|
+
'git',
|
|
214
|
+
'secretary',
|
|
215
|
+
'machine',
|
|
216
|
+
'sk',
|
|
217
|
+
'sh',
|
|
218
|
+
'cl-scope-lib.sh',
|
|
219
|
+
);
|
|
220
|
+
return fs.existsSync(defaultPath) ? defaultPath : null;
|
|
221
|
+
};
|
|
222
|
+
|
|
201
223
|
const buildGithubRepositoryParams = (
|
|
202
224
|
localStorageRepository: LocalStorageRepository,
|
|
203
225
|
token: string,
|
|
@@ -1302,6 +1324,27 @@ program
|
|
|
1302
1324
|
});
|
|
1303
1325
|
});
|
|
1304
1326
|
|
|
1327
|
+
program
|
|
1328
|
+
.command('attachOrCreate')
|
|
1329
|
+
.description(
|
|
1330
|
+
'Attach to an existing tmux session registered for the given issue URL, or create a new one. Looks up the cl session registry via cl-scope-lib.sh to find a registered session name; if that session is still alive it attaches to it, otherwise creates a new interactive session running cl for the issue URL.',
|
|
1331
|
+
)
|
|
1332
|
+
.requiredOption(
|
|
1333
|
+
'--issueUrl <url>',
|
|
1334
|
+
'GitHub issue URL to attach to or create a session for',
|
|
1335
|
+
)
|
|
1336
|
+
.action(async (options: AttachOrCreateOptions) => {
|
|
1337
|
+
const localCommandRunner = new NodeLocalCommandRunner();
|
|
1338
|
+
const tmuxSessionRepository = new NodeTmuxSessionRepository(
|
|
1339
|
+
localCommandRunner,
|
|
1340
|
+
);
|
|
1341
|
+
const scopeLibPath = resolveScopeLibPath();
|
|
1342
|
+
await tmuxSessionRepository.attachOrCreateInteractiveSession(
|
|
1343
|
+
options.issueUrl,
|
|
1344
|
+
scopeLibPath,
|
|
1345
|
+
);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1305
1348
|
export const reportFatalErrorAndExit = (error: unknown): void => {
|
|
1306
1349
|
console.error(error);
|
|
1307
1350
|
process.exit(1);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
readDoneProjectItemIds,
|
|
13
13
|
} from './consoleDoneStore';
|
|
14
14
|
import {
|
|
15
|
+
CONFLICT_RETURNED_MESSAGE,
|
|
15
16
|
type ConsoleOperationContext,
|
|
16
17
|
type ConsoleProjectBinding,
|
|
17
18
|
handleAttachmentUpload,
|
|
@@ -566,7 +567,7 @@ describe('consoleOperationApi', () => {
|
|
|
566
567
|
expect(issueRepository.mergePullRequest).not.toHaveBeenCalled();
|
|
567
568
|
});
|
|
568
569
|
|
|
569
|
-
it('returns
|
|
570
|
+
it('posts a conflict comment, moves to Awaiting workspace, and returns 200 when the pull request has a merge conflict', async () => {
|
|
570
571
|
issueRepository.getOpenPullRequest.mockResolvedValue({
|
|
571
572
|
url: 'https://github.com/o/r/pull/1',
|
|
572
573
|
branchName: null,
|
|
@@ -586,11 +587,24 @@ describe('consoleOperationApi', () => {
|
|
|
586
587
|
prUrl: 'https://github.com/o/r/pull/1',
|
|
587
588
|
projectItemId: 'PVTI_conflict',
|
|
588
589
|
});
|
|
589
|
-
expect(response.statusCode).toBe(
|
|
590
|
-
expect(
|
|
591
|
-
|
|
592
|
-
|
|
590
|
+
expect(response.statusCode).toBe(200);
|
|
591
|
+
expect(issueRepository.createCommentByUrl).toHaveBeenCalledWith(
|
|
592
|
+
'https://github.com/o/r/pull/1',
|
|
593
|
+
CONFLICT_RETURNED_MESSAGE,
|
|
594
|
+
);
|
|
595
|
+
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
596
|
+
project,
|
|
597
|
+
expect.objectContaining({
|
|
598
|
+
itemId: 'PVTI_conflict',
|
|
599
|
+
url: 'https://github.com/o/r/pull/1',
|
|
600
|
+
}),
|
|
601
|
+
'status_aw',
|
|
602
|
+
);
|
|
593
603
|
expect(issueRepository.mergePullRequest).not.toHaveBeenCalled();
|
|
604
|
+
expectRecordedOnlyIn(
|
|
605
|
+
'PVTI_conflict',
|
|
606
|
+
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
607
|
+
);
|
|
594
608
|
});
|
|
595
609
|
|
|
596
610
|
it('returns 400 with check names when required checks are not green', async () => {
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
import { findConsoleItemUrl } from './consoleItemUrlLookup';
|
|
21
21
|
|
|
22
22
|
export const AWAITING_WORKSPACE_STATUS_NAME = 'awaiting workspace';
|
|
23
|
+
export const CONFLICT_RETURNED_MESSAGE =
|
|
24
|
+
'Auto Status Check: CONFLICT\nThis pull request has a merge conflict and has been returned to Awaiting Workspace.';
|
|
23
25
|
export const IN_TMUX_BY_HUMAN_STATUS_NAME = 'in tmux by human';
|
|
24
26
|
export const CHORE_LABEL_NAME = 'chore';
|
|
25
27
|
|
|
@@ -367,7 +369,22 @@ export const handleReview = async (
|
|
|
367
369
|
);
|
|
368
370
|
}
|
|
369
371
|
if (prStatus.isConflicted) {
|
|
370
|
-
|
|
372
|
+
await issueRepository.createCommentByUrl(
|
|
373
|
+
prUrl,
|
|
374
|
+
CONFLICT_RETURNED_MESSAGE,
|
|
375
|
+
);
|
|
376
|
+
const conflictFailure = await updateStatusByName(
|
|
377
|
+
issueRepository,
|
|
378
|
+
project,
|
|
379
|
+
prUrl,
|
|
380
|
+
projectItemId,
|
|
381
|
+
AWAITING_WORKSPACE_STATUS_NAME,
|
|
382
|
+
);
|
|
383
|
+
if (conflictFailure !== null) {
|
|
384
|
+
return conflictFailure;
|
|
385
|
+
}
|
|
386
|
+
recordDoneForStatusChange(context, pjcode, projectItemId);
|
|
387
|
+
return ok();
|
|
371
388
|
}
|
|
372
389
|
if (!prStatus.isPassedAllCiJob) {
|
|
373
390
|
const missing = prStatus.missingRequiredCheckNames;
|
|
@@ -420,6 +420,51 @@ test('renders the stories tab with non-gray stories, their open item counts, and
|
|
|
420
420
|
).toHaveCount(0);
|
|
421
421
|
});
|
|
422
422
|
|
|
423
|
+
test('shows and hides gray stories with the Show archived toggle button on the stories tab', async ({
|
|
424
|
+
page,
|
|
425
|
+
}) => {
|
|
426
|
+
await page.goto(harness.appRootUrl);
|
|
427
|
+
|
|
428
|
+
await tabByLabel(page, 'Stories').click();
|
|
429
|
+
await expect(activeTabLabel(page)).toHaveText('Stories');
|
|
430
|
+
|
|
431
|
+
await expect(
|
|
432
|
+
page.locator('.console-op-button', { hasText: 'Show archived' }),
|
|
433
|
+
).toBeVisible();
|
|
434
|
+
|
|
435
|
+
await expect(
|
|
436
|
+
page.locator('.console-story-list-row', {
|
|
437
|
+
hasText: 'regular / workflow improvement',
|
|
438
|
+
}),
|
|
439
|
+
).toHaveCount(0);
|
|
440
|
+
|
|
441
|
+
await page
|
|
442
|
+
.locator('.console-op-button', { hasText: 'Show archived' })
|
|
443
|
+
.click();
|
|
444
|
+
|
|
445
|
+
await expect(
|
|
446
|
+
page.locator('.console-story-list-row', {
|
|
447
|
+
hasText: 'regular / workflow improvement',
|
|
448
|
+
}),
|
|
449
|
+
).toBeVisible();
|
|
450
|
+
await expect(
|
|
451
|
+
page.locator('.console-op-button', { hasText: 'Hide archived' }),
|
|
452
|
+
).toBeVisible();
|
|
453
|
+
|
|
454
|
+
await page
|
|
455
|
+
.locator('.console-op-button', { hasText: 'Hide archived' })
|
|
456
|
+
.click();
|
|
457
|
+
|
|
458
|
+
await expect(
|
|
459
|
+
page.locator('.console-story-list-row', {
|
|
460
|
+
hasText: 'regular / workflow improvement',
|
|
461
|
+
}),
|
|
462
|
+
).toHaveCount(0);
|
|
463
|
+
await expect(
|
|
464
|
+
page.locator('.console-op-button', { hasText: 'Show archived' }),
|
|
465
|
+
).toBeVisible();
|
|
466
|
+
});
|
|
467
|
+
|
|
423
468
|
test('shows the agent label and value in the list row and the agent chip in the detail view', async ({
|
|
424
469
|
page,
|
|
425
470
|
}) => {
|
|
@@ -295,6 +295,13 @@ const CONSOLE_E2E_STORIES_SNAPSHOT = {
|
|
|
295
295
|
storyViewUrl:
|
|
296
296
|
'https://github.com/orgs/HiromiShikata/projects/6/views/1?sliceBy%5Bvalue%5D=TDPM%20Console%20port',
|
|
297
297
|
},
|
|
298
|
+
{
|
|
299
|
+
storyName: 'regular / workflow improvement',
|
|
300
|
+
storyOptionId: '28415d6c',
|
|
301
|
+
color: 'GRAY',
|
|
302
|
+
openItemCount: 1,
|
|
303
|
+
storyViewUrl: null,
|
|
304
|
+
},
|
|
298
305
|
{
|
|
299
306
|
storyName: 'Publish product documentation site',
|
|
300
307
|
storyOptionId: 'f7cd5cbc',
|
|
@@ -47,13 +47,26 @@ const storiesWithUrl = storiesWithoutUrl.map((s) => ({
|
|
|
47
47
|
storyViewUrl: `${BASE_VIEW_URL}?sliceBy%5Bvalue%5D=${encodeURI(s.storyName).replace(/#/g, '%23').replace(/&/g, '%26')}`,
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
|
+
const storiesWithArchived = [
|
|
51
|
+
...storiesWithoutUrl,
|
|
52
|
+
{
|
|
53
|
+
storyName: 'regular / NO STORY; SET STORY FIELD',
|
|
54
|
+
storyOptionId: '28415d6c',
|
|
55
|
+
color: 'GRAY' as const,
|
|
56
|
+
openItemCount: 5,
|
|
57
|
+
storyViewUrl: null,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
50
61
|
const meta: Meta<typeof ConsoleStoryList> = {
|
|
51
62
|
title: 'Console/ConsoleStoryList',
|
|
52
63
|
component: ConsoleStoryList,
|
|
53
64
|
args: {
|
|
65
|
+
showGray: false,
|
|
54
66
|
onCreateIssue: () => Promise.resolve(),
|
|
55
67
|
onAddStory: () => Promise.resolve(),
|
|
56
68
|
onSelectColor: () => undefined,
|
|
69
|
+
onToggleGray: () => undefined,
|
|
57
70
|
optimisticColors: {},
|
|
58
71
|
colorChangeInFlight: null,
|
|
59
72
|
colorErrors: {},
|
|
@@ -103,3 +116,21 @@ export const ErrorState: Story = {
|
|
|
103
116
|
error: 'HTTP 404',
|
|
104
117
|
},
|
|
105
118
|
};
|
|
119
|
+
|
|
120
|
+
export const WithArchivedHidden: Story = {
|
|
121
|
+
args: {
|
|
122
|
+
stories: storiesWithArchived,
|
|
123
|
+
isLoading: false,
|
|
124
|
+
error: null,
|
|
125
|
+
showGray: false,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const WithArchivedShown: Story = {
|
|
130
|
+
args: {
|
|
131
|
+
stories: storiesWithArchived,
|
|
132
|
+
isLoading: false,
|
|
133
|
+
error: null,
|
|
134
|
+
showGray: true,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
@@ -19,13 +19,25 @@ const storyEntries: ConsoleStoryEntry[] = [
|
|
|
19
19
|
},
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
+
const grayStoryEntry: ConsoleStoryEntry = {
|
|
23
|
+
storyName: 'Archived Story',
|
|
24
|
+
storyOptionId: 'gray-id',
|
|
25
|
+
color: 'GRAY',
|
|
26
|
+
openItemCount: 3,
|
|
27
|
+
storyViewUrl: null,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const entriesWithGray: ConsoleStoryEntry[] = [...storyEntries, grayStoryEntry];
|
|
31
|
+
|
|
22
32
|
const defaultProps = {
|
|
23
33
|
stories: storyEntries,
|
|
24
34
|
isLoading: false,
|
|
25
35
|
error: null,
|
|
36
|
+
showGray: false,
|
|
26
37
|
onCreateIssue: () => Promise.resolve(),
|
|
27
38
|
onAddStory: () => Promise.resolve(),
|
|
28
39
|
onSelectColor: () => undefined,
|
|
40
|
+
onToggleGray: () => undefined,
|
|
29
41
|
optimisticColors: {} as Record<string, ConsoleColor>,
|
|
30
42
|
colorChangeInFlight: null as string | null,
|
|
31
43
|
colorErrors: {} as Record<string, string>,
|
|
@@ -370,4 +382,88 @@ describe('ConsoleStoryList', () => {
|
|
|
370
382
|
});
|
|
371
383
|
expect(writeText).toHaveBeenCalledWith(storyEntries[0].storyName);
|
|
372
384
|
});
|
|
385
|
+
|
|
386
|
+
describe('gray story toggle', () => {
|
|
387
|
+
it('hides gray stories when showGray is false', () => {
|
|
388
|
+
const { queryByText } = render(
|
|
389
|
+
<ConsoleStoryList
|
|
390
|
+
{...defaultProps}
|
|
391
|
+
stories={entriesWithGray}
|
|
392
|
+
showGray={false}
|
|
393
|
+
/>,
|
|
394
|
+
);
|
|
395
|
+
expect(queryByText('Archived Story')).toBeNull();
|
|
396
|
+
expect(queryByText('TDPM Console port')).toBeInTheDocument();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('shows gray stories when showGray is true', () => {
|
|
400
|
+
const { getByText } = render(
|
|
401
|
+
<ConsoleStoryList
|
|
402
|
+
{...defaultProps}
|
|
403
|
+
stories={entriesWithGray}
|
|
404
|
+
showGray={true}
|
|
405
|
+
/>,
|
|
406
|
+
);
|
|
407
|
+
expect(getByText('Archived Story')).toBeInTheDocument();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('shows the toggle button labeled Show archived when at least one gray story exists and showGray is false', () => {
|
|
411
|
+
const { getByRole } = render(
|
|
412
|
+
<ConsoleStoryList
|
|
413
|
+
{...defaultProps}
|
|
414
|
+
stories={entriesWithGray}
|
|
415
|
+
showGray={false}
|
|
416
|
+
/>,
|
|
417
|
+
);
|
|
418
|
+
expect(
|
|
419
|
+
getByRole('button', { name: 'Show archived' }),
|
|
420
|
+
).toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('shows the toggle button labeled Hide archived when showGray is true', () => {
|
|
424
|
+
const { getByRole } = render(
|
|
425
|
+
<ConsoleStoryList
|
|
426
|
+
{...defaultProps}
|
|
427
|
+
stories={entriesWithGray}
|
|
428
|
+
showGray={true}
|
|
429
|
+
/>,
|
|
430
|
+
);
|
|
431
|
+
expect(
|
|
432
|
+
getByRole('button', { name: 'Hide archived' }),
|
|
433
|
+
).toBeInTheDocument();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('does not show the toggle button when no gray stories exist', () => {
|
|
437
|
+
const { queryByRole } = render(
|
|
438
|
+
<ConsoleStoryList {...defaultProps} stories={storyEntries} />,
|
|
439
|
+
);
|
|
440
|
+
expect(queryByRole('button', { name: 'Show archived' })).toBeNull();
|
|
441
|
+
expect(queryByRole('button', { name: 'Hide archived' })).toBeNull();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('calls onToggleGray when the toggle button is clicked', () => {
|
|
445
|
+
const onToggleGray = jest.fn();
|
|
446
|
+
const { getByRole } = render(
|
|
447
|
+
<ConsoleStoryList
|
|
448
|
+
{...defaultProps}
|
|
449
|
+
stories={entriesWithGray}
|
|
450
|
+
showGray={false}
|
|
451
|
+
onToggleGray={onToggleGray}
|
|
452
|
+
/>,
|
|
453
|
+
);
|
|
454
|
+
fireEvent.click(getByRole('button', { name: 'Show archived' }));
|
|
455
|
+
expect(onToggleGray).toHaveBeenCalledTimes(1);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('shows No active stories when all stories are gray and showGray is false', () => {
|
|
459
|
+
const { getByText } = render(
|
|
460
|
+
<ConsoleStoryList
|
|
461
|
+
{...defaultProps}
|
|
462
|
+
stories={[grayStoryEntry]}
|
|
463
|
+
showGray={false}
|
|
464
|
+
/>,
|
|
465
|
+
);
|
|
466
|
+
expect(getByText('No active stories')).toBeInTheDocument();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
373
469
|
});
|
|
@@ -132,9 +132,11 @@ export type ConsoleStoryListProps = {
|
|
|
132
132
|
stories: ConsoleStoryEntry[];
|
|
133
133
|
isLoading: boolean;
|
|
134
134
|
error: string | null;
|
|
135
|
+
showGray: boolean;
|
|
135
136
|
onCreateIssue: (storyOptionId: string, title: string) => Promise<void>;
|
|
136
137
|
onAddStory: (storyName: string) => Promise<void>;
|
|
137
138
|
onSelectColor: (storyOptionId: string, newColor: ConsoleColor) => void;
|
|
139
|
+
onToggleGray: () => void;
|
|
138
140
|
optimisticColors: Record<string, ConsoleColor>;
|
|
139
141
|
colorChangeInFlight: string | null;
|
|
140
142
|
colorErrors: Record<string, string>;
|
|
@@ -144,9 +146,11 @@ export const ConsoleStoryList = ({
|
|
|
144
146
|
stories,
|
|
145
147
|
isLoading,
|
|
146
148
|
error,
|
|
149
|
+
showGray,
|
|
147
150
|
onCreateIssue,
|
|
148
151
|
onAddStory,
|
|
149
152
|
onSelectColor,
|
|
153
|
+
onToggleGray,
|
|
150
154
|
optimisticColors,
|
|
151
155
|
colorChangeInFlight,
|
|
152
156
|
colorErrors,
|
|
@@ -197,13 +201,18 @@ export const ConsoleStoryList = ({
|
|
|
197
201
|
onSelectColor(storyOptionId, newColor);
|
|
198
202
|
};
|
|
199
203
|
|
|
204
|
+
const visibleStories = showGray
|
|
205
|
+
? stories
|
|
206
|
+
: stories.filter((s) => s.color !== 'GRAY');
|
|
207
|
+
const hasGrayStories = stories.some((s) => s.color === 'GRAY');
|
|
208
|
+
|
|
200
209
|
return (
|
|
201
210
|
<div className="console-story-list-container">
|
|
202
|
-
{
|
|
211
|
+
{visibleStories.length === 0 ? (
|
|
203
212
|
<p className="console-list-empty">No active stories</p>
|
|
204
213
|
) : (
|
|
205
214
|
<ul className="console-story-list">
|
|
206
|
-
{
|
|
215
|
+
{visibleStories.map((entry) => {
|
|
207
216
|
const displayColor: ConsoleColor =
|
|
208
217
|
optimisticColors[entry.storyOptionId] ?? entry.color;
|
|
209
218
|
const palette = colorFromEnum(displayColor);
|
|
@@ -285,6 +294,15 @@ export const ConsoleStoryList = ({
|
|
|
285
294
|
})}
|
|
286
295
|
</ul>
|
|
287
296
|
)}
|
|
297
|
+
{hasGrayStories && (
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
className="console-op-button"
|
|
301
|
+
onClick={onToggleGray}
|
|
302
|
+
>
|
|
303
|
+
{showGray ? 'Hide archived' : 'Show archived'}
|
|
304
|
+
</button>
|
|
305
|
+
)}
|
|
288
306
|
<div className="console-add-story-section">
|
|
289
307
|
<button
|
|
290
308
|
type="button"
|