github-issue-tower-defence-management 1.184.1 → 1.185.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 +7 -0
- package/bin/adapter/entry-points/cli/index.js +31 -29
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +26 -2
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +5 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/webServer.js +4 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/domain/entities/Project.js +17 -0
- package/bin/domain/entities/Project.js.map +1 -1
- package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js +5 -16
- package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +57 -52
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +242 -12
- package/src/adapter/entry-points/console/consoleOperationApi.ts +43 -7
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +30 -10
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +13 -2
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +37 -2
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +20 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +127 -8
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +119 -71
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +26 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +20 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +12 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +74 -0
- package/src/adapter/entry-points/console/webServer.ts +8 -0
- package/src/domain/entities/Project.ts +23 -0
- package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +16 -34
- package/types/adapter/entry-points/cli/index.d.ts +1 -1
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +8 -3
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +5 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +3 -1
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/domain/entities/Project.d.ts +4 -0
- package/types/domain/entities/Project.d.ts.map +1 -1
- package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts +6 -8
- package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CMrnEYO1.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-CMrnEYO1.js +0 -86
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import { mock } from 'jest-mock-extended';
|
|
2
3
|
import * as os from 'os';
|
|
3
4
|
import * as path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { Issue } from '../../../domain/entities/Issue';
|
|
5
|
+
import type { Issue } from '../../../domain/entities/Issue';
|
|
6
|
+
import type { Project } from '../../../domain/entities/Project';
|
|
7
|
+
import type { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
10
|
+
CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
|
|
11
|
+
CONSOLE_DONE_TAB_NAMES,
|
|
12
|
+
readDoneProjectItemIds,
|
|
13
|
+
} from './consoleDoneStore';
|
|
14
|
+
import {
|
|
15
|
+
type ConsoleOperationContext,
|
|
16
|
+
type ConsoleProjectBinding,
|
|
11
17
|
handleAttachmentUpload,
|
|
12
18
|
handleComment,
|
|
13
19
|
handleCreateIssue,
|
|
@@ -15,14 +21,9 @@ import {
|
|
|
15
21
|
handleReorderStory,
|
|
16
22
|
handleReview,
|
|
17
23
|
handleReviewComment,
|
|
24
|
+
handleStoryAdd,
|
|
18
25
|
handleTriage,
|
|
19
26
|
} from './consoleOperationApi';
|
|
20
|
-
import {
|
|
21
|
-
CONSOLE_DONE_TAB_NAMES,
|
|
22
|
-
readDoneProjectItemIds,
|
|
23
|
-
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
24
|
-
CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
|
|
25
|
-
} from './consoleDoneStore';
|
|
26
27
|
|
|
27
28
|
describe('consoleOperationApi', () => {
|
|
28
29
|
let baseDir: string;
|
|
@@ -112,6 +113,8 @@ describe('consoleOperationApi', () => {
|
|
|
112
113
|
consoleDataOutputDir: baseDir,
|
|
113
114
|
issueAttachmentRepository: null,
|
|
114
115
|
updateStoryList: null,
|
|
116
|
+
resolveProjectRepository: null,
|
|
117
|
+
invalidateProject: null,
|
|
115
118
|
};
|
|
116
119
|
});
|
|
117
120
|
|
|
@@ -125,6 +128,8 @@ describe('consoleOperationApi', () => {
|
|
|
125
128
|
consoleDataOutputDir: baseDir,
|
|
126
129
|
issueAttachmentRepository: null,
|
|
127
130
|
updateStoryList: null,
|
|
131
|
+
resolveProjectRepository: null,
|
|
132
|
+
invalidateProject: null,
|
|
128
133
|
});
|
|
129
134
|
|
|
130
135
|
afterEach(() => {
|
|
@@ -868,6 +873,8 @@ describe('consoleOperationApi', () => {
|
|
|
868
873
|
consoleDataOutputDir: baseDir,
|
|
869
874
|
issueAttachmentRepository: null,
|
|
870
875
|
updateStoryList: null,
|
|
876
|
+
resolveProjectRepository: null,
|
|
877
|
+
invalidateProject: null,
|
|
871
878
|
};
|
|
872
879
|
});
|
|
873
880
|
|
|
@@ -1114,6 +1121,8 @@ describe('consoleOperationApi', () => {
|
|
|
1114
1121
|
consoleDataOutputDir: baseDir,
|
|
1115
1122
|
issueAttachmentRepository: null,
|
|
1116
1123
|
updateStoryList: null,
|
|
1124
|
+
resolveProjectRepository: null,
|
|
1125
|
+
invalidateProject: null,
|
|
1117
1126
|
};
|
|
1118
1127
|
const response = await handleTriage(multiContext, {
|
|
1119
1128
|
pjcode: 'globex',
|
|
@@ -1907,4 +1916,225 @@ describe('consoleOperationApi', () => {
|
|
|
1907
1916
|
);
|
|
1908
1917
|
});
|
|
1909
1918
|
});
|
|
1919
|
+
|
|
1920
|
+
describe('handleStoryAdd', () => {
|
|
1921
|
+
const buildProjectWithStories = (): Project => ({
|
|
1922
|
+
...project,
|
|
1923
|
+
url: 'https://github.com/orgs/acme-labs/projects/1',
|
|
1924
|
+
story: {
|
|
1925
|
+
name: 'Story',
|
|
1926
|
+
fieldId: 'storyField',
|
|
1927
|
+
databaseId: 1,
|
|
1928
|
+
stories: [
|
|
1929
|
+
{
|
|
1930
|
+
id: 'opt_first',
|
|
1931
|
+
name: 'First story',
|
|
1932
|
+
color: 'BLUE',
|
|
1933
|
+
description: '',
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
id: 'opt_second',
|
|
1937
|
+
name: 'Second story',
|
|
1938
|
+
color: 'GREEN',
|
|
1939
|
+
description: '',
|
|
1940
|
+
},
|
|
1941
|
+
],
|
|
1942
|
+
workflowManagementStory: { id: 'wms', name: 'workflow' },
|
|
1943
|
+
},
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
const updateStoryList = jest.fn();
|
|
1947
|
+
const projectRepositoryResolver = jest.fn(() => ({ updateStoryList }));
|
|
1948
|
+
|
|
1949
|
+
const addStoryContext = (p: Project): ConsoleOperationContext => ({
|
|
1950
|
+
...contextForProject(p),
|
|
1951
|
+
resolveProjectRepository: projectRepositoryResolver,
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
beforeEach(() => {
|
|
1955
|
+
updateStoryList.mockResolvedValue([]);
|
|
1956
|
+
projectRepositoryResolver.mockReturnValue({ updateStoryList });
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
it('inserts the new story at index 1 and calls updateStoryList', async () => {
|
|
1960
|
+
const p = buildProjectWithStories();
|
|
1961
|
+
const response = await handleStoryAdd(addStoryContext(p), {
|
|
1962
|
+
pjcode: 'acme',
|
|
1963
|
+
storyName: 'Brand new story',
|
|
1964
|
+
});
|
|
1965
|
+
expect(response.statusCode).toBe(200);
|
|
1966
|
+
expect(response.body).toEqual({ ok: true });
|
|
1967
|
+
expect(updateStoryList).toHaveBeenCalledWith(p, [
|
|
1968
|
+
{
|
|
1969
|
+
id: 'opt_first',
|
|
1970
|
+
name: 'First story',
|
|
1971
|
+
color: 'BLUE',
|
|
1972
|
+
description: '',
|
|
1973
|
+
},
|
|
1974
|
+
{ id: null, name: 'Brand new story', color: 'RED', description: '' },
|
|
1975
|
+
{
|
|
1976
|
+
id: 'opt_second',
|
|
1977
|
+
name: 'Second story',
|
|
1978
|
+
color: 'GREEN',
|
|
1979
|
+
description: '',
|
|
1980
|
+
},
|
|
1981
|
+
]);
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
it('inserts the new story as the only entry when the story list is empty', async () => {
|
|
1985
|
+
const emptyStoryProject: Project = {
|
|
1986
|
+
...buildProjectWithStories(),
|
|
1987
|
+
story: {
|
|
1988
|
+
name: 'Story',
|
|
1989
|
+
fieldId: 'storyField',
|
|
1990
|
+
databaseId: 1,
|
|
1991
|
+
stories: [],
|
|
1992
|
+
workflowManagementStory: { id: 'wms', name: 'workflow' },
|
|
1993
|
+
},
|
|
1994
|
+
};
|
|
1995
|
+
const response = await handleStoryAdd(
|
|
1996
|
+
addStoryContext(emptyStoryProject),
|
|
1997
|
+
{
|
|
1998
|
+
pjcode: 'acme',
|
|
1999
|
+
storyName: 'First ever story',
|
|
2000
|
+
},
|
|
2001
|
+
);
|
|
2002
|
+
expect(response.statusCode).toBe(200);
|
|
2003
|
+
expect(updateStoryList).toHaveBeenCalledWith(emptyStoryProject, [
|
|
2004
|
+
{ id: null, name: 'First ever story', color: 'RED', description: '' },
|
|
2005
|
+
]);
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
it('resolves the project repository using the project url', async () => {
|
|
2009
|
+
const p = buildProjectWithStories();
|
|
2010
|
+
await handleStoryAdd(addStoryContext(p), {
|
|
2011
|
+
pjcode: 'acme',
|
|
2012
|
+
storyName: 'Any story',
|
|
2013
|
+
});
|
|
2014
|
+
expect(projectRepositoryResolver).toHaveBeenCalledWith(p.url);
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
it('returns 502 when resolveProjectRepository is null', async () => {
|
|
2018
|
+
const response = await handleStoryAdd(
|
|
2019
|
+
{
|
|
2020
|
+
...contextForProject(buildProjectWithStories()),
|
|
2021
|
+
resolveProjectRepository: null,
|
|
2022
|
+
},
|
|
2023
|
+
{ pjcode: 'acme', storyName: 'Any story' },
|
|
2024
|
+
);
|
|
2025
|
+
expect(response).toEqual({
|
|
2026
|
+
statusCode: 502,
|
|
2027
|
+
body: { error: 'project repository is not configured' },
|
|
2028
|
+
});
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
it('rejects when storyName is missing', async () => {
|
|
2032
|
+
const response = await handleStoryAdd(
|
|
2033
|
+
addStoryContext(buildProjectWithStories()),
|
|
2034
|
+
{
|
|
2035
|
+
pjcode: 'acme',
|
|
2036
|
+
},
|
|
2037
|
+
);
|
|
2038
|
+
expect(response).toEqual({
|
|
2039
|
+
statusCode: 400,
|
|
2040
|
+
body: { error: 'storyName is required' },
|
|
2041
|
+
});
|
|
2042
|
+
});
|
|
2043
|
+
|
|
2044
|
+
it('rejects when pjcode is not configured', async () => {
|
|
2045
|
+
const response = await handleStoryAdd(
|
|
2046
|
+
addStoryContext(buildProjectWithStories()),
|
|
2047
|
+
{
|
|
2048
|
+
pjcode: 'unknown',
|
|
2049
|
+
storyName: 'Any story',
|
|
2050
|
+
},
|
|
2051
|
+
);
|
|
2052
|
+
expect(response).toEqual({
|
|
2053
|
+
statusCode: 400,
|
|
2054
|
+
body: { error: 'no project configured for pjcode "unknown"' },
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
it('rejects when the project has no story field', async () => {
|
|
2059
|
+
const projectWithoutStory: Project = {
|
|
2060
|
+
...buildProjectWithStories(),
|
|
2061
|
+
story: null,
|
|
2062
|
+
};
|
|
2063
|
+
const response = await handleStoryAdd(
|
|
2064
|
+
addStoryContext(projectWithoutStory),
|
|
2065
|
+
{
|
|
2066
|
+
pjcode: 'acme',
|
|
2067
|
+
storyName: 'Any story',
|
|
2068
|
+
},
|
|
2069
|
+
);
|
|
2070
|
+
expect(response).toEqual({
|
|
2071
|
+
statusCode: 400,
|
|
2072
|
+
body: { error: 'project does not have a story field' },
|
|
2073
|
+
});
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
it('invalidates the project cache after each successful add so consecutive adds both survive', async () => {
|
|
2077
|
+
const p = buildProjectWithStories();
|
|
2078
|
+
const pStory = p.story;
|
|
2079
|
+
if (pStory === null) {
|
|
2080
|
+
throw new Error('buildProjectWithStories must return a non-null story');
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
const projectAfterFirstAdd: Project = {
|
|
2084
|
+
...p,
|
|
2085
|
+
story: {
|
|
2086
|
+
...pStory,
|
|
2087
|
+
stories: [
|
|
2088
|
+
pStory.stories[0],
|
|
2089
|
+
{ id: 'opt_x', name: 'Story X', color: 'RED', description: '' },
|
|
2090
|
+
...pStory.stories.slice(1),
|
|
2091
|
+
],
|
|
2092
|
+
},
|
|
2093
|
+
};
|
|
2094
|
+
|
|
2095
|
+
let invalidateCalled = false;
|
|
2096
|
+
const invalidateProject = jest.fn((_pjcode: string) => {
|
|
2097
|
+
invalidateCalled = true;
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
const resolveProject = jest.fn(async (pjcode: string) => {
|
|
2101
|
+
if (pjcode !== 'acme') return null;
|
|
2102
|
+
return { pjcode, project: invalidateCalled ? projectAfterFirstAdd : p };
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
const consecutiveUpdateStoryList = jest.fn().mockResolvedValue([]);
|
|
2106
|
+
const consecutiveContext: ConsoleOperationContext = {
|
|
2107
|
+
resolveIssueRepository: () => issueRepository,
|
|
2108
|
+
resolveProject,
|
|
2109
|
+
isPjcodeConfigured: (pjcode: string) => pjcode === 'acme',
|
|
2110
|
+
consoleDataOutputDir: baseDir,
|
|
2111
|
+
issueAttachmentRepository: null,
|
|
2112
|
+
updateStoryList: null,
|
|
2113
|
+
resolveProjectRepository: jest.fn(() => ({
|
|
2114
|
+
updateStoryList: consecutiveUpdateStoryList,
|
|
2115
|
+
})),
|
|
2116
|
+
invalidateProject,
|
|
2117
|
+
};
|
|
2118
|
+
|
|
2119
|
+
await handleStoryAdd(consecutiveContext, {
|
|
2120
|
+
pjcode: 'acme',
|
|
2121
|
+
storyName: 'Story X',
|
|
2122
|
+
});
|
|
2123
|
+
await handleStoryAdd(consecutiveContext, {
|
|
2124
|
+
pjcode: 'acme',
|
|
2125
|
+
storyName: 'Story Y',
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
expect(invalidateProject).toHaveBeenCalledTimes(2);
|
|
2129
|
+
expect(invalidateProject).toHaveBeenCalledWith('acme');
|
|
2130
|
+
expect(consecutiveUpdateStoryList).toHaveBeenNthCalledWith(
|
|
2131
|
+
2,
|
|
2132
|
+
expect.anything(),
|
|
2133
|
+
expect.arrayContaining([
|
|
2134
|
+
expect.objectContaining({ name: 'Story X' }),
|
|
2135
|
+
expect.objectContaining({ name: 'Story Y' }),
|
|
2136
|
+
]),
|
|
2137
|
+
);
|
|
2138
|
+
});
|
|
2139
|
+
});
|
|
1910
2140
|
});
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import type { Issue } from '../../../domain/entities/Issue';
|
|
1
2
|
import {
|
|
3
|
+
buildStoryListWithNew,
|
|
4
|
+
type FieldOption,
|
|
5
|
+
type Project,
|
|
6
|
+
} from '../../../domain/entities/Project';
|
|
7
|
+
import type { IssueAttachmentRepository } from '../../../domain/usecases/adapter-interfaces/IssueAttachmentRepository';
|
|
8
|
+
import type {
|
|
2
9
|
IssueRepository,
|
|
3
10
|
PullRequestReviewCommentSide,
|
|
4
11
|
} from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
5
|
-
import {
|
|
6
|
-
import { FieldOption, Project } from '../../../domain/entities/Project';
|
|
7
|
-
import { Issue } from '../../../domain/entities/Issue';
|
|
12
|
+
import type { ProjectRepository } from '../../../domain/usecases/adapter-interfaces/ProjectRepository';
|
|
8
13
|
import {
|
|
9
|
-
recordDoneProjectItemIdAcrossTabs,
|
|
10
|
-
recordDoneProjectItemIdForTabs,
|
|
11
14
|
CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
|
|
12
15
|
CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
|
|
16
|
+
recordDoneProjectItemIdAcrossTabs,
|
|
17
|
+
recordDoneProjectItemIdForTabs,
|
|
13
18
|
} from './consoleDoneStore';
|
|
14
19
|
import { findConsoleItemUrl } from './consoleItemUrlLookup';
|
|
15
20
|
|
|
@@ -32,6 +37,10 @@ export type ConsoleIssueRepositoryResolver = (
|
|
|
32
37
|
issueOrPullRequestUrl: string,
|
|
33
38
|
) => IssueRepository;
|
|
34
39
|
|
|
40
|
+
export type ConsoleProjectRepositoryResolver = (
|
|
41
|
+
projectUrl: string,
|
|
42
|
+
) => Pick<ProjectRepository, 'updateStoryList'>;
|
|
43
|
+
|
|
35
44
|
export type ConsoleOperationContext = {
|
|
36
45
|
resolveIssueRepository: ConsoleIssueRepositoryResolver;
|
|
37
46
|
resolveProject: ConsoleProjectResolver;
|
|
@@ -41,6 +50,8 @@ export type ConsoleOperationContext = {
|
|
|
41
50
|
updateStoryList:
|
|
42
51
|
| ((project: Project, stories: FieldOption[]) => Promise<FieldOption[]>)
|
|
43
52
|
| null;
|
|
53
|
+
resolveProjectRepository: ConsoleProjectRepositoryResolver | null;
|
|
54
|
+
invalidateProject: ((pjcode: string) => void) | null;
|
|
44
55
|
};
|
|
45
56
|
|
|
46
57
|
export type ConsoleOperationResponse = {
|
|
@@ -193,8 +204,7 @@ const resolveBinding = async (
|
|
|
193
204
|
|
|
194
205
|
const isOperationResponse = (
|
|
195
206
|
value: ConsoleProjectBinding | ConsoleOperationResponse,
|
|
196
|
-
): value is ConsoleOperationResponse =>
|
|
197
|
-
Object.prototype.hasOwnProperty.call(value, 'statusCode');
|
|
207
|
+
): value is ConsoleOperationResponse => Object.hasOwn(value, 'statusCode');
|
|
198
208
|
|
|
199
209
|
// Validates that a pjcode is configured WITHOUT loading the ProjectV2 via
|
|
200
210
|
// GraphQL. Close operations only need the pjcode (for recordDone namespacing)
|
|
@@ -769,3 +779,29 @@ export const handleReorderStory = async (
|
|
|
769
779
|
await context.updateStoryList(project, reordered);
|
|
770
780
|
return ok();
|
|
771
781
|
};
|
|
782
|
+
|
|
783
|
+
export const handleStoryAdd = async (
|
|
784
|
+
context: ConsoleOperationContext,
|
|
785
|
+
body: Record<string, unknown>,
|
|
786
|
+
): Promise<ConsoleOperationResponse> => {
|
|
787
|
+
if (context.resolveProjectRepository === null) {
|
|
788
|
+
return badGateway('project repository is not configured');
|
|
789
|
+
}
|
|
790
|
+
const storyName = body.storyName;
|
|
791
|
+
if (!isNonEmptyString(storyName)) {
|
|
792
|
+
return badRequest('storyName is required');
|
|
793
|
+
}
|
|
794
|
+
const binding = await resolveBinding(context, body);
|
|
795
|
+
if (isOperationResponse(binding)) {
|
|
796
|
+
return binding;
|
|
797
|
+
}
|
|
798
|
+
const { pjcode, project } = binding;
|
|
799
|
+
if (project.story === null) {
|
|
800
|
+
return badRequest('project does not have a story field');
|
|
801
|
+
}
|
|
802
|
+
const newStoryList = buildStoryListWithNew(project.story.stories, storyName);
|
|
803
|
+
const projectRepository = context.resolveProjectRepository(project.url);
|
|
804
|
+
await projectRepository.updateStoryList(project, newStoryList);
|
|
805
|
+
context.invalidateProject?.(pjcode);
|
|
806
|
+
return ok();
|
|
807
|
+
};
|
|
@@ -61,18 +61,18 @@ describe('createConsoleProjectResolver', () => {
|
|
|
61
61
|
const loadProject = jest.fn(async (url: string) =>
|
|
62
62
|
url.includes('acme') ? acmeProject : globexProject,
|
|
63
63
|
);
|
|
64
|
-
const
|
|
64
|
+
const { resolve } = createConsoleProjectResolver(
|
|
65
65
|
{
|
|
66
66
|
acme: 'https://github.com/orgs/acme/projects/1',
|
|
67
67
|
globex: 'https://github.com/orgs/globex/projects/2',
|
|
68
68
|
},
|
|
69
69
|
loadProject,
|
|
70
70
|
);
|
|
71
|
-
await expect(
|
|
71
|
+
await expect(resolve('acme')).resolves.toEqual({
|
|
72
72
|
pjcode: 'acme',
|
|
73
73
|
project: acmeProject,
|
|
74
74
|
});
|
|
75
|
-
await expect(
|
|
75
|
+
await expect(resolve('globex')).resolves.toEqual({
|
|
76
76
|
pjcode: 'globex',
|
|
77
77
|
project: globexProject,
|
|
78
78
|
});
|
|
@@ -80,33 +80,53 @@ describe('createConsoleProjectResolver', () => {
|
|
|
80
80
|
|
|
81
81
|
it('returns null for a pjcode that has no configured project url', async () => {
|
|
82
82
|
const loadProject = jest.fn(async () => acmeProject);
|
|
83
|
-
const
|
|
83
|
+
const { resolve } = createConsoleProjectResolver(
|
|
84
84
|
{ acme: 'https://github.com/orgs/acme/projects/1' },
|
|
85
85
|
loadProject,
|
|
86
86
|
);
|
|
87
|
-
await expect(
|
|
87
|
+
await expect(resolve('unknown')).resolves.toBeNull();
|
|
88
88
|
expect(loadProject).not.toHaveBeenCalled();
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it('returns null when the project fails to load', async () => {
|
|
92
92
|
const loadProject = jest.fn(async () => null);
|
|
93
|
-
const
|
|
93
|
+
const { resolve } = createConsoleProjectResolver(
|
|
94
94
|
{ acme: 'https://github.com/orgs/acme/projects/1' },
|
|
95
95
|
loadProject,
|
|
96
96
|
);
|
|
97
|
-
await expect(
|
|
97
|
+
await expect(resolve('acme')).resolves.toBeNull();
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
it('loads each project at most once and serves later calls from cache', async () => {
|
|
101
101
|
const loadProject = jest.fn(async () => acmeProject);
|
|
102
|
-
const
|
|
102
|
+
const { resolve } = createConsoleProjectResolver(
|
|
103
103
|
{ acme: 'https://github.com/orgs/acme/projects/1' },
|
|
104
104
|
loadProject,
|
|
105
105
|
);
|
|
106
|
-
await
|
|
107
|
-
await
|
|
106
|
+
await resolve('acme');
|
|
107
|
+
await resolve('acme');
|
|
108
108
|
expect(loadProject).toHaveBeenCalledTimes(1);
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
it('re-fetches the project after invalidation', async () => {
|
|
112
|
+
const freshProject: Project = { ...mock<Project>(), id: 'PVT_fresh' };
|
|
113
|
+
let callCount = 0;
|
|
114
|
+
const loadProject = jest.fn(async () =>
|
|
115
|
+
callCount++ === 0 ? acmeProject : freshProject,
|
|
116
|
+
);
|
|
117
|
+
const { resolve, invalidate } = createConsoleProjectResolver(
|
|
118
|
+
{ acme: 'https://github.com/orgs/acme/projects/1' },
|
|
119
|
+
loadProject,
|
|
120
|
+
);
|
|
121
|
+
await expect(resolve('acme')).resolves.toMatchObject({
|
|
122
|
+
project: acmeProject,
|
|
123
|
+
});
|
|
124
|
+
invalidate('acme');
|
|
125
|
+
await expect(resolve('acme')).resolves.toMatchObject({
|
|
126
|
+
project: freshProject,
|
|
127
|
+
});
|
|
128
|
+
expect(loadProject).toHaveBeenCalledTimes(2);
|
|
129
|
+
});
|
|
110
130
|
});
|
|
111
131
|
|
|
112
132
|
describe('createConsoleProjectLoader', () => {
|
|
@@ -68,12 +68,19 @@ export const createPjcodeConfigChecker = (
|
|
|
68
68
|
Object.prototype.hasOwnProperty.call(pjcodeToProjectUrl, pjcode);
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
export type ConsoleProjectResolverBundle = {
|
|
72
|
+
resolve: ConsoleProjectResolver;
|
|
73
|
+
invalidate: (pjcode: string) => void;
|
|
74
|
+
};
|
|
75
|
+
|
|
71
76
|
export const createConsoleProjectResolver = (
|
|
72
77
|
pjcodeToProjectUrl: Record<string, string>,
|
|
73
78
|
loadProject: ConsoleProjectLoader,
|
|
74
|
-
):
|
|
79
|
+
): ConsoleProjectResolverBundle => {
|
|
75
80
|
const cache = new Map<string, ConsoleProjectBinding>();
|
|
76
|
-
|
|
81
|
+
const resolve: ConsoleProjectResolver = async (
|
|
82
|
+
pjcode: string,
|
|
83
|
+
): Promise<ConsoleProjectBinding | null> => {
|
|
77
84
|
const cached = cache.get(pjcode);
|
|
78
85
|
if (cached !== undefined) {
|
|
79
86
|
return cached;
|
|
@@ -90,4 +97,8 @@ export const createConsoleProjectResolver = (
|
|
|
90
97
|
cache.set(pjcode, binding);
|
|
91
98
|
return binding;
|
|
92
99
|
};
|
|
100
|
+
const invalidate = (pjcode: string): void => {
|
|
101
|
+
cache.delete(pjcode);
|
|
102
|
+
};
|
|
103
|
+
return { resolve, invalidate };
|
|
93
104
|
};
|
|
@@ -459,10 +459,10 @@ test('creates an issue for a story when the add-task button and form are used',
|
|
|
459
459
|
await tdpmRow.locator('.console-op-button', { hasText: 'Add task' }).click();
|
|
460
460
|
|
|
461
461
|
await page
|
|
462
|
-
.locator('.console-
|
|
462
|
+
.locator('.console-inline-input-form-input')
|
|
463
463
|
.fill('New task for TDPM Console port');
|
|
464
464
|
await page
|
|
465
|
-
.locator('.console-
|
|
465
|
+
.locator('.console-inline-input-form .console-op-button', {
|
|
466
466
|
hasText: 'Create',
|
|
467
467
|
})
|
|
468
468
|
.click();
|
|
@@ -479,6 +479,41 @@ test('creates an issue for a story when the add-task button and form are used',
|
|
|
479
479
|
);
|
|
480
480
|
});
|
|
481
481
|
|
|
482
|
+
test('creates a new story when the add-story button and form are used', async ({
|
|
483
|
+
page,
|
|
484
|
+
}) => {
|
|
485
|
+
await page.goto(harness.appRootUrl);
|
|
486
|
+
|
|
487
|
+
await tabByLabel(page, 'Stories').click();
|
|
488
|
+
|
|
489
|
+
await page
|
|
490
|
+
.locator('.console-add-story-section .console-op-button', {
|
|
491
|
+
hasText: 'Add story',
|
|
492
|
+
})
|
|
493
|
+
.click();
|
|
494
|
+
|
|
495
|
+
await page
|
|
496
|
+
.locator('.console-add-story-section .console-inline-input-form-input')
|
|
497
|
+
.fill('My new story');
|
|
498
|
+
await page
|
|
499
|
+
.locator(
|
|
500
|
+
'.console-add-story-section .console-inline-input-form .console-op-button',
|
|
501
|
+
{
|
|
502
|
+
hasText: 'Create',
|
|
503
|
+
},
|
|
504
|
+
)
|
|
505
|
+
.click();
|
|
506
|
+
|
|
507
|
+
await expect
|
|
508
|
+
.poll(() => harness.addStoryCalls.length, { timeout: 10000 })
|
|
509
|
+
.toBe(1);
|
|
510
|
+
expect(harness.addStoryCalls[0].storyName).toBe('My new story');
|
|
511
|
+
|
|
512
|
+
await expect(
|
|
513
|
+
page.locator('.console-add-story-section .console-inline-input-form'),
|
|
514
|
+
).toHaveCount(0);
|
|
515
|
+
});
|
|
516
|
+
|
|
482
517
|
test('reorders stories on the triage tab with up/down buttons', async ({
|
|
483
518
|
page,
|
|
484
519
|
}) => {
|
|
@@ -621,6 +621,10 @@ export type ConsoleE2eReorderStoryCall = {
|
|
|
621
621
|
storyOptionIds: string[];
|
|
622
622
|
};
|
|
623
623
|
|
|
624
|
+
export type ConsoleE2eAddStoryCall = {
|
|
625
|
+
storyName: string;
|
|
626
|
+
};
|
|
627
|
+
|
|
624
628
|
export type ConsoleE2eHarness = {
|
|
625
629
|
baseUrl: string;
|
|
626
630
|
appUrl: string;
|
|
@@ -630,6 +634,7 @@ export type ConsoleE2eHarness = {
|
|
|
630
634
|
requestChangesCalls: ConsoleE2eRequestChangesCall[];
|
|
631
635
|
createIssueCalls: ConsoleE2eCreateIssueCall[];
|
|
632
636
|
reorderStoryCalls: ConsoleE2eReorderStoryCall[];
|
|
637
|
+
addStoryCalls: ConsoleE2eAddStoryCall[];
|
|
633
638
|
stop: () => Promise<void>;
|
|
634
639
|
};
|
|
635
640
|
|
|
@@ -652,6 +657,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
652
657
|
const requestChangesCalls: ConsoleE2eRequestChangesCall[] = [];
|
|
653
658
|
const createIssueCalls: ConsoleE2eCreateIssueCall[] = [];
|
|
654
659
|
const reorderStoryCalls: ConsoleE2eReorderStoryCall[] = [];
|
|
660
|
+
const addStoryCalls: ConsoleE2eAddStoryCall[] = [];
|
|
655
661
|
|
|
656
662
|
const server = await startWebServer({
|
|
657
663
|
accessToken: CONSOLE_E2E_TOKEN,
|
|
@@ -676,6 +682,19 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
676
682
|
return result;
|
|
677
683
|
},
|
|
678
684
|
},
|
|
685
|
+
resolveProjectRepository: (_projectUrl) => ({
|
|
686
|
+
updateStoryList: async (_updatedProject, stories) => {
|
|
687
|
+
const newStory = stories.find((s) => s.id === null);
|
|
688
|
+
if (newStory !== undefined) {
|
|
689
|
+
addStoryCalls.push({ storyName: newStory.name });
|
|
690
|
+
}
|
|
691
|
+
const result = stories as FieldOption[];
|
|
692
|
+
if (project.story !== null) {
|
|
693
|
+
project.story.stories = result;
|
|
694
|
+
}
|
|
695
|
+
return result;
|
|
696
|
+
},
|
|
697
|
+
}),
|
|
679
698
|
resolveProject,
|
|
680
699
|
isPjcodeConfigured,
|
|
681
700
|
issueTitleStateCache: new IssueTitleStateCache(),
|
|
@@ -706,6 +725,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
706
725
|
requestChangesCalls,
|
|
707
726
|
createIssueCalls,
|
|
708
727
|
reorderStoryCalls,
|
|
728
|
+
addStoryCalls,
|
|
709
729
|
stop: async (): Promise<void> => {
|
|
710
730
|
await closeServer(server);
|
|
711
731
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
@@ -50,7 +50,10 @@ const storiesWithUrl = storiesWithoutUrl.map((s) => ({
|
|
|
50
50
|
const meta: Meta<typeof ConsoleStoryList> = {
|
|
51
51
|
title: 'Console/ConsoleStoryList',
|
|
52
52
|
component: ConsoleStoryList,
|
|
53
|
-
args: {
|
|
53
|
+
args: {
|
|
54
|
+
onCreateIssue: () => Promise.resolve(),
|
|
55
|
+
onAddStory: () => Promise.resolve(),
|
|
56
|
+
},
|
|
54
57
|
};
|
|
55
58
|
|
|
56
59
|
export default meta;
|