@shipfox/client-integrations 21.0.0 → 22.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/components/callback-status-shell.d.ts.map +1 -1
- package/dist/components/callback-status-shell.js +4 -3
- package/dist/components/callback-status-shell.js.map +1 -1
- package/dist/components/connection-picker.js +2 -2
- package/dist/components/connection-picker.js.map +1 -1
- package/dist/components/installed-integrations-section.d.ts.map +1 -1
- package/dist/components/installed-integrations-section.js +155 -141
- package/dist/components/installed-integrations-section.js.map +1 -1
- package/dist/components/integration-gallery.stories.d.ts.map +1 -1
- package/dist/components/integration-gallery.stories.js +7 -2
- package/dist/components/integration-gallery.stories.js.map +1 -1
- package/dist/components/provider-grid.d.ts +0 -2
- package/dist/components/provider-grid.d.ts.map +1 -1
- package/dist/components/provider-grid.js +88 -104
- package/dist/components/provider-grid.js.map +1 -1
- package/dist/components/redirect-install-page.d.ts.map +1 -1
- package/dist/components/redirect-install-page.js +3 -2
- package/dist/components/redirect-install-page.js.map +1 -1
- package/dist/components/repository-picker.d.ts +1 -4
- package/dist/components/repository-picker.d.ts.map +1 -1
- package/dist/components/repository-picker.js +24 -37
- package/dist/components/repository-picker.js.map +1 -1
- package/dist/pages/gitea-install-page.js +1 -1
- package/dist/pages/gitea-install-page.js.map +1 -1
- package/dist/pages/jira-callback-page.js +4 -4
- package/dist/pages/jira-callback-page.js.map +1 -1
- package/dist/pages/sentry-callback-page.js +4 -4
- package/dist/pages/sentry-callback-page.js.map +1 -1
- package/dist/pages/source-control-onboarding-page.js +1 -1
- package/dist/pages/source-control-onboarding-page.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/components/callback-status-shell.tsx +4 -3
- package/src/components/connection-picker.tsx +2 -2
- package/src/components/installed-integrations-section.tsx +110 -98
- package/src/components/integration-gallery.stories.tsx +7 -2
- package/src/components/integration-gallery.test.tsx +3 -2
- package/src/components/provider-grid.tsx +69 -79
- package/src/components/redirect-install-page.test.tsx +4 -0
- package/src/components/redirect-install-page.tsx +3 -2
- package/src/components/repository-picker.test.tsx +27 -13
- package/src/components/repository-picker.tsx +22 -47
- package/src/pages/gitea-install-page.tsx +1 -1
- package/src/pages/jira-callback-page.tsx +4 -4
- package/src/pages/sentry-callback-page.tsx +3 -3
- package/src/pages/slack-callback-page.test.tsx +7 -0
- package/src/pages/source-control-onboarding-page.tsx +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -17,6 +17,19 @@ afterEach(() => {
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
describe('RepositoryPicker', () => {
|
|
20
|
+
test('renders the default empty message without an internal search control', () => {
|
|
21
|
+
renderPicker();
|
|
22
|
+
|
|
23
|
+
expect(screen.getByText('No repositories found.')).toBeInTheDocument();
|
|
24
|
+
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('renders a custom empty message', () => {
|
|
28
|
+
renderPicker({emptyMessage: 'No repositories visible to this connection.'});
|
|
29
|
+
|
|
30
|
+
expect(screen.getByText('No repositories visible to this connection.')).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
20
33
|
test('renders repository names without owner or default branch metadata', () => {
|
|
21
34
|
renderPicker({
|
|
22
35
|
repositories: [
|
|
@@ -65,27 +78,28 @@ describe('RepositoryPicker', () => {
|
|
|
65
78
|
const {container} = renderPicker({
|
|
66
79
|
repositories: [],
|
|
67
80
|
isLoading: true,
|
|
68
|
-
searchValue: '',
|
|
69
|
-
onSearchChange: () => undefined,
|
|
70
81
|
});
|
|
71
82
|
|
|
72
83
|
expect(screen.getByRole('status')).toHaveTextContent('Loading repositories.');
|
|
73
|
-
expect(screen.
|
|
84
|
+
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
|
|
74
85
|
|
|
75
86
|
const loadingGrid = container.querySelector('[aria-hidden="true"]');
|
|
76
87
|
if (!loadingGrid) throw new Error('Repository loading grid was not rendered');
|
|
77
88
|
|
|
78
|
-
|
|
89
|
+
// The placeholders sit in the same hairline cell grid as the loaded cards,
|
|
90
|
+
// so the list does not change shape when the fetch resolves.
|
|
91
|
+
expect(loadingGrid).toHaveClass('grid', 'grid-cols-2', 'max-[760px]:grid-cols-1');
|
|
92
|
+
expect(loadingGrid).toHaveClass('min-[760px]:[&>*:nth-child(2n_of_:not(input))]:border-l');
|
|
93
|
+
expect(loadingGrid).not.toHaveClass('gap-inline');
|
|
79
94
|
expect(loadingGrid.querySelectorAll('[data-slot="skeleton"]')).toHaveLength(4);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
95
|
+
// The placeholder box belongs to `RadioGroupItemSkeleton`, so this only
|
|
96
|
+
// checks that each slot carries the indicator and one bar. react-ui owns
|
|
97
|
+
// keeping that surface identical to a real item.
|
|
98
|
+
const placeholders = loadingGrid.querySelectorAll(':scope > div');
|
|
99
|
+
expect(placeholders).toHaveLength(4);
|
|
100
|
+
for (const placeholder of placeholders) {
|
|
101
|
+
expect(placeholder.querySelector('[data-slot="radio-indicator"]')).not.toBeNull();
|
|
102
|
+
expect(placeholder.querySelector('[data-slot="skeleton"]')).not.toBeNull();
|
|
89
103
|
}
|
|
90
104
|
});
|
|
91
105
|
});
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import {Button} from '@shipfox/react-ui/button';
|
|
2
2
|
import {useIsTextTruncated} from '@shipfox/react-ui/hooks';
|
|
3
|
-
import {Input} from '@shipfox/react-ui/input';
|
|
4
3
|
import {Label} from '@shipfox/react-ui/label';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import {PanelGrid} from '@shipfox/react-ui/panel';
|
|
5
|
+
import {RadioGroup, RadioGroupItem, RadioGroupItemSkeleton} from '@shipfox/react-ui/radio-group';
|
|
7
6
|
import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
|
|
8
7
|
import {Text} from '@shipfox/react-ui/typography';
|
|
9
8
|
import {useId} from 'react';
|
|
10
9
|
import type {Repository} from '#core/models.js';
|
|
11
10
|
|
|
12
|
-
const REPOSITORY_GRID_CLASS_NAME = 'grid grid-cols-2 gap-inline max-[760px]:grid-cols-1';
|
|
13
11
|
const REPOSITORY_SKELETON_WIDTHS = ['w-64', 'w-96', 'w-80', 'w-112'] as const;
|
|
14
12
|
|
|
15
13
|
export function RepositoryPicker({
|
|
@@ -21,9 +19,6 @@ export function RepositoryPicker({
|
|
|
21
19
|
hasNextPage,
|
|
22
20
|
onLoadMore,
|
|
23
21
|
emptyMessage = 'No repositories found.',
|
|
24
|
-
searchValue,
|
|
25
|
-
onSearchChange,
|
|
26
|
-
searchDisabled,
|
|
27
22
|
}: {
|
|
28
23
|
repositories: Repository[];
|
|
29
24
|
selectedRepositoryId: string | undefined;
|
|
@@ -33,44 +28,29 @@ export function RepositoryPicker({
|
|
|
33
28
|
hasNextPage?: boolean;
|
|
34
29
|
onLoadMore?: () => void;
|
|
35
30
|
emptyMessage?: string;
|
|
36
|
-
searchValue?: string;
|
|
37
|
-
onSearchChange?: (value: string) => void;
|
|
38
|
-
searchDisabled?: boolean;
|
|
39
31
|
}) {
|
|
40
32
|
const labelId = useId();
|
|
41
|
-
const showSearch = onSearchChange !== undefined;
|
|
42
33
|
|
|
43
34
|
return (
|
|
44
|
-
<div className="flex flex-col
|
|
35
|
+
<div className="flex min-w-0 flex-col">
|
|
45
36
|
<Label id={labelId} className="sr-only">
|
|
46
37
|
Repository
|
|
47
38
|
</Label>
|
|
48
39
|
|
|
49
|
-
{showSearch ? (
|
|
50
|
-
<Input
|
|
51
|
-
type="search"
|
|
52
|
-
placeholder="Search repositories…"
|
|
53
|
-
aria-label="Search repositories"
|
|
54
|
-
value={searchValue ?? ''}
|
|
55
|
-
onChange={(event) => onSearchChange?.(event.target.value)}
|
|
56
|
-
disabled={searchDisabled}
|
|
57
|
-
/>
|
|
58
|
-
) : null}
|
|
59
|
-
|
|
60
40
|
{isLoading ? <RepositoryLoadingState /> : null}
|
|
61
41
|
|
|
62
42
|
{!isLoading && repositories.length === 0 ? (
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
</
|
|
43
|
+
<Text size="sm" className="px-row py-row">
|
|
44
|
+
{emptyMessage}
|
|
45
|
+
</Text>
|
|
66
46
|
) : null}
|
|
67
47
|
|
|
68
48
|
{repositories.length > 0 ? (
|
|
69
49
|
<RadioGroup
|
|
50
|
+
variant="cell"
|
|
70
51
|
aria-labelledby={labelId}
|
|
71
52
|
value={selectedRepositoryId ?? ''}
|
|
72
53
|
onValueChange={onSelect}
|
|
73
|
-
className={REPOSITORY_GRID_CLASS_NAME}
|
|
74
54
|
>
|
|
75
55
|
{repositories.map((repository) => (
|
|
76
56
|
<RepositoryCard key={repository.externalRepositoryId} repository={repository} />
|
|
@@ -79,15 +59,17 @@ export function RepositoryPicker({
|
|
|
79
59
|
) : null}
|
|
80
60
|
|
|
81
61
|
{hasNextPage && onLoadMore ? (
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
62
|
+
<div className="flex justify-center border-t border-border-neutral-base p-panel-compact">
|
|
63
|
+
<Button
|
|
64
|
+
type="button"
|
|
65
|
+
variant="secondary"
|
|
66
|
+
size="sm"
|
|
67
|
+
isLoading={isFetchingNextPage ?? false}
|
|
68
|
+
onClick={onLoadMore}
|
|
69
|
+
>
|
|
70
|
+
Load more
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
91
73
|
) : null}
|
|
92
74
|
</div>
|
|
93
75
|
);
|
|
@@ -99,7 +81,7 @@ function RepositoryCard({repository}: {repository: Repository}) {
|
|
|
99
81
|
return (
|
|
100
82
|
<Tooltip>
|
|
101
83
|
<TooltipTrigger asChild>
|
|
102
|
-
<RadioGroupItem value={repository.externalRepositoryId}
|
|
84
|
+
<RadioGroupItem value={repository.externalRepositoryId}>
|
|
103
85
|
<span ref={nameRef} className="block min-w-0 truncate">
|
|
104
86
|
<Text as="span" size="sm" bold>
|
|
105
87
|
{repository.name}
|
|
@@ -118,18 +100,11 @@ function RepositoryLoadingState() {
|
|
|
118
100
|
<div role="status" className="sr-only">
|
|
119
101
|
Loading repositories.
|
|
120
102
|
</div>
|
|
121
|
-
<div aria-hidden="true"
|
|
122
|
-
{/* The skeleton mirrors RadioGroupItem's own 14px padding contract so the
|
|
123
|
-
placeholder and the loaded card stay the same height. */}
|
|
103
|
+
<PanelGrid as="div" aria-hidden="true">
|
|
124
104
|
{REPOSITORY_SKELETON_WIDTHS.map((width) => (
|
|
125
|
-
<
|
|
126
|
-
key={width}
|
|
127
|
-
className="h-50 min-w-0 rounded-8 border border-border-neutral-base bg-background-neutral-base p-[14px]"
|
|
128
|
-
>
|
|
129
|
-
<Skeleton className={`h-20 ${width}`} />
|
|
130
|
-
</div>
|
|
105
|
+
<RadioGroupItemSkeleton key={width} variant="cell" labelClassName={width} />
|
|
131
106
|
))}
|
|
132
|
-
</
|
|
107
|
+
</PanelGrid>
|
|
133
108
|
</>
|
|
134
109
|
);
|
|
135
110
|
}
|
|
@@ -40,7 +40,7 @@ export function GiteaInstallPage() {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
return (
|
|
43
|
-
<div className="
|
|
43
|
+
<div className="flex w-full flex-col gap-section">
|
|
44
44
|
<header className="flex flex-col gap-inline">
|
|
45
45
|
<Header variant="h1">Install Gitea</Header>
|
|
46
46
|
<Text size="md" className="text-foreground-neutral-muted">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {useAuthState, useRefreshAuth} from '@shipfox/client-auth';
|
|
2
|
-
import {useRouteSearch} from '@shipfox/client-shell/runtime';
|
|
2
|
+
import {FocusedFrame, useRouteSearch} from '@shipfox/client-shell/runtime';
|
|
3
3
|
import {sessionStorageOrUndefined} from '@shipfox/client-ui';
|
|
4
4
|
import {Button, ButtonLink} from '@shipfox/react-ui/button';
|
|
5
5
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
@@ -300,8 +300,8 @@ function JiraSiteSelectionPage({
|
|
|
300
300
|
onSelect: (site: JiraSite) => void;
|
|
301
301
|
}) {
|
|
302
302
|
return (
|
|
303
|
-
<main className="flex min-h-screen px-
|
|
304
|
-
<
|
|
303
|
+
<main className="flex min-h-screen px-frame py-frame">
|
|
304
|
+
<FocusedFrame className="flex flex-col justify-center gap-section">
|
|
305
305
|
<header className="flex flex-col gap-inline">
|
|
306
306
|
<Header variant="h2">Choose a Jira site</Header>
|
|
307
307
|
<Text size="sm" className="text-foreground-neutral-muted">
|
|
@@ -360,7 +360,7 @@ function JiraSiteSelectionPage({
|
|
|
360
360
|
</ButtonLink>
|
|
361
361
|
)}
|
|
362
362
|
</div>
|
|
363
|
-
</
|
|
363
|
+
</FocusedFrame>
|
|
364
364
|
</main>
|
|
365
365
|
);
|
|
366
366
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {useAuthState, useRefreshAuth} from '@shipfox/client-auth';
|
|
2
|
-
import {useRouteSearch} from '@shipfox/client-shell/runtime';
|
|
2
|
+
import {FocusedFrame, useRouteSearch} from '@shipfox/client-shell/runtime';
|
|
3
3
|
import {createSingleFlight, sessionStorageOrUndefined} from '@shipfox/client-ui';
|
|
4
4
|
import {Button, ButtonLink} from '@shipfox/react-ui/button';
|
|
5
5
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
@@ -246,8 +246,8 @@ export function SentryCallbackPage() {
|
|
|
246
246
|
|
|
247
247
|
function CallbackColumn({children}: {children: React.ReactNode}) {
|
|
248
248
|
return (
|
|
249
|
-
<main className="flex min-h-screen px-
|
|
250
|
-
<
|
|
249
|
+
<main className="flex min-h-screen px-frame py-frame">
|
|
250
|
+
<FocusedFrame className="flex flex-col justify-center gap-section">{children}</FocusedFrame>
|
|
251
251
|
</main>
|
|
252
252
|
);
|
|
253
253
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import '@testing-library/jest-dom/vitest';
|
|
3
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
3
4
|
import {screen, waitFor} from '@testing-library/react';
|
|
4
5
|
import {StrictMode} from 'react';
|
|
5
6
|
import {SLACK_INSTALL_WORKSPACE_KEY} from '#slack-callback.js';
|
|
@@ -27,6 +28,12 @@ vi.mock('#hooks/api/integrations.js', async (importOriginal) => {
|
|
|
27
28
|
beforeEach(() => {
|
|
28
29
|
window.sessionStorage.clear();
|
|
29
30
|
completeCallbackMock.mockReset();
|
|
31
|
+
configureApiClient({
|
|
32
|
+
baseUrl: 'https://api.example.test',
|
|
33
|
+
fetchImpl: vi.fn((_input: RequestInfo | URL) =>
|
|
34
|
+
Promise.reject(new Error('Workspace list unavailable')),
|
|
35
|
+
),
|
|
36
|
+
});
|
|
30
37
|
});
|
|
31
38
|
|
|
32
39
|
describe('SlackCallbackPage', () => {
|
|
@@ -8,7 +8,7 @@ export function SourceControlOnboardingPage() {
|
|
|
8
8
|
const providersQuery = useIntegrationProvidersQuery({capability: 'source_control'});
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
|
-
<div className="
|
|
11
|
+
<div className="flex w-full flex-col gap-section">
|
|
12
12
|
<header className="flex flex-col gap-inline">
|
|
13
13
|
<Header variant="h1">Install source control</Header>
|
|
14
14
|
<Text size="md" className="text-foreground-neutral-muted">
|