github-issue-tower-defence-management 1.185.0 → 1.186.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 +17 -0
- package/README.md +2 -0
- package/bin/adapter/entry-points/cli/index.js +2 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +43 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +7 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/console/webServer.js +3 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +5 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
- package/bin/adapter/repositories/ProcClaudeHandoverSessionRepository.js +40 -3
- package/bin/adapter/repositories/ProcClaudeHandoverSessionRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +36 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +12 -0
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/entities/Project.js +11 -1
- package/bin/domain/entities/Project.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +12 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js +36 -0
- package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js.map +1 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js +19 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +1 -0
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -1
- package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js +36 -0
- package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js.map +1 -0
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +2 -5
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/TokenExhaustionHandoverUseCase.js +9 -1
- package/bin/domain/usecases/TokenExhaustionHandoverUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +14 -10
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +199 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +65 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +26 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +8 -1
- package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +40 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +114 -141
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +84 -3
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +23 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +73 -0
- package/src/adapter/entry-points/console/ui/src/index.css +35 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/console/webServer.ts +7 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
- package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
- package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +1 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +1 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
- package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
- package/src/adapter/repositories/ProcClaudeHandoverSessionRepository.test.ts +48 -0
- package/src/adapter/repositories/ProcClaudeHandoverSessionRepository.ts +55 -2
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +90 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +48 -2
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +107 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +20 -0
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
- package/src/domain/entities/ClaudeHandoverSession.ts +1 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/entities/Project.ts +11 -2
- package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.test.ts +1 -0
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +144 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +16 -0
- package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
- package/src/domain/usecases/IssueNoStatusUpdateUseCase.test.ts +165 -0
- package/src/domain/usecases/IssueNoStatusUpdateUseCase.ts +44 -0
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +120 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +21 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +34 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +1 -0
- package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.test.ts +188 -0
- package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.ts +57 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +44 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +4 -12
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
- package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/TokenExhaustionHandoverUseCase.test.ts +59 -1
- package/src/domain/usecases/TokenExhaustionHandoverUseCase.ts +14 -1
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
- package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
- package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -0
- 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/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
- package/types/adapter/repositories/ProcClaudeHandoverSessionRepository.d.ts +1 -0
- package/types/adapter/repositories/ProcClaudeHandoverSessionRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +4 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/entities/ClaudeHandoverSession.d.ts +1 -0
- package/types/domain/entities/ClaudeHandoverSession.d.ts.map +1 -1
- package/types/domain/entities/Issue.d.ts +1 -0
- package/types/domain/entities/Issue.d.ts.map +1 -1
- package/types/domain/entities/Project.d.ts +2 -1
- package/types/domain/entities/Project.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +6 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts +12 -0
- package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts.map +1 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts +12 -0
- package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts.map +1 -0
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/TokenExhaustionHandoverUseCase.d.ts +1 -0
- package/types/domain/usecases/TokenExhaustionHandoverUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
- package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fireEvent, render, waitFor } from '@testing-library/react';
|
|
2
|
-
import type { ConsoleStoryEntry } from '../../logic/types';
|
|
2
|
+
import type { ConsoleColor, ConsoleStoryEntry } from '../../logic/types';
|
|
3
3
|
import { ConsoleStoryList } from './ConsoleStoryList';
|
|
4
4
|
|
|
5
5
|
const storyEntries: ConsoleStoryEntry[] = [
|
|
@@ -20,20 +20,20 @@ const storyEntries: ConsoleStoryEntry[] = [
|
|
|
20
20
|
];
|
|
21
21
|
|
|
22
22
|
const defaultProps = {
|
|
23
|
+
stories: storyEntries,
|
|
24
|
+
isLoading: false,
|
|
25
|
+
error: null,
|
|
23
26
|
onCreateIssue: () => Promise.resolve(),
|
|
24
27
|
onAddStory: () => Promise.resolve(),
|
|
28
|
+
onSelectColor: () => undefined,
|
|
29
|
+
optimisticColors: {} as Record<string, ConsoleColor>,
|
|
30
|
+
colorChangeInFlight: null as string | null,
|
|
31
|
+
colorErrors: {} as Record<string, string>,
|
|
25
32
|
};
|
|
26
33
|
|
|
27
34
|
describe('ConsoleStoryList', () => {
|
|
28
35
|
it('renders each story name and its open item count', () => {
|
|
29
|
-
const { getByText } = render(
|
|
30
|
-
<ConsoleStoryList
|
|
31
|
-
stories={storyEntries}
|
|
32
|
-
isLoading={false}
|
|
33
|
-
error={null}
|
|
34
|
-
{...defaultProps}
|
|
35
|
-
/>,
|
|
36
|
-
);
|
|
36
|
+
const { getByText } = render(<ConsoleStoryList {...defaultProps} />);
|
|
37
37
|
expect(getByText('TDPM Console port')).toBeInTheDocument();
|
|
38
38
|
expect(getByText('12')).toBeInTheDocument();
|
|
39
39
|
expect(getByText('Move to Okinawa')).toBeInTheDocument();
|
|
@@ -41,26 +41,14 @@ describe('ConsoleStoryList', () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it('renders an Add task button for each story row', () => {
|
|
44
|
-
const { getAllByRole } = render(
|
|
45
|
-
<ConsoleStoryList
|
|
46
|
-
stories={storyEntries}
|
|
47
|
-
isLoading={false}
|
|
48
|
-
error={null}
|
|
49
|
-
{...defaultProps}
|
|
50
|
-
/>,
|
|
51
|
-
);
|
|
44
|
+
const { getAllByRole } = render(<ConsoleStoryList {...defaultProps} />);
|
|
52
45
|
const buttons = getAllByRole('button', { name: 'Add task' });
|
|
53
46
|
expect(buttons).toHaveLength(storyEntries.length);
|
|
54
47
|
});
|
|
55
48
|
|
|
56
49
|
it('shows the create form when Add task is clicked', () => {
|
|
57
50
|
const { getAllByRole, getByPlaceholderText } = render(
|
|
58
|
-
<ConsoleStoryList
|
|
59
|
-
stories={storyEntries}
|
|
60
|
-
isLoading={false}
|
|
61
|
-
error={null}
|
|
62
|
-
{...defaultProps}
|
|
63
|
-
/>,
|
|
51
|
+
<ConsoleStoryList {...defaultProps} />,
|
|
64
52
|
);
|
|
65
53
|
const [firstButton] = getAllByRole('button', { name: 'Add task' });
|
|
66
54
|
fireEvent.click(firstButton);
|
|
@@ -69,12 +57,7 @@ describe('ConsoleStoryList', () => {
|
|
|
69
57
|
|
|
70
58
|
it('hides the form when Add task is clicked again on the same row', () => {
|
|
71
59
|
const { getAllByRole, queryByPlaceholderText } = render(
|
|
72
|
-
<ConsoleStoryList
|
|
73
|
-
stories={storyEntries}
|
|
74
|
-
isLoading={false}
|
|
75
|
-
error={null}
|
|
76
|
-
{...defaultProps}
|
|
77
|
-
/>,
|
|
60
|
+
<ConsoleStoryList {...defaultProps} />,
|
|
78
61
|
);
|
|
79
62
|
const [firstButton] = getAllByRole('button', { name: 'Add task' });
|
|
80
63
|
fireEvent.click(firstButton);
|
|
@@ -85,13 +68,7 @@ describe('ConsoleStoryList', () => {
|
|
|
85
68
|
it('calls onCreateIssue with the correct storyOptionId and title', async () => {
|
|
86
69
|
const onCreateIssue = jest.fn().mockResolvedValue(undefined);
|
|
87
70
|
const { getAllByRole, getByPlaceholderText, getByRole } = render(
|
|
88
|
-
<ConsoleStoryList
|
|
89
|
-
stories={storyEntries}
|
|
90
|
-
isLoading={false}
|
|
91
|
-
error={null}
|
|
92
|
-
onCreateIssue={onCreateIssue}
|
|
93
|
-
onAddStory={() => Promise.resolve()}
|
|
94
|
-
/>,
|
|
71
|
+
<ConsoleStoryList {...defaultProps} onCreateIssue={onCreateIssue} />,
|
|
95
72
|
);
|
|
96
73
|
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
97
74
|
fireEvent.change(getByPlaceholderText('Issue title'), {
|
|
@@ -108,12 +85,7 @@ describe('ConsoleStoryList', () => {
|
|
|
108
85
|
|
|
109
86
|
it('shows a validation error when Create is clicked with an empty title', () => {
|
|
110
87
|
const { getAllByRole, getByRole, getByText } = render(
|
|
111
|
-
<ConsoleStoryList
|
|
112
|
-
stories={storyEntries}
|
|
113
|
-
isLoading={false}
|
|
114
|
-
error={null}
|
|
115
|
-
{...defaultProps}
|
|
116
|
-
/>,
|
|
88
|
+
<ConsoleStoryList {...defaultProps} />,
|
|
117
89
|
);
|
|
118
90
|
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
119
91
|
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
@@ -128,13 +100,7 @@ describe('ConsoleStoryList', () => {
|
|
|
128
100
|
getByRole,
|
|
129
101
|
queryByPlaceholderText,
|
|
130
102
|
} = render(
|
|
131
|
-
<ConsoleStoryList
|
|
132
|
-
stories={storyEntries}
|
|
133
|
-
isLoading={false}
|
|
134
|
-
error={null}
|
|
135
|
-
onCreateIssue={onCreateIssue}
|
|
136
|
-
onAddStory={() => Promise.resolve()}
|
|
137
|
-
/>,
|
|
103
|
+
<ConsoleStoryList {...defaultProps} onCreateIssue={onCreateIssue} />,
|
|
138
104
|
);
|
|
139
105
|
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
140
106
|
fireEvent.change(getByPlaceholderText('Issue title'), {
|
|
@@ -151,13 +117,7 @@ describe('ConsoleStoryList', () => {
|
|
|
151
117
|
.fn()
|
|
152
118
|
.mockRejectedValue(new Error('Network error'));
|
|
153
119
|
const { getAllByRole, getByPlaceholderText, getByRole, getByText } = render(
|
|
154
|
-
<ConsoleStoryList
|
|
155
|
-
stories={storyEntries}
|
|
156
|
-
isLoading={false}
|
|
157
|
-
error={null}
|
|
158
|
-
onCreateIssue={onCreateIssue}
|
|
159
|
-
onAddStory={() => Promise.resolve()}
|
|
160
|
-
/>,
|
|
120
|
+
<ConsoleStoryList {...defaultProps} onCreateIssue={onCreateIssue} />,
|
|
161
121
|
);
|
|
162
122
|
fireEvent.click(getAllByRole('button', { name: 'Add task' })[0]);
|
|
163
123
|
fireEvent.change(getByPlaceholderText('Issue title'), {
|
|
@@ -169,36 +129,21 @@ describe('ConsoleStoryList', () => {
|
|
|
169
129
|
|
|
170
130
|
it('shows a loading message when isLoading is true', () => {
|
|
171
131
|
const { getByText } = render(
|
|
172
|
-
<ConsoleStoryList
|
|
173
|
-
stories={[]}
|
|
174
|
-
isLoading={true}
|
|
175
|
-
error={null}
|
|
176
|
-
{...defaultProps}
|
|
177
|
-
/>,
|
|
132
|
+
<ConsoleStoryList {...defaultProps} stories={[]} isLoading={true} />,
|
|
178
133
|
);
|
|
179
134
|
expect(getByText('Loading stories...')).toBeInTheDocument();
|
|
180
135
|
});
|
|
181
136
|
|
|
182
137
|
it('shows an empty message when there are no stories', () => {
|
|
183
138
|
const { getByText } = render(
|
|
184
|
-
<ConsoleStoryList
|
|
185
|
-
stories={[]}
|
|
186
|
-
isLoading={false}
|
|
187
|
-
error={null}
|
|
188
|
-
{...defaultProps}
|
|
189
|
-
/>,
|
|
139
|
+
<ConsoleStoryList {...defaultProps} stories={[]} />,
|
|
190
140
|
);
|
|
191
141
|
expect(getByText('No active stories')).toBeInTheDocument();
|
|
192
142
|
});
|
|
193
143
|
|
|
194
144
|
it('shows an error message when error is set', () => {
|
|
195
145
|
const { getByRole, getByText } = render(
|
|
196
|
-
<ConsoleStoryList
|
|
197
|
-
stories={[]}
|
|
198
|
-
isLoading={false}
|
|
199
|
-
error="HTTP 503"
|
|
200
|
-
{...defaultProps}
|
|
201
|
-
/>,
|
|
146
|
+
<ConsoleStoryList {...defaultProps} stories={[]} error="HTTP 503" />,
|
|
202
147
|
);
|
|
203
148
|
expect(getByRole('alert')).toBeInTheDocument();
|
|
204
149
|
expect(getByText(/HTTP 503/)).toBeInTheDocument();
|
|
@@ -212,28 +157,14 @@ describe('ConsoleStoryList', () => {
|
|
|
212
157
|
openItemCount: 4,
|
|
213
158
|
} as unknown as ConsoleStoryEntry;
|
|
214
159
|
const { getByText } = render(
|
|
215
|
-
<ConsoleStoryList
|
|
216
|
-
stories={[oldFormatEntry]}
|
|
217
|
-
isLoading={false}
|
|
218
|
-
error={null}
|
|
219
|
-
onCreateIssue={() => Promise.resolve()}
|
|
220
|
-
onAddStory={() => Promise.resolve()}
|
|
221
|
-
/>,
|
|
160
|
+
<ConsoleStoryList {...defaultProps} stories={[oldFormatEntry]} />,
|
|
222
161
|
);
|
|
223
162
|
const nameEl = getByText('TDPM Console port');
|
|
224
163
|
expect(nameEl.tagName).toBe('SPAN');
|
|
225
164
|
});
|
|
226
165
|
|
|
227
166
|
it('renders story name as a span when storyViewUrl is null', () => {
|
|
228
|
-
const { getByText } = render(
|
|
229
|
-
<ConsoleStoryList
|
|
230
|
-
stories={storyEntries}
|
|
231
|
-
isLoading={false}
|
|
232
|
-
error={null}
|
|
233
|
-
onCreateIssue={() => Promise.resolve()}
|
|
234
|
-
onAddStory={() => Promise.resolve()}
|
|
235
|
-
/>,
|
|
236
|
-
);
|
|
167
|
+
const { getByText } = render(<ConsoleStoryList {...defaultProps} />);
|
|
237
168
|
const nameEl = getByText('TDPM Console port');
|
|
238
169
|
expect(nameEl.tagName).toBe('SPAN');
|
|
239
170
|
});
|
|
@@ -247,13 +178,7 @@ describe('ConsoleStoryList', () => {
|
|
|
247
178
|
},
|
|
248
179
|
];
|
|
249
180
|
const { getByText } = render(
|
|
250
|
-
<ConsoleStoryList
|
|
251
|
-
stories={entriesWithUrl}
|
|
252
|
-
isLoading={false}
|
|
253
|
-
error={null}
|
|
254
|
-
onCreateIssue={() => Promise.resolve()}
|
|
255
|
-
onAddStory={() => Promise.resolve()}
|
|
256
|
-
/>,
|
|
181
|
+
<ConsoleStoryList {...defaultProps} stories={entriesWithUrl} />,
|
|
257
182
|
);
|
|
258
183
|
const nameEl = getByText('TDPM Console port');
|
|
259
184
|
expect(nameEl.tagName).toBe('A');
|
|
@@ -266,25 +191,13 @@ describe('ConsoleStoryList', () => {
|
|
|
266
191
|
});
|
|
267
192
|
|
|
268
193
|
it('renders an Add story button', () => {
|
|
269
|
-
const { getByRole } = render(
|
|
270
|
-
<ConsoleStoryList
|
|
271
|
-
stories={storyEntries}
|
|
272
|
-
isLoading={false}
|
|
273
|
-
error={null}
|
|
274
|
-
{...defaultProps}
|
|
275
|
-
/>,
|
|
276
|
-
);
|
|
194
|
+
const { getByRole } = render(<ConsoleStoryList {...defaultProps} />);
|
|
277
195
|
expect(getByRole('button', { name: 'Add story' })).toBeInTheDocument();
|
|
278
196
|
});
|
|
279
197
|
|
|
280
198
|
it('shows the Add story form when Add story button is clicked', () => {
|
|
281
199
|
const { getByRole, getByPlaceholderText } = render(
|
|
282
|
-
<ConsoleStoryList
|
|
283
|
-
stories={storyEntries}
|
|
284
|
-
isLoading={false}
|
|
285
|
-
error={null}
|
|
286
|
-
{...defaultProps}
|
|
287
|
-
/>,
|
|
200
|
+
<ConsoleStoryList {...defaultProps} />,
|
|
288
201
|
);
|
|
289
202
|
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
290
203
|
expect(getByPlaceholderText('Story name')).toBeInTheDocument();
|
|
@@ -292,12 +205,7 @@ describe('ConsoleStoryList', () => {
|
|
|
292
205
|
|
|
293
206
|
it('hides the Add story form when Add story button is clicked again', () => {
|
|
294
207
|
const { getByRole, queryByPlaceholderText } = render(
|
|
295
|
-
<ConsoleStoryList
|
|
296
|
-
stories={storyEntries}
|
|
297
|
-
isLoading={false}
|
|
298
|
-
error={null}
|
|
299
|
-
{...defaultProps}
|
|
300
|
-
/>,
|
|
208
|
+
<ConsoleStoryList {...defaultProps} />,
|
|
301
209
|
);
|
|
302
210
|
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
303
211
|
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
@@ -307,13 +215,7 @@ describe('ConsoleStoryList', () => {
|
|
|
307
215
|
it('calls onAddStory with the story name and closes the form on success', async () => {
|
|
308
216
|
const onAddStory = jest.fn().mockResolvedValue(undefined);
|
|
309
217
|
const { getByRole, getByPlaceholderText, queryByPlaceholderText } = render(
|
|
310
|
-
<ConsoleStoryList
|
|
311
|
-
stories={storyEntries}
|
|
312
|
-
isLoading={false}
|
|
313
|
-
error={null}
|
|
314
|
-
onCreateIssue={() => Promise.resolve()}
|
|
315
|
-
onAddStory={onAddStory}
|
|
316
|
-
/>,
|
|
218
|
+
<ConsoleStoryList {...defaultProps} onAddStory={onAddStory} />,
|
|
317
219
|
);
|
|
318
220
|
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
319
221
|
fireEvent.change(getByPlaceholderText('Story name'), {
|
|
@@ -330,12 +232,7 @@ describe('ConsoleStoryList', () => {
|
|
|
330
232
|
|
|
331
233
|
it('shows a validation error when Add story Create is clicked with an empty name', () => {
|
|
332
234
|
const { getByRole, getByText } = render(
|
|
333
|
-
<ConsoleStoryList
|
|
334
|
-
stories={storyEntries}
|
|
335
|
-
isLoading={false}
|
|
336
|
-
error={null}
|
|
337
|
-
{...defaultProps}
|
|
338
|
-
/>,
|
|
235
|
+
<ConsoleStoryList {...defaultProps} />,
|
|
339
236
|
);
|
|
340
237
|
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
341
238
|
fireEvent.click(getByRole('button', { name: 'Create' }));
|
|
@@ -345,13 +242,7 @@ describe('ConsoleStoryList', () => {
|
|
|
345
242
|
it('shows an API error when onAddStory rejects', async () => {
|
|
346
243
|
const onAddStory = jest.fn().mockRejectedValue(new Error('API failure'));
|
|
347
244
|
const { getByRole, getByPlaceholderText, getByText } = render(
|
|
348
|
-
<ConsoleStoryList
|
|
349
|
-
stories={storyEntries}
|
|
350
|
-
isLoading={false}
|
|
351
|
-
error={null}
|
|
352
|
-
onCreateIssue={() => Promise.resolve()}
|
|
353
|
-
onAddStory={onAddStory}
|
|
354
|
-
/>,
|
|
245
|
+
<ConsoleStoryList {...defaultProps} onAddStory={onAddStory} />,
|
|
355
246
|
);
|
|
356
247
|
fireEvent.click(getByRole('button', { name: 'Add story' }));
|
|
357
248
|
fireEvent.change(getByPlaceholderText('Story name'), {
|
|
@@ -363,13 +254,95 @@ describe('ConsoleStoryList', () => {
|
|
|
363
254
|
|
|
364
255
|
it('renders Add story button even when there are no stories', () => {
|
|
365
256
|
const { getByRole } = render(
|
|
257
|
+
<ConsoleStoryList {...defaultProps} stories={[]} />,
|
|
258
|
+
);
|
|
259
|
+
expect(getByRole('button', { name: 'Add story' })).toBeInTheDocument();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('renders a Change color button for each story row', () => {
|
|
263
|
+
const { getAllByRole } = render(<ConsoleStoryList {...defaultProps} />);
|
|
264
|
+
const buttons = getAllByRole('button', { name: 'Change color' });
|
|
265
|
+
expect(buttons).toHaveLength(storyEntries.length);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('shows the color palette when Change color is clicked', () => {
|
|
269
|
+
const { getAllByRole } = render(<ConsoleStoryList {...defaultProps} />);
|
|
270
|
+
const [firstColorButton] = getAllByRole('button', { name: 'Change color' });
|
|
271
|
+
fireEvent.click(firstColorButton);
|
|
272
|
+
expect(getAllByRole('button', { name: /GRAY \(disable\)/ })).toHaveLength(
|
|
273
|
+
1,
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('hides the palette when Change color is clicked again on the same row', () => {
|
|
278
|
+
const { getAllByRole, queryByRole } = render(
|
|
279
|
+
<ConsoleStoryList {...defaultProps} />,
|
|
280
|
+
);
|
|
281
|
+
const [firstColorButton] = getAllByRole('button', { name: 'Change color' });
|
|
282
|
+
fireEvent.click(firstColorButton);
|
|
283
|
+
fireEvent.click(firstColorButton);
|
|
284
|
+
expect(queryByRole('button', { name: /GRAY \(disable\)/ })).toBeNull();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('calls onSelectColor with the correct storyOptionId and color when a swatch is clicked', () => {
|
|
288
|
+
const onSelectColor = jest.fn();
|
|
289
|
+
const { getAllByRole } = render(
|
|
290
|
+
<ConsoleStoryList {...defaultProps} onSelectColor={onSelectColor} />,
|
|
291
|
+
);
|
|
292
|
+
const [firstColorButton] = getAllByRole('button', { name: 'Change color' });
|
|
293
|
+
fireEvent.click(firstColorButton);
|
|
294
|
+
const greenSwatch = getAllByRole('button', { name: 'GREEN' })[0];
|
|
295
|
+
fireEvent.click(greenSwatch);
|
|
296
|
+
expect(onSelectColor).toHaveBeenCalledWith('1491051e', 'GREEN');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('closes the palette after a swatch is clicked', () => {
|
|
300
|
+
const { getAllByRole, queryByRole } = render(
|
|
301
|
+
<ConsoleStoryList {...defaultProps} />,
|
|
302
|
+
);
|
|
303
|
+
const [firstColorButton] = getAllByRole('button', { name: 'Change color' });
|
|
304
|
+
fireEvent.click(firstColorButton);
|
|
305
|
+
const greenSwatch = getAllByRole('button', { name: 'GREEN' })[0];
|
|
306
|
+
fireEvent.click(greenSwatch);
|
|
307
|
+
expect(queryByRole('button', { name: /GRAY \(disable\)/ })).toBeNull();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('GRAY swatch carries an accessible disable label', () => {
|
|
311
|
+
const { getAllByRole, getByRole } = render(
|
|
312
|
+
<ConsoleStoryList {...defaultProps} />,
|
|
313
|
+
);
|
|
314
|
+
const [firstColorButton] = getAllByRole('button', { name: 'Change color' });
|
|
315
|
+
fireEvent.click(firstColorButton);
|
|
316
|
+
expect(getByRole('button', { name: 'GRAY (disable)' })).toBeInTheDocument();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('disables Change color button when colorChangeInFlight matches that storyOptionId', () => {
|
|
320
|
+
const { getAllByRole } = render(
|
|
321
|
+
<ConsoleStoryList {...defaultProps} colorChangeInFlight="1491051e" />,
|
|
322
|
+
);
|
|
323
|
+
const colorButtons = getAllByRole('button', { name: 'Change color' });
|
|
324
|
+
expect(colorButtons[0]).toBeDisabled();
|
|
325
|
+
expect(colorButtons[1]).not.toBeDisabled();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('displays an optimistic color from optimisticColors prop', () => {
|
|
329
|
+
const { getAllByRole } = render(
|
|
366
330
|
<ConsoleStoryList
|
|
367
|
-
stories={[]}
|
|
368
|
-
isLoading={false}
|
|
369
|
-
error={null}
|
|
370
331
|
{...defaultProps}
|
|
332
|
+
optimisticColors={{ '1491051e': 'RED' }}
|
|
371
333
|
/>,
|
|
372
334
|
);
|
|
373
|
-
|
|
335
|
+
const colorButtons = getAllByRole('button', { name: 'Change color' });
|
|
336
|
+
expect(colorButtons[0]).toBeInTheDocument();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('shows a per-row error message from colorErrors prop', () => {
|
|
340
|
+
const { getByText } = render(
|
|
341
|
+
<ConsoleStoryList
|
|
342
|
+
{...defaultProps}
|
|
343
|
+
colorErrors={{ '1491051e': 'Color update failed' }}
|
|
344
|
+
/>,
|
|
345
|
+
);
|
|
346
|
+
expect(getByText('Color update failed')).toBeInTheDocument();
|
|
374
347
|
});
|
|
375
348
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { colorFromEnum } from '../../logic/colors';
|
|
3
|
-
import type { ConsoleStoryEntry } from '../../logic/types';
|
|
2
|
+
import { CONSOLE_COLOR_PALETTE, colorFromEnum } from '../../logic/colors';
|
|
3
|
+
import type { ConsoleColor, ConsoleStoryEntry } from '../../logic/types';
|
|
4
|
+
|
|
5
|
+
const ALL_COLORS = Object.keys(CONSOLE_COLOR_PALETTE) as ConsoleColor[];
|
|
4
6
|
|
|
5
7
|
type InlineInputFormProps = {
|
|
6
8
|
placeholder: string;
|
|
@@ -97,12 +99,44 @@ const StoryCreateForm = ({
|
|
|
97
99
|
/>
|
|
98
100
|
);
|
|
99
101
|
|
|
102
|
+
type ColorPaletteProps = {
|
|
103
|
+
onSelectColor: (color: ConsoleColor) => void;
|
|
104
|
+
disabled: boolean;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const ColorPalette = ({ onSelectColor, disabled }: ColorPaletteProps) => (
|
|
108
|
+
<div className="console-story-color-palette">
|
|
109
|
+
{ALL_COLORS.map((color) => {
|
|
110
|
+
const palette = CONSOLE_COLOR_PALETTE[color];
|
|
111
|
+
return (
|
|
112
|
+
<button
|
|
113
|
+
key={color}
|
|
114
|
+
type="button"
|
|
115
|
+
className="console-story-color-swatch"
|
|
116
|
+
aria-label={color === 'GRAY' ? `${color} (disable)` : color}
|
|
117
|
+
style={{ backgroundColor: palette.dot }}
|
|
118
|
+
onClick={() => onSelectColor(color)}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
>
|
|
121
|
+
{color === 'GRAY' && (
|
|
122
|
+
<span className="console-story-color-swatch-label">disable</span>
|
|
123
|
+
)}
|
|
124
|
+
</button>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
|
|
100
130
|
export type ConsoleStoryListProps = {
|
|
101
131
|
stories: ConsoleStoryEntry[];
|
|
102
132
|
isLoading: boolean;
|
|
103
133
|
error: string | null;
|
|
104
134
|
onCreateIssue: (storyOptionId: string, title: string) => Promise<void>;
|
|
105
135
|
onAddStory: (storyName: string) => Promise<void>;
|
|
136
|
+
onSelectColor: (storyOptionId: string, newColor: ConsoleColor) => void;
|
|
137
|
+
optimisticColors: Record<string, ConsoleColor>;
|
|
138
|
+
colorChangeInFlight: string | null;
|
|
139
|
+
colorErrors: Record<string, string>;
|
|
106
140
|
};
|
|
107
141
|
|
|
108
142
|
export const ConsoleStoryList = ({
|
|
@@ -111,9 +145,16 @@ export const ConsoleStoryList = ({
|
|
|
111
145
|
error,
|
|
112
146
|
onCreateIssue,
|
|
113
147
|
onAddStory,
|
|
148
|
+
onSelectColor,
|
|
149
|
+
optimisticColors,
|
|
150
|
+
colorChangeInFlight,
|
|
151
|
+
colorErrors,
|
|
114
152
|
}: ConsoleStoryListProps) => {
|
|
115
153
|
const [expandedOptionId, setExpandedOptionId] = useState<string | null>(null);
|
|
116
154
|
const [addStoryExpanded, setAddStoryExpanded] = useState(false);
|
|
155
|
+
const [colorPickerOptionId, setColorPickerOptionId] = useState<string | null>(
|
|
156
|
+
null,
|
|
157
|
+
);
|
|
117
158
|
|
|
118
159
|
if (error !== null) {
|
|
119
160
|
return (
|
|
@@ -141,6 +182,20 @@ export const ConsoleStoryList = ({
|
|
|
141
182
|
setExpandedOptionId(null);
|
|
142
183
|
};
|
|
143
184
|
|
|
185
|
+
const handleColorButtonClick = (storyOptionId: string): void => {
|
|
186
|
+
setColorPickerOptionId(
|
|
187
|
+
colorPickerOptionId === storyOptionId ? null : storyOptionId,
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleSwatchClick = (
|
|
192
|
+
storyOptionId: string,
|
|
193
|
+
newColor: ConsoleColor,
|
|
194
|
+
): void => {
|
|
195
|
+
setColorPickerOptionId(null);
|
|
196
|
+
onSelectColor(storyOptionId, newColor);
|
|
197
|
+
};
|
|
198
|
+
|
|
144
199
|
return (
|
|
145
200
|
<div className="console-story-list-container">
|
|
146
201
|
{stories.length === 0 ? (
|
|
@@ -148,8 +203,13 @@ export const ConsoleStoryList = ({
|
|
|
148
203
|
) : (
|
|
149
204
|
<ul className="console-story-list">
|
|
150
205
|
{stories.map((entry) => {
|
|
151
|
-
const
|
|
206
|
+
const displayColor: ConsoleColor =
|
|
207
|
+
optimisticColors[entry.storyOptionId] ?? entry.color;
|
|
208
|
+
const palette = colorFromEnum(displayColor);
|
|
152
209
|
const isExpanded = expandedOptionId === entry.storyOptionId;
|
|
210
|
+
const isPickerOpen = colorPickerOptionId === entry.storyOptionId;
|
|
211
|
+
const isInFlight = colorChangeInFlight === entry.storyOptionId;
|
|
212
|
+
const colorError = colorErrors[entry.storyOptionId] ?? null;
|
|
153
213
|
return (
|
|
154
214
|
<li key={entry.storyOptionId} className="console-story-list-row">
|
|
155
215
|
<div className="console-story-list-row-main">
|
|
@@ -189,7 +249,28 @@ export const ConsoleStoryList = ({
|
|
|
189
249
|
>
|
|
190
250
|
Add task
|
|
191
251
|
</button>
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
className="console-op-button"
|
|
255
|
+
onClick={() => handleColorButtonClick(entry.storyOptionId)}
|
|
256
|
+
disabled={isInFlight}
|
|
257
|
+
>
|
|
258
|
+
Change color
|
|
259
|
+
</button>
|
|
192
260
|
</div>
|
|
261
|
+
{isPickerOpen && (
|
|
262
|
+
<ColorPalette
|
|
263
|
+
onSelectColor={(newColor) =>
|
|
264
|
+
handleSwatchClick(entry.storyOptionId, newColor)
|
|
265
|
+
}
|
|
266
|
+
disabled={isInFlight}
|
|
267
|
+
/>
|
|
268
|
+
)}
|
|
269
|
+
{colorError !== null && (
|
|
270
|
+
<p role="alert" className="console-list-error">
|
|
271
|
+
{colorError}
|
|
272
|
+
</p>
|
|
273
|
+
)}
|
|
193
274
|
{isExpanded && (
|
|
194
275
|
<StoryCreateForm
|
|
195
276
|
storyOptionId={entry.storyOptionId}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ConsoleChangedFile,
|
|
3
|
+
ConsoleColor,
|
|
3
4
|
ConsoleComment,
|
|
4
5
|
ConsoleCommit,
|
|
5
6
|
ConsoleIssueState,
|
|
@@ -418,6 +419,28 @@ export type ConsoleCreateIssueRequest = {
|
|
|
418
419
|
nameWithOwner: string;
|
|
419
420
|
};
|
|
420
421
|
|
|
422
|
+
export const STORY_COLOR_OPERATION_PATH = '/api/storycolor';
|
|
423
|
+
|
|
424
|
+
export type ConsoleStoryColorRequest = {
|
|
425
|
+
pjcode: string;
|
|
426
|
+
storyOptionId: string;
|
|
427
|
+
newColor: ConsoleColor;
|
|
428
|
+
nameWithOwner: string;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
export const postConsoleStoryColor = async (
|
|
432
|
+
request: ConsoleStoryColorRequest,
|
|
433
|
+
): Promise<void> => {
|
|
434
|
+
const response = await fetch(STORY_COLOR_OPERATION_PATH, {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
headers: { 'Content-Type': 'application/json' },
|
|
437
|
+
body: JSON.stringify(request),
|
|
438
|
+
});
|
|
439
|
+
if (!response.ok) {
|
|
440
|
+
throw new Error(await readOperationErrorReason(response));
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
421
444
|
export const postConsoleCreateIssue = async (
|
|
422
445
|
request: ConsoleCreateIssueRequest,
|
|
423
446
|
): Promise<string> => {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
postConsoleAddStory,
|
|
25
25
|
postConsoleCreateIssue,
|
|
26
26
|
postConsoleReorderStory,
|
|
27
|
+
postConsoleStoryColor,
|
|
27
28
|
} from '../lib/consoleApi';
|
|
28
29
|
import { navigateAssign, navigateReplace } from '../lib/navigation';
|
|
29
30
|
import {
|
|
@@ -48,6 +49,7 @@ import type { ConsoleSwipeDirection } from '../logic/swipe';
|
|
|
48
49
|
import { findNextNonEmptyTabToRight } from '../logic/tabAdvance';
|
|
49
50
|
import { findNextPjcodeWithMinutes } from '../logic/timerSettings';
|
|
50
51
|
import type {
|
|
52
|
+
ConsoleColor,
|
|
51
53
|
ConsoleListItem,
|
|
52
54
|
ConsoleOverlayStatus,
|
|
53
55
|
ConsoleTabName,
|
|
@@ -386,6 +388,73 @@ export const ConsolePage = () => {
|
|
|
386
388
|
[pjcode],
|
|
387
389
|
);
|
|
388
390
|
|
|
391
|
+
const [storyOptimisticColors, setStoryOptimisticColors] = useState<
|
|
392
|
+
Record<string, ConsoleColor>
|
|
393
|
+
>({});
|
|
394
|
+
const [storyColorChangeInFlight, setStoryColorChangeInFlight] = useState<
|
|
395
|
+
string | null
|
|
396
|
+
>(null);
|
|
397
|
+
const [storyColorErrors, setStoryColorErrors] = useState<
|
|
398
|
+
Record<string, string>
|
|
399
|
+
>({});
|
|
400
|
+
|
|
401
|
+
const handleSelectColor = useCallback(
|
|
402
|
+
(storyOptionId: string, newColor: ConsoleColor): void => {
|
|
403
|
+
const originalEntry = storyEntries.find(
|
|
404
|
+
(e) => e.storyOptionId === storyOptionId,
|
|
405
|
+
);
|
|
406
|
+
const originalColor = originalEntry?.color;
|
|
407
|
+
setStoryColorChangeInFlight(storyOptionId);
|
|
408
|
+
setStoryOptimisticColors((prev) => ({
|
|
409
|
+
...prev,
|
|
410
|
+
[storyOptionId]: newColor,
|
|
411
|
+
}));
|
|
412
|
+
setStoryColorErrors((prev) => {
|
|
413
|
+
const next = { ...prev };
|
|
414
|
+
delete next[storyOptionId];
|
|
415
|
+
return next;
|
|
416
|
+
});
|
|
417
|
+
if (pjcode === null || defaultNameWithOwner === null) {
|
|
418
|
+
setStoryOptimisticColors((prev) =>
|
|
419
|
+
originalColor !== undefined
|
|
420
|
+
? { ...prev, [storyOptionId]: originalColor }
|
|
421
|
+
: prev,
|
|
422
|
+
);
|
|
423
|
+
setStoryColorErrors((prev) => ({
|
|
424
|
+
...prev,
|
|
425
|
+
[storyOptionId]: 'No project or repository configured.',
|
|
426
|
+
}));
|
|
427
|
+
setStoryColorChangeInFlight((current) =>
|
|
428
|
+
current === storyOptionId ? null : current,
|
|
429
|
+
);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
postConsoleStoryColor({
|
|
433
|
+
pjcode,
|
|
434
|
+
storyOptionId,
|
|
435
|
+
newColor,
|
|
436
|
+
nameWithOwner: defaultNameWithOwner,
|
|
437
|
+
})
|
|
438
|
+
.catch((err: unknown) => {
|
|
439
|
+
setStoryOptimisticColors((prev) =>
|
|
440
|
+
originalColor !== undefined
|
|
441
|
+
? { ...prev, [storyOptionId]: originalColor }
|
|
442
|
+
: prev,
|
|
443
|
+
);
|
|
444
|
+
setStoryColorErrors((prev) => ({
|
|
445
|
+
...prev,
|
|
446
|
+
[storyOptionId]: err instanceof Error ? err.message : String(err),
|
|
447
|
+
}));
|
|
448
|
+
})
|
|
449
|
+
.finally(() => {
|
|
450
|
+
setStoryColorChangeInFlight((current) =>
|
|
451
|
+
current === storyOptionId ? null : current,
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
},
|
|
455
|
+
[pjcode, defaultNameWithOwner, storyEntries],
|
|
456
|
+
);
|
|
457
|
+
|
|
389
458
|
return (
|
|
390
459
|
<main className="console-app">
|
|
391
460
|
{actionQueue.pending !== null && (
|
|
@@ -433,6 +502,10 @@ export const ConsolePage = () => {
|
|
|
433
502
|
error={error}
|
|
434
503
|
onCreateIssue={handleCreateIssue}
|
|
435
504
|
onAddStory={handleStoryAdd}
|
|
505
|
+
onSelectColor={handleSelectColor}
|
|
506
|
+
optimisticColors={storyOptimisticColors}
|
|
507
|
+
colorChangeInFlight={storyColorChangeInFlight}
|
|
508
|
+
colorErrors={storyColorErrors}
|
|
436
509
|
/>
|
|
437
510
|
) : selectedItem === null ? (
|
|
438
511
|
<>
|