@shipfox/client-integrations 0.2.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.
Files changed (243) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +4 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.turbo/turbo-build.log +2 -0
  5. package/.turbo/turbo-type$colon$emit.log +1 -0
  6. package/.turbo/turbo-type.log +1 -0
  7. package/CHANGELOG.md +207 -0
  8. package/LICENSE +21 -0
  9. package/dist/components/connection-picker.d.ts +7 -0
  10. package/dist/components/connection-picker.d.ts.map +1 -0
  11. package/dist/components/connection-picker.js +98 -0
  12. package/dist/components/connection-picker.js.map +1 -0
  13. package/dist/components/installed-integrations-section.d.ts +14 -0
  14. package/dist/components/installed-integrations-section.d.ts.map +1 -0
  15. package/dist/components/installed-integrations-section.js +203 -0
  16. package/dist/components/installed-integrations-section.js.map +1 -0
  17. package/dist/components/integration-delete-confirm-modal.d.ts +10 -0
  18. package/dist/components/integration-delete-confirm-modal.d.ts.map +1 -0
  19. package/dist/components/integration-delete-confirm-modal.js +61 -0
  20. package/dist/components/integration-delete-confirm-modal.js.map +1 -0
  21. package/dist/components/integration-gallery-for-workspace.d.ts +9 -0
  22. package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -0
  23. package/dist/components/integration-gallery-for-workspace.js +169 -0
  24. package/dist/components/integration-gallery-for-workspace.js.map +1 -0
  25. package/dist/components/integration-gallery.d.ts +8 -0
  26. package/dist/components/integration-gallery.d.ts.map +1 -0
  27. package/dist/components/integration-gallery.js +26 -0
  28. package/dist/components/integration-gallery.js.map +1 -0
  29. package/dist/components/integration-gallery.stories.d.ts +26 -0
  30. package/dist/components/integration-gallery.stories.d.ts.map +1 -0
  31. package/dist/components/integration-gallery.stories.js +335 -0
  32. package/dist/components/integration-gallery.stories.js.map +1 -0
  33. package/dist/components/integration-usage-events.d.ts +4 -0
  34. package/dist/components/integration-usage-events.d.ts.map +1 -0
  35. package/dist/components/integration-usage-events.js +127 -0
  36. package/dist/components/integration-usage-events.js.map +1 -0
  37. package/dist/components/integration-usage-modal.d.ts +16 -0
  38. package/dist/components/integration-usage-modal.d.ts.map +1 -0
  39. package/dist/components/integration-usage-modal.js +184 -0
  40. package/dist/components/integration-usage-modal.js.map +1 -0
  41. package/dist/components/provider-grid.d.ts +14 -0
  42. package/dist/components/provider-grid.d.ts.map +1 -0
  43. package/dist/components/provider-grid.js +152 -0
  44. package/dist/components/provider-grid.js.map +1 -0
  45. package/dist/components/redirect-install-page.d.ts +19 -0
  46. package/dist/components/redirect-install-page.d.ts.map +1 -0
  47. package/dist/components/redirect-install-page.js +68 -0
  48. package/dist/components/redirect-install-page.js.map +1 -0
  49. package/dist/components/repository-picker.d.ts +15 -0
  50. package/dist/components/repository-picker.d.ts.map +1 -0
  51. package/dist/components/repository-picker.js +78 -0
  52. package/dist/components/repository-picker.js.map +1 -0
  53. package/dist/components/webhook/copyable-value.d.ts +10 -0
  54. package/dist/components/webhook/copyable-value.d.ts.map +1 -0
  55. package/dist/components/webhook/copyable-value.js +65 -0
  56. package/dist/components/webhook/copyable-value.js.map +1 -0
  57. package/dist/components/webhook/webhook-create-modal.d.ts +10 -0
  58. package/dist/components/webhook/webhook-create-modal.d.ts.map +1 -0
  59. package/dist/components/webhook/webhook-create-modal.js +233 -0
  60. package/dist/components/webhook/webhook-create-modal.js.map +1 -0
  61. package/dist/components/webhook/webhook-form-errors.d.ts +11 -0
  62. package/dist/components/webhook/webhook-form-errors.d.ts.map +1 -0
  63. package/dist/components/webhook/webhook-form-errors.js +27 -0
  64. package/dist/components/webhook/webhook-form-errors.js.map +1 -0
  65. package/dist/components/webhook/webhook-modals.stories.d.ts +25 -0
  66. package/dist/components/webhook/webhook-modals.stories.d.ts.map +1 -0
  67. package/dist/components/webhook/webhook-modals.stories.js +222 -0
  68. package/dist/components/webhook/webhook-modals.stories.js.map +1 -0
  69. package/dist/components/webhook/webhook-public-endpoint-alert.d.ts +2 -0
  70. package/dist/components/webhook/webhook-public-endpoint-alert.d.ts.map +1 -0
  71. package/dist/components/webhook/webhook-public-endpoint-alert.js +20 -0
  72. package/dist/components/webhook/webhook-public-endpoint-alert.js.map +1 -0
  73. package/dist/components/webhook/webhook-usage-details.d.ts +7 -0
  74. package/dist/components/webhook/webhook-usage-details.d.ts.map +1 -0
  75. package/dist/components/webhook/webhook-usage-details.js +49 -0
  76. package/dist/components/webhook/webhook-usage-details.js.map +1 -0
  77. package/dist/connection-status-badge.d.ts +12 -0
  78. package/dist/connection-status-badge.d.ts.map +1 -0
  79. package/dist/connection-status-badge.js +34 -0
  80. package/dist/connection-status-badge.js.map +1 -0
  81. package/dist/feature.d.ts +37 -0
  82. package/dist/feature.d.ts.map +1 -0
  83. package/dist/feature.js +48 -0
  84. package/dist/feature.js.map +1 -0
  85. package/dist/hooks/api/integrations.d.ts +265 -0
  86. package/dist/hooks/api/integrations.d.ts.map +1 -0
  87. package/dist/hooks/api/integrations.js +229 -0
  88. package/dist/hooks/api/integrations.js.map +1 -0
  89. package/dist/hooks/api/webhook-connections.d.ts +91 -0
  90. package/dist/hooks/api/webhook-connections.d.ts.map +1 -0
  91. package/dist/hooks/api/webhook-connections.js +97 -0
  92. package/dist/hooks/api/webhook-connections.js.map +1 -0
  93. package/dist/index.d.ts +17 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +18 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/integration-icon.d.ts +17 -0
  98. package/dist/integration-icon.d.ts.map +1 -0
  99. package/dist/integration-icon.js +22 -0
  100. package/dist/integration-icon.js.map +1 -0
  101. package/dist/linear-callback.d.ts +17 -0
  102. package/dist/linear-callback.d.ts.map +1 -0
  103. package/dist/linear-callback.js +113 -0
  104. package/dist/linear-callback.js.map +1 -0
  105. package/dist/pages/gitea-form-errors.d.ts +11 -0
  106. package/dist/pages/gitea-form-errors.d.ts.map +1 -0
  107. package/dist/pages/gitea-form-errors.js +35 -0
  108. package/dist/pages/gitea-form-errors.js.map +1 -0
  109. package/dist/pages/gitea-install-page.d.ts +2 -0
  110. package/dist/pages/gitea-install-page.d.ts.map +1 -0
  111. package/dist/pages/gitea-install-page.js +139 -0
  112. package/dist/pages/gitea-install-page.js.map +1 -0
  113. package/dist/pages/github-install-page.d.ts +2 -0
  114. package/dist/pages/github-install-page.d.ts.map +1 -0
  115. package/dist/pages/github-install-page.js +11 -0
  116. package/dist/pages/github-install-page.js.map +1 -0
  117. package/dist/pages/linear-callback-page.d.ts +2 -0
  118. package/dist/pages/linear-callback-page.d.ts.map +1 -0
  119. package/dist/pages/linear-callback-page.js +192 -0
  120. package/dist/pages/linear-callback-page.js.map +1 -0
  121. package/dist/pages/linear-install-page.d.ts +2 -0
  122. package/dist/pages/linear-install-page.d.ts.map +1 -0
  123. package/dist/pages/linear-install-page.js +24 -0
  124. package/dist/pages/linear-install-page.js.map +1 -0
  125. package/dist/pages/sentry-callback-page.d.ts +2 -0
  126. package/dist/pages/sentry-callback-page.d.ts.map +1 -0
  127. package/dist/pages/sentry-callback-page.js +256 -0
  128. package/dist/pages/sentry-callback-page.js.map +1 -0
  129. package/dist/pages/sentry-install-page.d.ts +2 -0
  130. package/dist/pages/sentry-install-page.d.ts.map +1 -0
  131. package/dist/pages/sentry-install-page.js +17 -0
  132. package/dist/pages/sentry-install-page.js.map +1 -0
  133. package/dist/pages/source-control-onboarding-page.d.ts +2 -0
  134. package/dist/pages/source-control-onboarding-page.d.ts.map +1 -0
  135. package/dist/pages/source-control-onboarding-page.js +37 -0
  136. package/dist/pages/source-control-onboarding-page.js.map +1 -0
  137. package/dist/provider-catalog.d.ts +15 -0
  138. package/dist/provider-catalog.d.ts.map +1 -0
  139. package/dist/provider-catalog.js +28 -0
  140. package/dist/provider-catalog.js.map +1 -0
  141. package/dist/routes/gitea.d.ts +6 -0
  142. package/dist/routes/gitea.d.ts.map +1 -0
  143. package/dist/routes/gitea.js +7 -0
  144. package/dist/routes/gitea.js.map +1 -0
  145. package/dist/routes/github-callback.d.ts +6 -0
  146. package/dist/routes/github-callback.d.ts.map +1 -0
  147. package/dist/routes/github-callback.js +129 -0
  148. package/dist/routes/github-callback.js.map +1 -0
  149. package/dist/routes/github.d.ts +6 -0
  150. package/dist/routes/github.d.ts.map +1 -0
  151. package/dist/routes/github.js +7 -0
  152. package/dist/routes/github.js.map +1 -0
  153. package/dist/routes/integrations.d.ts +6 -0
  154. package/dist/routes/integrations.d.ts.map +1 -0
  155. package/dist/routes/integrations.js +7 -0
  156. package/dist/routes/integrations.js.map +1 -0
  157. package/dist/routes/linear-callback.d.ts +6 -0
  158. package/dist/routes/linear-callback.d.ts.map +1 -0
  159. package/dist/routes/linear-callback.js +7 -0
  160. package/dist/routes/linear-callback.js.map +1 -0
  161. package/dist/routes/linear.d.ts +6 -0
  162. package/dist/routes/linear.d.ts.map +1 -0
  163. package/dist/routes/linear.js +7 -0
  164. package/dist/routes/linear.js.map +1 -0
  165. package/dist/routes/sentry-callback.d.ts +6 -0
  166. package/dist/routes/sentry-callback.d.ts.map +1 -0
  167. package/dist/routes/sentry-callback.js +7 -0
  168. package/dist/routes/sentry-callback.js.map +1 -0
  169. package/dist/routes/sentry.d.ts +6 -0
  170. package/dist/routes/sentry.d.ts.map +1 -0
  171. package/dist/routes/sentry.js +7 -0
  172. package/dist/routes/sentry.js.map +1 -0
  173. package/dist/sentry-callback.d.ts +35 -0
  174. package/dist/sentry-callback.d.ts.map +1 -0
  175. package/dist/sentry-callback.js +132 -0
  176. package/dist/sentry-callback.js.map +1 -0
  177. package/dist/tsconfig.test.tsbuildinfo +1 -0
  178. package/package.json +120 -0
  179. package/src/components/connection-picker.tsx +89 -0
  180. package/src/components/installed-integrations-section.tsx +198 -0
  181. package/src/components/integration-delete-confirm-modal.tsx +62 -0
  182. package/src/components/integration-gallery-for-workspace.tsx +193 -0
  183. package/src/components/integration-gallery.stories.tsx +302 -0
  184. package/src/components/integration-gallery.test.tsx +692 -0
  185. package/src/components/integration-gallery.tsx +49 -0
  186. package/src/components/integration-usage-events.test.ts +80 -0
  187. package/src/components/integration-usage-events.ts +116 -0
  188. package/src/components/integration-usage-modal.tsx +178 -0
  189. package/src/components/provider-grid.tsx +163 -0
  190. package/src/components/redirect-install-page.test.tsx +98 -0
  191. package/src/components/redirect-install-page.tsx +68 -0
  192. package/src/components/repository-picker.tsx +102 -0
  193. package/src/components/webhook/copyable-value.test.tsx +41 -0
  194. package/src/components/webhook/copyable-value.tsx +72 -0
  195. package/src/components/webhook/webhook-create-modal.tsx +247 -0
  196. package/src/components/webhook/webhook-form-errors.test.ts +45 -0
  197. package/src/components/webhook/webhook-form-errors.ts +32 -0
  198. package/src/components/webhook/webhook-modals.stories.tsx +225 -0
  199. package/src/components/webhook/webhook-public-endpoint-alert.tsx +15 -0
  200. package/src/components/webhook/webhook-usage-details.tsx +44 -0
  201. package/src/connection-status-badge.tsx +38 -0
  202. package/src/feature.ts +47 -0
  203. package/src/hooks/api/integrations.test.ts +105 -0
  204. package/src/hooks/api/integrations.ts +287 -0
  205. package/src/hooks/api/webhook-connections.ts +115 -0
  206. package/src/index.ts +16 -0
  207. package/src/integration-icon.react.test.tsx +40 -0
  208. package/src/integration-icon.test.ts +19 -0
  209. package/src/integration-icon.tsx +26 -0
  210. package/src/linear-callback.test.ts +227 -0
  211. package/src/linear-callback.ts +136 -0
  212. package/src/pages/gitea-form-errors.test.ts +55 -0
  213. package/src/pages/gitea-form-errors.ts +36 -0
  214. package/src/pages/gitea-install-page.tsx +108 -0
  215. package/src/pages/github-install-page.tsx +11 -0
  216. package/src/pages/linear-callback-page.test.tsx +157 -0
  217. package/src/pages/linear-callback-page.tsx +175 -0
  218. package/src/pages/linear-install-page.tsx +26 -0
  219. package/src/pages/sentry-callback-page.test.tsx +205 -0
  220. package/src/pages/sentry-callback-page.tsx +238 -0
  221. package/src/pages/sentry-install-page.tsx +17 -0
  222. package/src/pages/source-control-onboarding-page.test.tsx +46 -0
  223. package/src/pages/source-control-onboarding-page.tsx +26 -0
  224. package/src/provider-catalog.test.ts +42 -0
  225. package/src/provider-catalog.ts +49 -0
  226. package/src/routes/gitea.tsx +4 -0
  227. package/src/routes/github-callback.tsx +114 -0
  228. package/src/routes/github.tsx +4 -0
  229. package/src/routes/integrations.tsx +4 -0
  230. package/src/routes/linear-callback.tsx +4 -0
  231. package/src/routes/linear.tsx +4 -0
  232. package/src/routes/sentry-callback.tsx +4 -0
  233. package/src/routes/sentry.tsx +4 -0
  234. package/src/sentry-callback.test.ts +239 -0
  235. package/src/sentry-callback.ts +142 -0
  236. package/test/render.tsx +111 -0
  237. package/test/setup.ts +3 -0
  238. package/tsconfig.build.json +9 -0
  239. package/tsconfig.build.tsbuildinfo +1 -0
  240. package/tsconfig.json +3 -0
  241. package/tsconfig.test.json +8 -0
  242. package/vercel.json +8 -0
  243. package/vitest.config.ts +75 -0
