@shipfox/client-integrations 13.0.0 → 14.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 (132) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/dist/application/complete-integration-callback.d.ts +11 -0
  4. package/dist/application/complete-integration-callback.d.ts.map +1 -1
  5. package/dist/application/complete-integration-callback.js +38 -8
  6. package/dist/application/complete-integration-callback.js.map +1 -1
  7. package/dist/components/callback-status-shell.d.ts +1 -1
  8. package/dist/components/callback-status-shell.d.ts.map +1 -1
  9. package/dist/components/callback-status-shell.js +3 -3
  10. package/dist/components/callback-status-shell.js.map +1 -1
  11. package/dist/components/connection-picker.js +3 -3
  12. package/dist/components/connection-picker.js.map +1 -1
  13. package/dist/components/installed-integrations-section.js +9 -9
  14. package/dist/components/installed-integrations-section.js.map +1 -1
  15. package/dist/components/integration-delete-confirm-modal.js +1 -1
  16. package/dist/components/integration-delete-confirm-modal.js.map +1 -1
  17. package/dist/components/integration-gallery-for-workspace.js +3 -3
  18. package/dist/components/integration-gallery-for-workspace.js.map +1 -1
  19. package/dist/components/integration-usage-modal.js +6 -6
  20. package/dist/components/integration-usage-modal.js.map +1 -1
  21. package/dist/components/provider-grid.d.ts +1 -1
  22. package/dist/components/provider-grid.d.ts.map +1 -1
  23. package/dist/components/provider-grid.js +10 -10
  24. package/dist/components/provider-grid.js.map +1 -1
  25. package/dist/components/redirect-install-page.js +1 -1
  26. package/dist/components/redirect-install-page.js.map +1 -1
  27. package/dist/components/repository-picker.js +4 -4
  28. package/dist/components/repository-picker.js.map +1 -1
  29. package/dist/components/webhook/copyable-value.js +3 -3
  30. package/dist/components/webhook/copyable-value.js.map +1 -1
  31. package/dist/components/webhook/webhook-create-modal.js +1 -1
  32. package/dist/components/webhook/webhook-create-modal.js.map +1 -1
  33. package/dist/components/webhook/webhook-usage-details.js +2 -2
  34. package/dist/components/webhook/webhook-usage-details.js.map +1 -1
  35. package/dist/core/models.d.ts +6 -0
  36. package/dist/core/models.d.ts.map +1 -1
  37. package/dist/core/models.js +12 -0
  38. package/dist/core/models.js.map +1 -1
  39. package/dist/feature.d.ts +8 -0
  40. package/dist/feature.d.ts.map +1 -1
  41. package/dist/feature.js +10 -0
  42. package/dist/feature.js.map +1 -1
  43. package/dist/hooks/api/integration-mapper.d.ts +3 -1
  44. package/dist/hooks/api/integration-mapper.d.ts.map +1 -1
  45. package/dist/hooks/api/integration-mapper.js +8 -0
  46. package/dist/hooks/api/integration-mapper.js.map +1 -1
  47. package/dist/hooks/api/integrations.d.ts +25 -1
  48. package/dist/hooks/api/integrations.d.ts.map +1 -1
  49. package/dist/hooks/api/integrations.js +44 -1
  50. package/dist/hooks/api/integrations.js.map +1 -1
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +4 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/jira-callback-state.d.ts +9 -0
  56. package/dist/jira-callback-state.d.ts.map +1 -0
  57. package/dist/jira-callback-state.js +16 -0
  58. package/dist/jira-callback-state.js.map +1 -0
  59. package/dist/jira-callback.d.ts +11 -0
  60. package/dist/jira-callback.d.ts.map +1 -0
  61. package/dist/jira-callback.js +59 -0
  62. package/dist/jira-callback.js.map +1 -0
  63. package/dist/jira-form-errors.d.ts +8 -0
  64. package/dist/jira-form-errors.d.ts.map +1 -0
  65. package/dist/jira-form-errors.js +55 -0
  66. package/dist/jira-form-errors.js.map +1 -0
  67. package/dist/pages/gitea-install-page.js +4 -4
  68. package/dist/pages/gitea-install-page.js.map +1 -1
  69. package/dist/pages/jira-callback-page.d.ts +2 -0
  70. package/dist/pages/jira-callback-page.d.ts.map +1 -0
  71. package/dist/pages/jira-callback-page.js +341 -0
  72. package/dist/pages/jira-callback-page.js.map +1 -0
  73. package/dist/pages/jira-install-page.d.ts +2 -0
  74. package/dist/pages/jira-install-page.d.ts.map +1 -0
  75. package/dist/pages/jira-install-page.js +26 -0
  76. package/dist/pages/jira-install-page.js.map +1 -0
  77. package/dist/pages/sentry-callback-page.js +7 -7
  78. package/dist/pages/sentry-callback-page.js.map +1 -1
  79. package/dist/pages/source-control-onboarding-page.js +2 -2
  80. package/dist/pages/source-control-onboarding-page.js.map +1 -1
  81. package/dist/provider-catalog.d.ts +1 -1
  82. package/dist/provider-catalog.d.ts.map +1 -1
  83. package/dist/provider-catalog.js +5 -0
  84. package/dist/provider-catalog.js.map +1 -1
  85. package/dist/routes/jira-callback.d.ts +8 -0
  86. package/dist/routes/jira-callback.d.ts.map +1 -0
  87. package/dist/routes/jira-callback.js +9 -0
  88. package/dist/routes/jira-callback.js.map +1 -0
  89. package/dist/routes/jira.d.ts +6 -0
  90. package/dist/routes/jira.d.ts.map +1 -0
  91. package/dist/routes/jira.js +7 -0
  92. package/dist/routes/jira.js.map +1 -0
  93. package/dist/tsconfig.test.tsbuildinfo +1 -1
  94. package/package.json +9 -8
  95. package/src/application/complete-integration-callback.test.ts +44 -1
  96. package/src/application/complete-integration-callback.ts +61 -8
  97. package/src/components/callback-status-shell.tsx +7 -4
  98. package/src/components/connection-picker.tsx +3 -3
  99. package/src/components/installed-integrations-section.tsx +9 -9
  100. package/src/components/integration-delete-confirm-modal.tsx +1 -1
  101. package/src/components/integration-gallery-for-workspace.tsx +3 -3
  102. package/src/components/integration-usage-events.test.ts +13 -0
  103. package/src/components/integration-usage-modal.tsx +6 -6
  104. package/src/components/provider-grid.tsx +10 -10
  105. package/src/components/redirect-install-page.tsx +1 -1
  106. package/src/components/repository-picker.test.tsx +2 -2
  107. package/src/components/repository-picker.tsx +6 -4
  108. package/src/components/webhook/copyable-value.tsx +3 -3
  109. package/src/components/webhook/webhook-create-modal.tsx +1 -1
  110. package/src/components/webhook/webhook-usage-details.tsx +2 -2
  111. package/src/core/models.ts +18 -0
  112. package/src/feature.ts +10 -0
  113. package/src/hooks/api/integration-mapper.ts +6 -0
  114. package/src/hooks/api/integrations.test.ts +92 -0
  115. package/src/hooks/api/integrations.ts +71 -0
  116. package/src/index.ts +4 -0
  117. package/src/jira-callback-state.ts +19 -0
  118. package/src/jira-callback.test.ts +76 -0
  119. package/src/jira-callback.ts +67 -0
  120. package/src/jira-form-errors.test.ts +57 -0
  121. package/src/jira-form-errors.ts +94 -0
  122. package/src/pages/gitea-install-page.tsx +4 -4
  123. package/src/pages/jira-callback-page.test.tsx +250 -0
  124. package/src/pages/jira-callback-page.tsx +366 -0
  125. package/src/pages/jira-install-page.tsx +28 -0
  126. package/src/pages/sentry-callback-page.tsx +7 -7
  127. package/src/pages/source-control-onboarding-page.tsx +2 -2
  128. package/src/provider-catalog.test.ts +6 -0
  129. package/src/provider-catalog.ts +7 -1
  130. package/src/routes/jira-callback.tsx +8 -0
  131. package/src/routes/jira.tsx +4 -0
  132. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-integrations",
