github-issue-tower-defence-management 1.86.0 → 1.87.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/.eslintrc.cjs +5 -1
- package/.github/workflows/console-ui.yml +47 -0
- package/.prettierignore +3 -0
- package/CHANGELOG.md +8 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +13 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +306 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/package.json +22 -2
- package/scripts/copyConsoleUiDist.mjs +35 -0
- package/src/adapter/entry-points/console/ui/.storybook/main.ts +12 -0
- package/src/adapter/entry-points/console/ui/.storybook/preview.ts +15 -0
- package/src/adapter/entry-points/console/ui/biome.json +47 -0
- package/src/adapter/entry-points/console/ui/components.json +20 -0
- package/src/adapter/entry-points/console/ui/index.html +12 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/badge.stories.tsx +35 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/badge.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/button.stories.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/button.tsx +50 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +44 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +58 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +32 -0
- package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +47 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +65 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.ts +64 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +19 -0
- package/src/adapter/entry-points/console/ui/src/features/console/types.ts +69 -0
- package/src/adapter/entry-points/console/ui/src/index.css +31 -0
- package/src/adapter/entry-points/console/ui/src/lib/utils.ts +4 -0
- package/src/adapter/entry-points/console/ui/src/main.tsx +15 -0
- package/src/adapter/entry-points/console/ui/src/vite-env.d.ts +1 -0
- package/src/adapter/entry-points/console/ui/tsconfig.json +24 -0
- package/src/adapter/entry-points/console/ui/vite.config.ts +19 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +13 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +630 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +492 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +51 -0
- package/tsconfig.build.json +7 -1
- package/tsconfig.json +6 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +18 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +47 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>TDPM Console</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Badge } from './badge';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Badge> = {
|
|
5
|
+
title: 'UI/Badge',
|
|
6
|
+
component: Badge,
|
|
7
|
+
args: {
|
|
8
|
+
children: 'PR',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof Badge>;
|
|
15
|
+
|
|
16
|
+
export const Pr: Story = {
|
|
17
|
+
args: {
|
|
18
|
+
variant: 'default',
|
|
19
|
+
children: 'PR',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Issue: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
variant: 'secondary',
|
|
26
|
+
children: 'Issue',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Outline: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
variant: 'outline',
|
|
33
|
+
children: 'story: TDPM Console port',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'border-transparent bg-primary text-primary-foreground',
|
|
11
|
+
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
12
|
+
outline: 'text-foreground',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
variant: 'default',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export type BadgeProps = React.HTMLAttributes<HTMLDivElement> &
|
|
22
|
+
VariantProps<typeof badgeVariants>;
|
|
23
|
+
|
|
24
|
+
export const Badge = ({ className, variant, ...props }: BadgeProps) => (
|
|
25
|
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export { badgeVariants };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Button } from './button';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Button> = {
|
|
5
|
+
title: 'UI/Button',
|
|
6
|
+
component: Button,
|
|
7
|
+
args: {
|
|
8
|
+
children: 'Awaiting Quality Check',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof Button>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {};
|
|
17
|
+
|
|
18
|
+
export const Ghost: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
variant: 'ghost',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Outline: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
variant: 'outline',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Small: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
size: 'sm',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
12
|
+
outline:
|
|
13
|
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
14
|
+
secondary:
|
|
15
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
16
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
default: 'h-9 px-4 py-2',
|
|
20
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
21
|
+
lg: 'h-10 rounded-md px-8',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: 'default',
|
|
26
|
+
size: 'default',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
32
|
+
VariantProps<typeof buttonVariants> & {
|
|
33
|
+
asChild?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
37
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
38
|
+
const Comp = asChild ? Slot : 'button';
|
|
39
|
+
return (
|
|
40
|
+
<Comp
|
|
41
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
42
|
+
ref={ref}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
Button.displayName = 'Button';
|
|
49
|
+
|
|
50
|
+
export { buttonVariants };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { consoleListItemsFixture } from '../fixtures';
|
|
3
|
+
import { ConsoleListView } from './ConsoleListView';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof ConsoleListView> = {
|
|
6
|
+
title: 'Console/ConsoleListView',
|
|
7
|
+
component: ConsoleListView,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof ConsoleListView>;
|
|
13
|
+
|
|
14
|
+
export const WithItems: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
items: consoleListItemsFixture,
|
|
17
|
+
isLoading: false,
|
|
18
|
+
error: null,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Loading: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
items: [],
|
|
25
|
+
isLoading: true,
|
|
26
|
+
error: null,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Empty: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
items: [],
|
|
33
|
+
isLoading: false,
|
|
34
|
+
error: null,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const ErrorState: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
items: [],
|
|
41
|
+
isLoading: false,
|
|
42
|
+
error: 'HTTP 404',
|
|
43
|
+
},
|
|
44
|
+
};
|
package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Badge } from '@/components/ui/badge';
|
|
2
|
+
import type { ConsoleListItem } from '../types';
|
|
3
|
+
|
|
4
|
+
export type ConsoleListViewProps = {
|
|
5
|
+
items: ConsoleListItem[];
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
error: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ConsoleListView = ({
|
|
11
|
+
items,
|
|
12
|
+
isLoading,
|
|
13
|
+
error,
|
|
14
|
+
}: ConsoleListViewProps) => {
|
|
15
|
+
if (error !== null) {
|
|
16
|
+
return (
|
|
17
|
+
<p role="alert" className="p-4 text-sm text-destructive">
|
|
18
|
+
Failed to load list: {error}
|
|
19
|
+
</p>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isLoading) {
|
|
24
|
+
return <p className="p-4 text-sm text-muted-foreground">Loading list...</p>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (items.length === 0) {
|
|
28
|
+
return <p className="p-4 text-sm text-muted-foreground">No items.</p>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<ul className="divide-y divide-border">
|
|
33
|
+
{items.map((item) => (
|
|
34
|
+
<li key={item.itemId} className="flex flex-col gap-1 p-3">
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<Badge variant={item.isPr ? 'default' : 'secondary'}>
|
|
37
|
+
{item.isPr ? 'PR' : 'Issue'}
|
|
38
|
+
</Badge>
|
|
39
|
+
<a
|
|
40
|
+
href={item.url}
|
|
41
|
+
className="font-medium underline-offset-2 hover:underline"
|
|
42
|
+
>
|
|
43
|
+
{item.title}
|
|
44
|
+
</a>
|
|
45
|
+
<span className="text-sm text-muted-foreground">
|
|
46
|
+
#{item.number}
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
50
|
+
<span>{item.repo}</span>
|
|
51
|
+
{item.story !== '' && <span>story: {item.story}</span>}
|
|
52
|
+
<span>{new Date(item.createdAt).toISOString()}</span>
|
|
53
|
+
</div>
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ul>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import type { ConsoleTabName } from '../types';
|
|
4
|
+
import { ConsoleTabBar } from './ConsoleTabBar';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof ConsoleTabBar> = {
|
|
7
|
+
title: 'Console/ConsoleTabBar',
|
|
8
|
+
component: ConsoleTabBar,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
|
|
13
|
+
type Story = StoryObj<typeof ConsoleTabBar>;
|
|
14
|
+
|
|
15
|
+
export const PrsActive: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
activeTab: 'prs',
|
|
18
|
+
onSelectTab: () => undefined,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const TriageActive: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
activeTab: 'triage',
|
|
25
|
+
onSelectTab: () => undefined,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Interactive: Story = {
|
|
30
|
+
render: () => {
|
|
31
|
+
const [activeTab, setActiveTab] = useState<ConsoleTabName>('prs');
|
|
32
|
+
return <ConsoleTabBar activeTab={activeTab} onSelectTab={setActiveTab} />;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import { CONSOLE_TABS, type ConsoleTabName } from '../types';
|
|
4
|
+
|
|
5
|
+
export type ConsoleTabBarProps = {
|
|
6
|
+
activeTab: ConsoleTabName;
|
|
7
|
+
onSelectTab: (tab: ConsoleTabName) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ConsoleTabBar = ({
|
|
11
|
+
activeTab,
|
|
12
|
+
onSelectTab,
|
|
13
|
+
}: ConsoleTabBarProps) => (
|
|
14
|
+
<nav
|
|
15
|
+
aria-label="Console tabs"
|
|
16
|
+
className="flex flex-wrap gap-1 border-b border-border p-2"
|
|
17
|
+
>
|
|
18
|
+
{CONSOLE_TABS.map((tab) => (
|
|
19
|
+
<Button
|
|
20
|
+
key={tab.name}
|
|
21
|
+
type="button"
|
|
22
|
+
size="sm"
|
|
23
|
+
variant={tab.name === activeTab ? 'default' : 'ghost'}
|
|
24
|
+
aria-current={tab.name === activeTab ? 'page' : undefined}
|
|
25
|
+
className={cn(tab.name === activeTab && 'font-semibold')}
|
|
26
|
+
onClick={() => onSelectTab(tab.name)}
|
|
27
|
+
>
|
|
28
|
+
{tab.label}
|
|
29
|
+
</Button>
|
|
30
|
+
))}
|
|
31
|
+
</nav>
|
|
32
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ConsoleListItem } from './types';
|
|
2
|
+
|
|
3
|
+
export const consoleListItemsFixture: ConsoleListItem[] = [
|
|
4
|
+
{
|
|
5
|
+
number: 844,
|
|
6
|
+
title: 'Add serveConsole server skeleton under entry-points',
|
|
7
|
+
url: 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/851',
|
|
8
|
+
repo: 'HiromiShikata/npm-cli-github-issue-tower-defence-management',
|
|
9
|
+
nameWithOwner:
|
|
10
|
+
'HiromiShikata/npm-cli-github-issue-tower-defence-management',
|
|
11
|
+
projectItemId: 'PVTI_lADOABCD1234zgABCD01',
|
|
12
|
+
itemId: 'PVTI_lADOABCD1234zgABCD01',
|
|
13
|
+
isPr: true,
|
|
14
|
+
story: 'TDPM Console port',
|
|
15
|
+
labels: ['claude'],
|
|
16
|
+
createdAt: '2026-06-17T02:14:33.000Z',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
number: 845,
|
|
20
|
+
title:
|
|
21
|
+
'Scaffold React console UI under entry-points with build bundling and minimal tab view',
|
|
22
|
+
url: 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/845',
|
|
23
|
+
repo: 'HiromiShikata/npm-cli-github-issue-tower-defence-management',
|
|
24
|
+
nameWithOwner:
|
|
25
|
+
'HiromiShikata/npm-cli-github-issue-tower-defence-management',
|
|
26
|
+
projectItemId: 'PVTI_lADOABCD1234zgABCD02',
|
|
27
|
+
itemId: 'PVTI_lADOABCD1234zgABCD02',
|
|
28
|
+
isPr: false,
|
|
29
|
+
story: 'TDPM Console port',
|
|
30
|
+
labels: ['claude'],
|
|
31
|
+
createdAt: '2026-06-17T05:48:09.000Z',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
number: 778,
|
|
35
|
+
title: 'Add Sonnet to Opus weekly-limit fallback routing per token',
|
|
36
|
+
url: 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/778',
|
|
37
|
+
repo: 'HiromiShikata/npm-cli-github-issue-tower-defence-management',
|
|
38
|
+
nameWithOwner:
|
|
39
|
+
'HiromiShikata/npm-cli-github-issue-tower-defence-management',
|
|
40
|
+
projectItemId: 'PVTI_lADOABCD1234zgABCD03',
|
|
41
|
+
itemId: 'PVTI_lADOABCD1234zgABCD03',
|
|
42
|
+
isPr: false,
|
|
43
|
+
story: 'regular / workflow improvement',
|
|
44
|
+
labels: [],
|
|
45
|
+
createdAt: '2026-06-12T23:01:55.000Z',
|
|
46
|
+
},
|
|
47
|
+
];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { ConsoleListItem, ConsoleTabName } from '../types';
|
|
3
|
+
import { useConsoleToken } from './useConsoleToken';
|
|
4
|
+
|
|
5
|
+
const buildListUrl = (tab: ConsoleTabName): string => `./${tab}/list.json`;
|
|
6
|
+
|
|
7
|
+
const extractItems = (payload: unknown): ConsoleListItem[] => {
|
|
8
|
+
if (
|
|
9
|
+
payload !== null &&
|
|
10
|
+
typeof payload === 'object' &&
|
|
11
|
+
'items' in payload &&
|
|
12
|
+
Array.isArray((payload as { items: unknown }).items)
|
|
13
|
+
) {
|
|
14
|
+
return (payload as { items: ConsoleListItem[] }).items;
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ConsoleListState = {
|
|
20
|
+
items: ConsoleListItem[];
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
error: string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const useConsoleList = (tab: ConsoleTabName): ConsoleListState => {
|
|
26
|
+
const { appendToken } = useConsoleToken();
|
|
27
|
+
const [items, setItems] = useState<ConsoleListItem[]>([]);
|
|
28
|
+
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
setIsLoading(true);
|
|
34
|
+
setError(null);
|
|
35
|
+
|
|
36
|
+
const url = appendToken(buildListUrl(tab));
|
|
37
|
+
fetch(url)
|
|
38
|
+
.then(async (response) => {
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`HTTP ${response.status}`);
|
|
41
|
+
}
|
|
42
|
+
return response.json();
|
|
43
|
+
})
|
|
44
|
+
.then((payload: unknown) => {
|
|
45
|
+
if (cancelled) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setItems(extractItems(payload));
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
})
|
|
51
|
+
.catch((cause: unknown) => {
|
|
52
|
+
if (cancelled) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
56
|
+
setIsLoading(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
cancelled = true;
|
|
61
|
+
};
|
|
62
|
+
}, [tab, appendToken]);
|
|
63
|
+
|
|
64
|
+
return { items, isLoading, error };
|
|
65
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const CONSOLE_TOKEN_STORAGE_KEY = 'tdpm-console-token';
|
|
4
|
+
|
|
5
|
+
const readTokenFromLocation = (search: string): string | null => {
|
|
6
|
+
const params = new URLSearchParams(search);
|
|
7
|
+
const tokenFromQuery = params.get('k');
|
|
8
|
+
if (tokenFromQuery !== null && tokenFromQuery !== '') {
|
|
9
|
+
return tokenFromQuery;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const readPersistedToken = (): string | null => {
|
|
15
|
+
if (typeof localStorage === 'undefined') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const persisted = localStorage.getItem(CONSOLE_TOKEN_STORAGE_KEY);
|
|
19
|
+
return persisted !== null && persisted !== '' ? persisted : null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ConsoleTokenState = {
|
|
23
|
+
token: string | null;
|
|
24
|
+
appendToken: (dataUrl: string) => string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const useConsoleToken = (): ConsoleTokenState => {
|
|
28
|
+
const [token, setToken] = useState<string | null>(() => {
|
|
29
|
+
const search = typeof window === 'undefined' ? '' : window.location.search;
|
|
30
|
+
return readTokenFromLocation(search) ?? readPersistedToken();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (token === null) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (typeof localStorage !== 'undefined') {
|
|
38
|
+
localStorage.setItem(CONSOLE_TOKEN_STORAGE_KEY, token);
|
|
39
|
+
}
|
|
40
|
+
}, [token]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (typeof window === 'undefined') {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const tokenFromQuery = readTokenFromLocation(window.location.search);
|
|
47
|
+
if (tokenFromQuery !== null) {
|
|
48
|
+
setToken(tokenFromQuery);
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const appendToken = useCallback(
|
|
53
|
+
(dataUrl: string): string => {
|
|
54
|
+
if (token === null) {
|
|
55
|
+
return dataUrl;
|
|
56
|
+
}
|
|
57
|
+
const separator = dataUrl.includes('?') ? '&' : '?';
|
|
58
|
+
return `${dataUrl}${separator}k=${encodeURIComponent(token)}`;
|
|
59
|
+
},
|
|
60
|
+
[token],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return { token, appendToken };
|
|
64
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { ConsoleListView } from '../components/ConsoleListView';
|
|
3
|
+
import { ConsoleTabBar } from '../components/ConsoleTabBar';
|
|
4
|
+
import { useConsoleList } from '../hooks/useConsoleList';
|
|
5
|
+
import { CONSOLE_TABS, type ConsoleTabName } from '../types';
|
|
6
|
+
|
|
7
|
+
export const ConsolePage = () => {
|
|
8
|
+
const [activeTab, setActiveTab] = useState<ConsoleTabName>(
|
|
9
|
+
CONSOLE_TABS[0].name,
|
|
10
|
+
);
|
|
11
|
+
const { items, isLoading, error } = useConsoleList(activeTab);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<main className="mx-auto flex max-w-3xl flex-col">
|
|
15
|
+
<ConsoleTabBar activeTab={activeTab} onSelectTab={setActiveTab} />
|
|
16
|
+
<ConsoleListView items={items} isLoading={isLoading} error={error} />
|
|
17
|
+
</main>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type ConsoleColor =
|
|
2
|
+
| 'GRAY'
|
|
3
|
+
| 'BLUE'
|
|
4
|
+
| 'GREEN'
|
|
5
|
+
| 'YELLOW'
|
|
6
|
+
| 'ORANGE'
|
|
7
|
+
| 'RED'
|
|
8
|
+
| 'PINK'
|
|
9
|
+
| 'PURPLE';
|
|
10
|
+
|
|
11
|
+
export type ConsoleListItem = {
|
|
12
|
+
number: number;
|
|
13
|
+
title: string;
|
|
14
|
+
url: string;
|
|
15
|
+
repo: string;
|
|
16
|
+
nameWithOwner: string;
|
|
17
|
+
projectItemId: string;
|
|
18
|
+
itemId: string;
|
|
19
|
+
isPr: boolean;
|
|
20
|
+
story: string;
|
|
21
|
+
labels: string[];
|
|
22
|
+
createdAt: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ConsoleFieldOption = {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
color: ConsoleColor;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ConsoleStatusTab = {
|
|
32
|
+
pjcode: string;
|
|
33
|
+
generatedAt: string;
|
|
34
|
+
statusOptions: ConsoleFieldOption[];
|
|
35
|
+
storyOrder: string[];
|
|
36
|
+
storyColors: Record<string, { color: ConsoleColor }>;
|
|
37
|
+
items: ConsoleListItem[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ConsoleTriageTab = {
|
|
41
|
+
pjcode: string;
|
|
42
|
+
generatedAt: string;
|
|
43
|
+
storyOptions: ConsoleFieldOption[];
|
|
44
|
+
storyOrder: string[];
|
|
45
|
+
storyColors: Record<string, ConsoleColor>;
|
|
46
|
+
items: ConsoleListItem[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type ConsoleTabData = ConsoleStatusTab | ConsoleTriageTab;
|
|
50
|
+
|
|
51
|
+
export type ConsoleTabName =
|
|
52
|
+
| 'prs'
|
|
53
|
+
| 'triage'
|
|
54
|
+
| 'unread'
|
|
55
|
+
| 'failed-preparation'
|
|
56
|
+
| 'todo-by-human';
|
|
57
|
+
|
|
58
|
+
export type ConsoleTab = {
|
|
59
|
+
name: ConsoleTabName;
|
|
60
|
+
label: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const CONSOLE_TABS: ConsoleTab[] = [
|
|
64
|
+
{ name: 'prs', label: 'Awaiting Quality Check' },
|
|
65
|
+
{ name: 'triage', label: 'Triage' },
|
|
66
|
+
{ name: 'unread', label: 'Unread' },
|
|
67
|
+
{ name: 'failed-preparation', label: 'Failed Preparation' },
|
|
68
|
+
{ name: 'todo-by-human', label: 'Todo By Human' },
|
|
69
|
+
];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-border: hsl(0 0% 89.8%);
|
|
5
|
+
--color-input: hsl(0 0% 89.8%);
|
|
6
|
+
--color-ring: hsl(0 0% 3.9%);
|
|
7
|
+
--color-background: hsl(0 0% 100%);
|
|
8
|
+
--color-foreground: hsl(0 0% 3.9%);
|
|
9
|
+
--color-primary: hsl(0 0% 9%);
|
|
10
|
+
--color-primary-foreground: hsl(0 0% 98%);
|
|
11
|
+
--color-secondary: hsl(0 0% 96.1%);
|
|
12
|
+
--color-secondary-foreground: hsl(0 0% 9%);
|
|
13
|
+
--color-muted: hsl(0 0% 96.1%);
|
|
14
|
+
--color-muted-foreground: hsl(0 0% 45.1%);
|
|
15
|
+
--color-accent: hsl(0 0% 96.1%);
|
|
16
|
+
--color-accent-foreground: hsl(0 0% 9%);
|
|
17
|
+
--color-destructive: hsl(0 84.2% 60.2%);
|
|
18
|
+
--color-destructive-foreground: hsl(0 0% 98%);
|
|
19
|
+
--color-card: hsl(0 0% 100%);
|
|
20
|
+
--color-card-foreground: hsl(0 0% 3.9%);
|
|
21
|
+
--radius: 0.5rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
background-color: var(--color-background);
|
|
27
|
+
color: var(--color-foreground);
|
|
28
|
+
font-family:
|
|
29
|
+
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
30
|
+
Roboto, Helvetica, Arial, sans-serif;
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { ConsolePage } from '@/features/console/pages/ConsolePage';
|
|
4
|
+
import './index.css';
|
|
5
|
+
|
|
6
|
+
const container = document.getElementById('root');
|
|
7
|
+
if (container === null) {
|
|
8
|
+
throw new Error('Root container #root not found');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
createRoot(container).render(
|
|
12
|
+
<StrictMode>
|
|
13
|
+
<ConsolePage />
|
|
14
|
+
</StrictMode>,
|
|
15
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "Bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUncheckedSideEffectImports": true,
|
|
19
|
+
"paths": {
|
|
20
|
+
"@/*": ["./src/*"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"include": ["src", ".storybook"]
|
|
24
|
+
}
|