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.
Files changed (49) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/bin/adapter/entry-points/cli/index.js +31 -29
  3. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  4. package/bin/adapter/entry-points/console/consoleOperationApi.js +26 -2
  5. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  6. package/bin/adapter/entry-points/console/consoleProjectResolver.js +5 -1
  7. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
  8. package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +86 -0
  9. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  10. package/bin/adapter/entry-points/console/webServer.js +4 -0
  11. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  12. package/bin/domain/entities/Project.js +17 -0
  13. package/bin/domain/entities/Project.js.map +1 -1
  14. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js +5 -16
  15. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
  16. package/package.json +1 -1
  17. package/src/adapter/entry-points/cli/index.ts +57 -52
  18. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +242 -12
  19. package/src/adapter/entry-points/console/consoleOperationApi.ts +43 -7
  20. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +30 -10
  21. package/src/adapter/entry-points/console/consoleProjectResolver.ts +13 -2
  22. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +37 -2
  23. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +20 -0
  24. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -1
  25. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +127 -8
  26. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +119 -71
  27. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +26 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +20 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +12 -0
  30. package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +86 -0
  31. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  32. package/src/adapter/entry-points/console/webServer.test.ts +74 -0
  33. package/src/adapter/entry-points/console/webServer.ts +8 -0
  34. package/src/domain/entities/Project.ts +23 -0
  35. package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +16 -34
  36. package/types/adapter/entry-points/cli/index.d.ts +1 -1
  37. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  38. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +8 -3
  39. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  40. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +5 -1
  41. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
  42. package/types/adapter/entry-points/console/webServer.d.ts +3 -1
  43. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  44. package/types/domain/entities/Project.d.ts +4 -0
  45. package/types/domain/entities/Project.d.ts.map +1 -1
  46. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts +6 -8
  47. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
  48. package/bin/adapter/entry-points/console/ui-dist/assets/index-CMrnEYO1.js +0 -86
  49. 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 { mock } from 'jest-mock-extended';
5
- import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
6
- import { Project } from '../../../domain/entities/Project';
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
- ConsoleOperationContext,
10
- ConsoleProjectBinding,
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 { IssueAttachmentRepository } from '../../../domain/usecases/adapter-interfaces/IssueAttachmentRepository';
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 resolver = createConsoleProjectResolver(
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(resolver('acme')).resolves.toEqual({
71
+ await expect(resolve('acme')).resolves.toEqual({
72
72
  pjcode: 'acme',
73
73
  project: acmeProject,
74
74
  });
75
- await expect(resolver('globex')).resolves.toEqual({
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 resolver = createConsoleProjectResolver(
83
+ const { resolve } = createConsoleProjectResolver(
84
84
  { acme: 'https://github.com/orgs/acme/projects/1' },
85
85
  loadProject,
86
86
  );
87
- await expect(resolver('unknown')).resolves.toBeNull();
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 resolver = createConsoleProjectResolver(
93
+ const { resolve } = createConsoleProjectResolver(
94
94
  { acme: 'https://github.com/orgs/acme/projects/1' },
95
95
  loadProject,
96
96
  );
97
- await expect(resolver('acme')).resolves.toBeNull();
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 resolver = createConsoleProjectResolver(
102
+ const { resolve } = createConsoleProjectResolver(
103
103
  { acme: 'https://github.com/orgs/acme/projects/1' },
104
104
  loadProject,
105
105
  );
106
- await resolver('acme');
107
- await resolver('acme');
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
- ): ConsoleProjectResolver => {
79
+ ): ConsoleProjectResolverBundle => {
75
80
  const cache = new Map<string, ConsoleProjectBinding>();
76
- return async (pjcode: string): Promise<ConsoleProjectBinding | null> => {
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-story-create-input')
462
+ .locator('.console-inline-input-form-input')
463
463
  .fill('New task for TDPM Console port');
464
464
  await page
465
- .locator('.console-story-create-form .console-op-button', {
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: { onCreateIssue: () => Promise.resolve() },
53
+ args: {
54
+ onCreateIssue: () => Promise.resolve(),
55
+ onAddStory: () => Promise.resolve(),
56
+ },
54
57
  };
55
58
 
56
59
  export default meta;