3
3
  "license": "MIT",
4
- "version": "13.0.0",
4
+ "version": "14.0.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -28,18 +28,19 @@
28
28
  "@swc/helpers": "^0.5.17",
29
29
  "@tanstack/react-form": "^1.32.0",
30
30
  "@shipfox/api-integration-core-dto": "12.0.0",
31
+ "@shipfox/api-integration-gitea-dto": "12.0.0",
31
32
  "@shipfox/api-integration-github-dto": "12.0.0",
33
+ "@shipfox/api-integration-jira-dto": "12.0.0",
32
34
  "@shipfox/api-integration-linear-dto": "12.0.0",
33
- "@shipfox/api-integration-gitea-dto": "12.0.0",
34
35
  "@shipfox/api-integration-slack-dto": "12.0.0",
36
+ "@shipfox/api-integration-sentry-dto": "12.0.0",
35
37
  "@shipfox/api-integration-webhook-dto": "12.0.0",
36
38
  "@shipfox/client-api": "6.0.1",
37
- "@shipfox/client-shell": "13.0.0",
38
- "@shipfox/integration-icons": "0.2.3",
39
- "@shipfox/api-integration-sentry-dto": "12.0.0",
40
- "@shipfox/client-ui": "13.0.0",
41
- "@shipfox/client-auth": "13.0.0",
42
- "@shipfox/react-ui": "0.4.0"
39
+ "@shipfox/client-auth": "14.0.1",
40
+ "@shipfox/client-shell": "14.0.1",
41
+ "@shipfox/client-ui": "14.0.1",
42
+ "@shipfox/integration-icons": "0.3.1",
43
+ "@shipfox/react-ui": "1.0.0"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "@tanstack/react-query": "^5.101.0",
@@ -1,7 +1,10 @@
1
1
  import {QueryClient} from '@tanstack/react-query';
