github-issue-tower-defence-management 1.101.6 → 1.101.8
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/consoleServer.js +32 -10
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-ES6SLB1Y.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-_g2CqG11.js +101 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleServer.test.ts +43 -0
- package/src/adapter/entry-points/console/consoleServer.ts +38 -11
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +139 -145
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleErrorToast.stories.tsx +23 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.test.tsx +44 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.test.ts +84 -1
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.ts +31 -6
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +2 -4
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +37 -10
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +19 -1
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +8 -23
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +0 -97
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +10 -40
- package/src/adapter/entry-points/console/ui/src/index.css +6 -16
- package/src/adapter/entry-points/console/ui-dist/assets/index-ES6SLB1Y.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-_g2CqG11.js +101 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-D4zghbcI.css +0 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DX0mapkS.js +0 -101
- package/src/adapter/entry-points/console/ui-dist/assets/index-D4zghbcI.css +0 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-DX0mapkS.js +0 -101
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
export type ConsoleQueuedAction = {
|
|
8
8
|
message: string;
|
|
9
9
|
color: ConsoleToastColor;
|
|
10
|
-
commit: () => void
|
|
10
|
+
commit: () => Promise<void>;
|
|
11
11
|
advance: () => void;
|
|
12
12
|
};
|
|
13
13
|
|
|
@@ -18,6 +18,18 @@ export type ConsolePendingActionView = {
|
|
|
18
18
|
progress: number;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
export type ConsoleActionError = {
|
|
22
|
+
message: string;
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const errorReason = (error: unknown): string => {
|
|
27
|
+
if (error instanceof Error && error.message.length > 0) {
|
|
28
|
+
return error.message;
|
|
29
|
+
}
|
|
30
|
+
return String(error);
|
|
31
|
+
};
|
|
32
|
+
|
|
21
33
|
const COUNTDOWN_TICK_MS = 100;
|
|
22
34
|
|
|
23
35
|
const computeRemainingSeconds = (elapsedMs: number): number =>
|
|
@@ -28,12 +40,15 @@ const computeProgress = (elapsedMs: number): number =>
|
|
|
28
40
|
|
|
29
41
|
export type ConsoleActionQueue = {
|
|
30
42
|
pending: ConsolePendingActionView | null;
|
|
43
|
+
error: ConsoleActionError | null;
|
|
31
44
|
enqueue: (action: ConsoleQueuedAction) => void;
|
|
32
45
|
undo: () => void;
|
|
46
|
+
dismissError: () => void;
|
|
33
47
|
};
|
|
34
48
|
|
|
35
49
|
export const useConsoleActionQueue = (): ConsoleActionQueue => {
|
|
36
50
|
const [pending, setPending] = useState<ConsolePendingActionView | null>(null);
|
|
51
|
+
const [error, setError] = useState<ConsoleActionError | null>(null);
|
|
37
52
|
const actionRef = useRef<ConsoleQueuedAction | null>(null);
|
|
38
53
|
const startRef = useRef<number>(0);
|
|
39
54
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
@@ -46,6 +61,12 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
|
|
|
46
61
|
}
|
|
47
62
|
}, []);
|
|
48
63
|
|
|
64
|
+
const runCommit = useCallback((action: ConsoleQueuedAction): void => {
|
|
65
|
+
action.commit().catch((cause: unknown) => {
|
|
66
|
+
setError({ message: action.message, reason: errorReason(cause) });
|
|
67
|
+
});
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
49
70
|
const commitPending = useCallback((): void => {
|
|
50
71
|
const action = actionRef.current;
|
|
51
72
|
clearTimer();
|
|
@@ -53,9 +74,9 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
|
|
|
53
74
|
setPending(null);
|
|
54
75
|
if (action !== null && !committedRef.current) {
|
|
55
76
|
committedRef.current = true;
|
|
56
|
-
action
|
|
77
|
+
runCommit(action);
|
|
57
78
|
}
|
|
58
|
-
}, [clearTimer]);
|
|
79
|
+
}, [clearTimer, runCommit]);
|
|
59
80
|
|
|
60
81
|
const undo = useCallback((): void => {
|
|
61
82
|
clearTimer();
|
|
@@ -64,13 +85,17 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
|
|
|
64
85
|
setPending(null);
|
|
65
86
|
}, [clearTimer]);
|
|
66
87
|
|
|
88
|
+
const dismissError = useCallback((): void => {
|
|
89
|
+
setError(null);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
67
92
|
const enqueue = useCallback(
|
|
68
93
|
(action: ConsoleQueuedAction): void => {
|
|
69
94
|
if (actionRef.current !== null && !committedRef.current) {
|
|
70
95
|
const previous = actionRef.current;
|
|
71
96
|
clearTimer();
|
|
72
97
|
committedRef.current = true;
|
|
73
|
-
previous
|
|
98
|
+
runCommit(previous);
|
|
74
99
|
}
|
|
75
100
|
committedRef.current = false;
|
|
76
101
|
actionRef.current = action;
|
|
@@ -96,10 +121,10 @@ export const useConsoleActionQueue = (): ConsoleActionQueue => {
|
|
|
96
121
|
});
|
|
97
122
|
}, COUNTDOWN_TICK_MS);
|
|
98
123
|
},
|
|
99
|
-
[clearTimer, commitPending],
|
|
124
|
+
[clearTimer, commitPending, runCommit],
|
|
100
125
|
);
|
|
101
126
|
|
|
102
127
|
useEffect(() => clearTimer, [clearTimer]);
|
|
103
128
|
|
|
104
|
-
return { pending, enqueue, undo };
|
|
129
|
+
return { pending, error, enqueue, undo, dismissError };
|
|
105
130
|
};
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts
CHANGED
|
@@ -133,7 +133,7 @@ describe('useConsoleOperations', () => {
|
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
it('
|
|
136
|
+
it('marks done on snooze outside the todo-by-human tab so the item disappears immediately', async () => {
|
|
137
137
|
captureFetch();
|
|
138
138
|
const { result } = setup();
|
|
139
139
|
await act(async () => {
|
|
@@ -145,7 +145,7 @@ describe('useConsoleOperations', () => {
|
|
|
145
145
|
const stored = JSON.parse(
|
|
146
146
|
localStorage.getItem(overlayStorageKey('umino')) ?? '{}',
|
|
147
147
|
);
|
|
148
|
-
expect(stored[issueItem.projectItemId]
|
|
148
|
+
expect(stored[issueItem.projectItemId].done).toBe(true);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
it('marks done on snooze in the todo-by-human tab so the item is skipped', async () => {
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts
CHANGED
|
@@ -146,11 +146,9 @@ export const useConsoleOperations = (
|
|
|
146
146
|
projectItemId: item.projectItemId,
|
|
147
147
|
};
|
|
148
148
|
await postConsoleOperation(appendToken, TRIAGE_OPERATION_PATH, request);
|
|
149
|
-
|
|
150
|
-
markDone(item);
|
|
151
|
-
}
|
|
149
|
+
markDone(item);
|
|
152
150
|
},
|
|
153
|
-
[pjcode, appendToken, markDone
|
|
151
|
+
[pjcode, appendToken, markDone],
|
|
154
152
|
);
|
|
155
153
|
|
|
156
154
|
const setStory = useCallback(
|
|
@@ -8,6 +8,17 @@ const mockFetchOnce = (body: unknown, ok = true): jest.Mock => {
|
|
|
8
8
|
ok,
|
|
9
9
|
status: ok ? 200 : 500,
|
|
10
10
|
json: async () => body,
|
|
11
|
+
text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
|
|
12
|
+
});
|
|
13
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
14
|
+
return fetchMock;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const mockFetchFailureOnce = (status: number, rawBody: string): jest.Mock => {
|
|
18
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
19
|
+
ok: false,
|
|
20
|
+
status,
|
|
21
|
+
text: async () => rawBody,
|
|
11
22
|
});
|
|
12
23
|
global.fetch = fetchMock as unknown as typeof fetch;
|
|
13
24
|
return fetchMock;
|
|
@@ -169,15 +180,31 @@ describe('postConsoleOperation', () => {
|
|
|
169
180
|
expect(init).toMatchObject({ method: 'POST' });
|
|
170
181
|
});
|
|
171
182
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
const failingReview = (): Promise<void> =>
|
|
184
|
+
postConsoleOperation(appendToken, '/api/review', {
|
|
185
|
+
pjcode: 'umino',
|
|
186
|
+
action: 'approve',
|
|
187
|
+
prUrl: 'https://github.com/o/r/pull/1',
|
|
188
|
+
projectItemId: 'PVTI_1',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('throws the error reason from a JSON error body', async () => {
|
|
192
|
+
mockFetchFailureOnce(
|
|
193
|
+
502,
|
|
194
|
+
JSON.stringify({ error: 'Failed to approve PR: HTTP 422' }),
|
|
195
|
+
);
|
|
196
|
+
await expect(failingReview()).rejects.toThrow(
|
|
197
|
+
'Failed to approve PR: HTTP 422',
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('throws the raw body when the error body is not JSON', async () => {
|
|
202
|
+
mockFetchFailureOnce(500, 'Internal Server Error');
|
|
203
|
+
await expect(failingReview()).rejects.toThrow('Internal Server Error');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('falls back to the status code when the error body is empty', async () => {
|
|
207
|
+
mockFetchFailureOnce(500, '');
|
|
208
|
+
await expect(failingReview()).rejects.toThrow('HTTP 500');
|
|
182
209
|
});
|
|
183
210
|
});
|
|
@@ -171,6 +171,24 @@ export const createConsoleApiClient = (
|
|
|
171
171
|
parseState(await requestJson(appendToken, '/api/issuetitle', url)),
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
const readOperationErrorReason = async (
|
|
175
|
+
response: Response,
|
|
176
|
+
): Promise<string> => {
|
|
177
|
+
const raw = await response.text().catch(() => '');
|
|
178
|
+
if (raw.length === 0) {
|
|
179
|
+
return `HTTP ${response.status}`;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const parsed: unknown = JSON.parse(raw);
|
|
183
|
+
if (isRecord(parsed) && typeof parsed.error === 'string') {
|
|
184
|
+
return parsed.error;
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
return raw;
|
|
188
|
+
}
|
|
189
|
+
return raw;
|
|
190
|
+
};
|
|
191
|
+
|
|
174
192
|
export const postConsoleOperation = async (
|
|
175
193
|
appendToken: AppendToken,
|
|
176
194
|
apiPath: string,
|
|
@@ -182,7 +200,7 @@ export const postConsoleOperation = async (
|
|
|
182
200
|
body: JSON.stringify(body),
|
|
183
201
|
});
|
|
184
202
|
if (!response.ok) {
|
|
185
|
-
throw new Error(
|
|
203
|
+
throw new Error(await readOperationErrorReason(response));
|
|
186
204
|
}
|
|
187
205
|
};
|
|
188
206
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
2
|
import { ConsoleCommentComposer } from '../components/detail/ConsoleCommentComposer';
|
|
3
3
|
import { ConsoleItemDetail } from '../components/detail/ConsoleItemDetail';
|
|
4
4
|
import { ConsoleOperationMenu } from '../components/operations/ConsoleOperationMenu';
|
|
@@ -22,7 +22,7 @@ import type {
|
|
|
22
22
|
export type ConsoleQueueActionInput = {
|
|
23
23
|
kind: ConsoleActionKind;
|
|
24
24
|
item: ConsoleListItem;
|
|
25
|
-
commit: () => void
|
|
25
|
+
commit: () => Promise<void>;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export type ConsoleItemDetailContainerProps = {
|
|
@@ -37,7 +37,6 @@ export type ConsoleItemDetailContainerProps = {
|
|
|
37
37
|
overlayStatus: ConsoleOverlayStatus | null;
|
|
38
38
|
now: number;
|
|
39
39
|
onQueueAction: (input: ConsoleQueueActionInput) => void;
|
|
40
|
-
scrollRef?: Ref<HTMLElement>;
|
|
41
40
|
};
|
|
42
41
|
|
|
43
42
|
export const ConsoleItemDetailContainer = ({
|
|
@@ -52,7 +51,6 @@ export const ConsoleItemDetailContainer = ({
|
|
|
52
51
|
overlayStatus,
|
|
53
52
|
now,
|
|
54
53
|
onQueueAction,
|
|
55
|
-
scrollRef,
|
|
56
54
|
}: ConsoleItemDetailContainerProps) => {
|
|
57
55
|
const detail = useConsoleItemDetailData(caches, item);
|
|
58
56
|
const { token } = useConsoleToken();
|
|
@@ -70,54 +68,42 @@ export const ConsoleItemDetailContainer = ({
|
|
|
70
68
|
onQueueAction({
|
|
71
69
|
kind: { type: 'review', action },
|
|
72
70
|
item,
|
|
73
|
-
commit: () =>
|
|
74
|
-
void operations.reviewPullRequest(item, prUrl, action);
|
|
75
|
-
},
|
|
71
|
+
commit: () => operations.reviewPullRequest(item, prUrl, action),
|
|
76
72
|
});
|
|
77
73
|
},
|
|
78
74
|
onSetNextActionDate: (action) => {
|
|
79
75
|
onQueueAction({
|
|
80
76
|
kind: { type: 'next_action_date', action },
|
|
81
77
|
item,
|
|
82
|
-
commit: () =>
|
|
83
|
-
void operations.setNextActionDate(item, action);
|
|
84
|
-
},
|
|
78
|
+
commit: () => operations.setNextActionDate(item, action),
|
|
85
79
|
});
|
|
86
80
|
},
|
|
87
81
|
onSetStory: (option: ConsoleFieldOption) => {
|
|
88
82
|
onQueueAction({
|
|
89
83
|
kind: { type: 'set_story', optionName: option.name },
|
|
90
84
|
item,
|
|
91
|
-
commit: () =>
|
|
92
|
-
void operations.setStory(item, option);
|
|
93
|
-
},
|
|
85
|
+
commit: () => operations.setStory(item, option),
|
|
94
86
|
});
|
|
95
87
|
},
|
|
96
88
|
onSetStatus: (option: ConsoleFieldOption) => {
|
|
97
89
|
onQueueAction({
|
|
98
90
|
kind: { type: 'set_status', optionName: option.name },
|
|
99
91
|
item,
|
|
100
|
-
commit: () =>
|
|
101
|
-
void operations.setStatus(item, option);
|
|
102
|
-
},
|
|
92
|
+
commit: () => operations.setStatus(item, option),
|
|
103
93
|
});
|
|
104
94
|
},
|
|
105
95
|
onSetInTmuxByHuman: (option: ConsoleFieldOption) => {
|
|
106
96
|
onQueueAction({
|
|
107
97
|
kind: { type: 'set_in_tmux_by_human', optionName: option.name },
|
|
108
98
|
item,
|
|
109
|
-
commit: () =>
|
|
110
|
-
void operations.setInTmuxByHuman(item, option);
|
|
111
|
-
},
|
|
99
|
+
commit: () => operations.setInTmuxByHuman(item, option),
|
|
112
100
|
});
|
|
113
101
|
},
|
|
114
102
|
onClose: (action) => {
|
|
115
103
|
onQueueAction({
|
|
116
104
|
kind: { type: 'close', action },
|
|
117
105
|
item,
|
|
118
|
-
commit: () =>
|
|
119
|
-
void operations.closeIssue(item, action);
|
|
120
|
-
},
|
|
106
|
+
commit: () => operations.closeIssue(item, action),
|
|
121
107
|
});
|
|
122
108
|
},
|
|
123
109
|
};
|
|
@@ -150,7 +136,6 @@ export const ConsoleItemDetailContainer = ({
|
|
|
150
136
|
commitsError={detail.commitsError}
|
|
151
137
|
relatedPullRequests={detail.relatedPullRequests}
|
|
152
138
|
now={now}
|
|
153
|
-
scrollRef={scrollRef}
|
|
154
139
|
buildImageProxyUrl={resolveImageProxyUrl}
|
|
155
140
|
commentComposer={
|
|
156
141
|
<ConsoleCommentComposer
|
|
@@ -391,103 +391,6 @@ describe('ConsolePage swipe navigation', () => {
|
|
|
391
391
|
});
|
|
392
392
|
});
|
|
393
393
|
|
|
394
|
-
describe('ConsolePage detail scroll reset', () => {
|
|
395
|
-
beforeEach(() => {
|
|
396
|
-
localStorage.clear();
|
|
397
|
-
window.history.replaceState({}, '', '/projects/umino/prs?k=token');
|
|
398
|
-
const fetchMock = jest.fn(async (url: string) => {
|
|
399
|
-
const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
|
|
400
|
-
if (listMatch !== null) {
|
|
401
|
-
return {
|
|
402
|
-
ok: true,
|
|
403
|
-
status: 200,
|
|
404
|
-
json: async () =>
|
|
405
|
-
listMatch[1] === 'prs'
|
|
406
|
-
? twoItemPrPayload()
|
|
407
|
-
: { ...twoItemPrPayload(), items: [] },
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
|
|
411
|
-
});
|
|
412
|
-
global.fetch = fetchMock as unknown as typeof fetch;
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('resets the detail scroll container to the top when navigating to the next item', async () => {
|
|
416
|
-
const { container, getByText, findByText } = render(<ConsolePage />);
|
|
417
|
-
await waitFor(() => {
|
|
418
|
-
expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
|
|
419
|
-
});
|
|
420
|
-
fireEvent.click(getByText('Add serveConsole subcommand'));
|
|
421
|
-
expect(await findByText('Approve')).toBeInTheDocument();
|
|
422
|
-
|
|
423
|
-
const scrollContainer = container.querySelector('.console-detail');
|
|
424
|
-
expect(scrollContainer).not.toBeNull();
|
|
425
|
-
(scrollContainer as HTMLElement).scrollTop = 240;
|
|
426
|
-
expect((scrollContainer as HTMLElement).scrollTop).toBe(240);
|
|
427
|
-
|
|
428
|
-
const detailScreen = container.querySelector('.console-detail-screen');
|
|
429
|
-
swipeDetailScreen(
|
|
430
|
-
detailScreen as HTMLElement,
|
|
431
|
-
{ clientX: 240, clientY: 100 },
|
|
432
|
-
{ clientX: 40, clientY: 110 },
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
await waitFor(() => {
|
|
436
|
-
expect(window.location.hash).toBe('#item/PVTI_2');
|
|
437
|
-
});
|
|
438
|
-
await waitFor(() => {
|
|
439
|
-
expect((scrollContainer as HTMLElement).scrollTop).toBe(0);
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
describe('ConsolePage next item prefetch', () => {
|
|
445
|
-
beforeEach(() => {
|
|
446
|
-
localStorage.clear();
|
|
447
|
-
window.history.replaceState({}, '', '/projects/umino/prs?k=token');
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it('warms the next pending item resources while the current item is open', async () => {
|
|
451
|
-
const fetchMock = jest.fn(async (url: string) => {
|
|
452
|
-
const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
|
|
453
|
-
if (listMatch !== null) {
|
|
454
|
-
return {
|
|
455
|
-
ok: true,
|
|
456
|
-
status: 200,
|
|
457
|
-
json: async () =>
|
|
458
|
-
listMatch[1] === 'prs'
|
|
459
|
-
? twoItemPrPayload()
|
|
460
|
-
: { ...twoItemPrPayload(), items: [] },
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
|
|
464
|
-
});
|
|
465
|
-
global.fetch = fetchMock as unknown as typeof fetch;
|
|
466
|
-
|
|
467
|
-
const { getByText, findByText, queryByText } = render(<ConsolePage />);
|
|
468
|
-
await waitFor(() => {
|
|
469
|
-
expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
|
|
470
|
-
});
|
|
471
|
-
fireEvent.click(getByText('Add serveConsole subcommand'));
|
|
472
|
-
expect(await findByText('Approve')).toBeInTheDocument();
|
|
473
|
-
expect(window.location.hash).toBe('#item/PVTI_1');
|
|
474
|
-
expect(queryByText('Add server-side console API handlers')).toBeNull();
|
|
475
|
-
|
|
476
|
-
const nextItemUrlFragment = encodeURIComponent(
|
|
477
|
-
'https://github.com/o/r/pull/852',
|
|
478
|
-
);
|
|
479
|
-
await waitFor(() => {
|
|
480
|
-
const calledNextItemBody = fetchMock.mock.calls.some(
|
|
481
|
-
([url]) =>
|
|
482
|
-
typeof url === 'string' &&
|
|
483
|
-
url.includes('/api/itembody') &&
|
|
484
|
-
url.includes(nextItemUrlFragment),
|
|
485
|
-
);
|
|
486
|
-
expect(calledNextItemBody).toBe(true);
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
});
|
|
490
|
-
|
|
491
394
|
describe('ConsolePage auto-advance', () => {
|
|
492
395
|
beforeEach(() => {
|
|
493
396
|
localStorage.clear();
|
|
@@ -1,7 +1,10 @@
|
|
|
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 {
|
|
4
|
+
import {
|
|
5
|
+
ConsoleErrorToast,
|
|
6
|
+
ConsoleUndoToast,
|
|
7
|
+
} from '../components/operations/ConsoleUndoToast';
|
|
5
8
|
import { useConsoleActionQueue } from '../hooks/useConsoleActionQueue';
|
|
6
9
|
import { useConsoleCaches } from '../hooks/useConsoleCaches';
|
|
7
10
|
import { useConsoleNavigation } from '../hooks/useConsoleNavigation';
|
|
@@ -191,44 +194,6 @@ export const ConsolePage = () => {
|
|
|
191
194
|
);
|
|
192
195
|
|
|
193
196
|
const detailScreenRef = useConsoleSwipeNavigation(handleSwipe);
|
|
194
|
-
const detailScrollElementRef = useRef<HTMLElement | null>(null);
|
|
195
|
-
|
|
196
|
-
useEffect(() => {
|
|
197
|
-
if (selectedItemKey === null) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const element = detailScrollElementRef.current;
|
|
201
|
-
if (element !== null) {
|
|
202
|
-
element.scrollTop = 0;
|
|
203
|
-
}
|
|
204
|
-
}, [selectedItemKey]);
|
|
205
|
-
|
|
206
|
-
const nextPendingItem = useMemo<ConsoleListItem | null>(() => {
|
|
207
|
-
if (selectedItemKey === null) {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
const nextKey = nextPendingKeyBrowse(orderedPendingKeys, selectedItemKey);
|
|
211
|
-
if (nextKey === null) {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
return (
|
|
215
|
-
pendingItems.find((item) => overlayKeyForItem(item) === nextKey) ?? null
|
|
216
|
-
);
|
|
217
|
-
}, [selectedItemKey, orderedPendingKeys, pendingItems]);
|
|
218
|
-
|
|
219
|
-
useEffect(() => {
|
|
220
|
-
if (nextPendingItem === null) {
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
const key = `${nextPendingItem.repo}#${nextPendingItem.number}`;
|
|
224
|
-
const url = nextPendingItem.url;
|
|
225
|
-
void caches.body.load(key, url);
|
|
226
|
-
void caches.state.load(key, url);
|
|
227
|
-
if (nextPendingItem.isPr) {
|
|
228
|
-
void caches.files.load(key, url);
|
|
229
|
-
void caches.commits.load(key, url);
|
|
230
|
-
}
|
|
231
|
-
}, [nextPendingItem, caches]);
|
|
232
197
|
|
|
233
198
|
return (
|
|
234
199
|
<main className="console-app">
|
|
@@ -241,6 +206,12 @@ export const ConsolePage = () => {
|
|
|
241
206
|
onUndo={actionQueue.undo}
|
|
242
207
|
/>
|
|
243
208
|
)}
|
|
209
|
+
{actionQueue.error !== null && (
|
|
210
|
+
<ConsoleErrorToast
|
|
211
|
+
message={`操作に失敗しました: ${actionQueue.error.reason}`}
|
|
212
|
+
onDismiss={actionQueue.dismissError}
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
244
215
|
<ConsoleTabList
|
|
245
216
|
activeTab={activeTab}
|
|
246
217
|
counts={counts}
|
|
@@ -273,7 +244,6 @@ export const ConsolePage = () => {
|
|
|
273
244
|
overlayStatus={overlayStatusForSelected}
|
|
274
245
|
now={now}
|
|
275
246
|
onQueueAction={handleQueueAction}
|
|
276
|
-
scrollRef={detailScrollElementRef}
|
|
277
247
|
/>
|
|
278
248
|
</div>
|
|
279
249
|
)}
|
|
@@ -33,10 +33,8 @@ body {
|
|
|
33
33
|
.console-app {
|
|
34
34
|
max-width: 920px;
|
|
35
35
|
margin: 0 auto;
|
|
36
|
-
height: 100dvh;
|
|
37
36
|
display: flex;
|
|
38
37
|
flex-direction: column;
|
|
39
|
-
min-height: 0;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
.console-tabbar {
|
|
@@ -116,10 +114,6 @@ body {
|
|
|
116
114
|
list-style: none;
|
|
117
115
|
margin: 0;
|
|
118
116
|
padding: 12px 18px 18px;
|
|
119
|
-
flex: 1;
|
|
120
|
-
min-height: 0;
|
|
121
|
-
overflow-y: auto;
|
|
122
|
-
-webkit-overflow-scrolling: touch;
|
|
123
117
|
}
|
|
124
118
|
|
|
125
119
|
.console-list-group {
|
|
@@ -264,10 +258,6 @@ body {
|
|
|
264
258
|
}
|
|
265
259
|
|
|
266
260
|
.console-detail {
|
|
267
|
-
flex: 1;
|
|
268
|
-
min-height: 0;
|
|
269
|
-
overflow-y: auto;
|
|
270
|
-
-webkit-overflow-scrolling: touch;
|
|
271
261
|
display: flex;
|
|
272
262
|
flex-direction: column;
|
|
273
263
|
gap: 12px;
|
|
@@ -473,11 +463,14 @@ body {
|
|
|
473
463
|
}
|
|
474
464
|
|
|
475
465
|
.console-actionbar {
|
|
476
|
-
|
|
466
|
+
position: fixed;
|
|
467
|
+
bottom: 0;
|
|
468
|
+
left: 0;
|
|
469
|
+
right: 0;
|
|
470
|
+
z-index: 100;
|
|
477
471
|
background: #161b22;
|
|
478
472
|
border-top: 2px solid #30363d;
|
|
479
473
|
padding: 10px 16px;
|
|
480
|
-
padding-bottom: calc(10px + env(safe-area-inset-bottom));
|
|
481
474
|
}
|
|
482
475
|
|
|
483
476
|
.console-operation-bar {
|
|
@@ -568,10 +561,7 @@ body {
|
|
|
568
561
|
}
|
|
569
562
|
|
|
570
563
|
.console-detail-screen {
|
|
571
|
-
|
|
572
|
-
min-height: 0;
|
|
573
|
-
display: flex;
|
|
574
|
-
flex-direction: column;
|
|
564
|
+
padding-bottom: 140px;
|
|
575
565
|
}
|
|
576
566
|
|
|
577
567
|
.console-panel-open-link {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-outline-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-input:#e5e5e5;--color-ring:#0a0a0a;--color-background:#fff;--color-foreground:#0a0a0a;--color-primary:#171717;--color-primary-foreground:#fafafa;--color-secondary:#f5f5f5;--color-secondary-foreground:#171717;--color-accent:#f5f5f5;--color-accent-foreground:#171717}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.relative{position:relative}.static{position:static}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.hidden{display:none}.inline-flex{display:inline-flex}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-input{border-color:var(--color-input)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background)}.bg-primary{background-color:var(--color-primary)}.bg-secondary{background-color:var(--color-secondary)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-foreground{color:var(--color-foreground)}.text-primary-foreground{color:var(--color-primary-foreground)}.text-secondary-foreground{color:var(--color-secondary-foreground)}.lowercase{text-transform:lowercase}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.hover\:bg-accent:hover{background-color:var(--color-accent)}.hover\:bg-primary\/90:hover{background-color:#171717e6}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--color-primary) 90%,transparent)}}.hover\:bg-secondary\/80:hover{background-color:#f5f5f5cc}@supports (color:color-mix(in lab,red,red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--color-secondary) 80%,transparent)}}.hover\:text-accent-foreground:hover{color:var(--color-accent-foreground)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}}body{color:#e6edf3;background-color:#0d1117;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.console-app{flex-direction:column;max-width:920px;margin:0 auto;display:flex}.console-tabbar{-webkit-overflow-scrolling:touch;background:#161b22;border-bottom:2px solid #30363d;flex-wrap:nowrap;align-items:stretch;gap:0;min-height:42px;padding:0 8px;display:flex;overflow-x:auto}.console-tab{color:#8b949e;white-space:nowrap;cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;align-items:center;gap:6px;margin-bottom:-2px;padding:10px 14px;font-size:13px;font-weight:500;line-height:1.2;text-decoration:none;display:inline-flex}.console-tab:hover{color:#e6edf3}.console-tab[data-active=true]{color:#e6edf3;border-bottom-color:#2f81f7;font-weight:700}.console-tab-badge{text-align:center;color:#e6edf3;background:#484f58;border-radius:20px;min-width:20px;padding:1px 7px;font-size:11px;font-weight:700;line-height:1.5}.console-tab-badge[data-zero=true]{color:#8b949e;background:#30363d}.console-tab-pjname{color:#8b949e;align-self:center;margin-left:auto;padding:0 8px;font-size:11.5px}.console-tab-geninfo{color:#8b949e;align-self:center;padding:0 4px;font-size:11px}.console-list{margin:0;padding:12px 18px 18px;list-style:none}.console-list-group{list-style:none}.console-item-row .console-item-icon{flex:none;margin-top:3px}.console-group-header{background:#0b0f14;border-bottom:1px solid #21262d;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-storytag{align-items:center;gap:8px;font-size:13px;font-weight:700;display:inline-flex}.console-story-dot{border-radius:999px;width:10px;height:10px;display:inline-block}.console-group-count{color:#8b949e;font-size:12px}.console-item-row{color:#e6edf3;text-align:left;cursor:pointer;background:#161b22;border:1px solid #30363d;border-radius:8px;align-items:flex-start;gap:14px;width:100%;margin-bottom:10px;padding:12px 16px;display:flex}.console-item-row:hover{background:#1a2029;border-color:#484f58}.console-item-row[data-active=true]{background:#1a2029;border-color:#4493f8}.console-item-meta{flex:1;min-width:0}.console-item-title{font-size:14.5px;font-weight:600;display:block}.console-item-sub{color:#8b949e;margin-top:3px;font-size:12.5px;display:block}.console-item-pill{color:#8b949e;border:1px solid #30363d;border-radius:20px;margin-right:6px;padding:1px 8px;font-size:11px;display:inline-block}.console-item-createdat{color:#8b949e;cursor:help}.console-item-fields{color:#adbac7;flex-wrap:wrap;gap:4px 10px;margin-top:4px;font-size:11.5px;display:flex}.console-item-field{word-break:break-all;align-items:baseline;gap:4px;min-width:0;display:inline-flex}.console-item-field-label{color:#6e7681;text-transform:uppercase;letter-spacing:.03em;font-size:10px}.console-list-message{color:#8b949e;padding:16px;font-size:14px}.console-list-empty{text-align:center;color:#8b949e;padding:40px;font-size:14px}.console-list-error,.console-comment-error,.console-files-error,.console-commits-error,.console-detail-body-error{color:#f85149}.console-detail{flex-direction:column;gap:12px;padding:16px;display:flex}.console-detail-title{align-items:center;gap:8px;margin:0;font-size:20px;display:flex}.console-detail-title-text{flex:1}.console-detail-number{color:#8b949e;font-weight:400}.console-detail-closed-label{color:#a371f7;font-size:13px}.console-detail-subbar{align-items:center;gap:12px;font-size:13px;display:flex}.console-detail-link{color:#4493f8}.console-detail-repo{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-detail-pill,.console-label-chip,.console-detail-status-chip{border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:12px;display:inline-block}.console-detail-labels{flex-wrap:wrap;gap:6px;display:flex}.console-detail-createdat{color:#8b949e;font-size:12px}.console-panel{border:1px solid #30363d;border-radius:8px;overflow:hidden}.console-panel-header{background:#161b22;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-panel-toggle{color:#e6edf3;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;font-size:14px;font-weight:600;display:inline-flex}.console-panel-body{padding:12px}.console-markdown{word-break:break-word;font-size:14px;line-height:1.5}.console-markdown ul{margin:.5em 0;padding-left:1.5em;list-style:outside}.console-markdown ol{margin:.5em 0;padding-left:1.5em;list-style:decimal}.console-markdown li{margin:.25em 0}.console-markdown table{border-collapse:collapse;margin:.5em 0}.console-markdown th,.console-markdown td{border:1px solid #30363d;padding:4px 8px}.console-markdown th{background:#161b22}.console-mermaid-error{color:#f85149;font-size:13px}.console-comment{border-top:1px solid #21262d;padding-top:8px}.console-comment-header{color:#8b949e;gap:8px;font-size:12px;display:flex}.console-comment-author{color:#e6edf3;font-weight:600}.console-files,.console-commits{margin:0;padding:0;list-style:none}.console-commit{align-items:center;gap:8px;padding:4px 0;font-size:13px;display:flex}.console-file{flex-direction:column;font-size:13px;display:flex}.console-file-badge{text-align:center;border:1px solid;border-radius:4px;width:18px;font-size:11px}.console-file-path,.console-commit-message{flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-add,.console-pr-add{color:#3fb950}.console-file-del,.console-pr-del{color:#f85149}.console-commit-sha{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-actionbar{z-index:100;background:#161b22;border-top:2px solid #30363d;padding:10px 16px;position:fixed;bottom:0;left:0;right:0}.console-operation-bar{flex-direction:column;gap:8px;max-width:920px;margin:0 auto;display:flex}.console-op-group{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.console-op-group-review{gap:10px}.console-op-group-stories{max-height:50vh;overflow-y:auto}.console-op-button{color:#e6edf3;white-space:nowrap;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:7px 16px;font-size:13px;font-weight:600}.console-op-button:hover{border-color:#484f58}.console-op-button-approve{color:#fff;background:#238636;border-color:#2ea043}.console-op-button-reject{color:#ffd166;background:#7d5000;border-color:#a06800}.console-op-button-wrong{color:#f85149;background:#3a1518;border-color:#f85149}.console-op-button-unneeded{color:#aab0b8;background:#2a2d31;border-color:#6e7681}.console-op-button-snooze{color:#79c0ff;background:#1c2b4a;border-color:#4493f8}.console-pr-section{border:1px solid #30363d;border-radius:8px;flex-direction:column;gap:10px;padding:12px;display:flex}.console-pr-statbar{color:#8b949e;gap:12px;font-size:12px;display:flex}.console-detail-screen{padding-bottom:140px}.console-panel-open-link{color:#4493f8;font-size:13px;font-weight:400}.console-composer{margin-top:4px}.console-composer-toggle{color:#8b949e;cursor:pointer;background:0 0;border:none;padding:2px 0;font-size:12.5px}.console-composer-toggle:hover{color:#e6edf3}.console-composer-posted{flex-direction:column;gap:8px;margin-top:8px;display:flex}.console-composer-form{margin-top:8px}.console-composer-input{box-sizing:border-box;color:#e6edf3;width:100%;font:inherit;resize:vertical;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:8px;font-size:14px}.console-composer-row{align-items:center;gap:8px;margin-top:6px;display:flex}.console-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #2ea043;border-radius:6px;padding:7px 16px;font-size:13px;font-weight:600}.console-composer-submit:disabled{opacity:.6;cursor:default}.console-composer-status{color:#8b949e;font-size:12px}.console-composer-error{color:#f85149}.console-undo-toast{z-index:9998;white-space:nowrap;border-radius:10px;align-items:center;gap:12px;max-width:92vw;padding:12px 18px;font-size:13.5px;display:flex;position:fixed;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 20px #0009}.console-undo-toast-green{color:#3fb950;background:#0d3320;border:1px solid #2ea043}.console-undo-toast-amber{color:#ffd166;background:#3a2800;border:1px solid #a06800}.console-undo-toast-red{color:#f85149;background:#3a1518;border:1px solid #da3633}.console-undo-toast-gray{color:#aab0b8;background:#2a2d31;border:1px solid #6e7681}.console-undo-toast-blue{color:#79c0ff;background:#0d1f40;border:1px solid #4493f8}.console-undo-toast-error{color:#f85149;background:#3a1518;border:1px solid #f85149}.console-undo-toast-message{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.console-undo-toast-undo{cursor:pointer;color:inherit;background:#ffffff1f;border:none;border-radius:6px;flex:none;padding:4px 12px;font-size:13px;font-weight:700}.console-undo-toast-undo:hover{background:#ffffff38}.console-undo-toast-countdown{opacity:.7;flex:none;font-size:11px}.console-undo-toast-bar{opacity:.5;background:currentColor;border-radius:0 0 10px 10px;height:3px;transition:width .1s linear;position:absolute;bottom:0;left:0}.console-file-row{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:4px 0;font-size:13px;display:flex}.console-file-row:hover{background:#1a2029}.console-file-caret{color:#8b949e;flex:none;width:1em}.console-file-diff{border-collapse:collapse;width:100%;margin:4px 0 8px;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-diff td{white-space:pre-wrap;word-break:break-word;vertical-align:top;padding:0 10px}.console-diff-ln{text-align:right;color:#6e7681;-webkit-user-select:none;user-select:none;white-space:nowrap;border-right:1px solid #30363d;width:1%;padding:0 8px}.console-file-diff td.console-diff-ln{white-space:nowrap;word-break:normal;overflow-wrap:normal}.console-diff-add td{background:#2ea04326}.console-diff-add .console-diff-code{color:#aff5b4}.console-diff-del td{background:#f8514926}.console-diff-del .console-diff-code{color:#ffdcd7}.console-diff-hunk td{color:#79c0ff;background:#161b22}.console-diff-ctx .console-diff-code{color:#c9d1d9}.console-file-diff-empty{color:#8b949e;padding:8px 14px;font-size:12px}.console-pr-header{flex-wrap:wrap;align-items:center;gap:10px;padding-top:4px;display:flex}.console-pr-section-title{color:#e6edf3;font-size:15px;font-weight:600}.console-pr-section-state{color:#8b949e;border:1px solid #6e7681;border-radius:999px;padding:1px 8px;font-size:11px}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|