github-issue-tower-defence-management 1.165.0 → 1.166.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 +14 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js +44 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleTabNames.js +1 -0
- package/bin/adapter/entry-points/console/consoleTabNames.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CrSqMAVR.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/webServer.js +2 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/repositories/GraphqlProjectRepository.js +3 -3
- package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
- package/bin/domain/entities/RequiredProjectField.js +0 -5
- package/bin/domain/entities/RequiredProjectField.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +12 -4
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js +13 -1
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +23 -0
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +211 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +63 -0
- package/src/adapter/entry-points/console/consoleTabNames.ts +1 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +61 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +63 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +77 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +196 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +163 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +15 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +29 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/tabAdvance.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +10 -1
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +43 -8
- package/src/adapter/entry-points/console/ui-dist/assets/index-CrSqMAVR.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.ts +3 -0
- package/src/adapter/repositories/GraphqlProjectRepository.ts +3 -3
- package/src/domain/entities/RequiredProjectField.ts +0 -5
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +14 -4
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +15 -5
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.test.ts +16 -5
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts +13 -1
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +86 -1
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +49 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleTabNames.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/domain/entities/RequiredProjectField.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts +18 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CHUtr_4X.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-CHUtr_4X.js +0 -86
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { fireEvent, render, waitFor } from '@testing-library/react';
|
|
2
|
+
import type { ConsoleStoryEntry } from '../../logic/types';
|
|
3
|
+
import { ConsoleStoryList } from './ConsoleStoryList';
|
|
4
|
+
|
|
5
|
+
const storyEntries: ConsoleStoryEntry[] = [
|
|
6
|
+
{
|
|
7
|
+
storyName: 'TDPM Console port',
|
|
8
|
+
storyOptionId: '1491051e',
|
|
9
|
+
color: 'BLUE',
|
|
10
|
+
openItemCount: 12,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
storyName: 'Move to Okinawa',
|
|
14
|
+
storyOptionId: '564803ee',
|
|
15
|
+
color: 'PURPLE',
|
|
16
|
+
openItemCount: 0,
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe('ConsoleStoryList', () => {
|
|
21
|
+
it('renders each story name and its open item count', () => {
|
|
22
|
+
const { getByText } = render(
|
|
23
|
+
<ConsoleStoryList
|
|
24
|
+
stories={storyEntries}
|
|
25
|
+
isLoading={false}
|
|
26
|
+
error={null}
|
|
27
|
+
onCreateIssue={() => Promise.resolve()}
|
|
28
|
+
/>,
|
|
29
|
+
);
|
|
30
|
+
expect(getByText('TDPM Console port')).toBeInTheDocument();
|
|
31
|
+
expect(getByText('12')).toBeInTheDocument();
|
|
32
|
+
expect(getByText('Move to Okinawa')).toBeInTheDocument();
|
|
33
|
+
expect(getByText('0')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders an Add task button for each story row', () => {
|
|
37
|
+
const { getAllByRole } = render(
|
|
38
|
+
<ConsoleStoryList
|
|
39
|
+
stories={storyEntries}
|
|
40
|
+
isLoading={false}
|
|
41
|
+
error={null}
|
|
42
|
+
onCreateIssue={() => Promise.resolve()}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
const buttons = getAllByRole('button', { name: 'Add task' });
|
|
46
|
+
expect(buttons).toHaveLength(storyEntries.length);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('shows the create form when Add task is clicked', () => {
|
|
50
|
+
const { getAllByRole, getByPlaceholderText } = render(
|
|
51
|
+
<ConsoleStoryList
|
|
52
|
+
stories={storyEntries}
|
|
53
|
+
isLoading={false}
|
|
54
|
+
error={null}
|
|
55
|
+
onCreateIssue={() => Promise.resolve()}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
const [firstButton] = getAllByRole('button', { name: 'Add task' });
|
|
59
|
+
fireEvent.click(firstButton);
|
|
60
|
+
expect(getByPlaceholderText('Issue title')).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('hides the form when Add task is clicked again on the same row', () => {
|
|
64
|
+
const { getAllByRole, queryByPlaceholderText } = render(
|
|
65
|
+
<ConsoleStoryList
|
|
66
|
+
stories={storyEntries}
|
|
67
|
+
isLoading={false}
|
|
68
|
+
error={null}
|
|
69
|
+
onCreateIssue={() => Promise.resolve()}
|
|
70
|
+
/>,
|
|
71
|
+
);
|
|
72
|
+
const [firstButton] = getAllByRole('button', { name: 'Add task' });
|
|
73
|
+
fireEvent.click(firstButton);
|
|
74
|
+
fireEvent.click(firstButton);
|
|
75
|
+
expect(queryByPlaceholderText('Issue title')).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('calls onCreateIssue with the correct storyOptionId and title', async () => {
|
|
79
|
+
const onCreateIssue = jest.fn().mockResolvedValue(undefined);
|
|
80
|
+
const { getAllByRole, getByPlaceholderText, getByRole } = render(
|
|
81
|
+
<ConsoleStoryList
|
|
82
|
+
stories={storyEntries}
|
|
83
|
+
isLoading={false}
|
|
84
|
+
error={null}
|
|
85
|
+
onCreateIssue={onCreateIssue}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
89
|
+
fireEvent.change(getByPlaceholderText('Issue title'), {
|
|
90
|
+
target: { value: 'New feature task' },
|
|
91
|
+
});
|
|
92
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
93
|
+
await waitFor(() =>
|
|
94
|
+
expect(onCreateIssue).toHaveBeenCalledWith(
|
|
95
|
+
'1491051e',
|
|
96
|
+
'New feature task',
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('shows a validation error when Create is clicked with an empty title', () => {
|
|
102
|
+
const { getAllByRole, getByRole, getByText } = render(
|
|
103
|
+
<ConsoleStoryList
|
|
104
|
+
stories={storyEntries}
|
|
105
|
+
isLoading={false}
|
|
106
|
+
error={null}
|
|
107
|
+
onCreateIssue={() => Promise.resolve()}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
111
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
112
|
+
expect(getByText('Title is required')).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('closes the form and clears the input after a successful create', async () => {
|
|
116
|
+
const onCreateIssue = jest.fn().mockResolvedValue(undefined);
|
|
117
|
+
const {
|
|
118
|
+
getAllByRole,
|
|
119
|
+
getByPlaceholderText,
|
|
120
|
+
getByRole,
|
|
121
|
+
queryByPlaceholderText,
|
|
122
|
+
} = render(
|
|
123
|
+
<ConsoleStoryList
|
|
124
|
+
stories={storyEntries}
|
|
125
|
+
isLoading={false}
|
|
126
|
+
error={null}
|
|
127
|
+
onCreateIssue={onCreateIssue}
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
131
|
+
fireEvent.change(getByPlaceholderText('Issue title'), {
|
|
132
|
+
target: { value: 'My task' },
|
|
133
|
+
});
|
|
134
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
135
|
+
await waitFor(() =>
|
|
136
|
+
expect(queryByPlaceholderText('Issue title')).toBeNull(),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('shows an API error when onCreateIssue rejects', async () => {
|
|
141
|
+
const onCreateIssue = jest
|
|
142
|
+
.fn()
|
|
143
|
+
.mockRejectedValue(new Error('Network error'));
|
|
144
|
+
const { getAllByRole, getByPlaceholderText, getByRole, getByText } = render(
|
|
145
|
+
<ConsoleStoryList
|
|
146
|
+
stories={storyEntries}
|
|
147
|
+
isLoading={false}
|
|
148
|
+
error={null}
|
|
149
|
+
onCreateIssue={onCreateIssue}
|
|
150
|
+
/>,
|
|
151
|
+
);
|
|
152
|
+
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
153
|
+
fireEvent.change(getByPlaceholderText('Issue title'), {
|
|
154
|
+
target: { value: 'My task' },
|
|
155
|
+
});
|
|
156
|
+
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
157
|
+
await waitFor(() => expect(getByText('Network error')).toBeInTheDocument());
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('shows a loading message when isLoading is true', () => {
|
|
161
|
+
const { getByText } = render(
|
|
162
|
+
<ConsoleStoryList
|
|
163
|
+
stories={[]}
|
|
164
|
+
isLoading={true}
|
|
165
|
+
error={null}
|
|
166
|
+
onCreateIssue={() => Promise.resolve()}
|
|
167
|
+
/>,
|
|
168
|
+
);
|
|
169
|
+
expect(getByText('Loading stories...')).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('shows an empty message when there are no stories', () => {
|
|
173
|
+
const { getByText } = render(
|
|
174
|
+
<ConsoleStoryList
|
|
175
|
+
stories={[]}
|
|
176
|
+
isLoading={false}
|
|
177
|
+
error={null}
|
|
178
|
+
onCreateIssue={() => Promise.resolve()}
|
|
179
|
+
/>,
|
|
180
|
+
);
|
|
181
|
+
expect(getByText('No active stories')).toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('shows an error message when error is set', () => {
|
|
185
|
+
const { getByRole, getByText } = render(
|
|
186
|
+
<ConsoleStoryList
|
|
187
|
+
stories={[]}
|
|
188
|
+
isLoading={false}
|
|
189
|
+
error="HTTP 503"
|
|
190
|
+
onCreateIssue={() => Promise.resolve()}
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
expect(getByRole('alert')).toBeInTheDocument();
|
|
194
|
+
expect(getByText(/HTTP 503/)).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { colorFromEnum } from '../../logic/colors';
|
|
3
|
+
import type { ConsoleStoryEntry } from '../../logic/types';
|
|
4
|
+
|
|
5
|
+
type StoryCreateFormProps = {
|
|
6
|
+
storyOptionId: string;
|
|
7
|
+
onSubmit: (storyOptionId: string, title: string) => Promise<void>;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const StoryCreateForm = ({
|
|
12
|
+
storyOptionId,
|
|
13
|
+
onSubmit,
|
|
14
|
+
onCancel,
|
|
15
|
+
}: StoryCreateFormProps) => {
|
|
16
|
+
const [titleInput, setTitleInput] = useState('');
|
|
17
|
+
const [submitting, setSubmitting] = useState(false);
|
|
18
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
19
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
inputRef.current?.focus();
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const handleSubmit = async (): Promise<void> => {
|
|
26
|
+
const trimmed = titleInput.trim();
|
|
27
|
+
if (trimmed.length === 0) {
|
|
28
|
+
setSubmitError('Title is required');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
setSubmitting(true);
|
|
32
|
+
setSubmitError(null);
|
|
33
|
+
try {
|
|
34
|
+
await onSubmit(storyOptionId, trimmed);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setSubmitError(err instanceof Error ? err.message : String(err));
|
|
37
|
+
} finally {
|
|
38
|
+
setSubmitting(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<form
|
|
44
|
+
className="console-story-create-form"
|
|
45
|
+
onSubmit={(event) => {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
void handleSubmit();
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<input
|
|
51
|
+
ref={inputRef}
|
|
52
|
+
type="text"
|
|
53
|
+
className="console-story-create-input"
|
|
54
|
+
placeholder="Issue title"
|
|
55
|
+
value={titleInput}
|
|
56
|
+
onChange={(e) => setTitleInput(e.target.value)}
|
|
57
|
+
disabled={submitting}
|
|
58
|
+
/>
|
|
59
|
+
<button type="submit" className="console-op-button" disabled={submitting}>
|
|
60
|
+
{submitting ? 'Creating…' : 'Create'}
|
|
61
|
+
</button>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
className="console-op-button"
|
|
65
|
+
onClick={onCancel}
|
|
66
|
+
disabled={submitting}
|
|
67
|
+
>
|
|
68
|
+
Cancel
|
|
69
|
+
</button>
|
|
70
|
+
{submitError !== null && (
|
|
71
|
+
<p role="alert" className="console-list-error">
|
|
72
|
+
{submitError}
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
</form>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type ConsoleStoryListProps = {
|
|
80
|
+
stories: ConsoleStoryEntry[];
|
|
81
|
+
isLoading: boolean;
|
|
82
|
+
error: string | null;
|
|
83
|
+
onCreateIssue: (storyOptionId: string, title: string) => Promise<void>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const ConsoleStoryList = ({
|
|
87
|
+
stories,
|
|
88
|
+
isLoading,
|
|
89
|
+
error,
|
|
90
|
+
onCreateIssue,
|
|
91
|
+
}: ConsoleStoryListProps) => {
|
|
92
|
+
const [expandedOptionId, setExpandedOptionId] = useState<string | null>(null);
|
|
93
|
+
|
|
94
|
+
if (error !== null) {
|
|
95
|
+
return (
|
|
96
|
+
<p role="alert" className="console-list-message console-list-error">
|
|
97
|
+
Failed to load stories: {error}
|
|
98
|
+
</p>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isLoading) {
|
|
103
|
+
return <p className="console-list-message">Loading stories...</p>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (stories.length === 0) {
|
|
107
|
+
return <p className="console-list-empty">No active stories</p>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const handleAddClick = (storyOptionId: string): void => {
|
|
111
|
+
setExpandedOptionId(
|
|
112
|
+
expandedOptionId === storyOptionId ? null : storyOptionId,
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleSubmit = async (
|
|
117
|
+
storyOptionId: string,
|
|
118
|
+
title: string,
|
|
119
|
+
): Promise<void> => {
|
|
120
|
+
await onCreateIssue(storyOptionId, title);
|
|
121
|
+
setExpandedOptionId(null);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<ul className="console-story-list">
|
|
126
|
+
{stories.map((entry) => {
|
|
127
|
+
const palette = colorFromEnum(entry.color);
|
|
128
|
+
const isExpanded = expandedOptionId === entry.storyOptionId;
|
|
129
|
+
return (
|
|
130
|
+
<li key={entry.storyOptionId} className="console-story-list-row">
|
|
131
|
+
<div className="console-story-list-row-main">
|
|
132
|
+
<span
|
|
133
|
+
className="console-storytag"
|
|
134
|
+
style={{ color: palette.fg, borderColor: palette.border }}
|
|
135
|
+
>
|
|
136
|
+
<span
|
|
137
|
+
className="console-story-dot"
|
|
138
|
+
style={{ backgroundColor: palette.dot }}
|
|
139
|
+
/>
|
|
140
|
+
{entry.storyName}
|
|
141
|
+
</span>
|
|
142
|
+
<span className="console-story-count">{entry.openItemCount}</span>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
className="console-op-button"
|
|
146
|
+
onClick={() => handleAddClick(entry.storyOptionId)}
|
|
147
|
+
>
|
|
148
|
+
Add task
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
{isExpanded && (
|
|
152
|
+
<StoryCreateForm
|
|
153
|
+
storyOptionId={entry.storyOptionId}
|
|
154
|
+
onSubmit={handleSubmit}
|
|
155
|
+
onCancel={() => setExpandedOptionId(null)}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
</li>
|
|
159
|
+
);
|
|
160
|
+
})}
|
|
161
|
+
</ul>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ConsoleFieldOption,
|
|
4
4
|
ConsoleListItem,
|
|
5
5
|
ConsoleStoryColorSource,
|
|
6
|
+
ConsoleStoryEntry,
|
|
6
7
|
ConsoleTabName,
|
|
7
8
|
} from '../logic/types';
|
|
8
9
|
import { CONSOLE_TABS } from '../logic/types';
|
|
@@ -13,6 +14,8 @@ export type ConsoleTabSnapshot = {
|
|
|
13
14
|
statusOptions: ConsoleFieldOption[];
|
|
14
15
|
storyOptions: ConsoleFieldOption[];
|
|
15
16
|
storyColors: ConsoleStoryColorSource;
|
|
17
|
+
stories: ConsoleStoryEntry[];
|
|
18
|
+
defaultNameWithOwner: string | null;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export type ConsoleTabDataState = {
|
|
@@ -51,6 +54,13 @@ const parseOptions = (value: unknown): ConsoleFieldOption[] => {
|
|
|
51
54
|
return value.filter(isRecord) as unknown as ConsoleFieldOption[];
|
|
52
55
|
};
|
|
53
56
|
|
|
57
|
+
const parseStoryEntries = (value: unknown): ConsoleStoryEntry[] => {
|
|
58
|
+
if (!Array.isArray(value)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return value.filter(isRecord) as unknown as ConsoleStoryEntry[];
|
|
62
|
+
};
|
|
63
|
+
|
|
54
64
|
const parseSnapshot = (payload: unknown): ConsoleTabSnapshot => ({
|
|
55
65
|
items: parseItems(payload),
|
|
56
66
|
generatedAt:
|
|
@@ -63,6 +73,11 @@ const parseSnapshot = (payload: unknown): ConsoleTabSnapshot => ({
|
|
|
63
73
|
isRecord(payload) && isRecord(payload.storyColors)
|
|
64
74
|
? (payload.storyColors as ConsoleStoryColorSource)
|
|
65
75
|
: {},
|
|
76
|
+
stories: isRecord(payload) ? parseStoryEntries(payload.stories) : [],
|
|
77
|
+
defaultNameWithOwner:
|
|
78
|
+
isRecord(payload) && typeof payload.defaultNameWithOwner === 'string'
|
|
79
|
+
? payload.defaultNameWithOwner
|
|
80
|
+
: null,
|
|
66
81
|
});
|
|
67
82
|
|
|
68
83
|
const emptySnapshots = (): Record<
|
|
@@ -339,3 +339,32 @@ export const postConsoleReviewComment = async (
|
|
|
339
339
|
throw new Error(await readOperationErrorReason(response));
|
|
340
340
|
}
|
|
341
341
|
};
|
|
342
|
+
|
|
343
|
+
export const CREATE_ISSUE_OPERATION_PATH = '/api/createissue';
|
|
344
|
+
|
|
345
|
+
export type ConsoleCreateIssueRequest = {
|
|
346
|
+
pjcode: string;
|
|
347
|
+
title: string;
|
|
348
|
+
storyOptionId: string;
|
|
349
|
+
nameWithOwner: string;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
export const postConsoleCreateIssue = async (
|
|
353
|
+
request: ConsoleCreateIssueRequest,
|
|
354
|
+
): Promise<string> => {
|
|
355
|
+
const response = await fetch(CREATE_ISSUE_OPERATION_PATH, {
|
|
356
|
+
method: 'POST',
|
|
357
|
+
headers: { 'Content-Type': 'application/json' },
|
|
358
|
+
body: JSON.stringify(request),
|
|
359
|
+
});
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
throw new Error(await readOperationErrorReason(response));
|
|
362
|
+
}
|
|
363
|
+
const payload: unknown = await response.json();
|
|
364
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
365
|
+
v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
366
|
+
if (!isRecord(payload) || typeof payload.issueUrl !== 'string') {
|
|
367
|
+
throw new Error('issueUrl was not returned');
|
|
368
|
+
}
|
|
369
|
+
return payload.issueUrl;
|
|
370
|
+
};
|
|
@@ -147,7 +147,15 @@ export type ConsoleTabName =
|
|
|
147
147
|
| 'unread'
|
|
148
148
|
| 'failed-preparation'
|
|
149
149
|
| 'todo-by-human'
|
|
150
|
-
| 'todo-by-agent'
|
|
150
|
+
| 'todo-by-agent'
|
|
151
|
+
| 'stories';
|
|
152
|
+
|
|
153
|
+
export type ConsoleStoryEntry = {
|
|
154
|
+
storyName: string;
|
|
155
|
+
storyOptionId: string;
|
|
156
|
+
color: ConsoleColor;
|
|
157
|
+
openItemCount: number;
|
|
158
|
+
};
|
|
151
159
|
|
|
152
160
|
export type ConsoleTab = {
|
|
153
161
|
name: ConsoleTabName;
|
|
@@ -162,4 +170,5 @@ export const CONSOLE_TABS: ConsoleTab[] = [
|
|
|
162
170
|
{ name: 'failed-preparation', label: 'Failed Preparation' },
|
|
163
171
|
{ name: 'todo-by-human', label: 'Todo by human' },
|
|
164
172
|
{ name: 'todo-by-agent', label: 'Todo by agent' },
|
|
173
|
+
{ name: 'stories', label: 'Stories' },
|
|
165
174
|
];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { ConsoleTabList } from '../components/layout/ConsoleTabList';
|
|
3
3
|
import { ConsoleItemList } from '../components/list/ConsoleItemList';
|
|
4
|
+
import { ConsoleStoryList } from '../components/list/ConsoleStoryList';
|
|
4
5
|
import {
|
|
5
6
|
ConsoleErrorToast,
|
|
6
7
|
ConsoleUndoToast,
|
|
@@ -14,6 +15,7 @@ import { useConsoleOverlay } from '../hooks/useConsoleOverlay';
|
|
|
14
15
|
import { useConsolePjcode } from '../hooks/useConsolePjcode';
|
|
15
16
|
import { useConsoleSwipeNavigation } from '../hooks/useConsoleSwipeNavigation';
|
|
16
17
|
import { useConsoleTabData } from '../hooks/useConsoleTabData';
|
|
18
|
+
import { postConsoleCreateIssue } from '../lib/consoleApi';
|
|
17
19
|
import {
|
|
18
20
|
actionAdvances,
|
|
19
21
|
actionToastColor,
|
|
@@ -67,13 +69,17 @@ export const ConsolePage = () => {
|
|
|
67
69
|
if (snapshot === null) {
|
|
68
70
|
continue;
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
snapshot.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
snapshot.
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
if (tab.name === 'stories') {
|
|
73
|
+
result[tab.name] = snapshot.stories.length;
|
|
74
|
+
} else {
|
|
75
|
+
result[tab.name] = countPendingItems(
|
|
76
|
+
snapshot.items,
|
|
77
|
+
overlayEntriesActedSinceSnapshot(
|
|
78
|
+
overlayState.overlay,
|
|
79
|
+
snapshot.generatedAt,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
77
83
|
}
|
|
78
84
|
return result;
|
|
79
85
|
}, [snapshots, overlayState.overlay]);
|
|
@@ -224,6 +230,28 @@ export const ConsolePage = () => {
|
|
|
224
230
|
|
|
225
231
|
const detailScreenRef = useConsoleSwipeNavigation(handleSwipe);
|
|
226
232
|
|
|
233
|
+
const storiesSnapshot = snapshots.stories;
|
|
234
|
+
const storyEntries = storiesSnapshot?.stories ?? [];
|
|
235
|
+
const defaultNameWithOwner = storiesSnapshot?.defaultNameWithOwner ?? null;
|
|
236
|
+
|
|
237
|
+
const handleCreateIssue = useCallback(
|
|
238
|
+
async (storyOptionId: string, title: string): Promise<void> => {
|
|
239
|
+
if (pjcode === null) {
|
|
240
|
+
throw new Error('No project specified in the URL path.');
|
|
241
|
+
}
|
|
242
|
+
if (defaultNameWithOwner === null) {
|
|
243
|
+
throw new Error('No repository configured for this project.');
|
|
244
|
+
}
|
|
245
|
+
await postConsoleCreateIssue({
|
|
246
|
+
pjcode,
|
|
247
|
+
title,
|
|
248
|
+
storyOptionId,
|
|
249
|
+
nameWithOwner: defaultNameWithOwner,
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
[pjcode, defaultNameWithOwner],
|
|
253
|
+
);
|
|
254
|
+
|
|
227
255
|
return (
|
|
228
256
|
<main className="console-app">
|
|
229
257
|
{actionQueue.pending !== null && (
|
|
@@ -249,7 +277,14 @@ export const ConsolePage = () => {
|
|
|
249
277
|
tabHref={navigation.tabHref}
|
|
250
278
|
onSelectTab={navigation.selectTab}
|
|
251
279
|
/>
|
|
252
|
-
{
|
|
280
|
+
{activeTab === 'stories' ? (
|
|
281
|
+
<ConsoleStoryList
|
|
282
|
+
stories={storyEntries}
|
|
283
|
+
isLoading={isLoading}
|
|
284
|
+
error={error}
|
|
285
|
+
onCreateIssue={handleCreateIssue}
|
|
286
|
+
/>
|
|
287
|
+
) : selectedItem === null ? (
|
|
253
288
|
<ConsoleItemList
|
|
254
289
|
rows={rows}
|
|
255
290
|
storyColors={storyColors}
|