2
2
  import type {IntegrationConnection} from '#core/models.js';
3
3
  import {integrationsQueryKeys} from '#hooks/api/integrations.js';
4
- import {completeIntegrationCallback} from './complete-integration-callback.js';
4
+ import {
5
+ completeIntegrationCallback,
6
+ completeIntegrationCallbackResult,
7
+ } from './complete-integration-callback.js';
5
8
 
6
9
  const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
7
10
 
@@ -38,4 +41,44 @@ describe('completeIntegrationCallback', () => {
38
41
  queryKey: integrationsQueryKeys.connectionsByWorkspace(WORKSPACE_ID),
39
42
  });
40
43
  });
44
+
45
+ test('does not invalidate a connection view for a result without a connection', async () => {
46
+ const queryClient = new QueryClient();
47
+ const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries');
48
+ const refreshAuth = vi.fn().mockResolvedValue({accessToken: 'access-token'});
49
+ const complete = vi.fn().mockResolvedValue({sites: []});
50
+
51
+ const result = await completeIntegrationCallbackResult({
52
+ input: {code: 'grant-code'},
53
+ refreshAuth,
54
+ complete,
55
+ getConnection: () => undefined,
56
+ queryClient,
57
+ });
58
+
59
+ expect(result).toEqual({sites: []});
60
+ expect(complete).toHaveBeenCalledWith({code: 'grant-code'}, 'access-token');
61
+ expect(invalidateQueries).not.toHaveBeenCalled();
62
+ });
63
+
64
+ test('invalidates a connection view for a result with a connection', async () => {
65
+ const queryClient = new QueryClient();
66
+ const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries');
67
+ const refreshAuth = vi.fn().mockResolvedValue({accessToken: 'access-token'});
68
+ const complete = vi.fn(async () => connection);
69
+
70
+ const result = await completeIntegrationCallbackResult({
71
+ input: {code: 'grant-code'},
72
+ refreshAuth,
73
+ complete,
74
+ getConnection: (value) => value,
75
+ queryClient,
76
+ });
77
+
78
+ expect(result).toBe(connection);
79
+ expect(complete).toHaveBeenCalledWith({code: 'grant-code'}, 'access-token');
80
+ expect(invalidateQueries).toHaveBeenCalledWith({
81
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(WORKSPACE_ID),
82
+ });
83
+ });
41
84
  });
@@ -10,22 +10,65 @@ export interface CompleteIntegrationCallbackOptions<TInput> {
10
10
  queryClient: QueryClient;
11
11
  }
12
12
 
13
+ export interface CompleteIntegrationCallbackResultOptions<TInput, TResult> {
14
+ input: TInput;
15
+ refreshAuth: () => Promise<{accessToken: string}>;
16
+ complete: (input: TInput, accessToken: string) => Promise<TResult>;
17
+ getConnection: (result: TResult) => IntegrationConnection | undefined;
18
+ queryClient: QueryClient;
19
+ }
20
+
13
21
  export async function completeIntegrationCallback<TInput>({
14
22
  input,
15
23
  refreshAuth,
16
24
  complete,
17
25
  queryClient,
18
26
  }: CompleteIntegrationCallbackOptions<TInput>): Promise<IntegrationConnection> {
27
+ return await completeIntegrationCallbackResultInternal({
28
+ input,
29
+ refreshAuth,
30
+ complete,
31
+ getConnection: (result) => result,
32
+ queryClient,
33
+ });
34
+ }
35
+
36
+ export async function completeIntegrationCallbackResult<TInput, TResult>({
37
+ input,
38
+ refreshAuth,
39
+ complete,
40
+ getConnection,
41
+ queryClient,
42
+ }: CompleteIntegrationCallbackResultOptions<TInput, TResult>): Promise<TResult> {
43
+ return await completeIntegrationCallbackResultInternal({
44
+ input,
45
+ refreshAuth,
46
+ complete,
47
+ getConnection,
48
+ queryClient,
49
+ });
50
+ }
51
+
52
+ async function completeIntegrationCallbackResultInternal<TInput, TResult>({
53
+ input,
54
+ refreshAuth,
55
+ complete,
56
+ getConnection,
57
+ queryClient,
58
+ }: CompleteIntegrationCallbackResultOptions<TInput, TResult>): Promise<TResult> {
19
59
  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.
60
+ const result = await complete(input, session.accessToken);
61
+ const connection = getConnection(result);
62
+ if (connection) {
63
+ try {
64
+ await queryClient.invalidateQueries({
65
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspaceId),
66
+ });
67
+ } catch {
68
+ // Cache refresh is best effort: the callback is already committed server-side.
69
+ }
27
70
  }
28
- return connection;
71
+ return result;
29
72
  }
30
73
 
31
74
  export function useCompleteIntegrationCallback() {
@@ -37,3 +80,13 @@ export function useCompleteIntegrationCallback() {
37
80
  [queryClient],
38
81
  );
39
82
  }
