@shipfox/client-integrations 5.0.0 → 6.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 (187) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +64 -0
  3. package/dist/application/complete-integration-callback.d.ts +13 -0
  4. package/dist/application/complete-integration-callback.d.ts.map +1 -0
  5. package/dist/application/complete-integration-callback.js +26 -0
  6. package/dist/application/complete-integration-callback.js.map +1 -0
  7. package/dist/components/connection-picker.d.ts +2 -2
  8. package/dist/components/connection-picker.d.ts.map +1 -1
  9. package/dist/components/connection-picker.js +4 -4
  10. package/dist/components/connection-picker.js.map +1 -1
  11. package/dist/components/installed-integrations-section.d.ts +8 -6
  12. package/dist/components/installed-integrations-section.d.ts.map +1 -1
  13. package/dist/components/installed-integrations-section.js +22 -15
  14. package/dist/components/installed-integrations-section.js.map +1 -1
  15. package/dist/components/integration-gallery-for-workspace.d.ts +2 -2
  16. package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -1
  17. package/dist/components/integration-gallery-for-workspace.js +14 -7
  18. package/dist/components/integration-gallery-for-workspace.js.map +1 -1
  19. package/dist/components/integration-gallery.d.ts +2 -2
  20. package/dist/components/integration-gallery.d.ts.map +1 -1
  21. package/dist/components/integration-gallery.js.map +1 -1
  22. package/dist/components/integration-usage-events.d.ts +1 -3
  23. package/dist/components/integration-usage-events.d.ts.map +1 -1
  24. package/dist/components/integration-usage-events.js +1 -125
  25. package/dist/components/integration-usage-events.js.map +1 -1
  26. package/dist/components/integration-usage-modal.d.ts +2 -2
  27. package/dist/components/integration-usage-modal.d.ts.map +1 -1
  28. package/dist/components/integration-usage-modal.js +2 -2
  29. package/dist/components/integration-usage-modal.js.map +1 -1
  30. package/dist/components/provider-grid.d.ts +7 -4
  31. package/dist/components/provider-grid.d.ts.map +1 -1
  32. package/dist/components/provider-grid.js +15 -10
  33. package/dist/components/provider-grid.js.map +1 -1
  34. package/dist/components/redirect-install-page.d.ts +2 -3
  35. package/dist/components/redirect-install-page.d.ts.map +1 -1
  36. package/dist/components/redirect-install-page.js +1 -1
  37. package/dist/components/redirect-install-page.js.map +1 -1
  38. package/dist/components/repository-picker.d.ts +2 -2
  39. package/dist/components/repository-picker.d.ts.map +1 -1
  40. package/dist/components/repository-picker.js +4 -4
  41. package/dist/components/repository-picker.js.map +1 -1
  42. package/dist/components/webhook/webhook-create-modal.d.ts +2 -2
  43. package/dist/components/webhook/webhook-create-modal.d.ts.map +1 -1
  44. package/dist/components/webhook/webhook-create-modal.js +8 -8
  45. package/dist/components/webhook/webhook-create-modal.js.map +1 -1
  46. package/dist/components/webhook/webhook-modals.stories.d.ts.map +1 -1
  47. package/dist/components/webhook/webhook-modals.stories.js +7 -7
  48. package/dist/components/webhook/webhook-modals.stories.js.map +1 -1
  49. package/dist/components/webhook/webhook-usage-details.d.ts.map +1 -1
  50. package/dist/components/webhook/webhook-usage-details.js +2 -2
  51. package/dist/components/webhook/webhook-usage-details.js.map +1 -1
  52. package/dist/connection-status-badge.d.ts +2 -2
  53. package/dist/connection-status-badge.d.ts.map +1 -1
  54. package/dist/connection-status-badge.js +11 -15
  55. package/dist/connection-status-badge.js.map +1 -1
  56. package/dist/core/models.d.ts +66 -0
  57. package/dist/core/models.d.ts.map +1 -0
  58. package/dist/core/models.js +156 -0
  59. package/dist/core/models.js.map +1 -0
  60. package/dist/feature.d.ts +7 -0
  61. package/dist/feature.d.ts.map +1 -1
  62. package/dist/feature.js +10 -9
  63. package/dist/feature.js.map +1 -1
  64. package/dist/hooks/api/integration-mapper.d.ts +11 -0
  65. package/dist/hooks/api/integration-mapper.d.ts.map +1 -0
  66. package/dist/hooks/api/integration-mapper.js +54 -0
  67. package/dist/hooks/api/integration-mapper.js.map +1 -0
  68. package/dist/hooks/api/integrations.d.ts +45 -240
  69. package/dist/hooks/api/integrations.d.ts.map +1 -1
  70. package/dist/hooks/api/integrations.js +78 -31
  71. package/dist/hooks/api/integrations.js.map +1 -1
  72. package/dist/hooks/api/webhook-connections.d.ts +12 -64
  73. package/dist/hooks/api/webhook-connections.d.ts.map +1 -1
  74. package/dist/hooks/api/webhook-connections.js +17 -9
  75. package/dist/hooks/api/webhook-connections.js.map +1 -1
  76. package/dist/index.d.ts +2 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +2 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/linear-callback.d.ts +2 -1
  81. package/dist/linear-callback.d.ts.map +1 -1
  82. package/dist/linear-callback.js +14 -15
  83. package/dist/linear-callback.js.map +1 -1
  84. package/dist/pages/gitea-install-page.d.ts.map +1 -1
  85. package/dist/pages/gitea-install-page.js +1 -10
  86. package/dist/pages/gitea-install-page.js.map +1 -1
  87. package/dist/pages/linear-callback-page.d.ts.map +1 -1
  88. package/dist/pages/linear-callback-page.js +19 -32
  89. package/dist/pages/linear-callback-page.js.map +1 -1
  90. package/dist/pages/linear-install-page.d.ts.map +1 -1
  91. package/dist/pages/linear-install-page.js +2 -1
  92. package/dist/pages/linear-install-page.js.map +1 -1
  93. package/dist/pages/sentry-callback-page.d.ts.map +1 -1
  94. package/dist/pages/sentry-callback-page.js +22 -25
  95. package/dist/pages/sentry-callback-page.js.map +1 -1
  96. package/dist/pages/sentry-install-page.d.ts.map +1 -1
  97. package/dist/pages/sentry-install-page.js +2 -1
  98. package/dist/pages/sentry-install-page.js.map +1 -1
  99. package/dist/pages/slack-callback-page.d.ts.map +1 -1
  100. package/dist/pages/slack-callback-page.js +21 -28
  101. package/dist/pages/slack-callback-page.js.map +1 -1
  102. package/dist/pages/slack-install-page.d.ts.map +1 -1
  103. package/dist/pages/slack-install-page.js +2 -1
  104. package/dist/pages/slack-install-page.js.map +1 -1
  105. package/dist/pages/source-control-onboarding-page.d.ts.map +1 -1
  106. package/dist/pages/source-control-onboarding-page.js +5 -1
  107. package/dist/pages/source-control-onboarding-page.js.map +1 -1
  108. package/dist/provider-catalog.d.ts.map +1 -1
  109. package/dist/provider-catalog.js +7 -6
  110. package/dist/provider-catalog.js.map +1 -1
  111. package/dist/routes/github-callback.d.ts +16 -0
  112. package/dist/routes/github-callback.d.ts.map +1 -1
  113. package/dist/routes/github-callback.js +43 -21
  114. package/dist/routes/github-callback.js.map +1 -1
  115. package/dist/routes/linear-callback.d.ts +2 -0
  116. package/dist/routes/linear-callback.d.ts.map +1 -1
  117. package/dist/routes/linear-callback.js +2 -0
  118. package/dist/routes/linear-callback.js.map +1 -1
  119. package/dist/routes/sentry-callback.d.ts +2 -0
  120. package/dist/routes/sentry-callback.d.ts.map +1 -1
  121. package/dist/routes/sentry-callback.js +2 -0
  122. package/dist/routes/sentry-callback.js.map +1 -1
  123. package/dist/routes/slack-callback.d.ts +2 -0
  124. package/dist/routes/slack-callback.d.ts.map +1 -1
  125. package/dist/routes/slack-callback.js +2 -0
  126. package/dist/routes/slack-callback.js.map +1 -1
  127. package/dist/sentry-callback.d.ts +2 -1
  128. package/dist/sentry-callback.d.ts.map +1 -1
  129. package/dist/sentry-callback.js +16 -19
  130. package/dist/sentry-callback.js.map +1 -1
  131. package/dist/slack-callback.d.ts +2 -1
  132. package/dist/slack-callback.d.ts.map +1 -1
  133. package/dist/slack-callback.js +14 -15
  134. package/dist/slack-callback.js.map +1 -1
  135. package/dist/tsconfig.test.tsbuildinfo +1 -1
  136. package/package.json +17 -17
  137. package/src/application/complete-integration-callback.test.ts +41 -0
  138. package/src/application/complete-integration-callback.ts +39 -0
  139. package/src/components/connection-picker.tsx +7 -7
  140. package/src/components/installed-integrations-section.tsx +29 -24
  141. package/src/components/integration-gallery-for-workspace.tsx +23 -16
  142. package/src/components/integration-gallery.tsx +3 -3
  143. package/src/components/integration-usage-events.ts +1 -116
  144. package/src/components/integration-usage-modal.tsx +4 -4
  145. package/src/components/provider-grid.tsx +30 -18
  146. package/src/components/redirect-install-page.test.tsx +2 -6
  147. package/src/components/redirect-install-page.tsx +3 -2
  148. package/src/components/repository-picker.tsx +6 -6
  149. package/src/components/webhook/webhook-create-modal.tsx +12 -11
  150. package/src/components/webhook/webhook-modals.stories.tsx +10 -10
  151. package/src/components/webhook/webhook-usage-details.tsx +2 -4
  152. package/src/connection-status-badge.tsx +8 -14
  153. package/src/core/models.ts +200 -0
  154. package/src/feature.ts +12 -10
  155. package/src/hooks/api/integration-mapper.ts +64 -0
  156. package/src/hooks/api/integrations.test.ts +44 -22
  157. package/src/hooks/api/integrations.ts +229 -84
  158. package/src/hooks/api/webhook-connections.ts +58 -17
  159. package/src/index.ts +2 -1
  160. package/src/linear-callback.ts +21 -16
  161. package/src/pages/gitea-install-page.tsx +1 -10
  162. package/src/pages/linear-callback-page.tsx +18 -30
  163. package/src/pages/linear-install-page.tsx +2 -1
  164. package/src/pages/sentry-callback-page.test.tsx +5 -1
  165. package/src/pages/sentry-callback-page.tsx +28 -25
  166. package/src/pages/sentry-install-page.tsx +2 -1
  167. package/src/pages/slack-callback-page.tsx +20 -26
  168. package/src/pages/slack-install-page.tsx +2 -1
  169. package/src/pages/source-control-onboarding-page.tsx +5 -1
  170. package/src/provider-catalog.ts +7 -6
  171. package/src/routes/github-callback.test.ts +35 -1
  172. package/src/routes/github-callback.tsx +47 -21
  173. package/src/routes/linear-callback.tsx +5 -1
  174. package/src/routes/sentry-callback.tsx +5 -1
  175. package/src/routes/slack-callback.tsx +6 -1
  176. package/src/sentry-callback.test.ts +2 -1
  177. package/src/sentry-callback.ts +28 -20
  178. package/src/slack-callback.ts +21 -16
  179. package/tsconfig.build.tsbuildinfo +1 -1
  180. package/vercel.json +5 -0
  181. package/dist/integration-icon.d.ts +0 -17
  182. package/dist/integration-icon.d.ts.map +0 -1
  183. package/dist/integration-icon.js +0 -22
  184. package/dist/integration-icon.js.map +0 -1
  185. package/src/integration-icon.react.test.tsx +0 -40
  186. package/src/integration-icon.test.ts +0 -19
  187. package/src/integration-icon.tsx +0 -26
