@shipfox/client-integrations 4.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 (194) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +86 -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 +18 -0
  61. package/dist/feature.d.ts.map +1 -1
  62. package/dist/feature.js +16 -1
  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 +47 -237
  69. package/dist/hooks/api/integrations.d.ts.map +1 -1
  70. package/dist/hooks/api/integrations.js +92 -42
  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/integrations-settings.d.ts +6 -0
  116. package/dist/routes/integrations-settings.d.ts.map +1 -0
  117. package/dist/routes/integrations-settings.js +7 -0
  118. package/dist/routes/integrations-settings.js.map +1 -0
  119. package/dist/routes/linear-callback.d.ts +2 -0
  120. package/dist/routes/linear-callback.d.ts.map +1 -1
  121. package/dist/routes/linear-callback.js +2 -0
  122. package/dist/routes/linear-callback.js.map +1 -1
  123. package/dist/routes/sentry-callback.d.ts +2 -0
  124. package/dist/routes/sentry-callback.d.ts.map +1 -1
  125. package/dist/routes/sentry-callback.js +2 -0
  126. package/dist/routes/sentry-callback.js.map +1 -1
  127. package/dist/routes/slack-callback.d.ts +2 -0
  128. package/dist/routes/slack-callback.d.ts.map +1 -1
  129. package/dist/routes/slack-callback.js +2 -0
  130. package/dist/routes/slack-callback.js.map +1 -1
  131. package/dist/sentry-callback.d.ts +2 -1
  132. package/dist/sentry-callback.d.ts.map +1 -1
  133. package/dist/sentry-callback.js +16 -19
  134. package/dist/sentry-callback.js.map +1 -1
  135. package/dist/slack-callback.d.ts +2 -1
  136. package/dist/slack-callback.d.ts.map +1 -1
  137. package/dist/slack-callback.js +14 -15
  138. package/dist/slack-callback.js.map +1 -1
  139. package/dist/tsconfig.test.tsbuildinfo +1 -1
  140. package/package.json +17 -17
  141. package/src/application/complete-integration-callback.test.ts +41 -0
  142. package/src/application/complete-integration-callback.ts +39 -0
  143. package/src/components/connection-picker.tsx +7 -7
  144. package/src/components/installed-integrations-section.tsx +29 -24
  145. package/src/components/integration-gallery-for-workspace.tsx +23 -16
  146. package/src/components/integration-gallery.tsx +3 -3
  147. package/src/components/integration-usage-events.ts +1 -116
  148. package/src/components/integration-usage-modal.tsx +4 -4
  149. package/src/components/provider-grid.tsx +30 -18
  150. package/src/components/redirect-install-page.test.tsx +2 -6
  151. package/src/components/redirect-install-page.tsx +3 -2
  152. package/src/components/repository-picker.tsx +6 -6
  153. package/src/components/webhook/webhook-create-modal.tsx +12 -11
  154. package/src/components/webhook/webhook-modals.stories.tsx +10 -10
  155. package/src/components/webhook/webhook-usage-details.tsx +2 -4
  156. package/src/connection-status-badge.tsx +8 -14
  157. package/src/core/models.ts +200 -0
  158. package/src/feature.ts +17 -1
  159. package/src/hooks/api/integration-mapper.ts +64 -0
  160. package/src/hooks/api/integrations.test.ts +44 -22
  161. package/src/hooks/api/integrations.ts +258 -89
  162. package/src/hooks/api/webhook-connections.ts +58 -17
  163. package/src/index.ts +2 -1
  164. package/src/linear-callback.ts +21 -16
  165. package/src/pages/gitea-install-page.tsx +1 -10
  166. package/src/pages/linear-callback-page.test.tsx +1 -1
  167. package/src/pages/linear-callback-page.tsx +18 -29
  168. package/src/pages/linear-install-page.tsx +2 -1
  169. package/src/pages/sentry-callback-page.test.tsx +6 -2
  170. package/src/pages/sentry-callback-page.tsx +28 -25
  171. package/src/pages/sentry-install-page.tsx +2 -1
  172. package/src/pages/slack-callback-page.tsx +20 -25
  173. package/src/pages/slack-install-page.tsx +2 -1
  174. package/src/pages/source-control-onboarding-page.tsx +5 -1
  175. package/src/provider-catalog.ts +7 -6
  176. package/src/routes/github-callback.test.ts +35 -1
  177. package/src/routes/github-callback.tsx +47 -21
  178. package/src/routes/integrations-settings.tsx +4 -0
  179. package/src/routes/linear-callback.tsx +5 -1
  180. package/src/routes/sentry-callback.tsx +5 -1
  181. package/src/routes/slack-callback.tsx +6 -1
  182. package/src/sentry-callback.test.ts +2 -1
  183. package/src/sentry-callback.ts +28 -20
  184. package/src/slack-callback.ts +21 -16
  185. package/test/render.tsx +1 -5
  186. package/tsconfig.build.tsbuildinfo +1 -1
  187. package/vercel.json +5 -0
  188. package/dist/integration-icon.d.ts +0 -17
  189. package/dist/integration-icon.d.ts.map +0 -1
  190. package/dist/integration-icon.js +0 -22
  191. package/dist/integration-icon.js.map +0 -1
  192. package/src/integration-icon.react.test.tsx +0 -40
  193. package/src/integration-icon.test.ts +0 -19
  194. package/src/integration-icon.tsx +0 -26
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-integrations",
3
3
  "license": "MIT",