83
+
84
+ export function useCompleteIntegrationCallbackResult() {
85
+ const queryClient = useQueryClient();
86
+ return useCallback(
87
+ async <TInput, TResult>(
88
+ options: Omit<CompleteIntegrationCallbackResultOptions<TInput, TResult>, 'queryClient'>,
89
+ ): Promise<TResult> => completeIntegrationCallbackResult({...options, queryClient}),
90
+ [queryClient],
91
+ );
92
+ }
@@ -19,7 +19,10 @@ export function CallbackStatusShell({
19
19
  startOver?: boolean;
20
20
  switchAccount?: boolean;
21
21
  workspaceSlug?: string | undefined;
22
- installPath: '/w/$workspaceSlug/integrations/linear' | '/w/$workspaceSlug/integrations/slack';
22
+ installPath:
23
+ | '/w/$workspaceSlug/integrations/linear'
24
+ | '/w/$workspaceSlug/integrations/slack'
25
+ | '/w/$workspaceSlug/integrations/jira';
23
26
  }) {
24
27
  const headingRef = useRef<HTMLHeadingElement>(null);
25
28
  useEffect(() => headingRef.current?.focus(), []);
@@ -40,15 +43,15 @@ export function CallbackStatusShell({
40
43
  : undefined;
41
44
 
42
45
  return (
43
- <main className="flex min-h-screen bg-background-subtle-base px-16 py-32">
44
- <div className="mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20">
46
+ <main className="flex min-h-screen bg-background-subtle-base px-row py-frame">
47
+ <div className="mx-auto flex w-full max-w-[480px] flex-col justify-center gap-section">
45
48
  <h2 ref={headingRef} tabIndex={-1} className="text-24 font-semibold outline-none">
46
49
  {title}
47
50
  </h2>
48
51
  <Callout role={status === 'error' ? 'alert' : 'status'} type={status}>
49
52
  <Text size="sm">{message}</Text>
50
53
  </Callout>
51
- <div className="flex flex-col gap-8 sm:flex-row sm:items-center">
54
+ <div className="flex flex-col gap-inline sm:flex-row sm:items-center">
52
55
  {switchAccount ? (
53
56
  <ButtonLink asChild className="min-h-44 w-full sm:w-fit">
54
57
  <Link
@@ -19,7 +19,7 @@ export function ConnectionPicker({
19
19
  const labelId = useId();
20
20
 
21
21
  return (
22
- <div className="flex flex-col gap-10">
22
+ <div className="flex flex-col gap-inline">
23
23
  <Label id={labelId} className="sr-only">
24
24
  Source integration
25
25
  </Label>
@@ -27,7 +27,7 @@ export function ConnectionPicker({
27
27
  aria-labelledby={labelId}
28
28
  value={selectedConnectionId ?? ''}
29
29
  onValueChange={onSelect}
30
- className="grid grid-cols-2 gap-8 max-[760px]:grid-cols-1"
30
+ className="grid grid-cols-2 gap-inline max-[760px]:grid-cols-1"
31
31
  >
32
32
  {connections.map((connection) => (
33
33
  <RadioGroupItem key={connection.id} value={connection.id}>
@@ -43,7 +43,7 @@ function ConnectionOption({connection}: {connection: IntegrationConnection}) {
43
43
  const {ref: nameRef, isTruncated} = useIsTextTruncated<HTMLSpanElement>(connection.displayName);
44
44
 
45
45
  return (
46
- <span className="flex min-w-0 items-center gap-10">
46
+ <span className="flex min-w-0 items-center gap-inline">
47
47
  <Tooltip>
48
48
  <TooltipTrigger asChild>
49
49
  <span className="shrink-0">
@@ -48,8 +48,8 @@ export function InstalledIntegrationsSection({
48
48
  providerDisplayName,
49
49
  }: InstalledIntegrationsSectionProps) {
50
50
  return (
51
- <section className="flex flex-col gap-16" aria-label="Installed integrations">
52
- <div className="flex flex-col gap-4">
51
+ <section className="flex flex-col gap-group" aria-label="Installed integrations">
52
+ <div className="flex flex-col gap-tight">
53
53
  <Header variant="h3">Installed integrations</Header>
54
54
  <Text size="sm" className="text-foreground-neutral-muted">
55
55
  Provider accounts installed in this workspace.
@@ -59,7 +59,7 @@ export function InstalledIntegrationsSection({
59
59
  {isPending ? <InstalledSkeleton label="Loading integrations" /> : null}
60
60
 
61
61
  {error ? (
62
- <div className={cn(INSTALLED_SURFACE_CLASS, 'px-16')}>
62
+ <div className={cn(INSTALLED_SURFACE_CLASS, 'px-row')}>
63
63
  <QueryLoadError
64
64
  query={{isError: true, isFetching, data: undefined, error, refetch: onRetry}}
65
65
  subject="integrations"
@@ -68,7 +68,7 @@ export function InstalledIntegrationsSection({
68
68
  ) : null}
69
69
 
70
70
  {!isPending && !error && connections.length === 0 ? (
71
- <div className={cn(INSTALLED_SURFACE_CLASS, 'px-16')}>
71
+ <div className={cn(INSTALLED_SURFACE_CLASS, 'px-row')}>
72
72
  <EmptyState
73
73
  icon="componentLine"
74
74
  title="No integrations installed yet"
@@ -120,7 +120,7 @@ function InstalledRow({
120
120
  const providerName = providerDisplayName(connection.provider);
121
121
 
122
122
  return (
123
- <li className="flex items-center gap-12 px-16 py-12 transition-colors hover:bg-background-components-hover">
123
+ <li className="flex items-center gap-cluster px-row py-row transition-colors hover:bg-background-components-hover">
124
124
  <IntegrationIcon
125
125
  source={connection.provider}
126
126
  aria-hidden
@@ -129,8 +129,8 @@ function InstalledRow({
129
129
  muted ? 'text-foreground-neutral-disabled' : 'text-foreground-neutral-base',
130
130
  )}
131
131
  />
132
- <div className="flex min-w-0 flex-1 flex-col gap-2">
133
- <div className="flex min-w-0 items-center gap-8">
132
+ <div className="flex min-w-0 flex-1 flex-col gap-tight">
133
+ <div className="flex min-w-0 items-center gap-inline">
134
134
  <Text
135
135
  size="md"
136
136
  bold
@@ -196,9 +196,9 @@ function InstalledSkeleton({label}: {label: string}) {
196
196
  className={cn('divide-y divide-border-neutral-base', INSTALLED_SURFACE_CLASS)}
197
197
  >
198
198
  {[0, 1, 2].map((row) => (
199
- <li key={row} className="flex items-center gap-12 px-16 py-12">
199
+ <li key={row} className="flex items-center gap-cluster px-row py-row">
200
200
  <Skeleton className="size-24 shrink-0" />
201
- <div className="flex min-w-0 flex-1 flex-col gap-2">
201
+ <div className="flex min-w-0 flex-1 flex-col gap-tight">
202
202
  <Skeleton className="h-16 w-120" />
203
203
  <Skeleton className="h-12 w-80" />
204
204
  </div>
@@ -36,7 +36,7 @@ export function IntegrationDeleteConfirmModal({
36
36
  <ModalContent aria-describedby={undefined}>
37
37
  <ModalTitle className="sr-only">Delete integration</ModalTitle>
38
38
  <ModalHeader title="Delete integration" showClose={!isPending} />
39
- <ModalBody className="gap-16">
39
+ <ModalBody className="gap-group">
40
40
  <Text size="sm">
41
41
  Are you sure you want to delete <strong className="font-medium">{name}</strong>? Once
42
42
  deleted, Shipfox will immediately stop processing events from this integration. This
@@ -123,7 +123,7 @@ export function IntegrationGalleryForWorkspace({
123
123
  }
124
124
 
125
125
  return (
126
- <div className="flex flex-col gap-24">
126
+ <div className="flex flex-col gap-section">
127
127
  <InstalledIntegrationsSection
128
128
  workspaceSlug={workspaceSlug}
129
129
  connections={sortedConnections}
@@ -145,8 +145,8 @@ export function IntegrationGalleryForWorkspace({
145
145
  providerDisplayName={providerDisplayName}
146
146
  />
147
147
 
148
- <section className="flex flex-col gap-16" aria-label="Available integrations">
149
- <div className="flex flex-col gap-4">
148
+ <section className="flex flex-col gap-group" aria-label="Available integrations">
149
+ <div className="flex flex-col gap-tight">
150
150
  <Header variant="h3">Available integrations</Header>
151
151
  <Text size="sm" className="text-foreground-neutral-muted">
152
152
  Providers available to install in this workspace.
@@ -1,4 +1,5 @@
1
1
  import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
2
+ import {jiraWebhookEventNames} from '@shipfox/api-integration-jira-dto';
2
3
  import {linearWebhookEventNames} from '@shipfox/api-integration-linear-dto';
3
4
  import {WEBHOOK_RECEIVED_EVENT} from '@shipfox/api-integration-webhook-dto';
4
5
  import {usageEventsForConnection} from './integration-usage-events.js';
@@ -67,6 +68,18 @@ describe('usageEventsForConnection', () => {
67
68
  expect(events.map((event) => event.value)).toEqual(linearWebhookEventNames);
68
69
  });
69
70
 
71
+ it('uses Jira webhook names directly', () => {
72
+ const connection = {
73
+ ...baseConnection,
74
+ provider: 'jira',
75
+ capabilities: ['agent_tools'],
76
+ } satisfies IntegrationConnectionDto;
77
+
78
+ const events = usageEventsForConnection(connection);
79
+
80
+ expect(events.map((event) => event.value)).toEqual(jiraWebhookEventNames);
81
+ });
82
+
70
83
  it('falls back to a generic received event for uncataloged providers', () => {
71
84
  const connection = {
72
85
  ...baseConnection,
@@ -76,8 +76,8 @@ export function IntegrationUsageModal({
76
76
  <ModalContent aria-describedby={undefined} className="max-h-[calc(100vh-32px)] max-w-[640px]">
77
77
  <ModalTitle className="sr-only">{title}</ModalTitle>
78
78
  <ModalHeader>
79
- <div className="flex min-w-0 flex-col gap-2">
80
- <div className="flex min-w-0 items-center gap-8">
79
+ <div className="flex min-w-0 flex-col gap-tight">
80
+ <div className="flex min-w-0 items-center gap-inline">
81
81
  <Text size="lg" aria-hidden="true" className="truncate">
82
82
  {title}
83
83
  </Text>
@@ -92,13 +92,13 @@ export function IntegrationUsageModal({
92
92
  </ModalHeader>
93
93
  {connection ? (
94
94
  <>
95
- <ModalBody className="min-h-0 flex-1 gap-24 overflow-y-auto overflow-x-clip scrollbar">
96
- <section className="flex w-full flex-col gap-12">
95
+ <ModalBody className="min-h-0 flex-1 gap-section overflow-y-auto overflow-x-clip scrollbar">
96
+ <section className="flex w-full flex-col gap-cluster">
97
97
  <Text size="md" bold>
98
98
  Usage
99
99
  </Text>
100
100
  {events.length > 1 ? (
101
- <div className="flex flex-col gap-6">
101
+ <div className="flex flex-col gap-inline">
102
102
  <label
103
103
  htmlFor="integration-usage-event"
104
104
  className="font-display text-sm font-medium text-foreground-neutral-base"
@@ -144,7 +144,7 @@ export function IntegrationUsageModal({
144
144
  </CodeBlock>
145
145
  </section>
146
146
  {children ? (
147
- <section className="flex w-full flex-col gap-12">
147
+ <section className="flex w-full flex-col gap-cluster">
148
148
  <Text size="md" bold>
149
149
  Details
150
150
  </Text>
@@ -23,7 +23,7 @@ export interface ProviderGridProps {
23
23
  onOpenProvider?: ((provider: string) => void) | undefined;
24
24
  }
25
25
 
26
- export const PROVIDER_GRID_CLASS = 'grid grid-cols-2 gap-12 max-[760px]:grid-cols-1';
26
+ export const PROVIDER_GRID_CLASS = 'grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1';
27
27
 
28
28
  export const PROVIDER_SURFACE_CLASS =
29
29
  'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
@@ -46,7 +46,7 @@ export function ProviderGrid({
46
46
 
47
47
  if (error) {
48
48
  return (
49
- <div className={cn(PROVIDER_SURFACE_CLASS, 'px-16')}>
49
+ <div className={cn(PROVIDER_SURFACE_CLASS, 'px-row')}>
50
50
  <QueryLoadError
51
51
  query={{
52
52
  isError: true,
@@ -63,7 +63,7 @@ export function ProviderGrid({
63
63
 
64
64
  if (installableProviders.length === 0) {
65
65
  return (
66
- <div className={cn(PROVIDER_SURFACE_CLASS, 'px-16')}>
66
+ <div className={cn(PROVIDER_SURFACE_CLASS, 'px-row')}>
67
67
  <EmptyState
68
68
  icon="componentLine"
69
69
  title="No integrations available"
@@ -106,7 +106,7 @@ function ProviderCard({
106
106
  type="button"
107
107
  aria-label={`Add ${provider.displayName}`}
108
108
  onClick={() => onOpenProvider?.(provider.provider)}
109
- className="group flex h-full w-full min-w-0 items-center justify-between gap-12 rounded-8 border border-border-neutral-base bg-background-neutral-base p-16 text-left transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
109
+ className="group flex h-full w-full min-w-0 items-center justify-between gap-cluster rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact text-left transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
110
110
  >
111
111
  <ProviderCardContent provider={provider} action="Add" />
112
112
  </button>
@@ -118,7 +118,7 @@ function ProviderCard({
118
118
  to={catalog.setupPath}
119
119
  params={{workspaceSlug}}
120
120
  aria-label={`Install ${provider.displayName}`}
121
- className="group flex h-full min-w-0 items-center justify-between gap-12 rounded-8 border border-border-neutral-base bg-background-neutral-base p-16 transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
121
+ className="group flex h-full min-w-0 items-center justify-between gap-cluster rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
122
122
  >
123
123
  <ProviderCardContent provider={provider} action="Install" />
124
124
  </Link>
@@ -134,7 +134,7 @@ function ProviderCardContent({
134
134
  }) {
135
135
  return (
136
136
  <>
137
- <span className="flex min-w-0 items-center gap-12">
137
+ <span className="flex min-w-0 items-center gap-cluster">
138
138
  <IntegrationIcon
139
139
  source={provider.provider}
140
140
  aria-hidden
@@ -144,7 +144,7 @@ function ProviderCardContent({
144
144
  {provider.displayName}
145
145
  </Text>
146
146
  </span>
147
- <span className="flex shrink-0 items-center gap-4 text-foreground-neutral-muted transition-colors group-hover:text-foreground-highlight-interactive">
147
+ <span className="flex shrink-0 items-center gap-tight text-foreground-neutral-muted transition-colors group-hover:text-foreground-highlight-interactive">
148
148
  <Text as="span" size="sm">
149
149
  {action}
150
150
  </Text>
@@ -159,9 +159,9 @@ function ProviderGridSkeleton({label}: {label: string}) {
159
159
  <ul role="status" aria-label={label} className={PROVIDER_GRID_CLASS}>
160
160
  {[0, 1, 2, 3].map((tile) => (
161
161
  <li key={tile}>
162
- <Card className="h-full p-16">
163
- <div className="flex items-center justify-between gap-12">
164
- <div className="flex min-w-0 items-center gap-12">
162
+ <Card className="h-full p-panel-compact">
163
+ <div className="flex items-center justify-between gap-cluster">
164
+ <div className="flex min-w-0 items-center gap-cluster">
165
165
  <Skeleton className="size-24 shrink-0" />
166
166
  <Skeleton className="h-16 w-100" />
167
167
  </div>
@@ -54,7 +54,7 @@ export function RedirectInstallPage({
54
54
 
55
55
  if (errorMessage) {
56
56
  return (
57
- <div className="mx-auto flex w-full max-w-[480px] flex-col gap-16">
57
+ <div className="mx-auto flex w-full max-w-[480px] flex-col gap-group">
58
58
  <Callout role="alert" type="error">
59
59
  <Text size="sm">{errorMessage}</Text>
60
60
  </Callout>
@@ -75,7 +75,7 @@ describe('RepositoryPicker', () => {
75
75
  const loadingGrid = container.querySelector('[aria-hidden="true"]');
76
76
  if (!loadingGrid) throw new Error('Repository loading grid was not rendered');
77
77
 
78
- expect(loadingGrid).toHaveClass('grid', 'grid-cols-2', 'gap-8', 'max-[760px]:grid-cols-1');
78
+ expect(loadingGrid).toHaveClass('grid', 'grid-cols-2', 'gap-inline', 'max-[760px]:grid-cols-1');
79
79
  expect(loadingGrid.querySelectorAll('[data-slot="skeleton"]')).toHaveLength(4);
80
80
  for (const placeholder of loadingGrid.querySelectorAll(':scope > div')) {
81
81
  expect(placeholder).toHaveClass(
@@ -84,7 +84,7 @@ describe('RepositoryPicker', () => {
84
84
  'border',
85
85
  'border-border-neutral-base',
86
86
  'bg-background-neutral-base',
87
- 'p-14',
87
+ 'p-[14px]',
88
88
  );
89
89
  }
90
90
  });
@@ -9,7 +9,7 @@ import {Text} from '@shipfox/react-ui/typography';
9
9
  import {useId} from 'react';
10
10
  import type {Repository} from '#core/models.js';
11
11
 
12
- const REPOSITORY_GRID_CLASS_NAME = 'grid grid-cols-2 gap-8 max-[760px]:grid-cols-1';
12
+ const REPOSITORY_GRID_CLASS_NAME = 'grid grid-cols-2 gap-inline max-[760px]:grid-cols-1';
13
13
  const REPOSITORY_SKELETON_WIDTHS = ['w-64', 'w-96', 'w-80', 'w-112'] as const;
14
14
 
15
15
  export function RepositoryPicker({
@@ -41,7 +41,7 @@ export function RepositoryPicker({
41
41
  const showSearch = onSearchChange !== undefined;
42
42
 
43
43
  return (
44
- <div className="flex flex-col gap-10">
44
+ <div className="flex flex-col gap-inline">
45
45
  <Label id={labelId} className="sr-only">
46
46
  Repository
47
47
  </Label>
@@ -60,7 +60,7 @@ export function RepositoryPicker({
60
60
  {isLoading ? <RepositoryLoadingState /> : null}
61
61
 
62
62
  {!isLoading && repositories.length === 0 ? (
63
- <div className="rounded-8 border border-border-neutral-base bg-background-subtle-base p-14">
63
+ <div className="rounded-8 border border-border-neutral-base bg-background-subtle-base p-panel-compact">
64
64
  <Text size="sm">{emptyMessage}</Text>
65
65
  </div>
66
66
  ) : null}
@@ -119,10 +119,12 @@ function RepositoryLoadingState() {
119
119
  Loading repositories.
120
120
  </div>
121
121
  <div aria-hidden="true" className={REPOSITORY_GRID_CLASS_NAME}>
122
+ {/* The skeleton mirrors RadioGroupItem's own 14px padding contract so the
123
+ placeholder and the loaded card stay the same height. */}
122
124
  {REPOSITORY_SKELETON_WIDTHS.map((width) => (
123
125
  <div
124
126
  key={width}
125
- className="h-50 min-w-0 rounded-8 border border-border-neutral-base bg-background-neutral-base p-14"
127
+ className="h-50 min-w-0 rounded-8 border border-border-neutral-base bg-background-neutral-base p-[14px]"
126
128
  >
127
129
  <Skeleton className={`h-20 ${width}`} />
128
130
  </div>
@@ -40,12 +40,12 @@ export function CopyableValue({label, value, note, className}: CopyableValueProp
40
40
  }
41
41
 
42
42
  return (
43
- <div className={cn('flex w-full min-w-0 flex-col gap-8', className)}>
44
- <div className="flex min-w-0 items-center gap-8">
43
+ <div className={cn('flex w-full min-w-0 flex-col gap-inline', className)}>
44
+ <div className="flex min-w-0 items-center gap-inline">
45
45
  <Code
46
46
  as="span"
47
47
  variant="paragraph"
48
- className="min-w-0 flex-1 break-all rounded-4 border border-border-neutral-base bg-background-components-base px-8 py-6 text-foreground-neutral-base"
48
+ className="min-w-0 flex-1 break-all rounded-4 border border-border-neutral-base bg-background-components-base px-tight py-[6px] text-foreground-neutral-base"
49
49
  >
50
50
  {value}
51
51
  </Code>
@@ -97,7 +97,7 @@ export function WebhookCreateModal({
97
97
  void form.handleSubmit();
98
98
  }}
99
99
  >
100
- <ModalBody className="gap-16">
100
+ <ModalBody className="gap-group">
101
101
  <Text size="sm" className="text-foreground-neutral-muted">
102
102
  Create a named inbound webhook URL for this workspace.
103
103
  </Text>
@@ -29,8 +29,8 @@ export function WebhookUsageDetails({workspaceId, connectionId}: WebhookUsageDet
29
29
  if (!connection) return null;
30
30
 
31
31
  return (
32
- <div className="flex w-full flex-col gap-16">
33
- <div className="flex w-full min-w-0 flex-col gap-8">
32
+ <div className="flex w-full flex-col gap-group">
33
+ <div className="flex w-full min-w-0 flex-col gap-inline">
34
34
  <Text size="sm" bold>
35
35
  Inbound URL
36
36
  </Text>
@@ -7,6 +7,13 @@ export interface IntegrationProvider {
7
7
  capabilities: IntegrationCapability[];
8
8
  }
9
9
 
10
+ export interface JiraSite {
11
+ cloudId: string;
12
+ name: string;
13
+ url: string;
14
+ scopes: string[];
15
+ }
16
+
10
17
  export interface IntegrationConnection {
11
18
  id: string;
12
19
  workspaceId: string;
@@ -163,6 +170,15 @@ const linearUsageEvents = [
163
170
 
164
171
  const sentryIssueActions = ['created', 'resolved', 'assigned', 'archived', 'unresolved'] as const;
165
172
 
173
+ const jiraUsageEvents = [
174
+ 'jira:issue_created',
175
+ 'jira:issue_updated',
176
+ 'jira:issue_deleted',
177
+ 'comment_created',
178
+ 'comment_updated',
179
+ 'comment_deleted',
180
+ ] as const;
181
+
166
182
  export function usageEventsForConnection(
167
183
  connection: Pick<IntegrationConnection, 'provider' | 'capabilities'>,
168
184
  ): IntegrationUsageEvent[] {
@@ -174,6 +190,8 @@ export function usageEventsForConnection(
174
190
  value: `issue.${action}`,
175
191
  label: `issue.${action}`,
176
192
  }));
193
+ if (connection.provider === 'jira')
194
+ return jiraUsageEvents.map((value) => ({value, label: value}));
177
195
  if (connection.provider === 'linear')
178
196
  return linearUsageEvents.map((value) => ({value, label: value}));
179
197
  if (connection.capabilities.includes('source_control')) return [{value: 'push', label: 'push'}];
package/src/feature.ts CHANGED
@@ -23,6 +23,11 @@ export const integrationsFeature = defineClientFeature({
23
23
  parent: 'root',
24
24
  impl: '@shipfox/client-integrations/routes/linear-callback',
25
25
  },
26
+ {
27
+ path: '/integrations/jira/callback',
28
+ parent: 'root',
29
+ impl: '@shipfox/client-integrations/routes/jira-callback',
30
+ },
26
31
  {
27
32
  path: '/integrations/sentry/callback',
28
33
  parent: 'root',
@@ -53,6 +58,11 @@ export const integrationsFeature = defineClientFeature({
53
58
  parent: 'workspaceLayout',
54
59
  impl: '@shipfox/client-integrations/routes/linear',
55
60
  },
61
+ {
62
+ path: '/w/$workspaceSlug/integrations/jira',
63
+ parent: 'workspaceLayout',
64
+ impl: '@shipfox/client-integrations/routes/jira',
65
+ },
56
66
  {
57
67
  path: '/w/$workspaceSlug/integrations/sentry',
58
68
  parent: 'workspaceLayout',