github-issue-tower-defence-management 1.94.3 → 1.95.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/cli/index.js +1 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleImageProxy.js +79 -0
- package/bin/adapter/entry-points/console/consoleImageProxy.js.map +1 -0
- package/bin/adapter/entry-points/console/consoleServer.js +29 -1
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BeJzGnfH.js +101 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +1 -0
- package/src/adapter/entry-points/console/consoleImageProxy.test.ts +140 -0
- package/src/adapter/entry-points/console/consoleImageProxy.ts +111 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +182 -0
- package/src/adapter/entry-points/console/consoleServer.ts +45 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +10 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +23 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.tsx +7 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +9 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +7 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.test.ts +78 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.ts +41 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/tabAdvance.test.ts +48 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/tabAdvance.ts +19 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +9 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +39 -4
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +29 -1
- package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +11 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-BeJzGnfH.js +101 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/types/adapter/entry-points/console/consoleImageProxy.d.ts +19 -0
- package/types/adapter/entry-points/console/consoleImageProxy.d.ts.map +1 -0
- package/types/adapter/entry-points/console/consoleServer.d.ts +4 -0
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-eSyo3r3C.js +0 -101
- package/src/adapter/entry-points/console/ui-dist/assets/index-eSyo3r3C.js +0 -101
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ImageProxyUrlBuilder } from '../../lib/imageProxy';
|
|
2
3
|
import { colorFromEnum } from '../../logic/colors';
|
|
3
4
|
import {
|
|
4
5
|
formatFullTimestamp,
|
|
@@ -54,6 +55,7 @@ export type ConsoleItemDetailProps = {
|
|
|
54
55
|
now: number;
|
|
55
56
|
commentComposer: ReactNode;
|
|
56
57
|
operationBar: ReactNode;
|
|
58
|
+
buildImageProxyUrl?: ImageProxyUrlBuilder;
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
export const ConsoleItemDetail = ({
|
|
@@ -78,6 +80,7 @@ export const ConsoleItemDetail = ({
|
|
|
78
80
|
now,
|
|
79
81
|
commentComposer,
|
|
80
82
|
operationBar,
|
|
83
|
+
buildImageProxyUrl,
|
|
81
84
|
}: ConsoleItemDetailProps) => {
|
|
82
85
|
const resolvedState = state?.state ?? 'open';
|
|
83
86
|
const merged = state?.merged ?? false;
|
|
@@ -192,7 +195,10 @@ export const ConsoleItemDetail = ({
|
|
|
192
195
|
) : bodyIsLoading ? (
|
|
193
196
|
<p className="console-detail-body-loading">Loading description...</p>
|
|
194
197
|
) : (
|
|
195
|
-
<ConsoleMarkdownContent
|
|
198
|
+
<ConsoleMarkdownContent
|
|
199
|
+
body={body}
|
|
200
|
+
buildImageProxyUrl={buildImageProxyUrl}
|
|
201
|
+
/>
|
|
196
202
|
)}
|
|
197
203
|
</ConsolePanel>
|
|
198
204
|
|
|
@@ -216,6 +222,7 @@ export const ConsoleItemDetail = ({
|
|
|
216
222
|
isLoading={commentsAreLoading}
|
|
217
223
|
error={commentsError}
|
|
218
224
|
now={now}
|
|
225
|
+
buildImageProxyUrl={buildImageProxyUrl}
|
|
219
226
|
/>
|
|
220
227
|
</ConsolePanel>
|
|
221
228
|
|
|
@@ -244,6 +251,7 @@ export const ConsoleItemDetail = ({
|
|
|
244
251
|
commitsAreLoading={related.commitsAreLoading}
|
|
245
252
|
commitsError={related.commitsError}
|
|
246
253
|
now={now}
|
|
254
|
+
buildImageProxyUrl={buildImageProxyUrl}
|
|
247
255
|
/>
|
|
248
256
|
))}
|
|
249
257
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ImageProxyUrlBuilder } from '../../lib/imageProxy';
|
|
1
2
|
import type {
|
|
2
3
|
ConsoleChangedFile,
|
|
3
4
|
ConsoleCommit,
|
|
@@ -19,6 +20,7 @@ export type ConsolePullRequestSectionProps = {
|
|
|
19
20
|
commitsAreLoading: boolean;
|
|
20
21
|
commitsError: string | null;
|
|
21
22
|
now: number;
|
|
23
|
+
buildImageProxyUrl?: ImageProxyUrlBuilder;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
export const ConsolePullRequestDetail = ({
|
|
@@ -32,6 +34,7 @@ export const ConsolePullRequestDetail = ({
|
|
|
32
34
|
commitsAreLoading,
|
|
33
35
|
commitsError,
|
|
34
36
|
now,
|
|
37
|
+
buildImageProxyUrl,
|
|
35
38
|
}: ConsolePullRequestSectionProps) => {
|
|
36
39
|
const summary = pullRequest.summary;
|
|
37
40
|
const filesCount =
|
|
@@ -71,7 +74,10 @@ export const ConsolePullRequestDetail = ({
|
|
|
71
74
|
{bodyIsLoading ? (
|
|
72
75
|
<p className="console-pr-body-loading">Loading description...</p>
|
|
73
76
|
) : (
|
|
74
|
-
<ConsoleMarkdownContent
|
|
77
|
+
<ConsoleMarkdownContent
|
|
78
|
+
body={summary?.body ?? body}
|
|
79
|
+
buildImageProxyUrl={buildImageProxyUrl}
|
|
80
|
+
/>
|
|
75
81
|
)}
|
|
76
82
|
</ConsolePanel>
|
|
77
83
|
<ConsolePanel title="Changed files" count={filesCount}>
|
|
@@ -59,6 +59,20 @@ export const ZeroCountActiveTabStaysVisible: Story = {
|
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
export const AfterAutoAdvanceToNextTab: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
activeTab: 'unread',
|
|
65
|
+
counts: {
|
|
66
|
+
prs: 0,
|
|
67
|
+
triage: 0,
|
|
68
|
+
unread: 7,
|
|
69
|
+
'failed-preparation': 2,
|
|
70
|
+
'todo-by-human': 4,
|
|
71
|
+
},
|
|
72
|
+
onSelectTab: () => {},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
62
76
|
export const Interactive: Story = {
|
|
63
77
|
render: (args) => {
|
|
64
78
|
const [activeTab, setActiveTab] = useState<ConsoleTabName>('prs');
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildImageProxyUrl,
|
|
3
|
+
isProxyableImageUrl,
|
|
4
|
+
rewriteGitHubImageSources,
|
|
5
|
+
} from './imageProxy';
|
|
6
|
+
|
|
7
|
+
describe('isProxyableImageUrl', () => {
|
|
8
|
+
it('matches github user-attachments urls', () => {
|
|
9
|
+
expect(
|
|
10
|
+
isProxyableImageUrl(
|
|
11
|
+
'https://github.com/user-attachments/assets/0a1b2c3d',
|
|
12
|
+
),
|
|
13
|
+
).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('matches githubusercontent subdomain urls', () => {
|
|
17
|
+
expect(
|
|
18
|
+
isProxyableImageUrl(
|
|
19
|
+
'https://private-user-images.githubusercontent.com/1/2.png',
|
|
20
|
+
),
|
|
21
|
+
).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does not match arbitrary external hosts', () => {
|
|
25
|
+
expect(isProxyableImageUrl('https://example.com/avatar.png')).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('buildImageProxyUrl', () => {
|
|
30
|
+
it('encodes the source url and appends the token', () => {
|
|
31
|
+
const src = 'https://github.com/user-attachments/assets/abc?x=1';
|
|
32
|
+
expect(buildImageProxyUrl(src, 'tk')).toBe(
|
|
33
|
+
`/api/img?url=${encodeURIComponent(src)}&k=tk`,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('omits the token when it is null', () => {
|
|
38
|
+
const src = 'https://github.com/user-attachments/assets/abc';
|
|
39
|
+
expect(buildImageProxyUrl(src, null)).toBe(
|
|
40
|
+
`/api/img?url=${encodeURIComponent(src)}`,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('rewriteGitHubImageSources', () => {
|
|
46
|
+
const buildProxyUrl = (src: string): string =>
|
|
47
|
+
buildImageProxyUrl(src, 'console-token');
|
|
48
|
+
|
|
49
|
+
it('rewrites an allow-listed github image src to the proxy url', () => {
|
|
50
|
+
const githubSrc = 'https://github.com/user-attachments/assets/abc';
|
|
51
|
+
const html = `<p><img src="${githubSrc}" alt="shot" /></p>`;
|
|
52
|
+
const result = rewriteGitHubImageSources(html, buildProxyUrl);
|
|
53
|
+
expect(result).toContain(`/api/img?url=${encodeURIComponent(githubSrc)}`);
|
|
54
|
+
expect(result).toContain('k=console-token');
|
|
55
|
+
expect(result).not.toContain(`src="${githubSrc}"`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rewrites a githubusercontent image src to the proxy url', () => {
|
|
59
|
+
const githubSrc =
|
|
60
|
+
'https://private-user-images.githubusercontent.com/1/2.png';
|
|
61
|
+
const html = `<img src="${githubSrc}" />`;
|
|
62
|
+
const result = rewriteGitHubImageSources(html, buildProxyUrl);
|
|
63
|
+
expect(result).toContain(`/api/img?url=${encodeURIComponent(githubSrc)}`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('leaves a non-matching image src unchanged', () => {
|
|
67
|
+
const externalSrc = 'https://example.com/avatar.png';
|
|
68
|
+
const html = `<img src="${externalSrc}" />`;
|
|
69
|
+
const result = rewriteGitHubImageSources(html, buildProxyUrl);
|
|
70
|
+
expect(result).toContain(`src="${externalSrc}"`);
|
|
71
|
+
expect(result).not.toContain('/api/img');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns the original html when there is no image to rewrite', () => {
|
|
75
|
+
const html = '<p>no images here</p>';
|
|
76
|
+
expect(rewriteGitHubImageSources(html, buildProxyUrl)).toBe(html);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const ALLOWED_IMAGE_URL =
|
|
2
|
+
/^https:\/\/github\.com\/user-attachments\/|^https:\/\/[a-z0-9][a-z0-9.-]*\.githubusercontent\.com\//;
|
|
3
|
+
|
|
4
|
+
export const isProxyableImageUrl = (src: string): boolean =>
|
|
5
|
+
ALLOWED_IMAGE_URL.test(src);
|
|
6
|
+
|
|
7
|
+
export type ImageProxyUrlBuilder = (src: string) => string;
|
|
8
|
+
|
|
9
|
+
export const buildImageProxyUrl = (
|
|
10
|
+
src: string,
|
|
11
|
+
token: string | null,
|
|
12
|
+
): string => {
|
|
13
|
+
if (token === null) {
|
|
14
|
+
return `/api/img?url=${encodeURIComponent(src)}`;
|
|
15
|
+
}
|
|
16
|
+
return `/api/img?url=${encodeURIComponent(src)}&k=${encodeURIComponent(token)}`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const rewriteGitHubImageSources = (
|
|
20
|
+
html: string,
|
|
21
|
+
buildProxyUrl: ImageProxyUrlBuilder,
|
|
22
|
+
): string => {
|
|
23
|
+
if (typeof document === 'undefined') {
|
|
24
|
+
return html;
|
|
25
|
+
}
|
|
26
|
+
const template = document.createElement('template');
|
|
27
|
+
template.innerHTML = html;
|
|
28
|
+
const images = template.content.querySelectorAll('img[src]');
|
|
29
|
+
let rewrote = false;
|
|
30
|
+
images.forEach((image) => {
|
|
31
|
+
const src = image.getAttribute('src') ?? '';
|
|
32
|
+
if (isProxyableImageUrl(src)) {
|
|
33
|
+
image.setAttribute('src', buildProxyUrl(src));
|
|
34
|
+
rewrote = true;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
if (!rewrote) {
|
|
38
|
+
return html;
|
|
39
|
+
}
|
|
40
|
+
return template.innerHTML;
|
|
41
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { findNextNonEmptyTabToRight } from './tabAdvance';
|
|
2
|
+
import type { ConsoleTabName } from './types';
|
|
3
|
+
|
|
4
|
+
const counts = (
|
|
5
|
+
overrides: Partial<Record<ConsoleTabName, number>>,
|
|
6
|
+
): Record<ConsoleTabName, number> => ({
|
|
7
|
+
prs: 0,
|
|
8
|
+
triage: 0,
|
|
9
|
+
unread: 0,
|
|
10
|
+
'failed-preparation': 0,
|
|
11
|
+
'todo-by-human': 0,
|
|
12
|
+
...overrides,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('findNextNonEmptyTabToRight', () => {
|
|
16
|
+
it('returns the first non-empty tab to the right of the active tab', () => {
|
|
17
|
+
expect(findNextNonEmptyTabToRight('prs', counts({ unread: 7 }))).toBe(
|
|
18
|
+
'unread',
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('skips empty tabs and returns the next non-empty tab further right', () => {
|
|
23
|
+
expect(
|
|
24
|
+
findNextNonEmptyTabToRight(
|
|
25
|
+
'prs',
|
|
26
|
+
counts({ triage: 0, unread: 0, 'todo-by-human': 4 }),
|
|
27
|
+
),
|
|
28
|
+
).toBe('todo-by-human');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns the immediately adjacent tab when it is non-empty', () => {
|
|
32
|
+
expect(
|
|
33
|
+
findNextNonEmptyTabToRight('prs', counts({ triage: 12, unread: 7 })),
|
|
34
|
+
).toBe('triage');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns null when no tab to the right has any items', () => {
|
|
38
|
+
expect(
|
|
39
|
+
findNextNonEmptyTabToRight('unread', counts({ prs: 35 })),
|
|
40
|
+
).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns null when the active tab is the last tab', () => {
|
|
44
|
+
expect(
|
|
45
|
+
findNextNonEmptyTabToRight('todo-by-human', counts({ prs: 35 })),
|
|
46
|
+
).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ConsoleTabName } from './types';
|
|
2
|
+
import { CONSOLE_TABS } from './types';
|
|
3
|
+
|
|
4
|
+
export const findNextNonEmptyTabToRight = (
|
|
5
|
+
activeTab: ConsoleTabName,
|
|
6
|
+
counts: Record<ConsoleTabName, number>,
|
|
7
|
+
): ConsoleTabName | null => {
|
|
8
|
+
const activeIndex = CONSOLE_TABS.findIndex((tab) => tab.name === activeTab);
|
|
9
|
+
if (activeIndex === -1) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
for (let index = activeIndex + 1; index < CONSOLE_TABS.length; index += 1) {
|
|
13
|
+
const candidate = CONSOLE_TABS[index].name;
|
|
14
|
+
if ((counts[candidate] ?? 0) > 0) {
|
|
15
|
+
return candidate;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
1
2
|
import { ConsoleCommentComposer } from '../components/detail/ConsoleCommentComposer';
|
|
2
3
|
import { ConsoleItemDetail } from '../components/detail/ConsoleItemDetail';
|
|
3
4
|
import { ConsoleOperationMenu } from '../components/operations/ConsoleOperationMenu';
|
|
4
5
|
import type { ConsoleCaches } from '../hooks/useConsoleCaches';
|
|
5
6
|
import { useConsoleItemDetailData } from '../hooks/useConsoleItemDetailData';
|
|
6
7
|
import type { ConsoleOperationsApi } from '../hooks/useConsoleOperations';
|
|
8
|
+
import { useConsoleToken } from '../hooks/useConsoleToken';
|
|
9
|
+
import { buildImageProxyUrl } from '../lib/imageProxy';
|
|
7
10
|
import type { ConsoleActionKind } from '../logic/actionToast';
|
|
8
11
|
import { resolveStoryColorEnum } from '../logic/grouping';
|
|
9
12
|
import type { ConsoleOperationHandlers } from '../logic/operations';
|
|
@@ -50,6 +53,11 @@ export const ConsoleItemDetailContainer = ({
|
|
|
50
53
|
onQueueAction,
|
|
51
54
|
}: ConsoleItemDetailContainerProps) => {
|
|
52
55
|
const detail = useConsoleItemDetailData(caches, item);
|
|
56
|
+
const { token } = useConsoleToken();
|
|
57
|
+
const resolveImageProxyUrl = useCallback(
|
|
58
|
+
(src: string): string => buildImageProxyUrl(src, token),
|
|
59
|
+
[token],
|
|
60
|
+
);
|
|
53
61
|
const hasPullRequest = item.isPr || detail.relatedPullRequests.length > 0;
|
|
54
62
|
|
|
55
63
|
const handlers: ConsoleOperationHandlers = {
|
|
@@ -140,6 +148,7 @@ export const ConsoleItemDetailContainer = ({
|
|
|
140
148
|
commitsError={detail.commitsError}
|
|
141
149
|
relatedPullRequests={detail.relatedPullRequests}
|
|
142
150
|
now={now}
|
|
151
|
+
buildImageProxyUrl={resolveImageProxyUrl}
|
|
143
152
|
commentComposer={
|
|
144
153
|
<ConsoleCommentComposer
|
|
145
154
|
isPr={item.isPr}
|
|
@@ -128,10 +128,8 @@ describe('ConsolePage', () => {
|
|
|
128
128
|
|
|
129
129
|
await waitFor(() => {
|
|
130
130
|
expect(
|
|
131
|
-
getByText('
|
|
132
|
-
|
|
133
|
-
?.querySelector('.console-tab-badge')?.textContent,
|
|
134
|
-
).toBe('0');
|
|
131
|
+
getByText('Unread').closest('a')?.getAttribute('aria-current'),
|
|
132
|
+
).toBe('page');
|
|
135
133
|
});
|
|
136
134
|
} finally {
|
|
137
135
|
jest.useRealTimers();
|
|
@@ -397,3 +395,40 @@ describe('ConsolePage auto-advance', () => {
|
|
|
397
395
|
}
|
|
398
396
|
});
|
|
399
397
|
});
|
|
398
|
+
|
|
399
|
+
describe('ConsolePage auto-advance tab', () => {
|
|
400
|
+
beforeEach(() => {
|
|
401
|
+
localStorage.clear();
|
|
402
|
+
window.history.replaceState({}, '', '/projects/umino/prs?k=token');
|
|
403
|
+
installFetch();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('auto-advances to the next non-empty tab on the right after the active tab is driven to zero', async () => {
|
|
407
|
+
jest.useFakeTimers();
|
|
408
|
+
try {
|
|
409
|
+
const { getByText, findByText } = render(<ConsolePage />);
|
|
410
|
+
await waitFor(() => {
|
|
411
|
+
expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
fireEvent.click(getByText('Add serveConsole subcommand'));
|
|
415
|
+
expect(await findByText('← Back to list')).toBeInTheDocument();
|
|
416
|
+
fireEvent.click(getByText('Approve'));
|
|
417
|
+
|
|
418
|
+
act(() => {
|
|
419
|
+
jest.advanceTimersByTime(5100);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
await waitFor(() => {
|
|
423
|
+
expect(
|
|
424
|
+
getByText('Notify finished issue preparation'),
|
|
425
|
+
).toBeInTheDocument();
|
|
426
|
+
});
|
|
427
|
+
expect(
|
|
428
|
+
getByText('Unread').closest('a')?.getAttribute('aria-current'),
|
|
429
|
+
).toBe('page');
|
|
430
|
+
} finally {
|
|
431
|
+
jest.useRealTimers();
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useMemo } from 'react';
|
|
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
4
|
import { ConsoleUndoToast } from '../components/operations/ConsoleUndoToast';
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
overlayKeyForItem,
|
|
28
28
|
} from '../logic/overlay';
|
|
29
29
|
import type { ConsoleSwipeDirection } from '../logic/swipe';
|
|
30
|
+
import { findNextNonEmptyTabToRight } from '../logic/tabAdvance';
|
|
30
31
|
import type {
|
|
31
32
|
ConsoleListItem,
|
|
32
33
|
ConsoleOverlayStatus,
|
|
@@ -109,6 +110,26 @@ export const ConsolePage = () => {
|
|
|
109
110
|
);
|
|
110
111
|
}, [selectedItemKey, activeSnapshot]);
|
|
111
112
|
|
|
113
|
+
const activeCount = counts[activeTab];
|
|
114
|
+
const previousActiveTabCountRef = useRef<{
|
|
115
|
+
tab: ConsoleTabName;
|
|
116
|
+
count: number;
|
|
117
|
+
}>({ tab: activeTab, count: activeCount });
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const previous = previousActiveTabCountRef.current;
|
|
120
|
+
previousActiveTabCountRef.current = { tab: activeTab, count: activeCount };
|
|
121
|
+
if (previous.tab !== activeTab) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (previous.count > 0 && activeCount === 0) {
|
|
125
|
+
const nextTab = findNextNonEmptyTabToRight(activeTab, counts);
|
|
126
|
+
if (nextTab !== null) {
|
|
127
|
+
navigation.selectTab(nextTab);
|
|
128
|
+
navigation.closeItem();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, [activeTab, activeCount, counts, navigation]);
|
|
132
|
+
|
|
112
133
|
const overlayStatusForSelected = ((): ConsoleOverlayStatus | null => {
|
|
113
134
|
if (selectedItem === null) {
|
|
114
135
|
return null;
|
|
@@ -202,6 +223,13 @@ export const ConsolePage = () => {
|
|
|
202
223
|
/>
|
|
203
224
|
) : (
|
|
204
225
|
<div className="console-detail-screen" ref={detailScreenRef}>
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
className="console-back-button"
|
|
229
|
+
onClick={closeItem}
|
|
230
|
+
>
|
|
231
|
+
← Back to list
|
|
232
|
+
</button>
|
|
205
233
|
<ConsoleItemDetailContainer
|
|
206
234
|
tab={activeTab}
|
|
207
235
|
item={selectedItem}
|
|
@@ -133,6 +133,17 @@ console bundle and the token-protected \`/api\` endpoints.
|
|
|
133
133
|
- [x] Token validation via \`?k=\` and the \`x-pv-token\` header
|
|
134
134
|
`;
|
|
135
135
|
|
|
136
|
+
export const consoleMarkdownImageBodyFixture = `## Screenshot
|
|
137
|
+
|
|
138
|
+
The failing screen is attached below.
|
|
139
|
+
|
|
140
|
+

|
|
141
|
+
|
|
142
|
+
For reference, the avatar comes from an external host:
|
|
143
|
+
|
|
144
|
+

|
|
145
|
+
`;
|
|
146
|
+
|
|
136
147
|
export const consoleMermaidBodyFixture = `Here is the request flow:
|
|
137
148
|
|
|
138
149
|
\`\`\`mermaid
|