github-issue-tower-defence-management 1.89.0 → 1.91.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/.github/CODEOWNERS +1 -2
- package/CHANGELOG.md +14 -0
- package/README.md +15 -2
- package/bin/adapter/entry-points/cli/index.js +16 -12
- 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 +43 -17
- 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/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/jest.config.js +57 -9
- package/package.json +17 -13
- package/src/adapter/entry-points/cli/index.test.ts +12 -2
- package/src/adapter/entry-points/cli/index.ts +30 -12
- 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 +86 -4
- package/src/adapter/entry-points/console/consoleServer.ts +53 -23
- 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/colors.test.ts +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/colors.ts +73 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.stories.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.test.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.stories.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.test.tsx +23 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.tsx +26 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.stories.tsx +29 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.test.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.tsx +66 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.test.tsx +53 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.tsx +53 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.stories.tsx +79 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.test.tsx +81 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.tsx +226 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.stories.tsx +82 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.test.tsx +52 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.tsx +32 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.test.tsx +43 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +22 -6
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.test.tsx +87 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +36 -32
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.stories.tsx +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.test.tsx +36 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.tsx +50 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.stories.tsx +22 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.test.tsx +38 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.tsx +65 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.stories.tsx +20 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.test.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.stories.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.test.tsx +85 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.stories.tsx +26 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.test.tsx +32 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.tsx +36 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.stories.tsx +29 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.test.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.stories.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.test.tsx +33 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.stories.tsx +31 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.test.tsx +40 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.tsx +88 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.stories.tsx +17 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.test.tsx +49 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.tsx +63 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.stories.tsx +17 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.test.tsx +45 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.stories.tsx +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.test.tsx +24 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +41 -5
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.test.tsx +59 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +28 -19
- package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.test.ts +35 -0
- package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.ts +21 -0
- package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +206 -9
- package/src/adapter/entry-points/console/ui/src/features/console/grouping.test.ts +91 -0
- package/src/adapter/entry-points/console/ui/src/features/console/grouping.ts +79 -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/useConsolePjcode.test.ts +24 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.ts +17 -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/itemIcons.test.ts +97 -0
- package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.ts +95 -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/operations.test.ts +37 -0
- package/src/adapter/entry-points/console/ui/src/features/console/operations.ts +35 -0
- package/src/adapter/entry-points/console/ui/src/features/console/overlay.test.ts +124 -0
- package/src/adapter/entry-points/console/ui/src/features/console/overlay.ts +101 -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 -7
- package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.test.ts +52 -0
- package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.ts +51 -0
- package/src/adapter/entry-points/console/ui/src/features/console/types.ts +73 -1
- package/src/adapter/entry-points/console/ui/src/index.css +352 -2
- package/src/adapter/entry-points/console/ui/tsconfig.json +3 -1
- package/src/adapter/entry-points/console/ui/vite.config.ts +6 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/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 +3 -3
- package/types/adapter/entry-points/console/consoleServer.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/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -65
- package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { mock } from 'jest-mock-extended';
|
|
2
|
+
import { Project } from '../../../domain/entities/Project';
|
|
3
|
+
import {
|
|
4
|
+
buildPjcodeToProjectUrl,
|
|
5
|
+
createConsoleProjectResolver,
|
|
6
|
+
} from './consoleProjectResolver';
|
|
7
|
+
|
|
8
|
+
describe('buildPjcodeToProjectUrl', () => {
|
|
9
|
+
it('adds the default pjcode entry when it is not already present', () => {
|
|
10
|
+
const mapping = buildPjcodeToProjectUrl(
|
|
11
|
+
'umino',
|
|
12
|
+
'https://github.com/orgs/umino/projects/1',
|
|
13
|
+
{ xmile: 'https://github.com/orgs/xmile/projects/2' },
|
|
14
|
+
);
|
|
15
|
+
expect(mapping).toEqual({
|
|
16
|
+
umino: 'https://github.com/orgs/umino/projects/1',
|
|
17
|
+
xmile: 'https://github.com/orgs/xmile/projects/2',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('keeps an explicit default pjcode entry from consoleProjects', () => {
|
|
22
|
+
const mapping = buildPjcodeToProjectUrl(
|
|
23
|
+
'umino',
|
|
24
|
+
'https://github.com/orgs/umino/projects/1',
|
|
25
|
+
{ umino: 'https://github.com/orgs/umino/projects/9' },
|
|
26
|
+
);
|
|
27
|
+
expect(mapping.umino).toBe('https://github.com/orgs/umino/projects/9');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('uses only the default entry when no consoleProjects mapping is configured', () => {
|
|
31
|
+
const mapping = buildPjcodeToProjectUrl(
|
|
32
|
+
'umino',
|
|
33
|
+
'https://github.com/orgs/umino/projects/1',
|
|
34
|
+
null,
|
|
35
|
+
);
|
|
36
|
+
expect(mapping).toEqual({
|
|
37
|
+
umino: 'https://github.com/orgs/umino/projects/1',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('createConsoleProjectResolver', () => {
|
|
43
|
+
const uminoProject: Project = { ...mock<Project>(), id: 'PVT_umino' };
|
|
44
|
+
const xmileProject: Project = { ...mock<Project>(), id: 'PVT_xmile' };
|
|
45
|
+
|
|
46
|
+
it('resolves a known pjcode to its loaded project', async () => {
|
|
47
|
+
const loadProject = jest.fn(async (url: string) =>
|
|
48
|
+
url.includes('umino') ? uminoProject : xmileProject,
|
|
49
|
+
);
|
|
50
|
+
const resolver = createConsoleProjectResolver(
|
|
51
|
+
{
|
|
52
|
+
umino: 'https://github.com/orgs/umino/projects/1',
|
|
53
|
+
xmile: 'https://github.com/orgs/xmile/projects/2',
|
|
54
|
+
},
|
|
55
|
+
loadProject,
|
|
56
|
+
);
|
|
57
|
+
await expect(resolver('umino')).resolves.toEqual({
|
|
58
|
+
pjcode: 'umino',
|
|
59
|
+
project: uminoProject,
|
|
60
|
+
});
|
|
61
|
+
await expect(resolver('xmile')).resolves.toEqual({
|
|
62
|
+
pjcode: 'xmile',
|
|
63
|
+
project: xmileProject,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns null for a pjcode that has no configured project url', async () => {
|
|
68
|
+
const loadProject = jest.fn(async () => uminoProject);
|
|
69
|
+
const resolver = createConsoleProjectResolver(
|
|
70
|
+
{ umino: 'https://github.com/orgs/umino/projects/1' },
|
|
71
|
+
loadProject,
|
|
72
|
+
);
|
|
73
|
+
await expect(resolver('unknown')).resolves.toBeNull();
|
|
74
|
+
expect(loadProject).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns null when the project fails to load', async () => {
|
|
78
|
+
const loadProject = jest.fn(async () => null);
|
|
79
|
+
const resolver = createConsoleProjectResolver(
|
|
80
|
+
{ umino: 'https://github.com/orgs/umino/projects/1' },
|
|
81
|
+
loadProject,
|
|
82
|
+
);
|
|
83
|
+
await expect(resolver('umino')).resolves.toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('loads each project at most once and serves later calls from cache', async () => {
|
|
87
|
+
const loadProject = jest.fn(async () => uminoProject);
|
|
88
|
+
const resolver = createConsoleProjectResolver(
|
|
89
|
+
{ umino: 'https://github.com/orgs/umino/projects/1' },
|
|
90
|
+
loadProject,
|
|
91
|
+
);
|
|
92
|
+
await resolver('umino');
|
|
93
|
+
await resolver('umino');
|
|
94
|
+
expect(loadProject).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Project } from '../../../domain/entities/Project';
|
|
2
|
+
import {
|
|
3
|
+
ConsoleProjectBinding,
|
|
4
|
+
ConsoleProjectResolver,
|
|
5
|
+
} from './consoleOperationApi';
|
|
6
|
+
|
|
7
|
+
export type ConsoleProjectLoader = (
|
|
8
|
+
projectUrl: string,
|
|
9
|
+
) => Promise<Project | null>;
|
|
10
|
+
|
|
11
|
+
export const buildPjcodeToProjectUrl = (
|
|
12
|
+
defaultPjcode: string,
|
|
13
|
+
defaultProjectUrl: string,
|
|
14
|
+
consoleProjects: Record<string, string> | null,
|
|
15
|
+
): Record<string, string> => {
|
|
16
|
+
const mapping: Record<string, string> = {};
|
|
17
|
+
if (consoleProjects !== null) {
|
|
18
|
+
for (const [pjcode, projectUrl] of Object.entries(consoleProjects)) {
|
|
19
|
+
mapping[pjcode] = projectUrl;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!(defaultPjcode in mapping)) {
|
|
23
|
+
mapping[defaultPjcode] = defaultProjectUrl;
|
|
24
|
+
}
|
|
25
|
+
return mapping;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const createConsoleProjectResolver = (
|
|
29
|
+
pjcodeToProjectUrl: Record<string, string>,
|
|
30
|
+
loadProject: ConsoleProjectLoader,
|
|
31
|
+
): ConsoleProjectResolver => {
|
|
32
|
+
const cache = new Map<string, ConsoleProjectBinding>();
|
|
33
|
+
return async (pjcode: string): Promise<ConsoleProjectBinding | null> => {
|
|
34
|
+
const cached = cache.get(pjcode);
|
|
35
|
+
if (cached !== undefined) {
|
|
36
|
+
return cached;
|
|
37
|
+
}
|
|
38
|
+
const projectUrl = pjcodeToProjectUrl[pjcode];
|
|
39
|
+
if (projectUrl === undefined) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const project = await loadProject(projectUrl);
|
|
43
|
+
if (project === null) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const binding: ConsoleProjectBinding = { pjcode, project };
|
|
47
|
+
cache.set(pjcode, binding);
|
|
48
|
+
return binding;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
hasDotSegment,
|
|
10
10
|
requiresToken,
|
|
11
11
|
isTokenValid,
|
|
12
|
+
isConsoleAppRoute,
|
|
12
13
|
extractProvidedToken,
|
|
13
14
|
startConsoleServer,
|
|
14
15
|
} from './consoleServer';
|
|
@@ -61,6 +62,35 @@ describe('consoleServer pure helpers', () => {
|
|
|
61
62
|
});
|
|
62
63
|
});
|
|
63
64
|
|
|
65
|
+
describe('isConsoleAppRoute', () => {
|
|
66
|
+
it('matches a per-project root route', () => {
|
|
67
|
+
expect(isConsoleAppRoute('/projects/umino')).toBe(true);
|
|
68
|
+
expect(isConsoleAppRoute('/projects/umino/')).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('matches a per-project tab route for every list tab', () => {
|
|
72
|
+
expect(isConsoleAppRoute('/projects/umino/prs')).toBe(true);
|
|
73
|
+
expect(isConsoleAppRoute('/projects/xmile/triage')).toBe(true);
|
|
74
|
+
expect(isConsoleAppRoute('/projects/xcare/unread')).toBe(true);
|
|
75
|
+
expect(isConsoleAppRoute('/projects/utage3/failed-preparation')).toBe(
|
|
76
|
+
true,
|
|
77
|
+
);
|
|
78
|
+
expect(isConsoleAppRoute('/projects/utage3/todo-by-human')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('does not match data, api, or unknown tab routes', () => {
|
|
82
|
+
expect(isConsoleAppRoute('/projects/umino/prs/list.json')).toBe(false);
|
|
83
|
+
expect(isConsoleAppRoute('/projects/umino/unknown')).toBe(false);
|
|
84
|
+
expect(isConsoleAppRoute('/projects')).toBe(false);
|
|
85
|
+
expect(isConsoleAppRoute('/api/review')).toBe(false);
|
|
86
|
+
expect(isConsoleAppRoute('/')).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not match a dot-prefixed pjcode', () => {
|
|
90
|
+
expect(isConsoleAppRoute('/projects/.git')).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
64
94
|
describe('isTokenValid', () => {
|
|
65
95
|
it('accepts a matching token', () => {
|
|
66
96
|
expect(isTokenValid('expected', 'expected')).toBe(true);
|
|
@@ -230,6 +260,57 @@ describe('consoleServer integration', () => {
|
|
|
230
260
|
}
|
|
231
261
|
});
|
|
232
262
|
|
|
263
|
+
it('serves the SPA index for per-project app routes without a token', async () => {
|
|
264
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
265
|
+
const uiDistDir = path.join(tmpDir, 'ui-dist');
|
|
266
|
+
fs.mkdirSync(uiDistDir, { recursive: true });
|
|
267
|
+
fs.writeFileSync(
|
|
268
|
+
path.join(uiDistDir, 'index.html'),
|
|
269
|
+
'<!DOCTYPE html><title>spa</title><div id="root"></div>',
|
|
270
|
+
);
|
|
271
|
+
const server = await startConsoleServer({
|
|
272
|
+
accessToken: testToken,
|
|
273
|
+
uiDistDir,
|
|
274
|
+
consoleDataOutputDir: null,
|
|
275
|
+
port: 0,
|
|
276
|
+
});
|
|
277
|
+
try {
|
|
278
|
+
const projectRoot = await requestServer(server, '/projects/umino');
|
|
279
|
+
expect(projectRoot.statusCode).toBe(200);
|
|
280
|
+
expect(projectRoot.body).toContain('spa');
|
|
281
|
+
expect(projectRoot.contentType).toContain('text/html');
|
|
282
|
+
expect(projectRoot.cacheControl).toBe('no-store');
|
|
283
|
+
|
|
284
|
+
const projectTab = await requestServer(server, '/projects/xmile/prs');
|
|
285
|
+
expect(projectTab.statusCode).toBe(200);
|
|
286
|
+
expect(projectTab.body).toContain('spa');
|
|
287
|
+
|
|
288
|
+
const unknownTab = await requestServer(server, '/projects/xmile/unknown');
|
|
289
|
+
expect(unknownTab.statusCode).toBe(404);
|
|
290
|
+
} finally {
|
|
291
|
+
await closeServer(server);
|
|
292
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('serves the placeholder index for per-project routes when ui-dist is absent', async () => {
|
|
297
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
298
|
+
const server = await startConsoleServer({
|
|
299
|
+
accessToken: testToken,
|
|
300
|
+
uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
|
|
301
|
+
consoleDataOutputDir: null,
|
|
302
|
+
port: 0,
|
|
303
|
+
});
|
|
304
|
+
try {
|
|
305
|
+
const projectRoot = await requestServer(server, '/projects/umino/triage');
|
|
306
|
+
expect(projectRoot.statusCode).toBe(200);
|
|
307
|
+
expect(projectRoot.body).toContain('TDPM Console');
|
|
308
|
+
} finally {
|
|
309
|
+
await closeServer(server);
|
|
310
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
233
314
|
it('rejects dot-prefixed paths with 404', async () => {
|
|
234
315
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
235
316
|
const uiDistDir = path.join(tmpDir, 'ui-dist');
|
|
@@ -399,7 +480,6 @@ describe('consoleServer new routes integration', () => {
|
|
|
399
480
|
accessToken: testToken,
|
|
400
481
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
401
482
|
consoleDataOutputDir: dataDir,
|
|
402
|
-
pjcode: 'umino',
|
|
403
483
|
port: 0,
|
|
404
484
|
});
|
|
405
485
|
try {
|
|
@@ -465,9 +545,9 @@ describe('consoleServer new routes integration', () => {
|
|
|
465
545
|
accessToken: testToken,
|
|
466
546
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
467
547
|
consoleDataOutputDir: dataDir,
|
|
468
|
-
pjcode: 'umino',
|
|
469
548
|
issueRepository,
|
|
470
|
-
|
|
549
|
+
resolveProject: async (pjcode) =>
|
|
550
|
+
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
471
551
|
port: 0,
|
|
472
552
|
});
|
|
473
553
|
try {
|
|
@@ -476,6 +556,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
476
556
|
'POST',
|
|
477
557
|
`/api/review?k=${testToken}`,
|
|
478
558
|
{
|
|
559
|
+
pjcode: 'umino',
|
|
479
560
|
action: 'approve',
|
|
480
561
|
prUrl: 'https://github.com/o/r/pull/1',
|
|
481
562
|
projectItemId: 'PVTI_op',
|
|
@@ -502,7 +583,8 @@ describe('consoleServer new routes integration', () => {
|
|
|
502
583
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
503
584
|
consoleDataOutputDir: null,
|
|
504
585
|
issueRepository,
|
|
505
|
-
|
|
586
|
+
resolveProject: async (pjcode) =>
|
|
587
|
+
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
506
588
|
port: 0,
|
|
507
589
|
});
|
|
508
590
|
try {
|
|
@@ -2,8 +2,8 @@ import * as http from 'http';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
5
|
-
import { Project } from '../../../domain/entities/Project';
|
|
6
5
|
import {
|
|
6
|
+
CONSOLE_LIST_TAB_NAMES,
|
|
7
7
|
buildConsoleDataResponse,
|
|
8
8
|
parseConsoleDataRoute,
|
|
9
9
|
} from './consoleDataDelivery';
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from './consoleReadApi';
|
|
19
19
|
import {
|
|
20
20
|
ConsoleOperationContext,
|
|
21
|
+
ConsoleProjectResolver,
|
|
21
22
|
handleIntmux,
|
|
22
23
|
handleReview,
|
|
23
24
|
handleTriage,
|
|
@@ -70,6 +71,29 @@ export const requiresToken = (requestPath: string): boolean =>
|
|
|
70
71
|
requestPath === '/api' ||
|
|
71
72
|
requestPath.endsWith('.json');
|
|
72
73
|
|
|
74
|
+
const SAFE_PJCODE = /^[A-Za-z0-9._-]+$/;
|
|
75
|
+
|
|
76
|
+
export const isConsoleAppRoute = (requestPath: string): boolean => {
|
|
77
|
+
const segments = requestPath
|
|
78
|
+
.split('/')
|
|
79
|
+
.filter((segment) => segment.length > 0);
|
|
80
|
+
if (segments.length < 2 || segments[0] !== 'projects') {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const pjcode = segments[1];
|
|
84
|
+
if (!SAFE_PJCODE.test(pjcode) || pjcode.startsWith('.')) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (segments.length === 2) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (segments.length !== 3) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const tab = segments[2];
|
|
94
|
+
return CONSOLE_LIST_TAB_NAMES.includes(tab);
|
|
95
|
+
};
|
|
96
|
+
|
|
73
97
|
export const isTokenValid = (
|
|
74
98
|
expectedToken: string,
|
|
75
99
|
providedToken: string | null,
|
|
@@ -129,9 +153,8 @@ export type ConsoleServerOptions = {
|
|
|
129
153
|
accessToken: string;
|
|
130
154
|
uiDistDir: string;
|
|
131
155
|
consoleDataOutputDir: string | null;
|
|
132
|
-
pjcode?: string | null;
|
|
133
156
|
issueRepository?: IssueRepository | null;
|
|
134
|
-
|
|
157
|
+
resolveProject?: ConsoleProjectResolver | null;
|
|
135
158
|
issueTitleStateCache?: IssueTitleStateCache | null;
|
|
136
159
|
};
|
|
137
160
|
|
|
@@ -159,6 +182,24 @@ const serveBootstrapIndex = (response: http.ServerResponse): void => {
|
|
|
159
182
|
response.end(PLACEHOLDER_INDEX_HTML);
|
|
160
183
|
};
|
|
161
184
|
|
|
185
|
+
const serveIndexHtml = (
|
|
186
|
+
options: ConsoleServerOptions,
|
|
187
|
+
response: http.ServerResponse,
|
|
188
|
+
): void => {
|
|
189
|
+
const indexFilePath = resolveStaticFilePath(options.uiDistDir, '/index.html');
|
|
190
|
+
const indexContent =
|
|
191
|
+
indexFilePath === null ? null : readStaticFile(indexFilePath);
|
|
192
|
+
if (indexContent === null) {
|
|
193
|
+
serveBootstrapIndex(response);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
response.writeHead(200, {
|
|
197
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
198
|
+
'Cache-Control': 'no-store',
|
|
199
|
+
});
|
|
200
|
+
response.end(indexContent);
|
|
201
|
+
};
|
|
202
|
+
|
|
162
203
|
const sendJson = (
|
|
163
204
|
response: http.ServerResponse,
|
|
164
205
|
statusCode: number,
|
|
@@ -249,15 +290,14 @@ const handleOperationApi = async (
|
|
|
249
290
|
body: Record<string, unknown>,
|
|
250
291
|
): Promise<{ statusCode: number; body: unknown } | null> => {
|
|
251
292
|
const issueRepository = options.issueRepository ?? null;
|
|
252
|
-
const
|
|
253
|
-
if (issueRepository === null ||
|
|
293
|
+
const resolveProject = options.resolveProject ?? null;
|
|
294
|
+
if (issueRepository === null || resolveProject === null) {
|
|
254
295
|
return null;
|
|
255
296
|
}
|
|
256
297
|
const context: ConsoleOperationContext = {
|
|
257
298
|
issueRepository,
|
|
258
|
-
|
|
299
|
+
resolveProject,
|
|
259
300
|
consoleDataOutputDir: options.consoleDataOutputDir,
|
|
260
|
-
pjcode: options.pjcode ?? null,
|
|
261
301
|
};
|
|
262
302
|
switch (requestPath) {
|
|
263
303
|
case '/api/review':
|
|
@@ -369,22 +409,12 @@ export const handleConsoleRequest = async (
|
|
|
369
409
|
return;
|
|
370
410
|
}
|
|
371
411
|
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
indexFilePath === null ? null : readStaticFile(indexFilePath);
|
|
379
|
-
if (indexContent === null) {
|
|
380
|
-
serveBootstrapIndex(response);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
response.writeHead(200, {
|
|
384
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
385
|
-
'Cache-Control': 'no-store',
|
|
386
|
-
});
|
|
387
|
-
response.end(indexContent);
|
|
412
|
+
if (
|
|
413
|
+
requestPath === '/' ||
|
|
414
|
+
requestPath === '/index.html' ||
|
|
415
|
+
isConsoleAppRoute(requestPath)
|
|
416
|
+
) {
|
|
417
|
+
serveIndexHtml(options, response);
|
|
388
418
|
return;
|
|
389
419
|
}
|
|
390
420
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CONSOLE_COLOR_PALETTE, colorFromEnum } from './colors';
|
|
2
|
+
|
|
3
|
+
describe('colorFromEnum', () => {
|
|
4
|
+
it('returns the exact palette entry for a known enum', () => {
|
|
5
|
+
expect(colorFromEnum('GREEN')).toEqual({
|
|
6
|
+
dot: '#3fb950',
|
|
7
|
+
bg: 'rgba(46,160,67,0.15)',
|
|
8
|
+
fg: '#3fb950',
|
|
9
|
+
border: 'rgba(46,160,67,0.4)',
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('is case insensitive', () => {
|
|
14
|
+
expect(colorFromEnum('purple')).toBe(CONSOLE_COLOR_PALETTE.PURPLE);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('falls back to GRAY for an unknown enum', () => {
|
|
18
|
+
expect(colorFromEnum('MAGENTA')).toBe(CONSOLE_COLOR_PALETTE.GRAY);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('falls back to GRAY for null', () => {
|
|
22
|
+
expect(colorFromEnum(null)).toBe(CONSOLE_COLOR_PALETTE.GRAY);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('uses the documented dot colors for every enum', () => {
|
|
26
|
+
expect(CONSOLE_COLOR_PALETTE.GRAY.dot).toBe('#848d97');
|
|
27
|
+
expect(CONSOLE_COLOR_PALETTE.BLUE.dot).toBe('#4493f8');
|
|
28
|
+
expect(CONSOLE_COLOR_PALETTE.YELLOW.dot).toBe('#d29922');
|
|
29
|
+
expect(CONSOLE_COLOR_PALETTE.ORANGE.dot).toBe('#db6d28');
|
|
30
|
+
expect(CONSOLE_COLOR_PALETTE.RED.dot).toBe('#f85149');
|
|
31
|
+
expect(CONSOLE_COLOR_PALETTE.PINK.dot).toBe('#db61a2');
|
|
32
|
+
expect(CONSOLE_COLOR_PALETTE.PURPLE.dot).toBe('#a371f7');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ConsoleColor } from './types';
|
|
2
|
+
|
|
3
|
+
export type ConsolePaletteEntry = {
|
|
4
|
+
dot: string;
|
|
5
|
+
bg: string;
|
|
6
|
+
fg: string;
|
|
7
|
+
border: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const CONSOLE_COLOR_PALETTE: Record<ConsoleColor, ConsolePaletteEntry> =
|
|
11
|
+
{
|
|
12
|
+
GRAY: {
|
|
13
|
+
dot: '#848d97',
|
|
14
|
+
bg: 'rgba(110,118,129,0.1)',
|
|
15
|
+
fg: '#8b949e',
|
|
16
|
+
border: 'rgba(110,118,129,0.4)',
|
|
17
|
+
},
|
|
18
|
+
BLUE: {
|
|
19
|
+
dot: '#4493f8',
|
|
20
|
+
bg: 'rgba(56,139,253,0.1)',
|
|
21
|
+
fg: '#388bfd',
|
|
22
|
+
border: 'rgba(56,139,253,0.4)',
|
|
23
|
+
},
|
|
24
|
+
GREEN: {
|
|
25
|
+
dot: '#3fb950',
|
|
26
|
+
bg: 'rgba(46,160,67,0.15)',
|
|
27
|
+
fg: '#3fb950',
|
|
28
|
+
border: 'rgba(46,160,67,0.4)',
|
|
29
|
+
},
|
|
30
|
+
YELLOW: {
|
|
31
|
+
dot: '#d29922',
|
|
32
|
+
bg: 'rgba(187,128,9,0.15)',
|
|
33
|
+
fg: '#d29922',
|
|
34
|
+
border: 'rgba(187,128,9,0.4)',
|
|
35
|
+
},
|
|
36
|
+
ORANGE: {
|
|
37
|
+
dot: '#db6d28',
|
|
38
|
+
bg: 'rgba(219,109,40,0.1)',
|
|
39
|
+
fg: '#db6d28',
|
|
40
|
+
border: 'rgba(219,109,40,0.4)',
|
|
41
|
+
},
|
|
42
|
+
RED: {
|
|
43
|
+
dot: '#f85149',
|
|
44
|
+
bg: 'rgba(248,81,73,0.1)',
|
|
45
|
+
fg: '#f85149',
|
|
46
|
+
border: 'rgba(248,81,73,0.4)',
|
|
47
|
+
},
|
|
48
|
+
PINK: {
|
|
49
|
+
dot: '#db61a2',
|
|
50
|
+
bg: 'rgba(219,97,162,0.1)',
|
|
51
|
+
fg: '#db61a2',
|
|
52
|
+
border: 'rgba(219,97,162,0.4)',
|
|
53
|
+
},
|
|
54
|
+
PURPLE: {
|
|
55
|
+
dot: '#a371f7',
|
|
56
|
+
bg: 'rgba(163,113,247,0.1)',
|
|
57
|
+
fg: '#a371f7',
|
|
58
|
+
border: 'rgba(163,113,247,0.4)',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const colorFromEnum = (
|
|
63
|
+
colorEnum: string | null,
|
|
64
|
+
): ConsolePaletteEntry => {
|
|
65
|
+
if (colorEnum === null) {
|
|
66
|
+
return CONSOLE_COLOR_PALETTE.GRAY;
|
|
67
|
+
}
|
|
68
|
+
const key = colorEnum.toUpperCase();
|
|
69
|
+
if (key in CONSOLE_COLOR_PALETTE) {
|
|
70
|
+
return CONSOLE_COLOR_PALETTE[key as ConsoleColor];
|
|
71
|
+
}
|
|
72
|
+
return CONSOLE_COLOR_PALETTE.GRAY;
|
|
73
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { consoleChangedFilesFixture } from '../fixtures';
|
|
3
|
+
import { ConsoleChangedFileList } from './ConsoleChangedFileList';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof ConsoleChangedFileList> = {
|
|
6
|
+
title: 'Console/ConsoleChangedFileList',
|
|
7
|
+
component: ConsoleChangedFileList,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof ConsoleChangedFileList>;
|
|
13
|
+
|
|
14
|
+
export const WithFiles: Story = {
|
|
15
|
+
args: { files: consoleChangedFilesFixture, isLoading: false, error: null },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const Loading: Story = {
|
|
19
|
+
args: { files: [], isLoading: true, error: null },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Empty: Story = {
|
|
23
|
+
args: { files: [], isLoading: false, error: null },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const ErrorState: Story = {
|
|
27
|
+
args: { files: [], isLoading: false, error: 'HTTP 502' },
|
|
28
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { consoleChangedFilesFixture } from '../fixtures';
|
|
3
|
+
import { ConsoleChangedFileList } from './ConsoleChangedFileList';
|
|
4
|
+
|
|
5
|
+
describe('ConsoleChangedFileList', () => {
|
|
6
|
+
it('renders each file path, status badge and additions/deletions', () => {
|
|
7
|
+
const { getByText, getAllByText } = render(
|
|
8
|
+
<ConsoleChangedFileList
|
|
9
|
+
files={consoleChangedFilesFixture}
|
|
10
|
+
isLoading={false}
|
|
11
|
+
error={null}
|
|
12
|
+
/>,
|
|
13
|
+
);
|
|
14
|
+
expect(
|
|
15
|
+
getByText('src/adapter/entry-points/console/consoleServer.ts'),
|
|
16
|
+
).toBeInTheDocument();
|
|
17
|
+
expect(getByText('+312')).toBeInTheDocument();
|
|
18
|
+
expect(getAllByText('A').length).toBe(2);
|
|
19
|
+
expect(getByText('M')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('shows the loading state', () => {
|
|
23
|
+
const { getByText } = render(
|
|
24
|
+
<ConsoleChangedFileList files={[]} isLoading error={null} />,
|
|
25
|
+
);
|
|
26
|
+
expect(getByText('Loading changed files...')).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('shows the empty state', () => {
|
|
30
|
+
const { getByText } = render(
|
|
31
|
+
<ConsoleChangedFileList files={[]} isLoading={false} error={null} />,
|
|
32
|
+
);
|
|
33
|
+
expect(getByText('No changed files.')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('shows the error state', () => {
|
|
37
|
+
const { getByRole } = render(
|
|
38
|
+
<ConsoleChangedFileList files={[]} isLoading={false} error="HTTP 502" />,
|
|
39
|
+
);
|
|
40
|
+
expect(getByRole('alert')).toHaveTextContent('HTTP 502');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { fileStatusBadge } from '../fileStatus';
|
|
2
|
+
import type { ConsoleChangedFile } from '../types';
|
|
3
|
+
|
|
4
|
+
export type ConsoleChangedFileListProps = {
|
|
5
|
+
files: ConsoleChangedFile[];
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
error: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ConsoleChangedFileList = ({
|
|
11
|
+
files,
|
|
12
|
+
isLoading,
|
|
13
|
+
error,
|
|
14
|
+
}: ConsoleChangedFileListProps) => {
|
|
15
|
+
if (error !== null) {
|
|
16
|
+
return (
|
|
17
|
+
<p role="alert" className="console-files-error">
|
|
18
|
+
Failed to load changed files: {error}
|
|
19
|
+
</p>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isLoading) {
|
|
24
|
+
return <p className="console-files-loading">Loading changed files...</p>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (files.length === 0) {
|
|
28
|
+
return <p className="console-files-empty">No changed files.</p>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<ul className="console-files">
|
|
33
|
+
{files.map((file) => {
|
|
34
|
+
const badge = fileStatusBadge(file.status);
|
|
35
|
+
return (
|
|
36
|
+
<li key={file.path} className="console-file">
|
|
37
|
+
<span
|
|
38
|
+
className="console-file-badge"
|
|
39
|
+
style={{ color: badge.color, borderColor: badge.color }}
|
|
40
|
+
>
|
|
41
|
+
{badge.label}
|
|
42
|
+
</span>
|
|
43
|
+
<span className="console-file-path">{file.path}</span>
|
|
44
|
+
<span className="console-file-stat console-file-add">
|
|
45
|
+
+{file.additions}
|
|
46
|
+
</span>
|
|
47
|
+
<span className="console-file-stat console-file-del">
|
|
48
|
+
-{file.deletions}
|
|
49
|
+
</span>
|
|
50
|
+
</li>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</ul>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { ConsoleCloseButtonGroup } from './ConsoleCloseButtonGroup';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ConsoleCloseButtonGroup> = {
|
|
5
|
+
title: 'Console/ConsoleCloseButtonGroup',
|
|
6
|
+
component: ConsoleCloseButtonGroup,
|
|
7
|
+
args: { onClose: () => {} },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof ConsoleCloseButtonGroup>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {};
|