@shipfox/client-integrations 21.0.0 → 22.0.1

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.
Files changed (59) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +28 -0
  3. package/dist/components/callback-status-shell.d.ts.map +1 -1
  4. package/dist/components/callback-status-shell.js +4 -3
  5. package/dist/components/callback-status-shell.js.map +1 -1
  6. package/dist/components/connection-picker.js +2 -2
  7. package/dist/components/connection-picker.js.map +1 -1
  8. package/dist/components/installed-integrations-section.d.ts.map +1 -1
  9. package/dist/components/installed-integrations-section.js +160 -153
  10. package/dist/components/installed-integrations-section.js.map +1 -1
  11. package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -1
  12. package/dist/components/integration-gallery-for-workspace.js +5 -12
  13. package/dist/components/integration-gallery-for-workspace.js.map +1 -1
  14. package/dist/components/integration-gallery.stories.d.ts.map +1 -1
  15. package/dist/components/integration-gallery.stories.js +7 -2
  16. package/dist/components/integration-gallery.stories.js.map +1 -1
  17. package/dist/components/provider-grid.d.ts +0 -2
  18. package/dist/components/provider-grid.d.ts.map +1 -1
  19. package/dist/components/provider-grid.js +88 -104
  20. package/dist/components/provider-grid.js.map +1 -1
  21. package/dist/components/redirect-install-page.d.ts.map +1 -1
  22. package/dist/components/redirect-install-page.js +3 -2
  23. package/dist/components/redirect-install-page.js.map +1 -1
  24. package/dist/components/repository-picker.d.ts +1 -4
  25. package/dist/components/repository-picker.d.ts.map +1 -1
  26. package/dist/components/repository-picker.js +24 -37
  27. package/dist/components/repository-picker.js.map +1 -1
  28. package/dist/pages/gitea-install-page.js +1 -1
  29. package/dist/pages/gitea-install-page.js.map +1 -1
  30. package/dist/pages/jira-callback-page.js +4 -4
  31. package/dist/pages/jira-callback-page.js.map +1 -1
  32. package/dist/pages/sentry-callback-page.js +4 -4
  33. package/dist/pages/sentry-callback-page.js.map +1 -1
  34. package/dist/pages/source-control-onboarding-page.js +1 -1
  35. package/dist/pages/source-control-onboarding-page.js.map +1 -1
  36. package/dist/routes/integrations-settings.d.ts +1 -2
  37. package/dist/routes/integrations-settings.d.ts.map +1 -1
  38. package/dist/routes/integrations-settings.js +12 -1
  39. package/dist/routes/integrations-settings.js.map +1 -1
  40. package/dist/tsconfig.test.tsbuildinfo +1 -1
  41. package/package.json +6 -6
  42. package/src/components/callback-status-shell.tsx +4 -3
  43. package/src/components/connection-picker.tsx +2 -2
  44. package/src/components/installed-integrations-section.tsx +110 -101
  45. package/src/components/integration-gallery-for-workspace.tsx +0 -3
  46. package/src/components/integration-gallery.stories.tsx +7 -2
  47. package/src/components/integration-gallery.test.tsx +9 -4
  48. package/src/components/provider-grid.tsx +69 -79
  49. package/src/components/redirect-install-page.test.tsx +4 -0
  50. package/src/components/redirect-install-page.tsx +3 -2
  51. package/src/components/repository-picker.test.tsx +27 -13
  52. package/src/components/repository-picker.tsx +22 -47
  53. package/src/pages/gitea-install-page.tsx +1 -1
  54. package/src/pages/jira-callback-page.tsx +4 -4
  55. package/src/pages/sentry-callback-page.tsx +3 -3
  56. package/src/pages/slack-callback-page.test.tsx +7 -0
  57. package/src/pages/source-control-onboarding-page.tsx +1 -1
  58. package/src/routes/integrations-settings.tsx +7 -1
  59. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,5 +1,6 @@
1
1
  import {ApiError} from '@shipfox/client-api';
2
2
  import {useActiveWorkspace} from '@shipfox/client-auth';
3
+ import {FocusedFrame} from '@shipfox/client-shell/runtime';
3
4
  import {ButtonLink} from '@shipfox/react-ui/button';
4
5
  import {Callout} from '@shipfox/react-ui/callout';
5
6
  import {FullPageLoader} from '@shipfox/react-ui/loader';
@@ -54,7 +55,7 @@ export function RedirectInstallPage({
54
55
 
55
56
  if (errorMessage) {
56
57
  return (
57
- <div className="mx-auto flex w-full max-w-[480px] flex-col gap-group">
58
+ <FocusedFrame className="flex flex-col gap-group">
58
59
  <Callout role="alert" type="error">
59
60
  <Text size="sm">{errorMessage}</Text>
60
61
  </Callout>
@@ -63,7 +64,7 @@ export function RedirectInstallPage({
63
64
  Back to integrations
64
65
  </Link>
65
66
  </ButtonLink>
66
- </div>
67
+ </FocusedFrame>
67
68
  );
68
69
  }
69
70
 
@@ -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.getByRole('searchbox')).toBeInTheDocument();
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
- expect(loadingGrid).toHaveClass('grid', 'grid-cols-2', 'gap-inline', 'max-[760px]:grid-cols-1');
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
- for (const placeholder of loadingGrid.querySelectorAll(':scope > div')) {
81
- expect(placeholder).toHaveClass(
82
- 'h-50',
83
- 'rounded-8',
84
- 'border',
85
- 'border-border-neutral-base',
86
- 'bg-background-neutral-base',
87
- 'p-[14px]',
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 {RadioGroup, RadioGroupItem} from '@shipfox/react-ui/radio-group';
6
- import {Skeleton} from '@shipfox/react-ui/skeleton';
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 gap-inline">
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
- <div className="rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact">
64
- <Text size="sm">{emptyMessage}</Text>
65
- </div>
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
- <Button
83
- type="button"
84
- variant="secondary"
85
- size="sm"
86
- isLoading={isFetchingNextPage ?? false}
87
- onClick={onLoadMore}
88
- >
89
- Load more
90
- </Button>
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} className="min-w-0">
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" className={REPOSITORY_GRID_CLASS_NAME}>
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
- <div
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
- </div>
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="mx-auto flex w-full flex-col gap-section">
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-row py-frame">
304
- <div className="mx-auto flex w-full flex-col justify-center gap-section">
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
- </div>
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-row py-frame">
250
- <div className="mx-auto flex w-full flex-col justify-center gap-section">{children}</div>
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="mx-auto flex w-full flex-col gap-section">
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">
@@ -1,7 +1,13 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
+ import {Header} from '@shipfox/react-ui/typography';
2
3
  import {IntegrationGallery} from '#index.js';
3
4
 
4
5
  export default defineRoute({
5
6
  staticData: {frame: 'content'},
6
- component: IntegrationGallery,
7
+ component: () => (
8
+ <div className="flex min-w-0 flex-col gap-section">
9
+ <Header variant="h1">Integrations</Header>
10
+ <IntegrationGallery />
11
+ </div>
12
+ ),
7
13
  });