@@ -0,0 +1,287 @@
1
+ import type {
2
+ IntegrationCapabilityDto,
3
+ IntegrationConnectionDto,
4
+ ListIntegrationConnectionsResponseDto,
5
+ ListIntegrationProvidersResponseDto,
6
+ ListRepositoriesResponseDto,
7
+ UpdateIntegrationConnectionBodyDto,
8
+ } 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,
16
+ } from '@shipfox/api-integration-github-dto';
17
+ import type {
18
+ CreateLinearInstallBodyDto,
19
+ CreateLinearInstallResponseDto,
20
+ LinearCallbackQueryDto,
21
+ LinearCallbackResponseDto,
22
+ } from '@shipfox/api-integration-linear-dto';
23
+ import type {
24
+ CreateSentryInstallBodyDto,
25
+ CreateSentryInstallResponseDto,
26
+ SentryConnectBodyDto,
27
+ SentryConnectResponseDto,
28
+ } from '@shipfox/api-integration-sentry-dto';
29
+ import {apiRequest} from '@shipfox/client-api';
30
+ import {useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
31
+ import {serializeLinearCallbackQuery} from '#linear-callback.js';
32
+
33
+ export const integrationsQueryKeys = {
34
+ all: ['integrations'] as const,
35
+ providers: (capability: IntegrationCapabilityDto | 'all') =>
36
+ [...integrationsQueryKeys.all, 'providers', capability] as const,
37
+ connections: (workspaceId: string, capability: IntegrationCapabilityDto | 'all') =>
38
+ [...integrationsQueryKeys.all, 'connections', workspaceId, capability] as const,
39
+ // Prefix matching every per-capability connections query for a workspace, so
40
+ // a mutation can refresh all connection views without touching providers or
41
+ // repositories.
42
+ connectionsByWorkspace: (workspaceId: string) =>
43
+ [...integrationsQueryKeys.all, 'connections', workspaceId] as const,
44
+ sourceConnections: (workspaceId: string) =>
45
+ integrationsQueryKeys.connections(workspaceId, 'source_control'),
46
+ repositories: (connectionId: string, search: string) =>
47
+ [...integrationsQueryKeys.all, 'repositories', connectionId, search] as const,
48
+ };
49
+
50
+ export async function listIntegrationProviders({
51
+ capability,
52
+ signal,
53
+ }: {
54
+ capability?: IntegrationCapabilityDto;
55
+ signal?: AbortSignal;
56
+ }) {
57
+ const search = new URLSearchParams();
58
+ if (capability) search.set('capability', capability);
59
+ const query = search.toString();
60
+ const path = query ? `/integration-providers?${query}` : '/integration-providers';
61
+ return await apiRequest<ListIntegrationProvidersResponseDto>(path, {signal});
62
+ }
63
+
64
+ export async function listIntegrationConnections({
65
+ workspaceId,
66
+ capability,
67
+ signal,
68
+ }: {
69
+ workspaceId: string;
70
+ capability?: IntegrationCapabilityDto | undefined;
71
+ signal?: AbortSignal | undefined;
72
+ }) {
73
+ const search = new URLSearchParams({workspace_id: workspaceId});
74
+ if (capability) search.set('capability', capability);
75
+ return await apiRequest<ListIntegrationConnectionsResponseDto>(
76
+ `/integration-connections?${search.toString()}`,
77
+ {signal},
78
+ );
79
+ }
80
+
81
+ export async function listSourceConnections({
82
+ workspaceId,
83
+ signal,
84
+ }: {
85
+ workspaceId: string;
86
+ signal?: AbortSignal;
87
+ }) {
88
+ const result = await listIntegrationConnections({
89
+ workspaceId,
90
+ capability: 'source_control',
91
+ signal,
92
+ });
93
+ // The endpoint returns every lifecycle status (the settings hub needs that),
94
+ // but source-control consumers (onboarding redirect, project creation) only
95
+ // act on usable connections — a disabled/error one must read as "not there".
96
+ return {
97
+ ...result,
98
+ connections: result.connections.filter(
99
+ (connection) => connection.lifecycle_status === 'active',
100
+ ),
101
+ };
102
+ }
103
+
104
+ export async function createGiteaConnection(body: CreateGiteaConnectionBodyDto) {
105
+ return await apiRequest<CreateGiteaConnectionResponseDto>('/integrations/gitea/connections', {
106
+ method: 'POST',
107
+ body,
108
+ });
109
+ }
110
+
111
+ export async function createGithubInstall(body: CreateGithubInstallBodyDto) {
112
+ return await apiRequest<CreateGithubInstallResponseDto>('/integrations/github/install', {
113
+ method: 'POST',
114
+ body,
115
+ });
116
+ }
117
+
118
+ export async function createSentryInstall(body: CreateSentryInstallBodyDto) {
119
+ return await apiRequest<CreateSentryInstallResponseDto>('/integrations/sentry/install', {
120
+ method: 'POST',
121
+ body,
122
+ });
123
+ }
124
+
125
+ export async function createLinearInstall(body: CreateLinearInstallBodyDto) {
126
+ return await apiRequest<CreateLinearInstallResponseDto>('/integrations/linear/install', {
127
+ method: 'POST',
128
+ body,
129
+ });
130
+ }
131
+
132
+ export async function completeLinearCallback({
133
+ query,
134
+ token,
135
+ }: {
136
+ query: LinearCallbackQueryDto;
137
+ token: string;
138
+ }) {
139
+ return await apiRequest<LinearCallbackResponseDto>(
140
+ `/integrations/linear/callback/api?${serializeLinearCallbackQuery(query)}`,
141
+ {headers: {authorization: `Bearer ${token}`}},
142
+ );
143
+ }
144
+
145
+ // Called from the callback route with an explicit bearer (same as the GitHub
146
+ // callback): the route refreshes auth itself before forwarding the grant code.
147
+ export async function connectSentry({body, token}: {body: SentryConnectBodyDto; token: string}) {
148
+ return await apiRequest<SentryConnectResponseDto>('/integrations/sentry/connect', {
149
+ method: 'POST',
150
+ body,
151
+ headers: {authorization: `Bearer ${token}`},
152
+ });
153
+ }
154
+
155
+ export async function listRepositories({
156
+ connectionId,
157
+ cursor,
158
+ search,
159
+ signal,
160
+ }: {
161
+ connectionId: string;
162
+ cursor?: string;
163
+ search?: string;
164
+ signal?: AbortSignal;
165
+ }) {
166
+ const params = new URLSearchParams();
167
+ if (cursor) params.set('cursor', cursor);
168
+ if (search) params.set('search', search);
169
+ const query = params.toString();
170
+ const path = query
171
+ ? `/integration-connections/${connectionId}/repositories?${query}`
172
+ : `/integration-connections/${connectionId}/repositories`;
173
+ return await apiRequest<ListRepositoriesResponseDto>(path, {signal});
174
+ }
175
+
176
+ export async function updateIntegrationConnection({
177
+ connectionId,
178
+ body,
179
+ }: {
180
+ connectionId: string;
181
+ body: UpdateIntegrationConnectionBodyDto;
182
+ }) {
183
+ return await apiRequest<IntegrationConnectionDto>(
184
+ `/integration-connections/${encodeURIComponent(connectionId)}`,
185
+ {method: 'PATCH', body},
186
+ );
187
+ }
188
+
189
+ export async function deleteIntegrationConnection({connectionId}: {connectionId: string}) {
190
+ await apiRequest<void>(`/integration-connections/${encodeURIComponent(connectionId)}`, {
191
+ method: 'DELETE',
192
+ });
193
+ }
194
+
195
+ export function useIntegrationProvidersQuery(params?: {capability?: IntegrationCapabilityDto}) {
196
+ const capability = params?.capability;
197
+ return useQuery({
198
+ queryKey: integrationsQueryKeys.providers(capability ?? 'all'),
199
+ queryFn: ({signal}) => listIntegrationProviders(capability ? {capability, signal} : {signal}),
200
+ });
201
+ }
202
+
203
+ export function useSourceConnectionsQuery(workspaceId: string | undefined) {
204
+ return useQuery({
205
+ queryKey: workspaceId
206
+ ? integrationsQueryKeys.sourceConnections(workspaceId)
207
+ : [...integrationsQueryKeys.all, 'source-connections'],
208
+ enabled: Boolean(workspaceId),
209
+ queryFn: ({signal}) => listSourceConnections({workspaceId: workspaceId ?? '', signal}),
210
+ });
211
+ }
212
+
213
+ export function useIntegrationConnectionsQuery(workspaceId: string | undefined) {
214
+ return useQuery({
215
+ queryKey: workspaceId
216
+ ? integrationsQueryKeys.connections(workspaceId, 'all')
217
+ : [...integrationsQueryKeys.all, 'connections'],
218
+ enabled: Boolean(workspaceId),
219
+ queryFn: ({signal}) => listIntegrationConnections({workspaceId: workspaceId ?? '', signal}),
220
+ });
221
+ }
222
+
223
+ export function useRepositoriesInfiniteQuery(
224
+ connectionId: string | undefined,
225
+ options?: {search?: string},
226
+ ) {
227
+ const trimmedSearch = options?.search?.trim() ?? '';
228
+ return useInfiniteQuery({
229
+ queryKey: connectionId
230
+ ? integrationsQueryKeys.repositories(connectionId, trimmedSearch)
231
+ : [...integrationsQueryKeys.all, 'repositories'],
232
+ enabled: Boolean(connectionId),
233
+ initialPageParam: undefined as string | undefined,
234
+ queryFn: ({pageParam, signal}) => {
235
+ const args: {connectionId: string; cursor?: string; search?: string; signal: AbortSignal} = {
236
+ connectionId: connectionId ?? '',
237
+ signal,
238
+ };
239
+ if (pageParam) args.cursor = pageParam;
240
+ if (trimmedSearch) args.search = trimmedSearch;
241
+ return listRepositories(args);
242
+ },
243
+ getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,
244
+ });
245
+ }
246
+
247
+ export function useCreateGiteaConnectionMutation() {
248
+ return useMutation({mutationFn: createGiteaConnection});
249
+ }
250
+
251
+ export function useCreateLinearInstallMutation() {
252
+ return useMutation({mutationFn: createLinearInstall});
253
+ }
254
+
255
+ export function useCompleteLinearCallbackMutation() {
256
+ return useMutation({mutationFn: completeLinearCallback});
257
+ }
258
+
259
+ export function useUpdateIntegrationConnectionMutation() {
260
+ const queryClient = useQueryClient();
261
+ return useMutation({
262
+ mutationFn: updateIntegrationConnection,
263
+ onSuccess: async (connection) => {
264
+ await queryClient.invalidateQueries({
265
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),
266
+ });
267
+ },
268
+ });
269
+ }
270
+
271
+ export function useDeleteIntegrationConnectionMutation() {
272
+ const queryClient = useQueryClient();
273
+ return useMutation({
274
+ mutationFn: ({
275
+ workspaceId: _workspaceId,
276
+ connectionId,
277
+ }: {
278
+ workspaceId: string;
279
+ connectionId: string;
280
+ }) => deleteIntegrationConnection({connectionId}),
281
+ onSuccess: async (_result, variables) => {
282
+ await queryClient.invalidateQueries({
283
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(variables.workspaceId),
284
+ });
285
+ },
286
+ });
287
+ }
@@ -0,0 +1,115 @@
1
+ import type {
2
+ CreateWebhookConnectionBodyDto,
3
+ ListWebhookConnectionsResponseDto,
4
+ UpdateWebhookConnectionBodyDto,
5
+ WebhookConnectionDto,
6
+ } from '@shipfox/api-integration-webhook-dto';
7
+ import {apiRequest} from '@shipfox/client-api';
8
+ import {type QueryClient, useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
9
+ import {integrationsQueryKeys} from './integrations.js';
10
+
11
+ export const webhookConnectionsQueryKeys = {
12
+ all: ['webhook-connections'] as const,
13
+ list: (workspaceId: string) => [...webhookConnectionsQueryKeys.all, 'list', workspaceId] as const,
14
+ };
15
+
16
+ export async function listWebhookConnections({
17
+ workspaceId,
18
+ signal,
19
+ }: {
20
+ workspaceId: string;
21
+ signal?: AbortSignal;
22
+ }) {
23
+ const search = new URLSearchParams({workspace_id: workspaceId});
24
+ return await apiRequest<ListWebhookConnectionsResponseDto>(
25
+ `/integrations/webhook/connections?${search.toString()}`,
26
+ {signal},
27
+ );
28
+ }
29
+
30
+ export async function createWebhookConnection(body: CreateWebhookConnectionBodyDto) {
31
+ return await apiRequest<WebhookConnectionDto>('/integrations/webhook/connections', {
32
+ method: 'POST',
33
+ body,
34
+ });
35
+ }
36
+
37
+ export async function updateWebhookConnection({
38
+ connectionId,
39
+ body,
40
+ }: {
41
+ connectionId: string;
42
+ body: UpdateWebhookConnectionBodyDto;
43
+ }) {
44
+ return await apiRequest<WebhookConnectionDto>(
45
+ `/integrations/webhook/connections/${encodeURIComponent(connectionId)}`,
46
+ {method: 'PATCH', body},
47
+ );
48
+ }
49
+
50
+ export async function deleteWebhookConnection({connectionId}: {connectionId: string}) {
51
+ await apiRequest<void>(`/integrations/webhook/connections/${encodeURIComponent(connectionId)}`, {
52
+ method: 'DELETE',
53
+ });
54
+ }
55
+
56
+ export function useWebhookConnectionsQuery(workspaceId: string | undefined) {
57
+ return useQuery({
58
+ queryKey: workspaceId
59
+ ? webhookConnectionsQueryKeys.list(workspaceId)
60
+ : [...webhookConnectionsQueryKeys.all, 'list'],
61
+ enabled: Boolean(workspaceId),
62
+ queryFn: ({signal}) => listWebhookConnections({workspaceId: workspaceId ?? '', signal}),
63
+ });
64
+ }
65
+
66
+ export function useCreateWebhookConnectionMutation() {
67
+ const queryClient = useQueryClient();
68
+ return useMutation({
69
+ mutationFn: createWebhookConnection,
70
+ onSuccess: async (connection) => {
71
+ await invalidateWebhookConnectionViews(queryClient, connection.workspace_id);
72
+ },
73
+ });
74
+ }
75
+
76
+ export function useUpdateWebhookConnectionMutation() {
77
+ const queryClient = useQueryClient();
78
+ return useMutation({
79
+ mutationFn: ({
80
+ connectionId,
81
+ body,
82
+ }: {
83
+ workspaceId: string;
84
+ connectionId: string;
85
+ body: UpdateWebhookConnectionBodyDto;
86
+ }) => updateWebhookConnection({connectionId, body}),
87
+ onSuccess: async (connection) => {
88
+ await invalidateWebhookConnectionViews(queryClient, connection.workspace_id);
89
+ },
90
+ });
91
+ }
92
+
93
+ export function useDeleteWebhookConnectionMutation() {
94
+ const queryClient = useQueryClient();
95
+ return useMutation({
96
+ mutationFn: ({connectionId}: {workspaceId: string; connectionId: string}) =>
97
+ deleteWebhookConnection({connectionId}),
98
+ onSuccess: async (_result, variables) => {
99
+ await invalidateWebhookConnectionViews(queryClient, variables.workspaceId);
100
+ },
101
+ });
102
+ }
103
+
104
+ async function invalidateWebhookConnectionViews(queryClient: QueryClient, workspaceId: string) {
105
+ await Promise.all([
106
+ queryClient.invalidateQueries({
107
+ queryKey: webhookConnectionsQueryKeys.list(workspaceId),
108
+ refetchType: 'all',
109
+ }),
110
+ queryClient.invalidateQueries({
111
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(workspaceId),
112
+ refetchType: 'all',
113
+ }),
114
+ ]);
115
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export * from './components/connection-picker.js';
2
+ export * from './components/integration-gallery.js';
3
+ export * from './components/redirect-install-page.js';
4
+ export * from './components/repository-picker.js';
5
+ export * from './connection-status-badge.js';
6
+ export * from './hooks/api/integrations.js';
7
+ export * from './integration-icon.js';
8
+ export * from './pages/gitea-install-page.js';
9
+ export * from './pages/github-install-page.js';
10
+ export * from './pages/linear-callback-page.js';
11
+ export * from './pages/linear-install-page.js';
12
+ export * from './pages/sentry-callback-page.js';
13
+ export * from './pages/sentry-install-page.js';
14
+ export * from './pages/source-control-onboarding-page.js';
15
+ export * from './provider-catalog.js';
16
+ export * from './sentry-callback.js';
@@ -0,0 +1,40 @@
1
+ // @vitest-environment jsdom
2
+ import '@testing-library/jest-dom/vitest';
3
+ import {render} from '@testing-library/react';
4
+ import {IntegrationIcon} from './integration-icon.js';
5
+
6
+ describe('IntegrationIcon', () => {
7
+ it('exposes a labeled icon to assistive tech when it is the sole identifier', () => {
8
+ const {container} = render(<IntegrationIcon source="github" aria-label="GitHub" />);
9
+
10
+ const svg = container.querySelector('svg');
11
+ expect(svg).toHaveAttribute('aria-label', 'GitHub');
12
+ expect(svg).not.toHaveAttribute('aria-hidden');
13
+ });
14
+
15
+ it('hides the icon from assistive tech when marked decorative', () => {
16
+ const {container} = render(<IntegrationIcon source="github" aria-hidden />);
17
+
18
+ const svg = container.querySelector('svg');
19
+ expect(svg).toHaveAttribute('aria-hidden', 'true');
20
+ });
21
+
22
+ it('forwards class names so callers control sizing and color', () => {
23
+ const {container} = render(
24
+ <IntegrationIcon source="github" className="size-24 text-foreground-neutral-base" />,
25
+ );
26
+
27
+ const svg = container.querySelector('svg');
28
+ expect(svg).toHaveClass('size-24', 'text-foreground-neutral-base');
29
+ });
30
+
31
+ it('forwards sizing and label to the neutral fallback glyph for an unknown source', () => {
32
+ const {container} = render(
33
+ <IntegrationIcon source="gitlab" aria-label="GitLab" className="size-24" />,
34
+ );
35
+
36
+ const svg = container.querySelector('svg');
37
+ expect(svg).toHaveAttribute('aria-label', 'GitLab');
38
+ expect(svg).toHaveClass('size-24');
39
+ });
40
+ });
@@ -0,0 +1,19 @@
1
+ import {FALLBACK_INTEGRATION_ICON, getIntegrationIcon} from './integration-icon.js';
2
+
3
+ describe('getIntegrationIcon', () => {
4
+ it('resolves cataloged providers to the icon they declare', () => {
5
+ expect(getIntegrationIcon('github')).toBe('github');
6
+ expect(getIntegrationIcon('sentry')).toBe('sentry');
7
+ expect(getIntegrationIcon('linear')).toBe('linear');
8
+ expect(getIntegrationIcon('gitea')).toBe('gitea');
9
+ });
10
+
11
+ it('falls back for sources missing from the catalog', () => {
12
+ expect(getIntegrationIcon('gitlab')).toBe(FALLBACK_INTEGRATION_ICON);
13
+ expect(getIntegrationIcon('mystery')).toBe(FALLBACK_INTEGRATION_ICON);
14
+ });
15
+
16
+ it.each([['' as const], [null], [undefined]])('falls back for an empty source (%s)', (source) => {
17
+ expect(getIntegrationIcon(source)).toBe(FALLBACK_INTEGRATION_ICON);
18
+ });
19
+ });
@@ -0,0 +1,26 @@
1
+ import {Icon, type IconName} from '@shipfox/react-ui/icon';
2
+ import type {ComponentProps} from 'react';
3
+ import {PROVIDER_CATALOG} from '#provider-catalog.js';
4
+
5
+ export const FALLBACK_INTEGRATION_ICON: IconName = 'componentLine';
6
+
7
+ /**
8
+ * Accepts the source strings used by connections and events; unknown values use
9
+ * the neutral fallback icon.
10
+ */
11
+ export function getIntegrationIcon(source: string | null | undefined): IconName {
12
+ if (!source) return FALLBACK_INTEGRATION_ICON;
13
+ return PROVIDER_CATALOG[source]?.iconName ?? FALLBACK_INTEGRATION_ICON;
14
+ }
15
+
16
+ export interface IntegrationIconProps extends Omit<ComponentProps<typeof Icon>, 'name'> {
17
+ source: string | null | undefined;
18
+ }
19
+
20
+ /**
21
+ * Preserves the base icon props so callers can decide whether the glyph is
22
+ * decorative or labeled.
23
+ */
24
+ export function IntegrationIcon({source, ...props}: IntegrationIconProps) {
25
+ return <Icon name={getIntegrationIcon(source)} {...props} />;
26
+ }