github-issue-tower-defence-management 1.91.3 → 1.91.4
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 +7 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js +28 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleServer.js +2 -0
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-BU6p3cGU.css → index-0IuY3q4G.css} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-D04N09aG.js +100 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +72 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +32 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +49 -0
- package/src/adapter/entry-points/console/consoleServer.ts +3 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.stories.tsx +30 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.test.tsx +81 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentComposer.tsx +118 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +20 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +31 -5
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.test.tsx +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsolePanel.tsx +4 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +18 -14
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +40 -13
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +67 -29
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.stories.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.test.tsx +7 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.tsx +4 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.stories.tsx +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.test.tsx +43 -6
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemSummary.tsx +26 -3
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.tsx +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsolePullRequestReviewActions.tsx +7 -8
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +76 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.ts +111 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +37 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +5 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +8 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +26 -9
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +21 -20
- package/src/adapter/entry-points/console/ui/src/index.css +241 -39
- package/src/adapter/entry-points/console/ui-dist/assets/{index-BU6p3cGU.css → index-0IuY3q4G.css} +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-D04N09aG.js +100 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +0 -100
- package/src/adapter/entry-points/console/ui-dist/assets/index-Cn4xr5-h.js +0 -100
|
@@ -6,6 +6,12 @@ import { ConsoleTabList } from './ConsoleTabList';
|
|
|
6
6
|
const meta: Meta<typeof ConsoleTabList> = {
|
|
7
7
|
title: 'Console/ConsoleTabList',
|
|
8
8
|
component: ConsoleTabList,
|
|
9
|
+
args: {
|
|
10
|
+
pjcode: 'umino',
|
|
11
|
+
generatedAt: '2026-06-19T08:42:11.000Z',
|
|
12
|
+
tabHref: (tab: ConsoleTabName) => `/projects/umino/${tab}`,
|
|
13
|
+
onSelectTab: () => {},
|
|
14
|
+
},
|
|
9
15
|
};
|
|
10
16
|
|
|
11
17
|
export default meta;
|
|
@@ -14,53 +20,51 @@ type Story = StoryObj<typeof ConsoleTabList>;
|
|
|
14
20
|
|
|
15
21
|
const counts: Record<ConsoleTabName, number> = {
|
|
16
22
|
prs: 35,
|
|
17
|
-
triage:
|
|
18
|
-
unread:
|
|
23
|
+
triage: 132,
|
|
24
|
+
unread: 18,
|
|
19
25
|
'failed-preparation': 2,
|
|
20
|
-
'todo-by-human':
|
|
26
|
+
'todo-by-human': 66,
|
|
21
27
|
};
|
|
22
28
|
|
|
23
29
|
export const AllTabsWithCounts: Story = {
|
|
24
30
|
args: {
|
|
25
31
|
activeTab: 'prs',
|
|
26
32
|
counts,
|
|
27
|
-
onSelectTab: () => {},
|
|
28
33
|
},
|
|
29
34
|
};
|
|
30
35
|
|
|
31
|
-
export const
|
|
36
|
+
export const ZeroCountTabsStillShown: Story = {
|
|
32
37
|
args: {
|
|
33
38
|
activeTab: 'prs',
|
|
34
39
|
counts: {
|
|
35
40
|
prs: 35,
|
|
36
41
|
triage: 0,
|
|
37
|
-
unread:
|
|
42
|
+
unread: 18,
|
|
38
43
|
'failed-preparation': 0,
|
|
39
44
|
'todo-by-human': 0,
|
|
40
45
|
},
|
|
41
|
-
onSelectTab: () => {},
|
|
42
46
|
},
|
|
43
47
|
};
|
|
44
48
|
|
|
45
|
-
export const
|
|
49
|
+
export const ZeroCountActiveTab: Story = {
|
|
46
50
|
args: {
|
|
47
|
-
activeTab: '
|
|
51
|
+
activeTab: 'failed-preparation',
|
|
48
52
|
counts: {
|
|
49
53
|
prs: 35,
|
|
50
|
-
triage:
|
|
51
|
-
unread:
|
|
54
|
+
triage: 132,
|
|
55
|
+
unread: 18,
|
|
52
56
|
'failed-preparation': 0,
|
|
53
|
-
'todo-by-human':
|
|
57
|
+
'todo-by-human': 66,
|
|
54
58
|
},
|
|
55
|
-
onSelectTab: () => {},
|
|
56
59
|
},
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
export const Interactive: Story = {
|
|
60
|
-
render: () => {
|
|
63
|
+
render: (args) => {
|
|
61
64
|
const [activeTab, setActiveTab] = useState<ConsoleTabName>('prs');
|
|
62
65
|
return (
|
|
63
66
|
<ConsoleTabList
|
|
67
|
+
{...args}
|
|
64
68
|
activeTab={activeTab}
|
|
65
69
|
counts={counts}
|
|
66
70
|
onSelectTab={setActiveTab}
|
|
@@ -10,35 +10,61 @@ const counts: Record<ConsoleTabName, number> = {
|
|
|
10
10
|
'todo-by-human': 2,
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
const baseProps = {
|
|
14
|
+
pjcode: 'umino',
|
|
15
|
+
generatedAt: '2026-06-19T08:42:11.000Z',
|
|
16
|
+
tabHref: (tab: ConsoleTabName) => `/projects/umino/${tab}`,
|
|
17
|
+
onSelectTab: () => {},
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
describe('ConsoleTabList', () => {
|
|
14
|
-
it('
|
|
21
|
+
it('shows every tab including zero-count tabs', () => {
|
|
15
22
|
const { queryByText } = render(
|
|
16
|
-
<ConsoleTabList activeTab="prs" counts={counts}
|
|
23
|
+
<ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
|
|
17
24
|
);
|
|
18
25
|
expect(queryByText('Awaiting Quality Check')).not.toBeNull();
|
|
26
|
+
expect(queryByText('Triage')).not.toBeNull();
|
|
19
27
|
expect(queryByText('Unread')).not.toBeNull();
|
|
28
|
+
expect(queryByText('Failed Preparation')).not.toBeNull();
|
|
20
29
|
expect(queryByText('Todo by human')).not.toBeNull();
|
|
21
|
-
expect(queryByText('Triage')).toBeNull();
|
|
22
|
-
expect(queryByText('Failed Preparation')).toBeNull();
|
|
23
30
|
});
|
|
24
31
|
|
|
25
|
-
it('
|
|
26
|
-
const {
|
|
27
|
-
<ConsoleTabList
|
|
28
|
-
activeTab="triage"
|
|
29
|
-
counts={counts}
|
|
30
|
-
onSelectTab={() => {}}
|
|
31
|
-
/>,
|
|
32
|
+
it('marks the active tab and renders zero badges with the zero attribute', () => {
|
|
33
|
+
const { getByText } = render(
|
|
34
|
+
<ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
|
|
32
35
|
);
|
|
33
|
-
|
|
36
|
+
const triageBadge = getByText('Triage')
|
|
37
|
+
.closest('a')
|
|
38
|
+
?.querySelector('.console-tab-badge');
|
|
39
|
+
expect(triageBadge).toHaveAttribute('data-zero', 'true');
|
|
40
|
+
expect(triageBadge?.textContent).toBe('0');
|
|
41
|
+
expect(getByText('Awaiting Quality Check').closest('a')).toHaveAttribute(
|
|
42
|
+
'aria-current',
|
|
43
|
+
'page',
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders the active tab count sub-heading', () => {
|
|
48
|
+
const { getByText } = render(
|
|
49
|
+
<ConsoleTabList {...baseProps} activeTab="triage" counts={counts} />,
|
|
50
|
+
);
|
|
51
|
+
expect(getByText('0 items to triage')).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders the project code and snapshot time', () => {
|
|
55
|
+
const { getByText } = render(
|
|
56
|
+
<ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
|
|
57
|
+
);
|
|
58
|
+
expect(getByText('umino')).toBeInTheDocument();
|
|
59
|
+
expect(getByText('snapshot: 2026-06-19T08:42:11.000Z')).toBeInTheDocument();
|
|
34
60
|
});
|
|
35
61
|
|
|
36
62
|
it('uses the exact lowercase Todo by human label', () => {
|
|
37
63
|
const { getByText } = render(
|
|
38
64
|
<ConsoleTabList
|
|
65
|
+
{...baseProps}
|
|
39
66
|
activeTab="todo-by-human"
|
|
40
67
|
counts={counts}
|
|
41
|
-
onSelectTab={() => {}}
|
|
42
68
|
/>,
|
|
43
69
|
);
|
|
44
70
|
expect(getByText('Todo by human')).toBeInTheDocument();
|
|
@@ -48,6 +74,7 @@ describe('ConsoleTabList', () => {
|
|
|
48
74
|
const onSelectTab = jest.fn();
|
|
49
75
|
const { getByText } = render(
|
|
50
76
|
<ConsoleTabList
|
|
77
|
+
{...baseProps}
|
|
51
78
|
activeTab="prs"
|
|
52
79
|
counts={counts}
|
|
53
80
|
onSelectTab={onSelectTab}
|
|
@@ -3,39 +3,77 @@ import { CONSOLE_TABS, type ConsoleTabName } from '../../logic/types';
|
|
|
3
3
|
export type ConsoleTabBarProps = {
|
|
4
4
|
activeTab: ConsoleTabName;
|
|
5
5
|
counts: Record<ConsoleTabName, number>;
|
|
6
|
+
pjcode: string | null;
|
|
7
|
+
generatedAt: string | null;
|
|
8
|
+
tabHref: (tab: ConsoleTabName) => string;
|
|
6
9
|
onSelectTab: (tab: ConsoleTabName) => void;
|
|
7
10
|
};
|
|
8
11
|
|
|
12
|
+
const COUNT_HEADING_LABELS: Record<ConsoleTabName, string> = {
|
|
13
|
+
prs: 'items awaiting quality check',
|
|
14
|
+
triage: 'items to triage',
|
|
15
|
+
unread: 'unread items',
|
|
16
|
+
'failed-preparation': 'failed preparation items',
|
|
17
|
+
'todo-by-human': 'todo by human items',
|
|
18
|
+
};
|
|
19
|
+
|
|
9
20
|
export const ConsoleTabList = ({
|
|
10
21
|
activeTab,
|
|
11
22
|
counts,
|
|
23
|
+
pjcode,
|
|
24
|
+
generatedAt,
|
|
25
|
+
tabHref,
|
|
12
26
|
onSelectTab,
|
|
13
|
-
}: ConsoleTabBarProps) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
}: ConsoleTabBarProps) => {
|
|
28
|
+
const activeCount = counts[activeTab] ?? 0;
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<nav aria-label="Console tabs" className="console-tabbar">
|
|
32
|
+
{CONSOLE_TABS.map((tab) => {
|
|
33
|
+
const count = counts[tab.name] ?? 0;
|
|
34
|
+
const isActive = tab.name === activeTab;
|
|
35
|
+
return (
|
|
36
|
+
<a
|
|
37
|
+
key={tab.name}
|
|
38
|
+
href={tabHref(tab.name)}
|
|
39
|
+
className="console-tab"
|
|
40
|
+
data-active={isActive ? 'true' : undefined}
|
|
41
|
+
aria-current={isActive ? 'page' : undefined}
|
|
42
|
+
onClick={(event) => {
|
|
43
|
+
if (
|
|
44
|
+
event.defaultPrevented ||
|
|
45
|
+
event.button !== 0 ||
|
|
46
|
+
event.metaKey ||
|
|
47
|
+
event.ctrlKey ||
|
|
48
|
+
event.shiftKey ||
|
|
49
|
+
event.altKey
|
|
50
|
+
) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
onSelectTab(tab.name);
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<span className="console-tab-label">{tab.label}</span>
|
|
58
|
+
<span
|
|
59
|
+
className="console-tab-badge"
|
|
60
|
+
data-zero={count === 0 ? 'true' : undefined}
|
|
61
|
+
>
|
|
62
|
+
{count}
|
|
63
|
+
</span>
|
|
64
|
+
</a>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
{pjcode !== null && (
|
|
68
|
+
<span className="console-tab-pjname">{pjcode}</span>
|
|
69
|
+
)}
|
|
70
|
+
{generatedAt !== null && (
|
|
71
|
+
<span className="console-tab-geninfo">snapshot: {generatedAt}</span>
|
|
72
|
+
)}
|
|
73
|
+
</nav>
|
|
74
|
+
<p className="console-tab-count-heading">
|
|
75
|
+
{activeCount} {COUNT_HEADING_LABELS[activeTab]}
|
|
76
|
+
</p>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { ConsoleItemList } from './ConsoleItemList';
|
|
8
8
|
|
|
9
9
|
const rows = buildConsoleListRows(consoleListItemsFixture, {});
|
|
10
|
+
const now = Date.parse('2026-06-19T12:00:00.000Z');
|
|
10
11
|
|
|
11
12
|
describe('ConsoleItemList', () => {
|
|
12
13
|
it('renders group headers and items in array order', () => {
|
|
@@ -15,6 +16,7 @@ describe('ConsoleItemList', () => {
|
|
|
15
16
|
rows={rows}
|
|
16
17
|
storyColors={consoleStoryColorsFixture}
|
|
17
18
|
activeItemId={null}
|
|
19
|
+
now={now}
|
|
18
20
|
isLoading={false}
|
|
19
21
|
error={null}
|
|
20
22
|
onSelectItem={() => {}}
|
|
@@ -32,6 +34,7 @@ describe('ConsoleItemList', () => {
|
|
|
32
34
|
rows={rows}
|
|
33
35
|
storyColors={consoleStoryColorsFixture}
|
|
34
36
|
activeItemId={null}
|
|
37
|
+
now={now}
|
|
35
38
|
isLoading={false}
|
|
36
39
|
error={null}
|
|
37
40
|
onSelectItem={onSelectItem}
|
|
@@ -49,6 +52,7 @@ describe('ConsoleItemList', () => {
|
|
|
49
52
|
rows={[]}
|
|
50
53
|
storyColors={{}}
|
|
51
54
|
activeItemId={null}
|
|
55
|
+
now={now}
|
|
52
56
|
isLoading
|
|
53
57
|
error={null}
|
|
54
58
|
onSelectItem={() => {}}
|
|
@@ -63,12 +67,13 @@ describe('ConsoleItemList', () => {
|
|
|
63
67
|
rows={[]}
|
|
64
68
|
storyColors={{}}
|
|
65
69
|
activeItemId={null}
|
|
70
|
+
now={now}
|
|
66
71
|
isLoading={false}
|
|
67
72
|
error={null}
|
|
68
73
|
onSelectItem={() => {}}
|
|
69
74
|
/>,
|
|
70
75
|
);
|
|
71
|
-
expect(getByText('No items
|
|
76
|
+
expect(getByText('No items')).toBeInTheDocument();
|
|
72
77
|
});
|
|
73
78
|
|
|
74
79
|
it('shows the error state', () => {
|
|
@@ -77,6 +82,7 @@ describe('ConsoleItemList', () => {
|
|
|
77
82
|
rows={[]}
|
|
78
83
|
storyColors={{}}
|
|
79
84
|
activeItemId={null}
|
|
85
|
+
now={now}
|
|
80
86
|
isLoading={false}
|
|
81
87
|
error="HTTP 404"
|
|
82
88
|
onSelectItem={() => {}}
|
package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.tsx
CHANGED
|
@@ -13,6 +13,7 @@ export type ConsoleListViewProps = {
|
|
|
13
13
|
rows: ConsoleListRow[];
|
|
14
14
|
storyColors: ConsoleStoryColorSource;
|
|
15
15
|
activeItemId: string | null;
|
|
16
|
+
now: number;
|
|
16
17
|
isLoading: boolean;
|
|
17
18
|
error: string | null;
|
|
18
19
|
onSelectItem: (item: ConsoleListItem) => void;
|
|
@@ -22,6 +23,7 @@ export const ConsoleItemList = ({
|
|
|
22
23
|
rows,
|
|
23
24
|
storyColors,
|
|
24
25
|
activeItemId,
|
|
26
|
+
now,
|
|
25
27
|
isLoading,
|
|
26
28
|
error,
|
|
27
29
|
onSelectItem,
|
|
@@ -39,7 +41,7 @@ export const ConsoleItemList = ({
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
if (rows.length === 0) {
|
|
42
|
-
return <p className="console-list-
|
|
44
|
+
return <p className="console-list-empty">No items</p>;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
return (
|
|
@@ -58,6 +60,7 @@ export const ConsoleItemList = ({
|
|
|
58
60
|
<ConsoleItemSummary
|
|
59
61
|
item={row.item}
|
|
60
62
|
isActive={row.item.itemId === activeItemId}
|
|
63
|
+
now={now}
|
|
61
64
|
onSelect={onSelectItem}
|
|
62
65
|
/>
|
|
63
66
|
</li>
|
|
@@ -5,7 +5,7 @@ import { ConsoleItemSummary } from './ConsoleItemSummary';
|
|
|
5
5
|
const meta: Meta<typeof ConsoleItemSummary> = {
|
|
6
6
|
title: 'Console/ConsoleItemSummary',
|
|
7
7
|
component: ConsoleItemSummary,
|
|
8
|
-
args: { onSelect: () => {} },
|
|
8
|
+
args: { onSelect: () => {}, now: Date.parse('2026-06-19T12:00:00.000Z') },
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export default meta;
|
|
@@ -1,34 +1,66 @@
|
|
|
1
1
|
import { fireEvent, render } from '@testing-library/react';
|
|
2
|
+
import { formatFullTimestamp } from '../../logic/relativeTime';
|
|
2
3
|
import { consoleListItemsFixture } from '../../testing/fixtures';
|
|
3
4
|
import { ConsoleItemSummary } from './ConsoleItemSummary';
|
|
4
5
|
|
|
6
|
+
const now = Date.parse('2026-06-19T12:00:00.000Z');
|
|
5
7
|
const prItem = consoleListItemsFixture[0];
|
|
6
8
|
const issueItem = consoleListItemsFixture[2];
|
|
7
9
|
|
|
8
10
|
describe('ConsoleItemSummary', () => {
|
|
9
|
-
it('renders
|
|
11
|
+
it('renders the number, repository and PR type pills for a pull request', () => {
|
|
10
12
|
const { getByText } = render(
|
|
11
|
-
<ConsoleItemSummary
|
|
13
|
+
<ConsoleItemSummary
|
|
14
|
+
item={prItem}
|
|
15
|
+
isActive={false}
|
|
16
|
+
now={now}
|
|
17
|
+
onSelect={() => {}}
|
|
18
|
+
/>,
|
|
12
19
|
);
|
|
13
|
-
expect(getByText(
|
|
20
|
+
expect(getByText(`#${prItem.number}`)).toBeInTheDocument();
|
|
21
|
+
expect(getByText(prItem.repo)).toBeInTheDocument();
|
|
22
|
+
expect(getByText('PR')).toBeInTheDocument();
|
|
14
23
|
expect(getByText(prItem.title)).toBeInTheDocument();
|
|
15
24
|
});
|
|
16
25
|
|
|
17
|
-
it('renders
|
|
26
|
+
it('renders the Issue type pill for an issue', () => {
|
|
18
27
|
const { getByText } = render(
|
|
19
28
|
<ConsoleItemSummary
|
|
20
29
|
item={issueItem}
|
|
21
30
|
isActive={false}
|
|
31
|
+
now={now}
|
|
22
32
|
onSelect={() => {}}
|
|
23
33
|
/>,
|
|
24
34
|
);
|
|
25
35
|
expect(getByText(`#${issueItem.number}`)).toBeInTheDocument();
|
|
36
|
+
expect(getByText('Issue')).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders the opened relative time with the full timestamp title', () => {
|
|
40
|
+
const { getByText } = render(
|
|
41
|
+
<ConsoleItemSummary
|
|
42
|
+
item={prItem}
|
|
43
|
+
isActive={false}
|
|
44
|
+
now={now}
|
|
45
|
+
onSelect={() => {}}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
const createdAt = getByText('2 days ago');
|
|
49
|
+
expect(createdAt).toHaveAttribute(
|
|
50
|
+
'title',
|
|
51
|
+
formatFullTimestamp(prItem.createdAt),
|
|
52
|
+
);
|
|
26
53
|
});
|
|
27
54
|
|
|
28
55
|
it('reports the item on click', () => {
|
|
29
56
|
const onSelect = jest.fn();
|
|
30
57
|
const { getByRole } = render(
|
|
31
|
-
<ConsoleItemSummary
|
|
58
|
+
<ConsoleItemSummary
|
|
59
|
+
item={prItem}
|
|
60
|
+
isActive={false}
|
|
61
|
+
now={now}
|
|
62
|
+
onSelect={onSelect}
|
|
63
|
+
/>,
|
|
32
64
|
);
|
|
33
65
|
fireEvent.click(getByRole('button'));
|
|
34
66
|
expect(onSelect).toHaveBeenCalledWith(prItem);
|
|
@@ -36,7 +68,12 @@ describe('ConsoleItemSummary', () => {
|
|
|
36
68
|
|
|
37
69
|
it('marks the active row', () => {
|
|
38
70
|
const { getByRole } = render(
|
|
39
|
-
<ConsoleItemSummary
|
|
71
|
+
<ConsoleItemSummary
|
|
72
|
+
item={prItem}
|
|
73
|
+
isActive
|
|
74
|
+
now={now}
|
|
75
|
+
onSelect={() => {}}
|
|
76
|
+
/>,
|
|
40
77
|
);
|
|
41
78
|
expect(getByRole('button')).toHaveAttribute('data-active', 'true');
|
|
42
79
|
});
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatFullTimestamp,
|
|
3
|
+
formatRelativeTime,
|
|
4
|
+
} from '../../logic/relativeTime';
|
|
1
5
|
import type { ConsoleListItem } from '../../logic/types';
|
|
2
6
|
import { ConsoleItemIcon } from '../detail/ConsoleItemIcon';
|
|
3
7
|
|
|
4
8
|
export type ConsoleListItemRowProps = {
|
|
5
9
|
item: ConsoleListItem;
|
|
6
10
|
isActive: boolean;
|
|
11
|
+
now: number;
|
|
7
12
|
onSelect: (item: ConsoleListItem) => void;
|
|
8
13
|
};
|
|
9
14
|
|
|
10
15
|
export const ConsoleItemSummary = ({
|
|
11
16
|
item,
|
|
12
17
|
isActive,
|
|
18
|
+
now,
|
|
13
19
|
onSelect,
|
|
14
20
|
}: ConsoleListItemRowProps) => (
|
|
15
21
|
<button
|
|
@@ -26,9 +32,26 @@ export const ConsoleItemSummary = ({
|
|
|
26
32
|
isDraft={false}
|
|
27
33
|
stateReason=""
|
|
28
34
|
/>
|
|
29
|
-
<span className="console-item-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
<span className="console-item-meta">
|
|
36
|
+
<span className="console-item-title">{item.title}</span>
|
|
37
|
+
<span className="console-item-sub">
|
|
38
|
+
<span className="console-item-pill">#{item.number}</span>
|
|
39
|
+
{item.repo !== '' && (
|
|
40
|
+
<span className="console-item-pill">{item.repo}</span>
|
|
41
|
+
)}
|
|
42
|
+
<span className="console-item-pill">{item.isPr ? 'PR' : 'Issue'}</span>
|
|
43
|
+
{item.createdAt !== '' && (
|
|
44
|
+
<span className="console-item-opened">
|
|
45
|
+
{' · opened '}
|
|
46
|
+
<span
|
|
47
|
+
className="console-item-createdat"
|
|
48
|
+
title={formatFullTimestamp(item.createdAt)}
|
|
49
|
+
>
|
|
50
|
+
{formatRelativeTime(item.createdAt, now)}
|
|
51
|
+
</span>
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</span>
|
|
32
55
|
</span>
|
|
33
56
|
</button>
|
|
34
57
|
);
|
|
@@ -12,14 +12,14 @@ export const ConsoleNextActionDateActions = ({
|
|
|
12
12
|
<div className="console-op-group">
|
|
13
13
|
<button
|
|
14
14
|
type="button"
|
|
15
|
-
className="console-op-button"
|
|
15
|
+
className="console-op-button console-op-button-snooze"
|
|
16
16
|
onClick={() => onSetNextActionDate('snooze_1day')}
|
|
17
17
|
>
|
|
18
18
|
+1 day
|
|
19
19
|
</button>
|
|
20
20
|
<button
|
|
21
21
|
type="button"
|
|
22
|
-
className="console-op-button"
|
|
22
|
+
className="console-op-button console-op-button-snooze"
|
|
23
23
|
onClick={() => onSetNextActionDate('snooze_1week')}
|
|
24
24
|
>
|
|
25
25
|
{isTodoByHuman ? '+1 week and skip' : '+1 week'}
|
|
@@ -7,24 +7,23 @@ export type ConsolePullRequestReviewGroupProps = {
|
|
|
7
7
|
const REVIEW_BUTTONS: {
|
|
8
8
|
action: ConsoleReviewAction;
|
|
9
9
|
label: string;
|
|
10
|
-
|
|
10
|
+
variant: string;
|
|
11
11
|
}[] = [
|
|
12
|
-
{ action: 'unnecessary', label: 'Unnecessary',
|
|
13
|
-
{ action: 'totally_wrong', label: 'Totally wrong',
|
|
14
|
-
{ action: 'request_changes', label: 'Reject',
|
|
15
|
-
{ action: 'approve', label: 'Approve',
|
|
12
|
+
{ action: 'unnecessary', label: 'Unnecessary', variant: 'unneeded' },
|
|
13
|
+
{ action: 'totally_wrong', label: 'Totally wrong', variant: 'wrong' },
|
|
14
|
+
{ action: 'request_changes', label: 'Reject', variant: 'reject' },
|
|
15
|
+
{ action: 'approve', label: 'Approve', variant: 'approve' },
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
export const ConsolePullRequestReviewActions = ({
|
|
19
19
|
onReview,
|
|
20
20
|
}: ConsolePullRequestReviewGroupProps) => (
|
|
21
|
-
<div className="console-op-group">
|
|
21
|
+
<div className="console-op-group console-op-group-review">
|
|
22
22
|
{REVIEW_BUTTONS.map((button) => (
|
|
23
23
|
<button
|
|
24
24
|
key={button.action}
|
|
25
25
|
type="button"
|
|
26
|
-
className=
|
|
27
|
-
style={{ color: button.color, borderColor: button.color }}
|
|
26
|
+
className={`console-op-button console-op-button-${button.variant}`}
|
|
28
27
|
onClick={() => onReview(button.action)}
|
|
29
28
|
>
|
|
30
29
|
{button.label}
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import {
|
|
3
|
+
parseItemKeyFromHash,
|
|
4
|
+
parseTabFromPath,
|
|
5
|
+
useConsoleNavigation,
|
|
6
|
+
} from './useConsoleNavigation';
|
|
7
|
+
|
|
8
|
+
describe('parseTabFromPath', () => {
|
|
9
|
+
it('reads a known tab from the project path', () => {
|
|
10
|
+
expect(parseTabFromPath('/projects/umino/triage')).toBe('triage');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns null for an unknown tab segment', () => {
|
|
14
|
+
expect(parseTabFromPath('/projects/umino/unknown')).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns null when there is no tab segment', () => {
|
|
18
|
+
expect(parseTabFromPath('/projects/umino')).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('parseItemKeyFromHash', () => {
|
|
23
|
+
it('decodes the item key from the hash', () => {
|
|
24
|
+
expect(parseItemKeyFromHash('#item/PVTI_lADO%20123')).toBe('PVTI_lADO 123');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns null when the hash is not an item hash', () => {
|
|
28
|
+
expect(parseItemKeyFromHash('#other')).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('useConsoleNavigation', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
window.history.replaceState({}, '', '/projects/umino/prs?k=token');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('reads the active tab from the path and no selected item', () => {
|
|
38
|
+
const { result } = renderHook(() => useConsoleNavigation('umino'));
|
|
39
|
+
expect(result.current.activeTab).toBe('prs');
|
|
40
|
+
expect(result.current.selectedItemKey).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('builds a project tab href', () => {
|
|
44
|
+
const { result } = renderHook(() => useConsoleNavigation('umino'));
|
|
45
|
+
expect(result.current.tabHref('triage')).toBe('/projects/umino/triage');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('selects a tab and updates the path', () => {
|
|
49
|
+
const { result } = renderHook(() => useConsoleNavigation('umino'));
|
|
50
|
+
act(() => {
|
|
51
|
+
result.current.selectTab('unread');
|
|
52
|
+
});
|
|
53
|
+
expect(result.current.activeTab).toBe('unread');
|
|
54
|
+
expect(window.location.pathname).toBe('/projects/umino/unread');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('opens an item and reflects it in the hash', () => {
|
|
58
|
+
const { result } = renderHook(() => useConsoleNavigation('umino'));
|
|
59
|
+
act(() => {
|
|
60
|
+
result.current.openItem('PVTI_open');
|
|
61
|
+
});
|
|
62
|
+
expect(result.current.selectedItemKey).toBe('PVTI_open');
|
|
63
|
+
expect(window.location.hash).toBe('#item/PVTI_open');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('closes an item and clears the hash', () => {
|
|
67
|
+
window.history.replaceState({}, '', '/projects/umino/prs#item/PVTI_open');
|
|
68
|
+
const { result } = renderHook(() => useConsoleNavigation('umino'));
|
|
69
|
+
expect(result.current.selectedItemKey).toBe('PVTI_open');
|
|
70
|
+
act(() => {
|
|
71
|
+
result.current.closeItem();
|
|
72
|
+
});
|
|
73
|
+
expect(result.current.selectedItemKey).toBeNull();
|
|
74
|
+
expect(window.location.hash).toBe('');
|
|
75
|
+
});
|
|
76
|
+
});
|