@@ -10,8 +10,8 @@ import {
10
10
 
11
11
  function connection(overrides: Partial<IntegrationConnectionDto> = {}): IntegrationConnectionDto {
12
12
  return {
13
- id: 'c1',
14
- workspace_id: 'ws-1',
13
+ id: '11111111-1111-4111-8111-111111111111',
14
+ workspace_id: '22222222-2222-4222-8222-222222222222',
15
15
  provider: 'github',
16
16
  external_account_id: 'acct',
17
17
  slug: 'github_acct',
@@ -40,19 +40,23 @@ describe('listSourceConnections', () => {
40
40
  return Promise.resolve(
41
41
  jsonResponse({
42
42
  connections: [
43
- connection({id: 'active-1', lifecycle_status: 'active'}),
44
- connection({id: 'disabled-1', lifecycle_status: 'disabled'}),
45
- connection({id: 'error-1', lifecycle_status: 'error'}),
43
+ connection({id: '33333333-3333-4333-8333-333333333333', lifecycle_status: 'active'}),
44
+ connection({id: '44444444-4444-4444-8444-444444444444', lifecycle_status: 'disabled'}),
45
+ connection({id: '55555555-5555-4555-8555-555555555555', lifecycle_status: 'error'}),
46
46
  ],
47
47
  }),
48
48
  );
49
49
  });
50
50
  configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
51
51
 
52
- const result = await listSourceConnections({workspaceId: 'ws-1'});
52
+ const result = await listSourceConnections({
53
+ workspaceId: '22222222-2222-4222-8222-222222222222',
54
+ });
53
55
 
54
56
  expect(requestedUrl).toContain('capability=source_control');
55
- expect(result.connections.map((connection) => connection.id)).toEqual(['active-1']);
57
+ expect(result.map((connection) => connection.id)).toEqual([
58
+ '33333333-3333-4333-8333-333333333333',
59
+ ]);
56
60
  });
57
61
  });
