github-issue-tower-defence-management 1.90.0 → 1.91.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 +19 -5
- package/bin/adapter/entry-points/cli/index.js +17 -13
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +54 -27
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +38 -0
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -0
- package/bin/adapter/entry-points/console/consoleServer.js +3 -4
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +1 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +3 -0
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
- package/jest.config.js +57 -9
- package/package.json +17 -13
- package/src/adapter/entry-points/cli/index.test.ts +18 -3
- package/src/adapter/entry-points/cli/index.ts +32 -14
- package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
- package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +5 -4
- package/src/adapter/entry-points/console/consoleServer.ts +5 -7
- package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
- package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.test.tsx +36 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +50 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMermaidDiagram.stories.tsx +22 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMermaidDiagram.test.tsx +38 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMermaidDiagram.tsx +65 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleChangedFileList.stories.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleChangedFileList.test.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleChangedFileList.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.stories.tsx +29 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.test.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.tsx +66 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommitList.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommitList.test.tsx +53 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommitList.tsx +53 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +79 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +81 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +229 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemIcon.stories.tsx +82 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemIcon.test.tsx +52 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemIcon.tsx +32 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.stories.tsx +31 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +40 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +88 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.stories.tsx +26 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.test.tsx +32 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.tsx +36 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/{ConsoleProjectHeader.stories.tsx → layout/ConsoleProjectSummary.stories.tsx} +5 -5
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleProjectSummary.test.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/{ConsoleProjectHeader.tsx → layout/ConsoleProjectSummary.tsx} +3 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +70 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +59 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +41 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.stories.tsx +60 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.test.tsx +87 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.tsx +68 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.test.tsx +43 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStorySummary.stories.tsx +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStorySummary.test.tsx +24 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStorySummary.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleCloseActions.stories.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleCloseActions.test.tsx +21 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleCloseActions.tsx +26 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.stories.tsx +20 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.test.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.stories.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.test.tsx +85 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.tsx +58 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.stories.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.test.tsx +33 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStatusActions.stories.tsx +17 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStatusActions.test.tsx +49 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStatusActions.tsx +66 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStoryActions.stories.tsx +17 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStoryActions.test.tsx +39 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleStoryActions.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/colors.test.ts +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/colors.ts +73 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/fileStatus.test.ts +35 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/fileStatus.ts +21 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/grouping.test.ts +91 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/grouping.ts +79 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/itemIcons.test.ts +97 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/itemIcons.ts +95 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/operations.test.ts +37 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/operations.ts +35 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +124 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.ts +101 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/relativeTime.test.ts +52 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/relativeTime.ts +51 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +141 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +137 -11
- package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +244 -0
- package/src/adapter/entry-points/console/ui/src/index.css +352 -2
- package/src/adapter/entry-points/console/ui/tsconfig.json +1 -0
- package/src/adapter/entry-points/console/ui/vite.config.ts +5 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-PtVrAcBb.js +100 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +27 -2
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +1 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +26 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +17 -1
- 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/consoleOperationApi.d.ts +6 -2
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
- package/types/adapter/entry-points/console/consoleServer.d.ts +2 -3
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts +2 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DDjYPXRT.js +0 -49
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DHlBLm7d.css +0 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +0 -44
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +0 -58
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +0 -34
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +0 -32
- package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +0 -47
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -78
- package/src/adapter/entry-points/console/ui/src/features/console/types.ts +0 -69
- package/src/adapter/entry-points/console/ui-dist/assets/index-DDjYPXRT.js +0 -49
- package/src/adapter/entry-points/console/ui-dist/assets/index-DHlBLm7d.css +0 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ConsoleFileStatusBadge = {
|
|
2
|
+
label: string;
|
|
3
|
+
color: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const fileStatusBadge = (status: string): ConsoleFileStatusBadge => {
|
|
7
|
+
switch (status) {
|
|
8
|
+
case 'added':
|
|
9
|
+
return { label: 'A', color: '#3fb950' };
|
|
10
|
+
case 'modified':
|
|
11
|
+
return { label: 'M', color: '#d29922' };
|
|
12
|
+
case 'removed':
|
|
13
|
+
return { label: 'D', color: '#f85149' };
|
|
14
|
+
case 'renamed':
|
|
15
|
+
return { label: 'R', color: '#a371f7' };
|
|
16
|
+
case 'changed':
|
|
17
|
+
return { label: 'M', color: '#d29922' };
|
|
18
|
+
default:
|
|
19
|
+
return { label: '?', color: '#8b949e' };
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildConsoleListRows,
|
|
3
|
+
CONSOLE_NO_STORY_LABEL,
|
|
4
|
+
resolveItemStory,
|
|
5
|
+
resolveStoryColorEnum,
|
|
6
|
+
} from './grouping';
|
|
7
|
+
import type { ConsoleListItem, ConsoleOverlay } from './types';
|
|
8
|
+
|
|
9
|
+
const item = (
|
|
10
|
+
overrides: Partial<ConsoleListItem> &
|
|
11
|
+
Pick<ConsoleListItem, 'number' | 'story'>,
|
|
12
|
+
): ConsoleListItem => ({
|
|
13
|
+
title: `Item ${overrides.number}`,
|
|
14
|
+
url: `https://github.com/o/r/issues/${overrides.number}`,
|
|
15
|
+
repo: 'o/r',
|
|
16
|
+
nameWithOwner: 'o/r',
|
|
17
|
+
projectItemId: `PVTI_${overrides.number}`,
|
|
18
|
+
itemId: `PVTI_${overrides.number}`,
|
|
19
|
+
isPr: false,
|
|
20
|
+
labels: [],
|
|
21
|
+
createdAt: '2026-06-10T00:00:00.000Z',
|
|
22
|
+
...overrides,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('resolveStoryColorEnum', () => {
|
|
26
|
+
it('reads a wrapped color object shape', () => {
|
|
27
|
+
expect(resolveStoryColorEnum({ s: { color: 'BLUE' } }, 's')).toBe('BLUE');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('reads a bare enum shape', () => {
|
|
31
|
+
expect(resolveStoryColorEnum({ s: 'RED' }, 's')).toBe('RED');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns null for an unknown story', () => {
|
|
35
|
+
expect(resolveStoryColorEnum({}, 's')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('resolveItemStory', () => {
|
|
40
|
+
it('prefers the overlay story name', () => {
|
|
41
|
+
const overlay: ConsoleOverlay = {
|
|
42
|
+
PVTI_1: {
|
|
43
|
+
ts: 1,
|
|
44
|
+
mode: 'triage',
|
|
45
|
+
story: { name: 'Overlay', color: 'BLUE' },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
expect(
|
|
49
|
+
resolveItemStory(item({ number: 1, story: 'Original' }), overlay),
|
|
50
|
+
).toBe('Overlay');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('uses the trimmed item story when no overlay story exists', () => {
|
|
54
|
+
expect(resolveItemStory(item({ number: 2, story: ' Real ' }), {})).toBe(
|
|
55
|
+
' Real ',
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('falls back to the no-story label when empty', () => {
|
|
60
|
+
expect(resolveItemStory(item({ number: 3, story: ' ' }), {})).toBe(
|
|
61
|
+
CONSOLE_NO_STORY_LABEL,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('buildConsoleListRows', () => {
|
|
67
|
+
it('inserts a group header whenever the story changes and keeps array order', () => {
|
|
68
|
+
const items = [
|
|
69
|
+
item({ number: 1, story: 'Alpha' }),
|
|
70
|
+
item({ number: 2, story: 'Alpha' }),
|
|
71
|
+
item({ number: 3, story: 'Beta' }),
|
|
72
|
+
item({ number: 4, story: 'Alpha' }),
|
|
73
|
+
];
|
|
74
|
+
const rows = buildConsoleListRows(items, {});
|
|
75
|
+
expect(rows.map((row) => row.kind)).toEqual([
|
|
76
|
+
'group-header',
|
|
77
|
+
'item',
|
|
78
|
+
'item',
|
|
79
|
+
'group-header',
|
|
80
|
+
'item',
|
|
81
|
+
'group-header',
|
|
82
|
+
'item',
|
|
83
|
+
]);
|
|
84
|
+
const firstHeader = rows[0];
|
|
85
|
+
expect(firstHeader.kind === 'group-header' && firstHeader.count).toBe(3);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns no rows for an empty list', () => {
|
|
89
|
+
expect(buildConsoleListRows([], {})).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConsoleColor,
|
|
3
|
+
ConsoleListItem,
|
|
4
|
+
ConsoleOverlay,
|
|
5
|
+
ConsoleStoryColorSource,
|
|
6
|
+
} from './types';
|
|
7
|
+
|
|
8
|
+
export const CONSOLE_NO_STORY_LABEL = '(No story)';
|
|
9
|
+
|
|
10
|
+
export const resolveStoryColorEnum = (
|
|
11
|
+
storyColors: ConsoleStoryColorSource,
|
|
12
|
+
storyName: string,
|
|
13
|
+
): ConsoleColor | null => {
|
|
14
|
+
const entry = storyColors[storyName];
|
|
15
|
+
if (entry === undefined) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (typeof entry === 'string') {
|
|
19
|
+
return entry;
|
|
20
|
+
}
|
|
21
|
+
return entry.color;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const resolveItemStory = (
|
|
25
|
+
item: ConsoleListItem,
|
|
26
|
+
overlay: ConsoleOverlay,
|
|
27
|
+
): string => {
|
|
28
|
+
const overlayKey =
|
|
29
|
+
item.projectItemId !== '' ? item.projectItemId : item.itemId;
|
|
30
|
+
const overlayEntry = overlay[overlayKey];
|
|
31
|
+
if (
|
|
32
|
+
overlayEntry?.story?.name !== undefined &&
|
|
33
|
+
overlayEntry.story.name !== ''
|
|
34
|
+
) {
|
|
35
|
+
return overlayEntry.story.name;
|
|
36
|
+
}
|
|
37
|
+
const trimmed = item.story.trim();
|
|
38
|
+
return trimmed !== '' ? item.story : CONSOLE_NO_STORY_LABEL;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ConsoleListGroupRow = {
|
|
42
|
+
kind: 'group-header';
|
|
43
|
+
story: string;
|
|
44
|
+
count: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ConsoleItemSummary = {
|
|
48
|
+
kind: 'item';
|
|
49
|
+
item: ConsoleListItem;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type ConsoleListRow = ConsoleListGroupRow | ConsoleItemSummary;
|
|
53
|
+
|
|
54
|
+
export const buildConsoleListRows = (
|
|
55
|
+
items: ConsoleListItem[],
|
|
56
|
+
overlay: ConsoleOverlay,
|
|
57
|
+
): ConsoleListRow[] => {
|
|
58
|
+
const storyCounts = new Map<string, number>();
|
|
59
|
+
for (const item of items) {
|
|
60
|
+
const story = resolveItemStory(item, overlay);
|
|
61
|
+
storyCounts.set(story, (storyCounts.get(story) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const rows: ConsoleListRow[] = [];
|
|
65
|
+
let previousStory: string | null = null;
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
const story = resolveItemStory(item, overlay);
|
|
68
|
+
if (story !== previousStory) {
|
|
69
|
+
rows.push({
|
|
70
|
+
kind: 'group-header',
|
|
71
|
+
story,
|
|
72
|
+
count: storyCounts.get(story) ?? 0,
|
|
73
|
+
});
|
|
74
|
+
previousStory = story;
|
|
75
|
+
}
|
|
76
|
+
rows.push({ kind: 'item', item });
|
|
77
|
+
}
|
|
78
|
+
return rows;
|
|
79
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { CONSOLE_ITEM_ICONS, resolveConsoleItemIconKind } from './itemIcons';
|
|
2
|
+
|
|
3
|
+
describe('resolveConsoleItemIconKind', () => {
|
|
4
|
+
it('resolves a draft pull request to prDraft', () => {
|
|
5
|
+
expect(
|
|
6
|
+
resolveConsoleItemIconKind({
|
|
7
|
+
isPr: true,
|
|
8
|
+
state: 'open',
|
|
9
|
+
merged: false,
|
|
10
|
+
isDraft: true,
|
|
11
|
+
stateReason: '',
|
|
12
|
+
}),
|
|
13
|
+
).toBe('prDraft');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('resolves a merged pull request to prMerged', () => {
|
|
17
|
+
expect(
|
|
18
|
+
resolveConsoleItemIconKind({
|
|
19
|
+
isPr: true,
|
|
20
|
+
state: 'closed',
|
|
21
|
+
merged: true,
|
|
22
|
+
isDraft: false,
|
|
23
|
+
stateReason: '',
|
|
24
|
+
}),
|
|
25
|
+
).toBe('prMerged');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('resolves a closed pull request to prClosed', () => {
|
|
29
|
+
expect(
|
|
30
|
+
resolveConsoleItemIconKind({
|
|
31
|
+
isPr: true,
|
|
32
|
+
state: 'closed',
|
|
33
|
+
merged: false,
|
|
34
|
+
isDraft: false,
|
|
35
|
+
stateReason: '',
|
|
36
|
+
}),
|
|
37
|
+
).toBe('prClosed');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('resolves an open pull request to prOpen', () => {
|
|
41
|
+
expect(
|
|
42
|
+
resolveConsoleItemIconKind({
|
|
43
|
+
isPr: true,
|
|
44
|
+
state: 'open',
|
|
45
|
+
merged: false,
|
|
46
|
+
isDraft: false,
|
|
47
|
+
stateReason: '',
|
|
48
|
+
}),
|
|
49
|
+
).toBe('prOpen');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('resolves a completed closed issue to issueClosed', () => {
|
|
53
|
+
expect(
|
|
54
|
+
resolveConsoleItemIconKind({
|
|
55
|
+
isPr: false,
|
|
56
|
+
state: 'closed',
|
|
57
|
+
merged: false,
|
|
58
|
+
isDraft: false,
|
|
59
|
+
stateReason: 'completed',
|
|
60
|
+
}),
|
|
61
|
+
).toBe('issueClosed');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('resolves a not-planned closed issue to issueClosedNotPlanned', () => {
|
|
65
|
+
expect(
|
|
66
|
+
resolveConsoleItemIconKind({
|
|
67
|
+
isPr: false,
|
|
68
|
+
state: 'closed',
|
|
69
|
+
merged: false,
|
|
70
|
+
isDraft: false,
|
|
71
|
+
stateReason: 'not_planned',
|
|
72
|
+
}),
|
|
73
|
+
).toBe('issueClosedNotPlanned');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('resolves an open issue to issueOpen', () => {
|
|
77
|
+
expect(
|
|
78
|
+
resolveConsoleItemIconKind({
|
|
79
|
+
isPr: false,
|
|
80
|
+
state: 'open',
|
|
81
|
+
merged: false,
|
|
82
|
+
isDraft: false,
|
|
83
|
+
stateReason: '',
|
|
84
|
+
}),
|
|
85
|
+
).toBe('issueOpen');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('uses the documented colors for each kind', () => {
|
|
89
|
+
expect(CONSOLE_ITEM_ICONS.prDraft.color).toBe('#8b949e');
|
|
90
|
+
expect(CONSOLE_ITEM_ICONS.prMerged.color).toBe('#a371f7');
|
|
91
|
+
expect(CONSOLE_ITEM_ICONS.prClosed.color).toBe('#f85149');
|
|
92
|
+
expect(CONSOLE_ITEM_ICONS.prOpen.color).toBe('#3fb950');
|
|
93
|
+
expect(CONSOLE_ITEM_ICONS.issueClosedNotPlanned.color).toBe('#848d97');
|
|
94
|
+
expect(CONSOLE_ITEM_ICONS.issueClosed.color).toBe('#a371f7');
|
|
95
|
+
expect(CONSOLE_ITEM_ICONS.issueOpen.color).toBe('#3fb950');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type ConsoleItemIconKind =
|
|
2
|
+
| 'issueOpen'
|
|
3
|
+
| 'issueClosed'
|
|
4
|
+
| 'issueClosedNotPlanned'
|
|
5
|
+
| 'prOpen'
|
|
6
|
+
| 'prMerged'
|
|
7
|
+
| 'prClosed'
|
|
8
|
+
| 'prDraft';
|
|
9
|
+
|
|
10
|
+
export type ConsoleItemIconDefinition = {
|
|
11
|
+
color: string;
|
|
12
|
+
paths: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const CONSOLE_ITEM_ICONS: Record<
|
|
16
|
+
ConsoleItemIconKind,
|
|
17
|
+
ConsoleItemIconDefinition
|
|
18
|
+
> = {
|
|
19
|
+
issueOpen: {
|
|
20
|
+
color: '#3fb950',
|
|
21
|
+
paths: [
|
|
22
|
+
'M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z',
|
|
23
|
+
'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
issueClosed: {
|
|
27
|
+
color: '#a371f7',
|
|
28
|
+
paths: [
|
|
29
|
+
'M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z',
|
|
30
|
+
'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z',
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
issueClosedNotPlanned: {
|
|
34
|
+
color: '#848d97',
|
|
35
|
+
paths: [
|
|
36
|
+
'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
prOpen: {
|
|
40
|
+
color: '#3fb950',
|
|
41
|
+
paths: [
|
|
42
|
+
'M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
prMerged: {
|
|
46
|
+
color: '#a371f7',
|
|
47
|
+
paths: [
|
|
48
|
+
'M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z',
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
prClosed: {
|
|
52
|
+
color: '#f85149',
|
|
53
|
+
paths: [
|
|
54
|
+
'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
prDraft: {
|
|
58
|
+
color: '#8b949e',
|
|
59
|
+
paths: [
|
|
60
|
+
'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 14a2.25 2.25 0 1 1 0-4.5 2.25 2.25 0 0 1 0 4.5ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM14 7.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm0-4.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z',
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type ConsoleItemIconInput = {
|
|
66
|
+
isPr: boolean;
|
|
67
|
+
state: string;
|
|
68
|
+
merged: boolean;
|
|
69
|
+
isDraft: boolean;
|
|
70
|
+
stateReason: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const resolveConsoleItemIconKind = ({
|
|
74
|
+
isPr,
|
|
75
|
+
state,
|
|
76
|
+
merged,
|
|
77
|
+
isDraft,
|
|
78
|
+
stateReason,
|
|
79
|
+
}: ConsoleItemIconInput): ConsoleItemIconKind => {
|
|
80
|
+
if (isPr) {
|
|
81
|
+
if (isDraft) {
|
|
82
|
+
return 'prDraft';
|
|
83
|
+
}
|
|
84
|
+
if (merged) {
|
|
85
|
+
return 'prMerged';
|
|
86
|
+
}
|
|
87
|
+
return state === 'closed' ? 'prClosed' : 'prOpen';
|
|
88
|
+
}
|
|
89
|
+
if (state === 'closed') {
|
|
90
|
+
return stateReason === 'not_planned'
|
|
91
|
+
? 'issueClosedNotPlanned'
|
|
92
|
+
: 'issueClosed';
|
|
93
|
+
}
|
|
94
|
+
return 'issueOpen';
|
|
95
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IN_TMUX_BY_HUMAN_NAME,
|
|
3
|
+
isTodoByHumanTab,
|
|
4
|
+
STATUS_BUTTON_NAMES,
|
|
5
|
+
TOTALLY_WRONG_COMMENT_BODY,
|
|
6
|
+
UNNECESSARY_COMMENT_BODY,
|
|
7
|
+
} from './operations';
|
|
8
|
+
|
|
9
|
+
describe('operation constants', () => {
|
|
10
|
+
it('defines the totally wrong comment body', () => {
|
|
11
|
+
expect(TOTALLY_WRONG_COMMENT_BODY).toBe('totally wrong');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('defines the unnecessary comment body', () => {
|
|
15
|
+
expect(UNNECESSARY_COMMENT_BODY).toBe('This pull request is unnecessary.');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('lists the status buttons left to right', () => {
|
|
19
|
+
expect(STATUS_BUTTON_NAMES).toEqual([
|
|
20
|
+
'In Tmux by agent',
|
|
21
|
+
'In Tmux by human',
|
|
22
|
+
'Todo by human',
|
|
23
|
+
'Awaiting Workspace',
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('names the in-tmux-by-human status', () => {
|
|
28
|
+
expect(IN_TMUX_BY_HUMAN_NAME).toBe('In Tmux by human');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('isTodoByHumanTab', () => {
|
|
33
|
+
it('is true only for the todo-by-human tab', () => {
|
|
34
|
+
expect(isTodoByHumanTab('todo-by-human')).toBe(true);
|
|
35
|
+
expect(isTodoByHumanTab('prs')).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ConsoleFieldOption, ConsoleTabName } from './types';
|
|
2
|
+
|
|
3
|
+
export const TOTALLY_WRONG_COMMENT_BODY = 'totally wrong';
|
|
4
|
+
export const UNNECESSARY_COMMENT_BODY = 'This pull request is unnecessary.';
|
|
5
|
+
|
|
6
|
+
export type ConsoleReviewAction =
|
|
7
|
+
| 'approve'
|
|
8
|
+
| 'request_changes'
|
|
9
|
+
| 'unnecessary'
|
|
10
|
+
| 'totally_wrong';
|
|
11
|
+
|
|
12
|
+
export type ConsoleNextActionDateAction = 'snooze_1day' | 'snooze_1week';
|
|
13
|
+
|
|
14
|
+
export type ConsoleCloseAction = 'close' | 'close_not_planned';
|
|
15
|
+
|
|
16
|
+
export type ConsoleOperationHandlers = {
|
|
17
|
+
onReview: (action: ConsoleReviewAction) => void;
|
|
18
|
+
onSetNextActionDate: (action: ConsoleNextActionDateAction) => void;
|
|
19
|
+
onSetStory: (option: ConsoleFieldOption) => void;
|
|
20
|
+
onSetStatus: (option: ConsoleFieldOption) => void;
|
|
21
|
+
onSetInTmuxByHuman: (option: ConsoleFieldOption) => void;
|
|
22
|
+
onClose: (action: ConsoleCloseAction) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const STATUS_BUTTON_NAMES: string[] = [
|
|
26
|
+
'In Tmux by agent',
|
|
27
|
+
'In Tmux by human',
|
|
28
|
+
'Todo by human',
|
|
29
|
+
'Awaiting Workspace',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export const IN_TMUX_BY_HUMAN_NAME = 'In Tmux by human';
|
|
33
|
+
|
|
34
|
+
export const isTodoByHumanTab = (tab: ConsoleTabName): boolean =>
|
|
35
|
+
tab === 'todo-by-human';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
countPendingItems,
|
|
3
|
+
filterPendingItems,
|
|
4
|
+
getOverlayEntry,
|
|
5
|
+
isOverlayEntryActedForMode,
|
|
6
|
+
isOverlayEntryExpiredForMode,
|
|
7
|
+
overlayKeyForItem,
|
|
8
|
+
overlayStorageKey,
|
|
9
|
+
parseGeneratedAtMs,
|
|
10
|
+
writeOverlayEntry,
|
|
11
|
+
} from './overlay';
|
|
12
|
+
import type { ConsoleListItem, ConsoleOverlay } from './types';
|
|
13
|
+
|
|
14
|
+
const item = (number: number): ConsoleListItem => ({
|
|
15
|
+
number,
|
|
16
|
+
title: `Item ${number}`,
|
|
17
|
+
url: `https://github.com/o/r/issues/${number}`,
|
|
18
|
+
repo: 'o/r',
|
|
19
|
+
nameWithOwner: 'o/r',
|
|
20
|
+
projectItemId: `PVTI_${number}`,
|
|
21
|
+
itemId: `PVTI_${number}`,
|
|
22
|
+
isPr: false,
|
|
23
|
+
story: 'Story',
|
|
24
|
+
labels: [],
|
|
25
|
+
createdAt: '2026-06-10T00:00:00.000Z',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('overlay helpers', () => {
|
|
29
|
+
it('builds the per-project storage key', () => {
|
|
30
|
+
expect(overlayStorageKey('umino')).toBe('pv_overlay_umino');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('uses the projectItemId as the overlay key when present', () => {
|
|
34
|
+
expect(overlayKeyForItem(item(5))).toBe('PVTI_5');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('parses a generatedAt timestamp', () => {
|
|
38
|
+
expect(parseGeneratedAtMs('2026-06-10T00:00:00.000Z')).toBe(
|
|
39
|
+
Date.parse('2026-06-10T00:00:00.000Z'),
|
|
40
|
+
);
|
|
41
|
+
expect(parseGeneratedAtMs('not-a-date')).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('mode-aware expiry', () => {
|
|
46
|
+
it('expires a same-mode entry written before the snapshot', () => {
|
|
47
|
+
expect(
|
|
48
|
+
isOverlayEntryExpiredForMode(
|
|
49
|
+
{ ts: 100, mode: 'prs', done: true },
|
|
50
|
+
200,
|
|
51
|
+
'prs',
|
|
52
|
+
),
|
|
53
|
+
).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not expire an entry written in a different mode', () => {
|
|
57
|
+
expect(
|
|
58
|
+
isOverlayEntryExpiredForMode(
|
|
59
|
+
{ ts: 100, mode: 'triage', done: true },
|
|
60
|
+
200,
|
|
61
|
+
'prs',
|
|
62
|
+
),
|
|
63
|
+
).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not expire an entry written after the snapshot', () => {
|
|
67
|
+
expect(
|
|
68
|
+
isOverlayEntryExpiredForMode(
|
|
69
|
+
{ ts: 300, mode: 'prs', done: true },
|
|
70
|
+
200,
|
|
71
|
+
'prs',
|
|
72
|
+
),
|
|
73
|
+
).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('counts driven to zero do not revive on tab switch', () => {
|
|
78
|
+
it('keeps a done item subtracted in its own mode', () => {
|
|
79
|
+
const overlay: ConsoleOverlay = {
|
|
80
|
+
PVTI_1: { ts: 500, mode: 'prs', done: true },
|
|
81
|
+
};
|
|
82
|
+
const generatedAtMs = 400;
|
|
83
|
+
expect(countPendingItems([item(1)], overlay, generatedAtMs, 'prs')).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('treats a done entry from another mode as still acted', () => {
|
|
87
|
+
const overlay: ConsoleOverlay = {
|
|
88
|
+
PVTI_1: { ts: 100, mode: 'triage', done: true },
|
|
89
|
+
};
|
|
90
|
+
expect(isOverlayEntryActedForMode(overlay.PVTI_1, 999, 'prs')).toBe(true);
|
|
91
|
+
expect(countPendingItems([item(1)], overlay, 999, 'prs')).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('revives the count only when a newer same-mode snapshot supersedes the entry', () => {
|
|
95
|
+
const overlay: ConsoleOverlay = {
|
|
96
|
+
PVTI_1: { ts: 100, mode: 'prs', done: true },
|
|
97
|
+
};
|
|
98
|
+
expect(countPendingItems([item(1)], overlay, 200, 'prs')).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('filterPendingItems', () => {
|
|
103
|
+
it('drops acted items and keeps the rest', () => {
|
|
104
|
+
const overlay: ConsoleOverlay = {
|
|
105
|
+
PVTI_1: { ts: 500, mode: 'prs', done: true },
|
|
106
|
+
};
|
|
107
|
+
const result = filterPendingItems([item(1), item(2)], overlay, 400, 'prs');
|
|
108
|
+
expect(result.map((entry) => entry.number)).toEqual([2]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('getOverlayEntry and writeOverlayEntry', () => {
|
|
113
|
+
it('returns null for an expired entry', () => {
|
|
114
|
+
const overlay: ConsoleOverlay = {
|
|
115
|
+
PVTI_1: { ts: 100, mode: 'prs', done: true },
|
|
116
|
+
};
|
|
117
|
+
expect(getOverlayEntry(overlay, item(1), 200, 'prs')).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('stamps the timestamp and mode on write', () => {
|
|
121
|
+
const next = writeOverlayEntry({}, 'PVTI_1', { done: true }, 'prs', 1234);
|
|
122
|
+
expect(next.PVTI_1).toEqual({ done: true, ts: 1234, mode: 'prs' });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConsoleListItem,
|
|
3
|
+
ConsoleOverlay,
|
|
4
|
+
ConsoleOverlayEntry,
|
|
5
|
+
ConsoleTabName,
|
|
6
|
+
} from './types';
|
|
7
|
+
|
|
8
|
+
export const overlayStorageKey = (pjcode: string): string =>
|
|
9
|
+
`pv_overlay_${pjcode}`;
|
|
10
|
+
|
|
11
|
+
export const overlayKeyForItem = (item: ConsoleListItem): string =>
|
|
12
|
+
item.projectItemId !== '' ? item.projectItemId : item.itemId;
|
|
13
|
+
|
|
14
|
+
export const parseGeneratedAtMs = (generatedAt: string): number => {
|
|
15
|
+
const parsed = Date.parse(generatedAt);
|
|
16
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const isOverlayEntryExpiredForMode = (
|
|
20
|
+
entry: ConsoleOverlayEntry,
|
|
21
|
+
generatedAtMs: number,
|
|
22
|
+
mode: ConsoleTabName,
|
|
23
|
+
): boolean =>
|
|
24
|
+
entry.ts > 0 &&
|
|
25
|
+
generatedAtMs > 0 &&
|
|
26
|
+
entry.mode === mode &&
|
|
27
|
+
entry.ts < generatedAtMs;
|
|
28
|
+
|
|
29
|
+
export const getOverlayEntry = (
|
|
30
|
+
overlay: ConsoleOverlay,
|
|
31
|
+
item: ConsoleListItem,
|
|
32
|
+
generatedAtMs: number,
|
|
33
|
+
mode: ConsoleTabName,
|
|
34
|
+
): ConsoleOverlayEntry | null => {
|
|
35
|
+
const entry = overlay[overlayKeyForItem(item)];
|
|
36
|
+
if (entry === undefined) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (isOverlayEntryExpiredForMode(entry, generatedAtMs, mode)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return entry;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const isOverlayEntryActedForMode = (
|
|
46
|
+
entry: ConsoleOverlayEntry | undefined,
|
|
47
|
+
generatedAtMs: number,
|
|
48
|
+
mode: ConsoleTabName,
|
|
49
|
+
): boolean => {
|
|
50
|
+
if (entry === undefined || entry.done !== true) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return !isOverlayEntryExpiredForMode(entry, generatedAtMs, mode);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const countPendingItems = (
|
|
57
|
+
items: ConsoleListItem[],
|
|
58
|
+
overlay: ConsoleOverlay,
|
|
59
|
+
generatedAtMs: number,
|
|
60
|
+
mode: ConsoleTabName,
|
|
61
|
+
): number =>
|
|
62
|
+
items.filter(
|
|
63
|
+
(item) =>
|
|
64
|
+
!isOverlayEntryActedForMode(
|
|
65
|
+
overlay[overlayKeyForItem(item)],
|
|
66
|
+
generatedAtMs,
|
|
67
|
+
mode,
|
|
68
|
+
),
|
|
69
|
+
).length;
|
|
70
|
+
|
|
71
|
+
export const filterPendingItems = (
|
|
72
|
+
items: ConsoleListItem[],
|
|
73
|
+
overlay: ConsoleOverlay,
|
|
74
|
+
generatedAtMs: number,
|
|
75
|
+
mode: ConsoleTabName,
|
|
76
|
+
): ConsoleListItem[] =>
|
|
77
|
+
items.filter(
|
|
78
|
+
(item) =>
|
|
79
|
+
!isOverlayEntryActedForMode(
|
|
80
|
+
overlay[overlayKeyForItem(item)],
|
|
81
|
+
generatedAtMs,
|
|
82
|
+
mode,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
export const writeOverlayEntry = (
|
|
87
|
+
overlay: ConsoleOverlay,
|
|
88
|
+
key: string,
|
|
89
|
+
patch: Partial<Omit<ConsoleOverlayEntry, 'ts' | 'mode'>>,
|
|
90
|
+
mode: ConsoleTabName,
|
|
91
|
+
now: number,
|
|
92
|
+
): ConsoleOverlay => {
|
|
93
|
+
const existing = overlay[key];
|
|
94
|
+
const next: ConsoleOverlayEntry = {
|
|
95
|
+
...(existing ?? {}),
|
|
96
|
+
...patch,
|
|
97
|
+
ts: now,
|
|
98
|
+
mode,
|
|
99
|
+
};
|
|
100
|
+
return { ...overlay, [key]: next };
|
|
101
|
+
};
|