@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +32 -0
- package/dist/application/complete-integration-callback.d.ts +11 -0
- package/dist/application/complete-integration-callback.d.ts.map +1 -1
- package/dist/application/complete-integration-callback.js +38 -8
- package/dist/application/complete-integration-callback.js.map +1 -1
- package/dist/components/callback-status-shell.d.ts +1 -1
- package/dist/components/callback-status-shell.d.ts.map +1 -1
- package/dist/components/callback-status-shell.js +3 -3
- package/dist/components/callback-status-shell.js.map +1 -1
- package/dist/components/connection-picker.js +3 -3
- package/dist/components/connection-picker.js.map +1 -1
- package/dist/components/installed-integrations-section.js +9 -9
- package/dist/components/installed-integrations-section.js.map +1 -1
- package/dist/components/integration-delete-confirm-modal.js +1 -1
- package/dist/components/integration-delete-confirm-modal.js.map +1 -1
- package/dist/components/integration-gallery-for-workspace.js +3 -3
- package/dist/components/integration-gallery-for-workspace.js.map +1 -1
- package/dist/components/integration-usage-modal.js +6 -6
- package/dist/components/integration-usage-modal.js.map +1 -1
- package/dist/components/provider-grid.d.ts +1 -1
- package/dist/components/provider-grid.d.ts.map +1 -1
- package/dist/components/provider-grid.js +10 -10
- package/dist/components/provider-grid.js.map +1 -1
- package/dist/components/redirect-install-page.js +1 -1
- package/dist/components/redirect-install-page.js.map +1 -1
- package/dist/components/repository-picker.js +4 -4
- package/dist/components/repository-picker.js.map +1 -1
- package/dist/components/webhook/copyable-value.js +3 -3
- package/dist/components/webhook/copyable-value.js.map +1 -1
- package/dist/components/webhook/webhook-create-modal.js +1 -1
- package/dist/components/webhook/webhook-create-modal.js.map +1 -1
- package/dist/components/webhook/webhook-usage-details.js +2 -2
- package/dist/components/webhook/webhook-usage-details.js.map +1 -1
- package/dist/core/models.d.ts +6 -0
- package/dist/core/models.d.ts.map +1 -1
- package/dist/core/models.js +12 -0
- package/dist/core/models.js.map +1 -1
- package/dist/feature.d.ts +8 -0
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +10 -0
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/integration-mapper.d.ts +3 -1
- package/dist/hooks/api/integration-mapper.d.ts.map +1 -1
- package/dist/hooks/api/integration-mapper.js +8 -0
- package/dist/hooks/api/integration-mapper.js.map +1 -1
- package/dist/hooks/api/integrations.d.ts +25 -1
- package/dist/hooks/api/integrations.d.ts.map +1 -1
- package/dist/hooks/api/integrations.js +44 -1
- package/dist/hooks/api/integrations.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/jira-callback-state.d.ts +9 -0
- package/dist/jira-callback-state.d.ts.map +1 -0
- package/dist/jira-callback-state.js +16 -0
- package/dist/jira-callback-state.js.map +1 -0
- package/dist/jira-callback.d.ts +11 -0
- package/dist/jira-callback.d.ts.map +1 -0
- package/dist/jira-callback.js +59 -0
- package/dist/jira-callback.js.map +1 -0
- package/dist/jira-form-errors.d.ts +8 -0
- package/dist/jira-form-errors.d.ts.map +1 -0
- package/dist/jira-form-errors.js +55 -0
- package/dist/jira-form-errors.js.map +1 -0
- package/dist/pages/gitea-install-page.js +4 -4
- package/dist/pages/gitea-install-page.js.map +1 -1
- package/dist/pages/jira-callback-page.d.ts +2 -0
- package/dist/pages/jira-callback-page.d.ts.map +1 -0
- package/dist/pages/jira-callback-page.js +341 -0
- package/dist/pages/jira-callback-page.js.map +1 -0
- package/dist/pages/jira-install-page.d.ts +2 -0
- package/dist/pages/jira-install-page.d.ts.map +1 -0
- package/dist/pages/jira-install-page.js +26 -0
- package/dist/pages/jira-install-page.js.map +1 -0
- package/dist/pages/sentry-callback-page.js +7 -7
- package/dist/pages/sentry-callback-page.js.map +1 -1
- package/dist/pages/source-control-onboarding-page.js +2 -2
- package/dist/pages/source-control-onboarding-page.js.map +1 -1
- package/dist/provider-catalog.d.ts +1 -1
- package/dist/provider-catalog.d.ts.map +1 -1
- package/dist/provider-catalog.js +5 -0
- package/dist/provider-catalog.js.map +1 -1
- package/dist/routes/jira-callback.d.ts +8 -0
- package/dist/routes/jira-callback.d.ts.map +1 -0
- package/dist/routes/jira-callback.js +9 -0
- package/dist/routes/jira-callback.js.map +1 -0
- package/dist/routes/jira.d.ts +6 -0
- package/dist/routes/jira.d.ts.map +1 -0
- package/dist/routes/jira.js +7 -0
- package/dist/routes/jira.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -8
- package/src/application/complete-integration-callback.test.ts +44 -1
- package/src/application/complete-integration-callback.ts +61 -8
- package/src/components/callback-status-shell.tsx +7 -4
- package/src/components/connection-picker.tsx +3 -3
- package/src/components/installed-integrations-section.tsx +9 -9
- package/src/components/integration-delete-confirm-modal.tsx +1 -1
- package/src/components/integration-gallery-for-workspace.tsx +3 -3
- package/src/components/integration-usage-events.test.ts +13 -0
- package/src/components/integration-usage-modal.tsx +6 -6
- package/src/components/provider-grid.tsx +10 -10
- package/src/components/redirect-install-page.tsx +1 -1
- package/src/components/repository-picker.test.tsx +2 -2
- package/src/components/repository-picker.tsx +6 -4
- package/src/components/webhook/copyable-value.tsx +3 -3
- package/src/components/webhook/webhook-create-modal.tsx +1 -1
- package/src/components/webhook/webhook-usage-details.tsx +2 -2
- package/src/core/models.ts +18 -0
- package/src/feature.ts +10 -0
- package/src/hooks/api/integration-mapper.ts +6 -0
- package/src/hooks/api/integrations.test.ts +92 -0
- package/src/hooks/api/integrations.ts +71 -0
- package/src/index.ts +4 -0
- package/src/jira-callback-state.ts +19 -0
- package/src/jira-callback.test.ts +76 -0
- package/src/jira-callback.ts +67 -0
- package/src/jira-form-errors.test.ts +57 -0
- package/src/jira-form-errors.ts +94 -0
- package/src/pages/gitea-install-page.tsx +4 -4
- package/src/pages/jira-callback-page.test.tsx +250 -0
- package/src/pages/jira-callback-page.tsx +366 -0
- package/src/pages/jira-install-page.tsx +28 -0
- package/src/pages/sentry-callback-page.tsx +7 -7
- package/src/pages/source-control-onboarding-page.tsx +2 -2
- package/src/provider-catalog.test.ts +6 -0
- package/src/provider-catalog.ts +7 -1
- package/src/routes/jira-callback.tsx +8 -0
- package/src/routes/jira.tsx +4 -0
- 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": "
|
|
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-
|
|
38
|
-
"@shipfox/
|
|
39
|
-
"@shipfox/
|
|
40
|
-
"@shipfox/
|
|
41
|
-
"@shipfox/
|
|
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 {
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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:
|
|
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-
|
|
44
|
-
<div className="mx-auto flex w-full max-w-[480px] flex-col justify-center gap-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
52
|
-
<div className="flex flex-col gap-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
133
|
-
<div className="flex min-w-0 items-center gap-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
149
|
-
<div className="flex flex-col gap-
|
|
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-
|
|
80
|
-
<div className="flex min-w-0 items-center gap-
|
|
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-
|
|
96
|
-
<section className="flex w-full flex-col gap-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
163
|
-
<div className="flex items-center justify-between gap-
|
|
164
|
-
<div className="flex min-w-0 items-center gap-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
44
|
-
<div className="flex min-w-0 items-center gap-
|
|
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-
|
|
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-
|
|
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-
|
|
33
|
-
<div className="flex w-full min-w-0 flex-col gap-
|
|
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>
|
package/src/core/models.ts
CHANGED
|
@@ -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',
|