58
62
 
@@ -63,11 +67,20 @@ describe('Slack transport', () => {
63
67
  baseUrl: 'https://api.example.test',
64
68
  fetchImpl: vi.fn((input, init) => {
65
69
  requests.push(new Request(input, init));
66
- return Promise.resolve(jsonResponse(connection({provider: 'slack'})));
70
+ const url = input instanceof Request ? input.url : String(input);
71
+ return Promise.resolve(
72
+ jsonResponse(
73
+ url.endsWith('/install')
74
+ ? {install_url: 'https://slack.example.test/install'}
75
+ : connection({provider: 'slack'}),
76
+ ),
77
+ );
67
78
  }),
68
79
  });
69
80
 
70
- await createSlackInstall({workspace_id: '11111111-1111-4111-8111-111111111111'});
81
+ const install = await createSlackInstall({
82
+ workspace_id: '11111111-1111-4111-8111-111111111111',
83
+ });
71
84
  await completeSlackCallback({
72
85
  query: {code: 'grant code', state: 'signed state'},
73
86
  token: 'session-token',
@@ -77,6 +90,7 @@ describe('Slack transport', () => {
77
90
  expect(await requests[0]?.json()).toEqual({
78
91
  workspace_id: '11111111-1111-4111-8111-111111111111',
79
92
  });
93
+ expect(install).toEqual({installUrl: 'https://slack.example.test/install'});
80
94
  expect(requests[1]?.url).toBe(
81
95
  'https://api.example.test/integrations/slack/callback/api?code=grant+code&state=signed+state',
82
96
  );
@@ -89,24 +103,31 @@ describe('Linear transport', () => {
89
103
  const requests: Request[] = [];
90
104
  const fetchImpl = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
91
105
  requests.push(new Request(input, init));
106
+ const url = input instanceof Request ? input.url : String(input);
92
107
  return Promise.resolve(
93
- jsonResponse({
94
- id: 'connection-1',
95
- workspace_id: '11111111-1111-4111-8111-111111111111',
96
- provider: 'linear',
97
- external_account_id: 'linear-org',
98
- slug: 'linear_org',
99
- display_name: 'Linear org',
100
- lifecycle_status: 'active',
101
- capabilities: [],
102
- created_at: '2026-01-01T00:00:00.000Z',
103
- updated_at: '2026-01-01T00:00:00.000Z',
104
- }),
108
+ jsonResponse(
109
+ url.endsWith('/install')
110
+ ? {install_url: 'https://linear.example.test/install'}
111
+ : {
112
+ id: '33333333-3333-4333-8333-333333333333',
113
+ workspace_id: '11111111-1111-4111-8111-111111111111',
114
+ provider: 'linear',
115
+ external_account_id: 'linear-org',
116
+ slug: 'linear_org',
117
+ display_name: 'Linear org',
118
+ lifecycle_status: 'active',
119
+ capabilities: [],
120
+ created_at: '2026-01-01T00:00:00.000Z',
121
+ updated_at: '2026-01-01T00:00:00.000Z',
122
+ },
123
+ ),
105
124
  );
106
125
  });
107
126
  configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
108
127
 
109
- await createLinearInstall({workspace_id: '11111111-1111-4111-8111-111111111111'});
128
+ const install = await createLinearInstall({
129
+ workspace_id: '11111111-1111-4111-8111-111111111111',
130
+ });
110
131
  await completeLinearCallback({
111
132
  query: {code: 'grant code', state: 'signed state'},
112
133
  token: 'session-token',
@@ -124,6 +145,7 @@ describe('Linear transport', () => {
124
145
  expect(await requests[0]?.json()).toEqual({
125
146
  workspace_id: '11111111-1111-4111-8111-111111111111',
126
147
  });
148
+ expect(install).toEqual({installUrl: 'https://linear.example.test/install'});
127
149
  expect(requests[1]?.url).toBe(
128
150
  'https://api.example.test/integrations/linear/callback/api?code=grant+code&state=signed+state',
129
151
  );
@@ -1,48 +1,71 @@
1
1
  import type {
2
2
  IntegrationCapabilityDto,
3
- IntegrationConnectionDto,
4
- ListIntegrationConnectionsResponseDto,
5
- ListIntegrationProvidersResponseDto,
6
- ListRepositoriesResponseDto,
7
3
  UpdateIntegrationConnectionBodyDto,
8
4
  } from '@shipfox/api-integration-core-dto';
9
- import type {
10
- CreateGiteaConnectionBodyDto,
11
- CreateGiteaConnectionResponseDto,
12
- } from '@shipfox/api-integration-gitea-dto';
13
- import type {
14
- CreateGithubInstallBodyDto,
15
- CreateGithubInstallResponseDto,
5
+ import {
6
+ integrationConnectionDtoSchema,
7
+ listIntegrationConnectionsResponseSchema,
8
+ listIntegrationProvidersResponseSchema,
9
+ listRepositoriesResponseSchema,
10
+ } from '@shipfox/api-integration-core-dto';
11
+ import type {CreateGiteaConnectionBodyDto} from '@shipfox/api-integration-gitea-dto';
12
+ import {createGiteaConnectionResponseSchema} from '@shipfox/api-integration-gitea-dto';
13
+ import type {CreateGithubInstallBodyDto} from '@shipfox/api-integration-github-dto';
14
+ import {
15
+ createGithubInstallResponseSchema,
16
+ githubCallbackResponseSchema,
16
17
  } from '@shipfox/api-integration-github-dto';
17
18
  import type {
18
19
  CreateLinearInstallBodyDto,
19
- CreateLinearInstallResponseDto,
20
20
  LinearCallbackQueryDto,
21
- LinearCallbackResponseDto,
21
+ } from '@shipfox/api-integration-linear-dto';
22
+ import {
23
+ createLinearInstallResponseSchema,
24
+ linearCallbackResponseSchema,
22
25
  } from '@shipfox/api-integration-linear-dto';
23
26
  import type {
24
27
  CreateSentryInstallBodyDto,
25
- CreateSentryInstallResponseDto,
26
28
  SentryConnectBodyDto,
27
- SentryConnectResponseDto,
29
+ } from '@shipfox/api-integration-sentry-dto';
30
+ import {
31
+ createSentryInstallResponseSchema,
32
+ sentryConnectResponseSchema,
28
33
  } from '@shipfox/api-integration-sentry-dto';
29
34
  import type {
30
35
  CreateSlackInstallBodyDto,
31
- CreateSlackInstallResponseDto,
32
36
  SlackCallbackQueryDto,
33
- SlackCallbackResponseDto,
34
37
  } from '@shipfox/api-integration-slack-dto';
35
- import {apiRequest} from '@shipfox/client-api';
38
+ import {
39
+ createSlackInstallResponseSchema,
40
+ slackCallbackResponseSchema,
41
+ } from '@shipfox/api-integration-slack-dto';
42
+ import {checkedApiRequest, emptyResponseSchema} from '@shipfox/client-api';
36
43
  import {
37
44
  type FetchQueryOptions,
45
+ type InfiniteData,
46
+ infiniteQueryOptions,
38
47
  queryOptions,
48
+ type UseInfiniteQueryOptions,
39
49
  useInfiniteQuery,
40
50
  useMutation,
41
51
  useQuery,
42
52
  useQueryClient,
43
53
  } from '@tanstack/react-query';
54
+ import {
55
+ type InstallRedirect,
56
+ type IntegrationConnection,
57
+ type IntegrationProvider,
58
+ isUsableConnection,
59
+ type RepositoryPage,
60
+ } from '#core/models.js';
44
61
  import {serializeLinearCallbackQuery} from '#linear-callback.js';
45
62
  import {serializeSlackCallbackQuery} from '#slack-callback.js';
63
+ import {
64
+ toInstallRedirect,
65
+ toIntegrationConnection,
66
+ toIntegrationProvider,
67
+ toRepository,
68
+ } from './integration-mapper.js';
46
69
 
47
70
  export const integrationsQueryKeys = {
48
71
  all: ['integrations'] as const,
@@ -66,24 +89,57 @@ type SourceConnectionsQueryKey =
66
89
  | readonly ['integrations', 'source-connections'];
67
90
 
68
91
  type SourceConnectionsQueryOptions = FetchQueryOptions<
69
- ListIntegrationConnectionsResponseDto,
92
+ IntegrationConnection[],
70
93
  Error,
71
- ListIntegrationConnectionsResponseDto,
94
+ IntegrationConnection[],
72
95
  SourceConnectionsQueryKey
73
96
  >;
74
97
 
98
+ type ProvidersQueryKey = ReturnType<typeof integrationsQueryKeys.providers>;
99
+
100
+ type ProvidersQueryOptions = FetchQueryOptions<
101
+ IntegrationProvider[],
102
+ Error,
103
+ IntegrationProvider[],
104
+ ProvidersQueryKey
105
+ >;
106
+
107
+ type ConnectionsQueryKey =
108
+ | ReturnType<typeof integrationsQueryKeys.connections>
109
+ | readonly ['integrations', 'connections'];
110
+
111
+ type ConnectionsQueryOptions = FetchQueryOptions<
112
+ IntegrationConnection[],
113
+ Error,
114
+ IntegrationConnection[],
115
+ ConnectionsQueryKey
116
+ >;
117
+
118
+ type RepositoriesQueryKey =
119
+ | ReturnType<typeof integrationsQueryKeys.repositories>
120
+ | readonly ['integrations', 'repositories'];
121
+
122
+ type RepositoriesInfiniteQueryOptions = UseInfiniteQueryOptions<
123
+ RepositoryPage,
124
+ Error,
125
+ InfiniteData<RepositoryPage, string | undefined>,
126
+ RepositoriesQueryKey,
127
+ string | undefined
128
+ >;
129
+
75
130
  export async function listIntegrationProviders({
76
131
  capability,
77
132
  signal,
78
133
  }: {
79
134
  capability?: IntegrationCapabilityDto;
80
135
  signal?: AbortSignal;
81
- }) {
136
+ }): Promise<IntegrationProvider[]> {
82
137
  const search = new URLSearchParams();
83
138
  if (capability) search.set('capability', capability);
84
139
  const query = search.toString();
85
140
  const path = query ? `/integration-providers?${query}` : '/integration-providers';
86
- return await apiRequest<ListIntegrationProvidersResponseDto>(path, {signal});
141
+ const response = await checkedApiRequest(listIntegrationProvidersResponseSchema, path, {signal});
142
+ return response.providers.map(toIntegrationProvider);
87
143
  }
88
144
 
89
145
  export async function listIntegrationConnections({
@@ -94,13 +150,15 @@ export async function listIntegrationConnections({
94
150
  workspaceId: string;
95
151
  capability?: IntegrationCapabilityDto | undefined;
96
152
  signal?: AbortSignal | undefined;
97
- }) {
153
+ }): Promise<IntegrationConnection[]> {
98
154
  const search = new URLSearchParams({workspace_id: workspaceId});
99
155
  if (capability) search.set('capability', capability);
100
- return await apiRequest<ListIntegrationConnectionsResponseDto>(
156
+ const response = await checkedApiRequest(
157
+ listIntegrationConnectionsResponseSchema,
101
158
  `/integration-connections?${search.toString()}`,
102
159
  {signal},
103
160
  );
161
+ return response.connections.map(toIntegrationConnection);
104
162
  }
105
163
 
106
164
  export async function listSourceConnections({
@@ -109,8 +167,8 @@ export async function listSourceConnections({
109
167
  }: {
110
168
  workspaceId: string;
111
169
  signal?: AbortSignal;
112
- }) {
113
- const result = await listIntegrationConnections({
170
+ }): Promise<IntegrationConnection[]> {
171
+ const connections = await listIntegrationConnections({
114
172
  workspaceId,
115
173
  capability: 'source_control',
116
174
  signal,
@@ -118,12 +176,7 @@ export async function listSourceConnections({
118
176
  // The endpoint returns every lifecycle status (the settings hub needs that),
119
177
  // but source-control consumers (onboarding redirect, project creation) only
120
178
  // act on usable connections — a disabled/error one must read as "not there".
121
- return {
122
- ...result,
123
- connections: result.connections.filter(
124
- (connection) => connection.lifecycle_status === 'active',
125
- ),
126
- };
179
+ return connections.filter(isUsableConnection);
127
180
  }
128
181
 
129
182
  export function sourceConnectionsQueryOptions(
@@ -138,39 +191,85 @@ export function sourceConnectionsQueryOptions(
138
191
  });
139
192
  }
140
193
 
141
- export async function createGiteaConnection(body: CreateGiteaConnectionBodyDto) {
142
- return await apiRequest<CreateGiteaConnectionResponseDto>('/integrations/gitea/connections', {
143
- method: 'POST',
144
- body,
145
- });
194
+ export async function createGiteaConnection(
195
+ body: CreateGiteaConnectionBodyDto,
196
+ ): Promise<IntegrationConnection> {
197
+ const response = await checkedApiRequest(
198
+ createGiteaConnectionResponseSchema,
199
+ '/integrations/gitea/connections',
200
+ {
201
+ method: 'POST',
202
+ body,
203
+ },
204
+ );
205
+ return toIntegrationConnection(response);
146
206
  }
147
207
 
148
- export async function createGithubInstall(body: CreateGithubInstallBodyDto) {
149
- return await apiRequest<CreateGithubInstallResponseDto>('/integrations/github/install', {
150
- method: 'POST',
151
- body,
152
- });
208
+ export async function createGithubInstall(
209
+ body: CreateGithubInstallBodyDto,
210
+ ): Promise<InstallRedirect> {
211
+ return toInstallRedirect(
212
+ await checkedApiRequest(createGithubInstallResponseSchema, '/integrations/github/install', {
213
+ method: 'POST',
214
+ body,
215
+ }),
216
+ );
153
217
  }
154
218
 
155
- export async function createSentryInstall(body: CreateSentryInstallBodyDto) {
156
- return await apiRequest<CreateSentryInstallResponseDto>('/integrations/sentry/install', {
157
- method: 'POST',
158
- body,
159
- });
219
+ export async function completeGithubCallback({
220
+ code,
221
+ installationId,
222
+ state,
223
+ setupAction,
224
+ token,
225
+ }: {
226
+ code: string;
227
+ installationId: number;
228
+ state: string;
229
+ setupAction?: string;
230
+ token: string;
231
+ }): Promise<IntegrationConnection> {
232
+ const query = new URLSearchParams({code, installation_id: String(installationId), state});
233
+ if (setupAction) query.set('setup_action', setupAction);
234
+ const response = await checkedApiRequest(
235
+ githubCallbackResponseSchema,
236
+ `/integrations/github/callback/api?${query.toString()}`,
237
+ {headers: {authorization: `Bearer ${token}`}},
238
+ );
239
+ return toIntegrationConnection(response);
160
240
  }
161
241
 
162
- export async function createLinearInstall(body: CreateLinearInstallBodyDto) {
163
- return await apiRequest<CreateLinearInstallResponseDto>('/integrations/linear/install', {
164
- method: 'POST',
165
- body,
166
- });
242
+ export async function createSentryInstall(
243
+ body: CreateSentryInstallBodyDto,
244
+ ): Promise<InstallRedirect> {
245
+ return toInstallRedirect(
246
+ await checkedApiRequest(createSentryInstallResponseSchema, '/integrations/sentry/install', {
247
+ method: 'POST',
248
+ body,
249
+ }),
250
+ );
167
251
  }
168
252
 
169
- export async function createSlackInstall(body: CreateSlackInstallBodyDto) {
170
- return await apiRequest<CreateSlackInstallResponseDto>('/integrations/slack/install', {
171
- method: 'POST',
172
- body,
173
- });
253
+ export async function createLinearInstall(
254
+ body: CreateLinearInstallBodyDto,
255
+ ): Promise<InstallRedirect> {
256
+ return toInstallRedirect(
257
+ await checkedApiRequest(createLinearInstallResponseSchema, '/integrations/linear/install', {
258
+ method: 'POST',
259
+ body,
260
+ }),
261
+ );
262
+ }
263
+
264
+ export async function createSlackInstall(
265
+ body: CreateSlackInstallBodyDto,
266
+ ): Promise<InstallRedirect> {
267
+ return toInstallRedirect(
268
+ await checkedApiRequest(createSlackInstallResponseSchema, '/integrations/slack/install', {
269
+ method: 'POST',
270
+ body,
271
+ }),
272
+ );
174
273
  }
175
274
 
176
275
  export async function completeLinearCallback({
@@ -179,11 +278,13 @@ export async function completeLinearCallback({
179
278
  }: {
180
279
  query: LinearCallbackQueryDto;
181
280
  token: string;
182
- }) {
183
- return await apiRequest<LinearCallbackResponseDto>(
281
+ }): Promise<IntegrationConnection> {
282
+ const response = await checkedApiRequest(
283
+ linearCallbackResponseSchema,
184
284
  `/integrations/linear/callback/api?${serializeLinearCallbackQuery(query)}`,
185
285
  {headers: {authorization: `Bearer ${token}`}},
186
286
  );
287
+ return toIntegrationConnection(response);
187
288
  }
188
289
 
189
290
  export async function completeSlackCallback({
@@ -192,21 +293,28 @@ export async function completeSlackCallback({
192
293
  }: {
193
294
  query: SlackCallbackQueryDto;
194
295
  token: string;
195
- }) {
196
- return await apiRequest<SlackCallbackResponseDto>(
296
+ }): Promise<IntegrationConnection> {
297
+ const response = await checkedApiRequest(
298
+ slackCallbackResponseSchema,
197
299
  `/integrations/slack/callback/api?${serializeSlackCallbackQuery(query)}`,
198
300
  {headers: {authorization: `Bearer ${token}`}},
199
301
  );
302
+ return toIntegrationConnection(response);
200
303
  }
201
304
 
202
305
  // Called from the callback route with an explicit bearer (same as the GitHub
203
306
  // callback): the route refreshes auth itself before forwarding the grant code.
204
307
  export async function connectSentry({body, token}: {body: SentryConnectBodyDto; token: string}) {
205
- return await apiRequest<SentryConnectResponseDto>('/integrations/sentry/connect', {
206
- method: 'POST',
207
- body,
208
- headers: {authorization: `Bearer ${token}`},
209
- });
308
+ const response = await checkedApiRequest(
309
+ sentryConnectResponseSchema,
310
+ '/integrations/sentry/connect',
311
+ {
312
+ method: 'POST',
313
+ body,
314
+ headers: {authorization: `Bearer ${token}`},
315
+ },
316
+ );
317
+ return toIntegrationConnection(response);
210
318
  }
211
319
 
212
320
  export async function listRepositories({
@@ -219,7 +327,7 @@ export async function listRepositories({
219
327
  cursor?: string;
220
328
  search?: string;
221
329
  signal?: AbortSignal;
222
- }) {
330
+ }): Promise<RepositoryPage> {
223
331
  const params = new URLSearchParams();
224
332
  if (cursor) params.set('cursor', cursor);
225
333
  if (search) params.set('search', search);
@@ -227,7 +335,11 @@ export async function listRepositories({
227
335
  const path = query
228
336
  ? `/integration-connections/${connectionId}/repositories?${query}`
229
337
  : `/integration-connections/${connectionId}/repositories`;
230
- return await apiRequest<ListRepositoriesResponseDto>(path, {signal});
338
+ const response = await checkedApiRequest(listRepositoriesResponseSchema, path, {signal});
339
+ return {
340
+ repositories: response.repositories.map(toRepository),
341
+ ...(response.next_cursor == null ? {} : {nextCursor: response.next_cursor}),
342
+ };
231
343
  }
232
344
 
233
345
  export async function updateIntegrationConnection({
@@ -236,22 +348,33 @@ export async function updateIntegrationConnection({
236
348
  }: {
237
349
  connectionId: string;
238
350
  body: UpdateIntegrationConnectionBodyDto;
239
- }) {
240
- return await apiRequest<IntegrationConnectionDto>(
351
+ }): Promise<IntegrationConnection> {
352
+ const response = await checkedApiRequest(
353
+ integrationConnectionDtoSchema,
241
354
  `/integration-connections/${encodeURIComponent(connectionId)}`,
242
355
  {method: 'PATCH', body},
243
356
  );
357
+ return toIntegrationConnection(response);
244
358
  }
245
359
 
246
360
  export async function deleteIntegrationConnection({connectionId}: {connectionId: string}) {
247
- await apiRequest<void>(`/integration-connections/${encodeURIComponent(connectionId)}`, {
248
- method: 'DELETE',
249
- });
361
+ await checkedApiRequest(
362
+ emptyResponseSchema,
363
+ `/integration-connections/${encodeURIComponent(connectionId)}`,
364
+ {
365
+ method: 'DELETE',
366
+ },
367
+ );
250
368
  }
251
369
 
252
370
  export function useIntegrationProvidersQuery(params?: {capability?: IntegrationCapabilityDto}) {
253
- const capability = params?.capability;
254
- return useQuery({
371
+ return useQuery(integrationProvidersQueryOptions(params?.capability));
372
+ }
373
+
374
+ export function integrationProvidersQueryOptions(
375
+ capability?: IntegrationCapabilityDto,
376
+ ): ProvidersQueryOptions {
377
+ return queryOptions({
255
378
  queryKey: integrationsQueryKeys.providers(capability ?? 'all'),
256
379
  queryFn: ({signal}) => listIntegrationProviders(capability ? {capability, signal} : {signal}),
257
380
  });
@@ -262,24 +385,30 @@ export function useSourceConnectionsQuery(workspaceId: string | undefined) {
262
385
  }
263
386
 
264
387
  export function useIntegrationConnectionsQuery(workspaceId: string | undefined) {
265
- return useQuery({
388
+ return useQuery(integrationConnectionsQueryOptions(workspaceId));
389
+ }
390
+
391
+ export function integrationConnectionsQueryOptions(
392
+ workspaceId: string | undefined,
393
+ ): ConnectionsQueryOptions {
394
+ return queryOptions({
266
395
  queryKey: workspaceId
267
396
  ? integrationsQueryKeys.connections(workspaceId, 'all')
268
- : [...integrationsQueryKeys.all, 'connections'],
397
+ : ([...integrationsQueryKeys.all, 'connections'] as const),
269
398
  enabled: Boolean(workspaceId),
270
399
  queryFn: ({signal}) => listIntegrationConnections({workspaceId: workspaceId ?? '', signal}),
271
400
  });
272
401
  }
273
402
 
274
- export function useRepositoriesInfiniteQuery(
403
+ export function repositoriesInfiniteQueryOptions(
275
404
  connectionId: string | undefined,
276
405
  options?: {search?: string},
277
- ) {
406
+ ): RepositoriesInfiniteQueryOptions {
278
407
  const trimmedSearch = options?.search?.trim() ?? '';
279
- return useInfiniteQuery({
408
+ return infiniteQueryOptions({
280
409
  queryKey: connectionId
281
410
  ? integrationsQueryKeys.repositories(connectionId, trimmedSearch)
282
- : [...integrationsQueryKeys.all, 'repositories'],
411
+ : ([...integrationsQueryKeys.all, 'repositories'] as const),
283
412
  enabled: Boolean(connectionId),
284
413
  initialPageParam: undefined as string | undefined,
285
414
  queryFn: ({pageParam, signal}) => {
@@ -291,12 +420,28 @@ export function useRepositoriesInfiniteQuery(
291
420
  if (trimmedSearch) args.search = trimmedSearch;
292
421
  return listRepositories(args);
293
422
  },
294
- getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,
423
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
295
424
  });
296
425
  }
297
426
 
427
+ export function useRepositoriesInfiniteQuery(
428
+ connectionId: string | undefined,
429
+ options?: {search?: string},
430
+ ) {
431
+ return useInfiniteQuery(repositoriesInfiniteQueryOptions(connectionId, options));
432
+ }
433
+
298
434
  export function useCreateGiteaConnectionMutation() {
299
- return useMutation({mutationFn: createGiteaConnection});
435
+ const queryClient = useQueryClient();
436
+ return useMutation({
437
+ mutationFn: createGiteaConnection,
438
+ onSuccess: async (connection) => {
439
+ await queryClient.invalidateQueries({
440
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspaceId),
441
+ refetchType: 'all',
442
+ });
443
+ },
444
+ });
300
445
  }
301
446
 
302
447
  export function useCreateLinearInstallMutation() {
@@ -321,7 +466,7 @@ export function useUpdateIntegrationConnectionMutation() {
321
466
  mutationFn: updateIntegrationConnection,
322
467
  onSuccess: async (connection) => {
323
468
  await queryClient.invalidateQueries({
324
- queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),
469
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspaceId),
325
470
  });
326
471
  },
327
472
  });