4
- "version": "4.0.0",
4
+ "version": "6.0.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -10,10 +10,6 @@
10
10
  "type": "module",
11
11
  "main": "dist/index.js",
12
12
  "types": "dist/index.d.ts",
13
- "imports": {
14
- "#*": "./dist/*",
15
- "#test/*": "./test/*"
16
- },
17
13
  "exports": {
18
14
  ".": {
19
15
  "types": "./dist/index.d.ts",
@@ -31,18 +27,19 @@
31
27
  "dependencies": {
32
28
  "@swc/helpers": "^0.5.17",
33
29
  "@tanstack/react-form": "^1.32.0",
34
- "@shipfox/api-integration-core-dto": "6.0.0",
35
- "@shipfox/api-integration-gitea-dto": "6.0.0",
36
- "@shipfox/api-integration-github-dto": "6.0.0",
37
- "@shipfox/api-integration-linear-dto": "6.0.0",
38
- "@shipfox/api-integration-slack-dto": "6.0.0",
39
- "@shipfox/api-integration-sentry-dto": "6.0.0",
40
- "@shipfox/api-integration-webhook-dto": "6.0.0",
41
- "@shipfox/client-api": "4.0.0",
42
- "@shipfox/client-auth": "4.0.0",
43
- "@shipfox/client-shell": "4.0.0",
44
- "@shipfox/client-ui": "4.0.0",
45
- "@shipfox/react-ui": "0.3.5"
30
+ "@shipfox/api-integration-core-dto": "9.0.1",
31
+ "@shipfox/api-integration-gitea-dto": "9.0.1",
32
+ "@shipfox/api-integration-github-dto": "9.0.1",
33
+ "@shipfox/api-integration-linear-dto": "9.0.1",
34
+ "@shipfox/api-integration-slack-dto": "9.0.1",
35
+ "@shipfox/api-integration-sentry-dto": "9.0.1",
36
+ "@shipfox/api-integration-webhook-dto": "9.0.1",
37
+ "@shipfox/client-api": "6.0.1",
38
+ "@shipfox/client-auth": "6.0.1",
39
+ "@shipfox/client-shell": "6.0.1",
40
+ "@shipfox/client-ui": "6.0.1",
41
+ "@shipfox/integration-icons": "0.2.1",
42
+ "@shipfox/react-ui": "0.3.6"
46
43
  },
47
44
  "peerDependencies": {
48
45
  "@tanstack/react-query": "^5.101.0",
@@ -51,6 +48,9 @@
51
48
  "react": "^19.0.0",
52
49
  "react-dom": "^19.0.0"
53
50
  },
51
+ "imports": {
52
+ "#*": "./dist/*"
53
+ },
54
54
  "scripts": {
55
55
  "build": "shipfox-swc",
56
56
  "check": "shipfox-biome-check",
@@ -0,0 +1,41 @@
1
+ import {QueryClient} from '@tanstack/react-query';
2
+ import type {IntegrationConnection} from '#core/models.js';
3
+ import {integrationsQueryKeys} from '#hooks/api/integrations.js';
4
+ import {completeIntegrationCallback} from './complete-integration-callback.js';
5
+
6
+ const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
7
+
8
+ const connection: IntegrationConnection = {
9
+ id: '22222222-2222-4222-8222-222222222222',
10
+ workspaceId: WORKSPACE_ID,
11
+ provider: 'github',
12
+ externalAccountId: 'account',
13
+ slug: 'github-account',
14
+ displayName: 'GitHub',
15
+ lifecycleStatus: 'active',
16
+ capabilities: ['source_control'],
17
+ createdAt: '2026-07-23T00:00:00.000Z',
18
+ updatedAt: '2026-07-23T00:00:00.000Z',
19
+ };
20
+
21
+ describe('completeIntegrationCallback', () => {
22
+ test('invalidates the workspace connection views after callback completion', async () => {
23
+ const queryClient = new QueryClient();
24
+ const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries');
25
+ const refreshAuth = vi.fn().mockResolvedValue({accessToken: 'access-token'});
26
+ const complete = vi.fn().mockResolvedValue(connection);
27
+
28
+ const result = await completeIntegrationCallback({
29
+ input: {code: 'grant-code'},
30
+ refreshAuth,
31
+ complete,
32
+ queryClient,
33
+ });
34
+
35
+ expect(result).toBe(connection);
36
+ expect(complete).toHaveBeenCalledWith({code: 'grant-code'}, 'access-token');
37
+ expect(invalidateQueries).toHaveBeenCalledWith({
38
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(WORKSPACE_ID),
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,39 @@
1
+ import {type QueryClient, useQueryClient} from '@tanstack/react-query';
2
+ import {useCallback} from 'react';
3
+ import type {IntegrationConnection} from '#core/models.js';
4
+ import {integrationsQueryKeys} from '#hooks/api/integrations.js';
5
+
6
+ export interface CompleteIntegrationCallbackOptions<TInput> {
7
+ input: TInput;
8
+ refreshAuth: () => Promise<{accessToken: string}>;
9
+ complete: (input: TInput, accessToken: string) => Promise<IntegrationConnection>;
10
+ queryClient: QueryClient;
11
+ }
12
+
13
+ export async function completeIntegrationCallback<TInput>({
14
+ input,
15
+ refreshAuth,
16
+ complete,
17
+ queryClient,
18
+ }: CompleteIntegrationCallbackOptions<TInput>): Promise<IntegrationConnection> {
19
+ const session = await refreshAuth();
20
+ const connection = await complete(input, session.accessToken);
21
+ try {
22
+ await queryClient.invalidateQueries({
23
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspaceId),
24
+ });
25
+ } catch {
26
+ // Cache refresh is best effort: the successful callback is already committed server-side.
27
+ }
28
+ return connection;
29
+ }
30
+
31
+ export function useCompleteIntegrationCallback() {
32
+ const queryClient = useQueryClient();
33
+ return useCallback(
34
+ async <TInput>(
35
+ options: Omit<CompleteIntegrationCallbackOptions<TInput>, 'queryClient'>,
36
+ ): Promise<IntegrationConnection> => completeIntegrationCallback({...options, queryClient}),
37
+ [queryClient],
38
+ );
39
+ }
@@ -1,18 +1,18 @@
1
- import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
1
+ import {IntegrationIcon} from '@shipfox/integration-icons';
2
2
  import {useIsTextTruncated} from '@shipfox/react-ui/hooks';
3
3
  import {Label} from '@shipfox/react-ui/label';
4
4
  import {RadioGroup, RadioGroupItem} from '@shipfox/react-ui/radio-group';
5
5
  import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
6
6
  import {Text} from '@shipfox/react-ui/typography';
7
7
  import {useId} from 'react';
8
- import {IntegrationIcon} from '#integration-icon.js';
8
+ import type {IntegrationConnection} from '#core/models.js';
9
9
 
10
10
  export function ConnectionPicker({
11
11
  connections,
12
12
  selectedConnectionId,
13
13
  onSelect,
14
14
  }: {
15
- connections: IntegrationConnectionDto[];
15
+ connections: IntegrationConnection[];
16
16
  selectedConnectionId: string | undefined;
17
17
  onSelect: (connectionId: string) => void;
18
18
  }) {
@@ -39,8 +39,8 @@ export function ConnectionPicker({
39
39
  );
40
40
  }
41
41
 
42
- function ConnectionOption({connection}: {connection: IntegrationConnectionDto}) {
43
- const {ref: nameRef, isTruncated} = useIsTextTruncated<HTMLSpanElement>(connection.display_name);
42
+ function ConnectionOption({connection}: {connection: IntegrationConnection}) {
43
+ const {ref: nameRef, isTruncated} = useIsTextTruncated<HTMLSpanElement>(connection.displayName);
44
44
 
45
45
  return (
46
46
  <span className="flex min-w-0 items-center gap-10">
@@ -61,11 +61,11 @@ function ConnectionOption({connection}: {connection: IntegrationConnectionDto})
61
61
  <TooltipTrigger asChild>
62
62
  <span ref={nameRef} className="min-w-0 truncate">
63
63
  <Text as="span" size="sm" bold>
64
- {connection.display_name}
64
+ {connection.displayName}
65
65
  </Text>
66
66
  </span>
67
67
  </TooltipTrigger>
68
- {isTruncated ? <TooltipContent>{connection.display_name}</TooltipContent> : null}
68
+ {isTruncated ? <TooltipContent>{connection.displayName}</TooltipContent> : null}
69
69
  </Tooltip>
70
70
  </span>
71
71
  );
@@ -1,8 +1,5 @@
1
- import type {
2
- IntegrationConnectionDto,
3
- ListIntegrationConnectionsResponseDto,
4
- } from '@shipfox/api-integration-core-dto';
5
1
  import {QueryLoadError} from '@shipfox/client-ui';
2
+ import {IntegrationIcon} from '@shipfox/integration-icons';
6
3
  import {IconButton} from '@shipfox/react-ui/button';
7
4
  import {
8
5
  DropdownMenu,
@@ -15,18 +12,20 @@ import {EmptyState} from '@shipfox/react-ui/empty-state';
15
12
  import {Skeleton} from '@shipfox/react-ui/skeleton';
16
13
  import {Header, Text} from '@shipfox/react-ui/typography';
17
14
  import {cn, formatDate} from '@shipfox/react-ui/utils';
18
- import type {UseQueryResult} from '@tanstack/react-query';
19
15
  import {Link} from '@tanstack/react-router';
20
16
  import {ConnectionStatusBadge} from '#connection-status-badge.js';
21
- import {IntegrationIcon} from '#integration-icon.js';
17
+ import {type IntegrationConnection, isUsableConnection} from '#core/models.js';
22
18
  import {usageEventsForConnection} from './integration-usage-events.js';
23
19
 
24
20
  interface InstalledIntegrationsSectionProps {
25
- connectionsQuery: UseQueryResult<ListIntegrationConnectionsResponseDto, Error>;
26
- connections: IntegrationConnectionDto[];
21
+ connections: IntegrationConnection[];
22
+ isPending: boolean;
23
+ isFetching: boolean;
24
+ error?: Error | null | undefined;
25
+ onRetry: () => void;
27
26
  isMutating: boolean;
28
27
  onUse: (connectionId: string) => void;
29
- onSetActive: (connection: IntegrationConnectionDto, active: boolean) => void;
28
+ onSetActive: (connection: IntegrationConnection, active: boolean) => void;
30
29
  onDelete: (connectionId: string) => void;
31
30
  providerDisplayName: (provider: string) => string | undefined;
32
31
  }
@@ -35,8 +34,11 @@ const INSTALLED_SURFACE_CLASS =
35
34
  'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
36
35
 
37
36
  export function InstalledIntegrationsSection({
38
- connectionsQuery,
39
37
  connections,
38
+ isPending,
39
+ isFetching,
40
+ error,
41
+ onRetry,
40
42
  isMutating,
41
43
  onUse,
42
44
  onSetActive,
@@ -52,15 +54,18 @@ export function InstalledIntegrationsSection({
52
54
  </Text>
53
55
  </div>
54
56
 
55
- {connectionsQuery.isPending ? <InstalledSkeleton label="Loading integrations" /> : null}
57
+ {isPending ? <InstalledSkeleton label="Loading integrations" /> : null}
56
58
 
57
- {connectionsQuery.isError && connectionsQuery.data === undefined ? (
59
+ {error ? (
58
60
  <div className={cn(INSTALLED_SURFACE_CLASS, 'px-16')}>
59
- <QueryLoadError query={connectionsQuery} subject="integrations" />
61
+ <QueryLoadError
62
+ query={{isError: true, isFetching, data: undefined, error, refetch: onRetry}}
63
+ subject="integrations"
64
+ />
60
65
  </div>
61
66
  ) : null}
62
67
 
63
- {connectionsQuery.data !== undefined && connections.length === 0 ? (
68
+ {!isPending && !error && connections.length === 0 ? (
64
69
  <div className={cn(INSTALLED_SURFACE_CLASS, 'px-16')}>
65
70
  <EmptyState
66
71
  icon="componentLine"
@@ -97,15 +102,15 @@ function InstalledRow({
97
102
  onDelete,
98
103
  providerDisplayName,
99
104
  }: {
100
- connection: IntegrationConnectionDto;
105
+ connection: IntegrationConnection;
101
106
  isMutating: boolean;
102
107
  onUse: (connectionId: string) => void;
103
108
  onSetActive: (active: boolean) => void;
104
109
  onDelete: (connectionId: string) => void;
105
110
  providerDisplayName: (provider: string) => string | undefined;
106
111
  }) {
107
- const muted = connection.lifecycle_status === 'disabled';
108
- const active = connection.lifecycle_status === 'active';
112
+ const muted = connection.lifecycleStatus === 'disabled';
113
+ const active = isUsableConnection(connection);
109
114
  const recentEventsEvent = usageEventsForConnection(connection)[0]?.value ?? 'received';
110
115
  const providerName = providerDisplayName(connection.provider);
111
116
 
@@ -126,12 +131,12 @@ function InstalledRow({
126
131
  bold
127
132
  className={cn('truncate', muted ? 'text-foreground-neutral-disabled' : undefined)}
128
133
  >
129
- {connection.display_name}
134
+ {connection.displayName}
130
135
  </Text>
131
- <ConnectionStatusBadge status={connection.lifecycle_status} className="shrink-0" />
136
+ <ConnectionStatusBadge status={connection.lifecycleStatus} className="shrink-0" />
132
137
  </div>
133
138
  <Text size="sm" className="truncate text-foreground-neutral-muted">
134
- Added {formatDate(connection.created_at)}
139
+ Added {formatDate(connection.createdAt)}
135
140
  </Text>
136
141
  </div>
137
142
  <DropdownMenu>
@@ -140,13 +145,13 @@ function InstalledRow({
140
145
  size="sm"
141
146
  variant="transparent"
142
147
  icon="more2Line"
143
- aria-label={`Open ${connection.display_name} integration actions`}
148
+ aria-label={`Open ${connection.displayName} integration actions`}
144
149
  />
145
150
  </DropdownMenuTrigger>
146
151
  <DropdownMenuContent align="end">
147
- {connection.external_url ? (
152
+ {connection.externalUrl ? (
148
153
  <DropdownMenuItem asChild>
149
- <a href={connection.external_url} target="_blank" rel="noreferrer noopener">
154
+ <a href={connection.externalUrl} target="_blank" rel="noreferrer noopener">
150
155
  {providerName ? `Open in ${providerName}` : 'Open provider settings'}
151
156
  </a>
152
157
  </DropdownMenuItem>
@@ -157,7 +162,7 @@ function InstalledRow({
157
162
  <DropdownMenuItem asChild>
158
163
  <Link
159
164
  to="/workspaces/$wid/settings/events"
160
- params={{wid: connection.workspace_id}}
165
+ params={{wid: connection.workspaceId}}
161
166
  search={{source: [connection.slug], event: [recentEventsEvent]}}
162
167
  >
163
168
  View recent events
@@ -1,11 +1,11 @@
1
- import type {
2
- IntegrationCapabilityDto,
3
- IntegrationConnectionDto,
4
- IntegrationProviderDto,
5
- } from '@shipfox/api-integration-core-dto';
6
1
  import {toast} from '@shipfox/react-ui/toast';
7
2
  import {Header, Text} from '@shipfox/react-ui/typography';
8
3
  import {useState} from 'react';
4
+ import type {
5
+ IntegrationCapability,
6
+ IntegrationConnection,
7
+ IntegrationProvider,
8
+ } from '#core/models.js';
9
9
  import {
10
10
  useDeleteIntegrationConnectionMutation,
11
11
  useIntegrationConnectionsQuery,
@@ -25,7 +25,7 @@ import {WebhookCreateModal} from './webhook/webhook-create-modal.js';
25
25
  import {WebhookUsageDetails} from './webhook/webhook-usage-details.js';
26
26
 
27
27
  interface IntegrationGalleryForWorkspaceProps {
28
- capability: IntegrationCapabilityDto | undefined;
28
+ capability: IntegrationCapability | undefined;
29
29
  emptyProvidersMessage: string;
30
30
  workspaceId: string;
31
31
  }
@@ -38,7 +38,7 @@ export function IntegrationGalleryForWorkspace({
38
38
  const [createProvider, setCreateProvider] = useState<string | undefined>();
39
39
  const [usageConnectionId, setUsageConnectionId] = useState<string | undefined>();
40
40
  const [createdUsageConnection, setCreatedUsageConnection] = useState<
41
- IntegrationConnectionDto | undefined
41
+ IntegrationConnection | undefined
42
42
  >();
43
43
  const [deleteConnectionId, setDeleteConnectionId] = useState<string | undefined>();
44
44
  const providersQuery = useIntegrationProvidersQuery(capability ? {capability} : undefined);
@@ -48,12 +48,12 @@ export function IntegrationGalleryForWorkspace({
48
48
  const updateWebhookConnection = useUpdateWebhookConnectionMutation();
49
49
  const deleteWebhookConnection = useDeleteWebhookConnectionMutation();
50
50
 
51
- const providers = providersQuery.data?.providers ?? [];
52
- const providersMap = new Map<string, IntegrationProviderDto>(
51
+ const providers = providersQuery.data ?? [];
52
+ const providersMap = new Map<string, IntegrationProvider>(
53
53
  providers.map((provider) => [provider.provider, provider]),
54
54
  );
55
55
 
56
- const allConnections = connectionsQuery.data?.connections ?? [];
56
+ const allConnections = connectionsQuery.data ?? [];
57
57
  // Filter in memory so the all-status cache key stays shared; passing capability
58
58
  // into the hook would collide with the active-only `source_control` key used
59
59
  // elsewhere.
@@ -61,13 +61,13 @@ export function IntegrationGalleryForWorkspace({
61
61
  ? allConnections.filter((connection) => connection.capabilities.includes(capability))
62
62
  : allConnections;
63
63
 
64
- const providerDisplayName = (provider: string) => providersMap.get(provider)?.display_name;
64
+ const providerDisplayName = (provider: string) => providersMap.get(provider)?.displayName;
65
65
  const providerLabel = (provider: string) => providerDisplayName(provider) ?? provider;
66
66
 
67
67
  const sortedConnections = [...connections].sort((a, b) => {
68
68
  const byProvider = providerLabel(a.provider).localeCompare(providerLabel(b.provider));
69
69
  if (byProvider !== 0) return byProvider;
70
- return a.created_at.localeCompare(b.created_at);
70
+ return a.createdAt.localeCompare(b.createdAt);
71
71
  });
72
72
  const usageConnection =
73
73
  sortedConnections.find((connection) => connection.id === usageConnectionId) ??
@@ -77,7 +77,7 @@ export function IntegrationGalleryForWorkspace({
77
77
  (connection) => connection.id === deleteConnectionId,
78
78
  );
79
79
 
80
- async function setConnectionActive(connection: IntegrationConnectionDto, active: boolean) {
80
+ async function setConnectionActive(connection: IntegrationConnection, active: boolean) {
81
81
  try {
82
82
  const body = {lifecycle_status: active ? 'active' : 'disabled'} as const;
83
83
  if (connection.provider === 'webhook') {
@@ -122,8 +122,11 @@ export function IntegrationGalleryForWorkspace({
122
122
  return (
123
123
  <div className="flex flex-col gap-24">
124
124
  <InstalledIntegrationsSection
125
- connectionsQuery={connectionsQuery}
126
125
  connections={sortedConnections}
126
+ isPending={connectionsQuery.isPending}
127
+ isFetching={connectionsQuery.isFetching}
128
+ error={connectionsQuery.isError ? connectionsQuery.error : undefined}
129
+ onRetry={() => void connectionsQuery.refetch()}
127
130
  isMutating={
128
131
  updateConnection.isPending ||
129
132
  deleteConnection.isPending ||
@@ -147,7 +150,11 @@ export function IntegrationGalleryForWorkspace({
147
150
  </div>
148
151
 
149
152
  <ProviderGrid
150
- providersQuery={providersQuery}
153
+ providers={providers}
154
+ isPending={providersQuery.isPending}
155
+ isFetching={providersQuery.isFetching}
156
+ error={providersQuery.isError ? providersQuery.error : undefined}
157
+ onRetry={() => void providersQuery.refetch()}
151
158
  workspaceId={workspaceId}
152
159
  emptyMessage={emptyProvidersMessage}
153
160
  onOpenProvider={setCreateProvider}
@@ -178,7 +185,7 @@ export function IntegrationGalleryForWorkspace({
178
185
  ) : null}
179
186
  </IntegrationUsageModal>
180
187
  <IntegrationDeleteConfirmModal
181
- connectionName={deleteConnectionTarget?.display_name}
188
+ connectionName={deleteConnectionTarget?.displayName}
182
189
  open={deleteConnectionId !== undefined}
183
190
  isPending={deleteConnection.isPending || deleteWebhookConnection.isPending}
184
191
  onOpenChange={(open) => {
@@ -1,9 +1,9 @@
1
- import type {IntegrationCapabilityDto} from '@shipfox/api-integration-core-dto';
2
1
  import {useActiveWorkspace} from '@shipfox/client-auth';
2
+ import type {IntegrationCapability} from '#core/models.js';
3
3
  import {IntegrationGalleryForWorkspace} from './integration-gallery-for-workspace.js';
4
4
 
5
5
  export interface IntegrationGalleryProps {
6
- capability?: IntegrationCapabilityDto;
6
+ capability?: IntegrationCapability;
7
7
  emptyProvidersMessage?: string;
8
8
  workspaceId?: string;
9
9
  }
@@ -35,7 +35,7 @@ function RoutedIntegrationGallery({
35
35
  capability,
36
36
  emptyProvidersMessage,
37
37
  }: {
38
- capability: IntegrationCapabilityDto | undefined;
38
+ capability: IntegrationCapability | undefined;
39
39
  emptyProvidersMessage: string;
40
40
  }) {
41
41
  const workspace = useActiveWorkspace();
@@ -1,116 +1 @@
1
- import type {IntegrationConnectionDto, SentryIssueAction} from '@shipfox/api-integration-core-dto';
2
- import {SENTRY_ISSUE_ACTIONS} from '@shipfox/api-integration-core-dto';
3
- import {linearWebhookEventNames} from '@shipfox/api-integration-linear-dto';
4
- import {WEBHOOK_RECEIVED_EVENT} from '@shipfox/api-integration-webhook-dto';
5
- import type {IntegrationUsageEvent} from './integration-usage-modal.js';
6
-
7
- const GITHUB_EVENTS = [
8
- 'push',
9
- 'branch_protection_configuration',
10
- 'branch_protection_rule',
11
- 'check_run',
12
- 'check_suite',
13
- 'code_scanning_alert',
14
- 'commit_comment',
15
- 'create',
16
- 'custom_property',
17
- 'custom_property_values',
18
- 'delete',
19
- 'dependabot_alert',
20
- 'deploy_key',
21
- 'deployment',
22
- 'deployment_protection_rule',
23
- 'deployment_review',
24
- 'deployment_status',
25
- 'discussion',
26
- 'discussion_comment',
27
- 'fork',
28
- 'github_app_authorization',
29
- 'gollum',
30
- 'installation',
31
- 'installation_repositories',
32
- 'installation_target',
33
- 'issue_comment',
34
- 'issue_dependencies',
35
- 'issues',
36
- 'label',
37
- 'marketplace_purchase',
38
- 'member',
39
- 'membership',
40
- 'merge_group',
41
- 'meta',
42
- 'milestone',
43
- 'org_block',
44
- 'organization',
45
- 'package',
46
- 'page_build',
47
- 'personal_access_token_request',
48
- 'ping',
49
- 'project',
50
- 'project_card',
51
- 'project_column',
52
- 'projects_v2',
53
- 'projects_v2_item',
54
- 'projects_v2_status_update',
55
- 'public',
56
- 'pull_request',
57
- 'pull_request_review',
58
- 'pull_request_review_comment',
59
- 'pull_request_review_thread',
60
- 'registry_package',
61
- 'release',
62
- 'repository',
63
- 'repository_advisory',
64
- 'repository_dispatch',
65
- 'repository_import',
66
- 'repository_ruleset',
67
- 'repository_vulnerability_alert',
68
- 'secret_scanning_alert',
69
- 'secret_scanning_alert_location',
70
- 'secret_scanning_scan',
71
- 'security_advisory',
72
- 'security_and_analysis',
73
- 'sponsorship',
74
- 'star',
75
- 'status',
76
- 'sub_issues',
77
- 'team',
78
- 'team_add',
79
- 'watch',
80
- 'workflow_dispatch',
81
- 'workflow_job',
82
- 'workflow_run',
83
- ] as const;
84
-
85
- export function usageEventsForConnection(
86
- connection: IntegrationConnectionDto,
87
- ): IntegrationUsageEvent[] {
88
- if (connection.provider === 'webhook') {
89
- return [{value: WEBHOOK_RECEIVED_EVENT, label: WEBHOOK_RECEIVED_EVENT}];
90
- }
91
-
92
- if (connection.provider === 'github') {
93
- return GITHUB_EVENTS.map((event) => ({value: event, label: event}));
94
- }
95
-
96
- if (connection.provider === 'sentry') {
97
- return SENTRY_ISSUE_ACTIONS.map((action) => ({
98
- value: `issue.${action}`,
99
- label: sentryIssueEventLabel(action),
100
- }));
101
- }
102
-
103
- if (connection.provider === 'linear') {
104
- return linearWebhookEventNames.map((event) => ({value: event, label: event}));
105
- }
106
-
107
- if (connection.capabilities.includes('source_control')) {
108
- return [{value: 'push', label: 'push'}];
109
- }
110
-
111
- return [{value: 'received', label: 'received'}];
112
- }
113
-
114
- function sentryIssueEventLabel(action: SentryIssueAction): string {
115
- return `issue.${action}`;
116
- }
1
+ export {usageEventsForConnection} from '#core/models.js';
@@ -1,4 +1,3 @@
1
- import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
2
1
  import {Button} from '@shipfox/react-ui/button';
3
2
  import {
4
3
  CodeBlock,
@@ -29,6 +28,7 @@ import {Text} from '@shipfox/react-ui/typography';
29
28
  import type {ReactNode} from 'react';
30
29
  import {useEffect, useMemo, useState} from 'react';
31
30
  import {ConnectionStatusBadge} from '#connection-status-badge.js';
31
+ import type {IntegrationConnection} from '#core/models.js';
32
32
 
33
33
  export interface IntegrationUsageEvent {
34
34
  value: string;
@@ -36,7 +36,7 @@ export interface IntegrationUsageEvent {
36
36
  }
37
37
 
38
38
  interface IntegrationUsageModalProps {
39
- connection: IntegrationConnectionDto | null;
39
+ connection: IntegrationConnection | null;
40
40
  events: IntegrationUsageEvent[];
41
41
  open: boolean;
42
42
  onOpenChange: (open: boolean) => void;
@@ -69,7 +69,7 @@ export function IntegrationUsageModal({
69
69
  () => [{language: 'yaml', filename: WORKFLOW_FILENAME, code: workflowExample}],
70
70
  [workflowExample],
71
71
  );
72
- const title = connection ? `Use ${connection.display_name}` : 'Use integration';
72
+ const title = connection ? `Use ${connection.displayName}` : 'Use integration';
73
73
 
74
74
  return (
75
75
  <Modal open={open && connection !== null} onOpenChange={onOpenChange}>
@@ -82,7 +82,7 @@ export function IntegrationUsageModal({
82
82
  {title}
83
83
  </Text>
84
84
  {connection ? (
85
- <ConnectionStatusBadge status={connection.lifecycle_status} className="shrink-0" />
85
+ <ConnectionStatusBadge status={connection.lifecycleStatus} className="shrink-0" />
86
86
  ) : null}
87
87
  </div>
88
88
  <Text size="sm" className="text-foreground-neutral-muted">