github-issue-tower-defence-management 1.146.0 → 1.146.2
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 +23 -9
- package/README.md +1 -1
- package/bin/adapter/entry-points/cli/index.js +29 -7
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +3 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js +41 -0
- package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js.map +1 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js +34 -16
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/ensureConsoleRunning.js +6 -1
- package/bin/adapter/entry-points/console/ensureConsoleRunning.js.map +1 -1
- package/bin/adapter/entry-points/console/webServer.js +3 -4
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/GraphqlProjectRepository.js +75 -4
- package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
- package/bin/adapter/repositories/utils.js +2 -4
- package/bin/adapter/repositories/utils.js.map +1 -1
- package/bin/domain/entities/ProjectFieldName.js +6 -0
- package/bin/domain/entities/ProjectFieldName.js.map +1 -0
- package/bin/domain/entities/RequiredProjectField.js +47 -0
- package/bin/domain/entities/RequiredProjectField.js.map +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +5 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js +25 -0
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js.map +1 -0
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js +6 -1
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js.map +1 -1
- package/bin/domain/usecases/dashboard/DashboardProjectCode.js +17 -13
- package/bin/domain/usecases/dashboard/DashboardProjectCode.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +62 -2
- package/src/adapter/entry-points/cli/index.ts +58 -11
- package/src/adapter/entry-points/cli/projectConfig.ts +8 -0
- package/src/adapter/entry-points/console/consoleDataDelivery.test.ts +37 -39
- package/src/adapter/entry-points/console/consoleDoneStore.test.ts +33 -33
- package/src/adapter/entry-points/console/consoleGithubTokenResolver.test.ts +135 -0
- package/src/adapter/entry-points/console/consoleGithubTokenResolver.ts +55 -0
- package/src/adapter/entry-points/console/consoleItemUrlLookup.test.ts +6 -6
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +92 -70
- package/src/adapter/entry-points/console/consoleOperationApi.ts +51 -44
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +35 -35
- package/src/adapter/entry-points/console/dashboardComposeService.test.ts +28 -28
- package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +26 -20
- package/src/adapter/entry-points/console/ensureConsoleRunning.test.ts +19 -2
- package/src/adapter/entry-points/console/ensureConsoleRunning.ts +8 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +1 -1
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +3 -3
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +19 -31
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +24 -32
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +6 -6
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.test.ts +4 -4
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +8 -8
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +5 -5
- package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +13 -13
- package/src/adapter/entry-points/console/webServer.test.ts +55 -52
- package/src/adapter/entry-points/console/webServer.ts +5 -4
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +2 -2
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +4 -0
- package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +5 -5
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +5 -5
- package/src/adapter/repositories/GraphqlProjectRepository.ts +110 -4
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +1 -1
- package/src/adapter/repositories/utils.ts +3 -3
- package/src/domain/entities/ProjectFieldName.ts +2 -0
- package/src/domain/entities/RequiredProjectField.ts +64 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +5 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +3 -3
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.test.ts +159 -0
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts +32 -0
- package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +19 -19
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +24 -14
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.ts +10 -1
- package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -0
- package/src/domain/usecases/dashboard/ComposeDashboardUseCase.test.ts +28 -28
- package/src/domain/usecases/dashboard/DashboardProjectCode.test.ts +21 -21
- package/src/domain/usecases/dashboard/DashboardProjectCode.ts +21 -15
- 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/consoleGithubTokenResolver.d.ts +6 -0
- package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts.map +1 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/ensureConsoleRunning.d.ts +1 -1
- package/types/adapter/entry-points/console/ensureConsoleRunning.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -2
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts +4 -1
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
- package/types/adapter/repositories/utils.d.ts.map +1 -1
- package/types/domain/entities/ProjectFieldName.d.ts +2 -0
- package/types/domain/entities/ProjectFieldName.d.ts.map +1 -0
- package/types/domain/entities/RequiredProjectField.d.ts +13 -0
- package/types/domain/entities/RequiredProjectField.d.ts.map +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts +10 -0
- package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts.map +1 -0
- package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +3 -0
- package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -1
- package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts +2 -2
- package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts.map +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { FieldOption } from './Project';
|
|
2
|
+
|
|
3
|
+
export type RequiredProjectFieldDataType = 'DATE' | 'TEXT' | 'SINGLE_SELECT';
|
|
4
|
+
|
|
5
|
+
export type RequiredProjectFieldDefinition = {
|
|
6
|
+
name: string;
|
|
7
|
+
dataType: RequiredProjectFieldDataType;
|
|
8
|
+
options: Omit<FieldOption, 'id'>[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const storyOption = (
|
|
12
|
+
name: string,
|
|
13
|
+
color: FieldOption['color'],
|
|
14
|
+
): Omit<FieldOption, 'id'> => ({ name, color, description: '' });
|
|
15
|
+
|
|
16
|
+
const REQUIRED_STORY_OPTIONS: Omit<FieldOption, 'id'>[] = [
|
|
17
|
+
storyOption('regular / NO STORY', 'RED'),
|
|
18
|
+
storyOption('regular / WORKFLOW BLOCKER', 'RED'),
|
|
19
|
+
storyOption('regular / high priority', 'RED'),
|
|
20
|
+
storyOption('regular / workflow management', 'YELLOW'),
|
|
21
|
+
storyOption('regular / routine management', 'YELLOW'),
|
|
22
|
+
storyOption('regular / middle bug', 'GRAY'),
|
|
23
|
+
storyOption('regular / minor bug', 'GRAY'),
|
|
24
|
+
storyOption('regular / refactor', 'GRAY'),
|
|
25
|
+
storyOption('regular / backlog', 'GRAY'),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const REQUIRED_NEXT_ACTION_HOUR_OPTIONS: Omit<FieldOption, 'id'>[] = Array.from(
|
|
29
|
+
{ length: 23 },
|
|
30
|
+
(_, index) => ({
|
|
31
|
+
name: `${index + 1}`,
|
|
32
|
+
color: 'GRAY' as const,
|
|
33
|
+
description: '',
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const STORY_FIELD_NAME = 'Story';
|
|
38
|
+
export const NEXT_ACTION_DATE_FIELD_NAME = 'Next Action Date';
|
|
39
|
+
export const NEXT_ACTION_HOUR_FIELD_NAME = 'Next Action Hour';
|
|
40
|
+
export const DEPENDED_ISSUE_URL_FIELD_NAME =
|
|
41
|
+
'Depended Issue URL separated by comma';
|
|
42
|
+
|
|
43
|
+
export const REQUIRED_PROJECT_FIELDS: RequiredProjectFieldDefinition[] = [
|
|
44
|
+
{
|
|
45
|
+
name: STORY_FIELD_NAME,
|
|
46
|
+
dataType: 'SINGLE_SELECT',
|
|
47
|
+
options: REQUIRED_STORY_OPTIONS,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: NEXT_ACTION_DATE_FIELD_NAME,
|
|
51
|
+
dataType: 'DATE',
|
|
52
|
+
options: [],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: NEXT_ACTION_HOUR_FIELD_NAME,
|
|
56
|
+
dataType: 'SINGLE_SELECT',
|
|
57
|
+
options: REQUIRED_NEXT_ACTION_HOUR_OPTIONS,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: DEPENDED_ISSUE_URL_FIELD_NAME,
|
|
61
|
+
dataType: 'TEXT',
|
|
62
|
+
options: [],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
@@ -24,6 +24,7 @@ import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCa
|
|
|
24
24
|
import { StartPreparationUseCase } from './StartPreparationUseCase';
|
|
25
25
|
import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
|
|
26
26
|
import { RevertNotReadyReviewQueueIssueUseCase } from './RevertNotReadyReviewQueueIssueUseCase';
|
|
27
|
+
import { ProjectRequiredFieldCreateUseCase } from './ProjectRequiredFieldCreateUseCase';
|
|
27
28
|
import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
|
|
28
29
|
import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
|
|
29
30
|
import { DailySecurityScanUseCase } from './DailySecurityScanUseCase';
|
|
@@ -85,6 +86,8 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
85
86
|
});
|
|
86
87
|
|
|
87
88
|
describe('run', () => {
|
|
89
|
+
const mockProjectRequiredFieldCreateUseCase =
|
|
90
|
+
mock<ProjectRequiredFieldCreateUseCase>();
|
|
88
91
|
const mockSetupTowerDefenceProjectUseCase =
|
|
89
92
|
mock<SetupTowerDefenceProjectUseCase>();
|
|
90
93
|
const mockActionAnnouncementUseCase = mock<ActionAnnouncementUseCase>();
|
|
@@ -128,6 +131,7 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
128
131
|
const mockIssueRepository = mock<IssueRepository>();
|
|
129
132
|
|
|
130
133
|
const useCase = new HandleScheduledEventUseCase(
|
|
134
|
+
mockProjectRequiredFieldCreateUseCase,
|
|
131
135
|
mockSetupTowerDefenceProjectUseCase,
|
|
132
136
|
mockActionAnnouncementUseCase,
|
|
133
137
|
mockSetWorkflowManagementIssueToStoryUseCase,
|
|
@@ -29,6 +29,7 @@ import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUse
|
|
|
29
29
|
import { RevertNotReadyReviewQueueIssueUseCase } from './RevertNotReadyReviewQueueIssueUseCase';
|
|
30
30
|
import { resolveLabelsAsLlmAgentName } from './resolveLabelsAsLlmAgentName';
|
|
31
31
|
import { resolveAllowedIssueAuthors } from './resolveAllowedIssueAuthors';
|
|
32
|
+
import { ProjectRequiredFieldCreateUseCase } from './ProjectRequiredFieldCreateUseCase';
|
|
32
33
|
import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
|
|
33
34
|
import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
|
|
34
35
|
import {
|
|
@@ -114,6 +115,7 @@ const isTransientSpreadsheetApiError = (error: Error): boolean => {
|
|
|
114
115
|
|
|
115
116
|
export class HandleScheduledEventUseCase {
|
|
116
117
|
constructor(
|
|
118
|
+
readonly projectRequiredFieldCreateUseCase: ProjectRequiredFieldCreateUseCase,
|
|
117
119
|
readonly setupTowerDefenceProjectUseCase: SetupTowerDefenceProjectUseCase,
|
|
118
120
|
readonly actionAnnouncementUseCase: ActionAnnouncementUseCase,
|
|
119
121
|
readonly setWorkflowManagementIssueToStoryUseCase: SetWorkflowManagementIssueToStoryUseCase,
|
|
@@ -187,6 +189,9 @@ export class HandleScheduledEventUseCase {
|
|
|
187
189
|
if (input.disabled) {
|
|
188
190
|
return null;
|
|
189
191
|
}
|
|
192
|
+
await this.projectRequiredFieldCreateUseCase.run({
|
|
193
|
+
projectUrl: input.projectUrl,
|
|
194
|
+
});
|
|
190
195
|
await this.setupTowerDefenceProjectUseCase.run({
|
|
191
196
|
projectUrl: input.projectUrl,
|
|
192
197
|
});
|
|
@@ -755,18 +755,18 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
755
755
|
});
|
|
756
756
|
|
|
757
757
|
it('parks a role-named leader session while its latest owner call is unanswered', async () => {
|
|
758
|
-
setupLiveInteractiveSession('
|
|
758
|
+
setupLiveInteractiveSession('acmepm');
|
|
759
759
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
760
760
|
[
|
|
761
761
|
{
|
|
762
|
-
sessionName: '
|
|
762
|
+
sessionName: 'acmepm',
|
|
763
763
|
lastOutputEpochSeconds:
|
|
764
764
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
765
765
|
},
|
|
766
766
|
],
|
|
767
767
|
);
|
|
768
768
|
mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
|
|
769
|
-
new Map([['
|
|
769
|
+
new Map([['acmepm', nowEpochSeconds - 60]]),
|
|
770
770
|
);
|
|
771
771
|
|
|
772
772
|
await useCase.run(runParams());
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { mock } from 'jest-mock-extended';
|
|
2
|
+
import { Project } from '../entities/Project';
|
|
3
|
+
import {
|
|
4
|
+
DEPENDED_ISSUE_URL_FIELD_NAME,
|
|
5
|
+
NEXT_ACTION_DATE_FIELD_NAME,
|
|
6
|
+
NEXT_ACTION_HOUR_FIELD_NAME,
|
|
7
|
+
STORY_FIELD_NAME,
|
|
8
|
+
} from '../entities/RequiredProjectField';
|
|
9
|
+
import { ProjectRequiredFieldCreateUseCase } from './ProjectRequiredFieldCreateUseCase';
|
|
10
|
+
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
11
|
+
|
|
12
|
+
describe('ProjectRequiredFieldCreateUseCase', () => {
|
|
13
|
+
const projectUrl = 'https://github.com/orgs/meta-site/projects/18';
|
|
14
|
+
const project: Project = {
|
|
15
|
+
id: 'PVT_kwDOBjp5zs4BfQGn',
|
|
16
|
+
url: projectUrl,
|
|
17
|
+
databaseId: 18,
|
|
18
|
+
name: 'cmg',
|
|
19
|
+
status: {
|
|
20
|
+
name: 'Status',
|
|
21
|
+
fieldId: 'PVTSSF_status',
|
|
22
|
+
statuses: [],
|
|
23
|
+
},
|
|
24
|
+
nextActionDate: null,
|
|
25
|
+
nextActionHour: null,
|
|
26
|
+
story: null,
|
|
27
|
+
remainingEstimationMinutes: null,
|
|
28
|
+
dependedIssueUrlSeparatedByComma: null,
|
|
29
|
+
completionDate50PercentConfidence: null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const createUseCase = (existingFieldNames: string[]) => {
|
|
33
|
+
const projectRepository =
|
|
34
|
+
mock<
|
|
35
|
+
Pick<ProjectRepository, 'getByUrl' | 'listFieldNames' | 'createField'>
|
|
36
|
+
>();
|
|
37
|
+
projectRepository.getByUrl.mockResolvedValue(project);
|
|
38
|
+
projectRepository.listFieldNames.mockResolvedValue(existingFieldNames);
|
|
39
|
+
projectRepository.createField.mockResolvedValue(undefined);
|
|
40
|
+
return {
|
|
41
|
+
projectRepository,
|
|
42
|
+
useCase: new ProjectRequiredFieldCreateUseCase(projectRepository),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
it('should create every required field when the project has none of them', async () => {
|
|
47
|
+
const { projectRepository, useCase } = createUseCase([
|
|
48
|
+
'Title',
|
|
49
|
+
'Assignees',
|
|
50
|
+
'Status',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
await useCase.run({ projectUrl });
|
|
54
|
+
|
|
55
|
+
const createdFieldNames = projectRepository.createField.mock.calls.map(
|
|
56
|
+
(call) => call[1].name,
|
|
57
|
+
);
|
|
58
|
+
expect(createdFieldNames).toEqual([
|
|
59
|
+
STORY_FIELD_NAME,
|
|
60
|
+
NEXT_ACTION_DATE_FIELD_NAME,
|
|
61
|
+
NEXT_ACTION_HOUR_FIELD_NAME,
|
|
62
|
+
DEPENDED_ISSUE_URL_FIELD_NAME,
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should create nothing when the project already has every required field', async () => {
|
|
67
|
+
const { projectRepository, useCase } = createUseCase([
|
|
68
|
+
'Title',
|
|
69
|
+
'Status',
|
|
70
|
+
'story',
|
|
71
|
+
'nextactiondate',
|
|
72
|
+
'nextactionhour',
|
|
73
|
+
'Depended Issue URL separated by comma',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
await useCase.run({ projectUrl });
|
|
77
|
+
|
|
78
|
+
expect(projectRepository.createField).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should create only the missing fields', async () => {
|
|
82
|
+
const { projectRepository, useCase } = createUseCase([
|
|
83
|
+
'Title',
|
|
84
|
+
'Status',
|
|
85
|
+
'Story',
|
|
86
|
+
'Next Action Date',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
await useCase.run({ projectUrl });
|
|
90
|
+
|
|
91
|
+
const createdFieldNames = projectRepository.createField.mock.calls.map(
|
|
92
|
+
(call) => call[1].name,
|
|
93
|
+
);
|
|
94
|
+
expect(createdFieldNames).toEqual([
|
|
95
|
+
NEXT_ACTION_HOUR_FIELD_NAME,
|
|
96
|
+
DEPENDED_ISSUE_URL_FIELD_NAME,
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should create the story field with the standard story options', async () => {
|
|
101
|
+
const { projectRepository, useCase } = createUseCase(['Title', 'Status']);
|
|
102
|
+
|
|
103
|
+
await useCase.run({ projectUrl });
|
|
104
|
+
|
|
105
|
+
const storyCall = projectRepository.createField.mock.calls.find(
|
|
106
|
+
(call) => call[1].name === STORY_FIELD_NAME,
|
|
107
|
+
);
|
|
108
|
+
expect(storyCall?.[1].dataType).toBe('SINGLE_SELECT');
|
|
109
|
+
expect(storyCall?.[1].options.map((option) => option.name)).toEqual([
|
|
110
|
+
'regular / NO STORY',
|
|
111
|
+
'regular / WORKFLOW BLOCKER',
|
|
112
|
+
'regular / high priority',
|
|
113
|
+
'regular / workflow management',
|
|
114
|
+
'regular / routine management',
|
|
115
|
+
'regular / middle bug',
|
|
116
|
+
'regular / minor bug',
|
|
117
|
+
'regular / refactor',
|
|
118
|
+
'regular / backlog',
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should create the next action hour field with the hour options from 1 to 23', async () => {
|
|
123
|
+
const { projectRepository, useCase } = createUseCase(['Title', 'Status']);
|
|
124
|
+
|
|
125
|
+
await useCase.run({ projectUrl });
|
|
126
|
+
|
|
127
|
+
const hourCall = projectRepository.createField.mock.calls.find(
|
|
128
|
+
(call) => call[1].name === NEXT_ACTION_HOUR_FIELD_NAME,
|
|
129
|
+
);
|
|
130
|
+
expect(hourCall?.[1].dataType).toBe('SINGLE_SELECT');
|
|
131
|
+
expect(hourCall?.[1].options.map((option) => option.name)).toEqual(
|
|
132
|
+
Array.from({ length: 23 }, (_, index) => `${index + 1}`),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should create the depended issue url field as a text field', async () => {
|
|
137
|
+
const { projectRepository, useCase } = createUseCase(['Title', 'Status']);
|
|
138
|
+
|
|
139
|
+
await useCase.run({ projectUrl });
|
|
140
|
+
|
|
141
|
+
const dependedCall = projectRepository.createField.mock.calls.find(
|
|
142
|
+
(call) => call[1].name === DEPENDED_ISSUE_URL_FIELD_NAME,
|
|
143
|
+
);
|
|
144
|
+
expect(dependedCall?.[1].dataType).toBe('TEXT');
|
|
145
|
+
expect(dependedCall?.[1].options).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should create the next action date field as a date field', async () => {
|
|
149
|
+
const { projectRepository, useCase } = createUseCase(['Title', 'Status']);
|
|
150
|
+
|
|
151
|
+
await useCase.run({ projectUrl });
|
|
152
|
+
|
|
153
|
+
const dateCall = projectRepository.createField.mock.calls.find(
|
|
154
|
+
(call) => call[1].name === NEXT_ACTION_DATE_FIELD_NAME,
|
|
155
|
+
);
|
|
156
|
+
expect(dateCall?.[1].dataType).toBe('DATE');
|
|
157
|
+
expect(dateCall?.[1].options).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Project } from '../entities/Project';
|
|
2
|
+
import { normalizeProjectFieldName } from '../entities/ProjectFieldName';
|
|
3
|
+
import { REQUIRED_PROJECT_FIELDS } from '../entities/RequiredProjectField';
|
|
4
|
+
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
5
|
+
|
|
6
|
+
export class ProjectRequiredFieldCreateUseCase {
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly projectRepository: Pick<
|
|
9
|
+
ProjectRepository,
|
|
10
|
+
'getByUrl' | 'listFieldNames' | 'createField'
|
|
11
|
+
>,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
run = async (params: { projectUrl: string }): Promise<void> => {
|
|
15
|
+
const project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
16
|
+
await this.createMissingFields(project);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
private createMissingFields = async (project: Project): Promise<void> => {
|
|
20
|
+
const existingFieldNames = (
|
|
21
|
+
await this.projectRepository.listFieldNames(project)
|
|
22
|
+
).map(normalizeProjectFieldName);
|
|
23
|
+
for (const required of REQUIRED_PROJECT_FIELDS) {
|
|
24
|
+
if (
|
|
25
|
+
existingFieldNames.includes(normalizeProjectFieldName(required.name))
|
|
26
|
+
) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
await this.projectRepository.createField(project, required);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -381,9 +381,9 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
|
|
|
381
381
|
nextActionDate: null,
|
|
382
382
|
nextActionHour: null,
|
|
383
383
|
isPr: false,
|
|
384
|
-
org: '
|
|
385
|
-
repo: '
|
|
386
|
-
url: 'https://github.com/
|
|
384
|
+
org: 'initech-medical',
|
|
385
|
+
repo: 'initech-platform',
|
|
386
|
+
url: 'https://github.com/initech-medical/initech-platform/issues/1445',
|
|
387
387
|
};
|
|
388
388
|
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
389
389
|
|
|
@@ -401,8 +401,8 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
|
|
|
401
401
|
expect(mockIssueRepository.searchIssue.mock.calls).toEqual([
|
|
402
402
|
[
|
|
403
403
|
{
|
|
404
|
-
owner: '
|
|
405
|
-
repositoryName: '
|
|
404
|
+
owner: 'initech-medical',
|
|
405
|
+
repositoryName: 'initech-platform',
|
|
406
406
|
type: 'issue',
|
|
407
407
|
state: 'open',
|
|
408
408
|
title:
|
|
@@ -412,16 +412,16 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
|
|
|
412
412
|
]);
|
|
413
413
|
expect(mockIssueRepository.createNewIssue).toHaveBeenCalledTimes(1);
|
|
414
414
|
const createCall = mockIssueRepository.createNewIssue.mock.calls[0];
|
|
415
|
-
expect(createCall[0]).toEqual('
|
|
416
|
-
expect(createCall[1]).toEqual('
|
|
415
|
+
expect(createCall[0]).toEqual('initech-medical');
|
|
416
|
+
expect(createCall[1]).toEqual('initech-platform');
|
|
417
417
|
expect(createCall[2]).toEqual(
|
|
418
418
|
'TDPM: story label "story:routine-management" has no matching "regular / routine-management" Story option',
|
|
419
419
|
);
|
|
420
420
|
expect(createCall[3]).toContain(
|
|
421
|
-
'https://github.com/
|
|
421
|
+
'https://github.com/initech-medical/initech-platform/issues/1445',
|
|
422
422
|
);
|
|
423
423
|
expect(createCall[3]).toContain('story:routine-management');
|
|
424
|
-
expect(createCall[4]).toEqual(['
|
|
424
|
+
expect(createCall[4]).toEqual(['initech-medical']);
|
|
425
425
|
expect(createCall[5]).toEqual([]);
|
|
426
426
|
});
|
|
427
427
|
|
|
@@ -434,13 +434,13 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
|
|
|
434
434
|
nextActionDate: null,
|
|
435
435
|
nextActionHour: null,
|
|
436
436
|
isPr: false,
|
|
437
|
-
org: '
|
|
438
|
-
repo: '
|
|
439
|
-
url: 'https://github.com/
|
|
437
|
+
org: 'initech-medical',
|
|
438
|
+
repo: 'initech-platform',
|
|
439
|
+
url: 'https://github.com/initech-medical/initech-platform/issues/1445',
|
|
440
440
|
};
|
|
441
441
|
mockIssueRepository.searchIssue.mockResolvedValue([
|
|
442
442
|
{
|
|
443
|
-
url: 'https://github.com/
|
|
443
|
+
url: 'https://github.com/initech-medical/initech-platform/issues/9999',
|
|
444
444
|
title:
|
|
445
445
|
'TDPM: story label "story:routine-management" has no matching "regular / routine-management" Story option',
|
|
446
446
|
number: '9999',
|
|
@@ -471,9 +471,9 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
|
|
|
471
471
|
nextActionDate: null,
|
|
472
472
|
nextActionHour: null,
|
|
473
473
|
isPr: false,
|
|
474
|
-
org: '
|
|
475
|
-
repo: '
|
|
476
|
-
url: 'https://github.com/
|
|
474
|
+
org: 'initech-medical',
|
|
475
|
+
repo: 'initech-platform',
|
|
476
|
+
url: 'https://github.com/initech-medical/initech-platform/issues/1500',
|
|
477
477
|
};
|
|
478
478
|
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
479
479
|
|
|
@@ -503,9 +503,9 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
|
|
|
503
503
|
nextActionDate: null,
|
|
504
504
|
nextActionHour: null,
|
|
505
505
|
isPr: false,
|
|
506
|
-
org: '
|
|
507
|
-
repo: '
|
|
508
|
-
url: 'https://github.com/
|
|
506
|
+
org: 'initech-medical',
|
|
507
|
+
repo: 'initech-platform',
|
|
508
|
+
url: 'https://github.com/initech-medical/initech-platform/issues/1445',
|
|
509
509
|
};
|
|
510
510
|
const matchedIssue: Issue = {
|
|
511
511
|
...mock<Issue>(),
|
|
@@ -367,22 +367,31 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
367
367
|
expect(payload[1].color).toBe(REQUIRED_WORKFLOW_STATUSES[1].color);
|
|
368
368
|
});
|
|
369
369
|
|
|
370
|
-
it('should
|
|
370
|
+
it('should reuse the default template "Todo" option ID for Unread so a newly added item lands in Unread', async () => {
|
|
371
371
|
const mockProjectRepository =
|
|
372
372
|
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
373
373
|
const mockIssueRepository =
|
|
374
374
|
mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
|
|
375
|
-
const statuses: FieldOption[] =
|
|
376
|
-
|
|
377
|
-
id:
|
|
378
|
-
name:
|
|
379
|
-
|
|
380
|
-
? LEGACY_TODO_STATUS_NAME
|
|
381
|
-
: required.name,
|
|
382
|
-
color: required.color,
|
|
375
|
+
const statuses: FieldOption[] = [
|
|
376
|
+
{
|
|
377
|
+
id: 'template-todo',
|
|
378
|
+
name: LEGACY_TODO_STATUS_NAME,
|
|
379
|
+
color: 'PINK',
|
|
383
380
|
description: '',
|
|
384
|
-
}
|
|
385
|
-
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: 'template-in-progress',
|
|
384
|
+
name: 'In progress',
|
|
385
|
+
color: 'YELLOW',
|
|
386
|
+
description: 'This is actively being worked on',
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: 'template-done',
|
|
390
|
+
name: DONE_STATUS_NAME,
|
|
391
|
+
color: 'PURPLE',
|
|
392
|
+
description: '',
|
|
393
|
+
},
|
|
394
|
+
];
|
|
386
395
|
const project = buildProject(statuses);
|
|
387
396
|
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
388
397
|
mockProjectRepository.updateStatusList.mockResolvedValue([]);
|
|
@@ -400,9 +409,10 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
400
409
|
|
|
401
410
|
expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
|
|
402
411
|
const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
|
|
403
|
-
const
|
|
404
|
-
expect(
|
|
405
|
-
|
|
412
|
+
const unreadEntry = payload.find((s) => s.name === DEFAULT_STATUS_NAME);
|
|
413
|
+
expect(unreadEntry?.id).toBe('template-todo');
|
|
414
|
+
const todoByHumanEntry = payload.find((s) => s.name === TODO_STATUS_NAME);
|
|
415
|
+
expect(todoByHumanEntry?.id).toBeNull();
|
|
406
416
|
expect(payload.some((s) => s.name === LEGACY_TODO_STATUS_NAME)).toBe(false);
|
|
407
417
|
});
|
|
408
418
|
|
|
@@ -2,6 +2,7 @@ import { FieldOption } from '../entities/Project';
|
|
|
2
2
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
3
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
4
4
|
import {
|
|
5
|
+
DEFAULT_STATUS_NAME,
|
|
5
6
|
IN_TMUX_STATUS_NAME,
|
|
6
7
|
LEGACY_AWAITING_TASK_BREAKDOWN_STATUS_NAME,
|
|
7
8
|
LEGACY_IN_TMUX_STATUS_NAME,
|
|
@@ -27,6 +28,7 @@ export class SetupTowerDefenceProjectUseCase {
|
|
|
27
28
|
private static readonly LEGACY_STATUS_NAMES: Readonly<
|
|
28
29
|
Record<string, string>
|
|
29
30
|
> = {
|
|
31
|
+
[DEFAULT_STATUS_NAME]: LEGACY_TODO_STATUS_NAME,
|
|
30
32
|
[TODO_STATUS_NAME]: LEGACY_TODO_STATUS_NAME,
|
|
31
33
|
[IN_TMUX_STATUS_NAME]: LEGACY_IN_TMUX_STATUS_NAME,
|
|
32
34
|
};
|
|
@@ -84,6 +86,7 @@ export class SetupTowerDefenceProjectUseCase {
|
|
|
84
86
|
!SetupTowerDefenceProjectUseCase.MIGRATED_FROM_NAMES.has(status.name),
|
|
85
87
|
);
|
|
86
88
|
|
|
89
|
+
const reusedOptionIds = new Set<FieldOption['id']>();
|
|
87
90
|
const newStatusList: (Omit<FieldOption, 'id'> & {
|
|
88
91
|
id: FieldOption['id'] | null;
|
|
89
92
|
})[] = [
|
|
@@ -93,8 +96,14 @@ export class SetupTowerDefenceProjectUseCase {
|
|
|
93
96
|
const found =
|
|
94
97
|
existing.find((status) => status.name === required.name) ??
|
|
95
98
|
(legacyName !== undefined
|
|
96
|
-
? existing.find(
|
|
99
|
+
? existing.find(
|
|
100
|
+
(status) =>
|
|
101
|
+
status.name === legacyName && !reusedOptionIds.has(status.id),
|
|
102
|
+
)
|
|
97
103
|
: undefined);
|
|
104
|
+
if (found) {
|
|
105
|
+
reusedOptionIds.add(found.id);
|
|
106
|
+
}
|
|
98
107
|
return {
|
|
99
108
|
id: found ? found.id : null,
|
|
100
109
|
name: required.name,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { FieldOption, Project } from '../../entities/Project';
|
|
2
|
+
import { RequiredProjectFieldDefinition } from '../../entities/RequiredProjectField';
|
|
2
3
|
|
|
3
4
|
export interface ProjectRepository {
|
|
5
|
+
listFieldNames: (project: Project) => Promise<string[]>;
|
|
6
|
+
createField: (
|
|
7
|
+
project: Project,
|
|
8
|
+
field: RequiredProjectFieldDefinition,
|
|
9
|
+
) => Promise<void>;
|
|
4
10
|
findProjectIdByUrl: (projectUrl: string) => Promise<Project['id'] | null>;
|
|
5
11
|
getProject: (projectId: Project['id']) => Promise<Project | null>;
|
|
6
12
|
updateStoryList: (
|