@shipfox/client-integrations 12.0.2 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +52 -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 +4 -3
- package/dist/components/callback-status-shell.d.ts.map +1 -1
- package/dist/components/callback-status-shell.js +11 -11
- package/dist/components/callback-status-shell.js.map +1 -1
- package/dist/components/installed-integrations-section.d.ts +2 -1
- package/dist/components/installed-integrations-section.d.ts.map +1 -1
- package/dist/components/installed-integrations-section.js +7 -6
- package/dist/components/installed-integrations-section.js.map +1 -1
- package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -1
- package/dist/components/integration-gallery-for-workspace.js +11 -2
- package/dist/components/integration-gallery-for-workspace.js.map +1 -1
- package/dist/components/integration-gallery.stories.js +10 -9
- package/dist/components/integration-gallery.stories.js.map +1 -1
- package/dist/components/provider-grid.d.ts +2 -2
- package/dist/components/provider-grid.d.ts.map +1 -1
- package/dist/components/provider-grid.js +4 -4
- package/dist/components/provider-grid.js.map +1 -1
- package/dist/components/redirect-install-page.d.ts +1 -1
- package/dist/components/redirect-install-page.js +2 -2
- package/dist/components/redirect-install-page.js.map +1 -1
- package/dist/components/webhook/webhook-modals.stories.js +2 -2
- package/dist/components/webhook/webhook-modals.stories.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 +15 -7
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +17 -7
- 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 +45 -2
- 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/linear-callback-page.d.ts.map +1 -1
- package/dist/pages/linear-callback-page.js +58 -12
- package/dist/pages/linear-callback-page.js.map +1 -1
- package/dist/pages/sentry-callback-page.d.ts.map +1 -1
- package/dist/pages/sentry-callback-page.js +22 -9
- package/dist/pages/sentry-callback-page.js.map +1 -1
- package/dist/pages/slack-callback-page.d.ts.map +1 -1
- package/dist/pages/slack-callback-page.js +50 -18
- package/dist/pages/slack-callback-page.js.map +1 -1
- package/dist/pages/source-control-onboarding-page.js +1 -1
- 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 +10 -5
- 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/sentry-callback.js +2 -2
- package/dist/sentry-callback.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/dist/workspace-navigation.d.ts +13 -0
- package/dist/workspace-navigation.d.ts.map +1 -0
- package/dist/workspace-navigation.js +17 -0
- package/dist/workspace-navigation.js.map +1 -0
- package/package.json +14 -13
- 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 +17 -10
- package/src/components/installed-integrations-section.tsx +16 -9
- package/src/components/integration-gallery-for-workspace.tsx +20 -10
- package/src/components/integration-gallery.stories.tsx +10 -10
- package/src/components/integration-gallery.test.tsx +30 -15
- package/src/components/integration-usage-events.test.ts +13 -0
- package/src/components/provider-grid.tsx +6 -6
- package/src/components/redirect-install-page.test.tsx +3 -3
- package/src/components/redirect-install-page.tsx +2 -2
- package/src/components/webhook/webhook-modals.stories.tsx +2 -2
- package/src/core/models.ts +18 -0
- package/src/feature.ts +17 -7
- package/src/hooks/api/integration-mapper.ts +6 -0
- package/src/hooks/api/integrations.test.ts +92 -0
- package/src/hooks/api/integrations.ts +72 -1
- 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 +2 -2
- 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/linear-callback-page.test.tsx +25 -12
- package/src/pages/linear-callback-page.tsx +73 -12
- package/src/pages/sentry-callback-page.test.tsx +8 -5
- package/src/pages/sentry-callback-page.tsx +32 -8
- package/src/pages/slack-callback-page.test.tsx +78 -0
- package/src/pages/slack-callback-page.tsx +56 -18
- package/src/pages/source-control-onboarding-page.test.tsx +4 -4
- package/src/pages/source-control-onboarding-page.tsx +1 -1
- package/src/provider-catalog.test.ts +10 -4
- package/src/provider-catalog.ts +16 -10
- package/src/routes/jira-callback.tsx +8 -0
- package/src/routes/jira.tsx +4 -0
- package/src/sentry-callback.ts +2 -2
- package/src/workspace-navigation.test.ts +43 -0
- package/src/workspace-navigation.ts +38 -0
- package/test/render.tsx +2 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
interface WorkspaceSlugCandidate {
|
|
3
|
+
id: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function rememberCallbackKey(keys: Set<string>, key: string, maxSize?: number): void;
|
|
7
|
+
export declare function resolveWorkspaceSlug({ workspaceId, fallbackWorkspaces, queryClient, }: {
|
|
8
|
+
workspaceId: string;
|
|
9
|
+
fallbackWorkspaces: readonly WorkspaceSlugCandidate[];
|
|
10
|
+
queryClient: QueryClient;
|
|
11
|
+
}): Promise<string | undefined>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=workspace-navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-navigation.d.ts","sourceRoot":"","sources":["../src/workspace-navigation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAEvD,UAAU,sBAAsB;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAMD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,IAAI,CAMtF;AAED,wBAAsB,oBAAoB,CAAC,EACzC,WAAW,EACX,kBAAkB,EAClB,WAAW,GACZ,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACtD,WAAW,EAAE,WAAW,CAAC;CAC1B,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS9B"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { listUserWorkspaces, userWorkspacesQueryKey } from '@shipfox/client-shell/runtime';
|
|
2
|
+
export function rememberCallbackKey(keys, key, maxSize = 32) {
|
|
3
|
+
keys.delete(key);
|
|
4
|
+
keys.add(key);
|
|
5
|
+
if (keys.size <= maxSize) return;
|
|
6
|
+
const oldest = keys.values().next().value;
|
|
7
|
+
if (typeof oldest === 'string') keys.delete(oldest);
|
|
8
|
+
}
|
|
9
|
+
export async function resolveWorkspaceSlug({ workspaceId, fallbackWorkspaces, queryClient }) {
|
|
10
|
+
const currentWorkspaces = await listUserWorkspaces().then(({ memberships })=>memberships).catch(()=>{
|
|
11
|
+
const refreshedWorkspaces = queryClient.getQueryData(userWorkspacesQueryKey);
|
|
12
|
+
return refreshedWorkspaces?.memberships ?? fallbackWorkspaces;
|
|
13
|
+
});
|
|
14
|
+
return currentWorkspaces.find(({ id })=>id === workspaceId)?.slug;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=workspace-navigation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/workspace-navigation.ts"],"sourcesContent":["import {listUserWorkspaces, userWorkspacesQueryKey} from '@shipfox/client-shell/runtime';\nimport type {QueryClient} from '@tanstack/react-query';\n\ninterface WorkspaceSlugCandidate {\n id: string;\n slug: string;\n}\n\ninterface UserWorkspacesQueryData {\n memberships: WorkspaceSlugCandidate[];\n}\n\nexport function rememberCallbackKey(keys: Set<string>, key: string, maxSize = 32): void {\n keys.delete(key);\n keys.add(key);\n if (keys.size <= maxSize) return;\n const oldest = keys.values().next().value;\n if (typeof oldest === 'string') keys.delete(oldest);\n}\n\nexport async function resolveWorkspaceSlug({\n workspaceId,\n fallbackWorkspaces,\n queryClient,\n}: {\n workspaceId: string;\n fallbackWorkspaces: readonly WorkspaceSlugCandidate[];\n queryClient: QueryClient;\n}): Promise<string | undefined> {\n const currentWorkspaces = await listUserWorkspaces()\n .then(({memberships}) => memberships)\n .catch(() => {\n const refreshedWorkspaces =\n queryClient.getQueryData<UserWorkspacesQueryData>(userWorkspacesQueryKey);\n return refreshedWorkspaces?.memberships ?? fallbackWorkspaces;\n });\n return currentWorkspaces.find(({id}) => id === workspaceId)?.slug;\n}\n"],"names":["listUserWorkspaces","userWorkspacesQueryKey","rememberCallbackKey","keys","key","maxSize","delete","add","size","oldest","values","next","value","resolveWorkspaceSlug","workspaceId","fallbackWorkspaces","queryClient","currentWorkspaces","then","memberships","catch","refreshedWorkspaces","getQueryData","find","id","slug"],"mappings":"AAAA,SAAQA,kBAAkB,EAAEC,sBAAsB,QAAO,gCAAgC;AAYzF,OAAO,SAASC,oBAAoBC,IAAiB,EAAEC,GAAW,EAAEC,UAAU,EAAE;IAC9EF,KAAKG,MAAM,CAACF;IACZD,KAAKI,GAAG,CAACH;IACT,IAAID,KAAKK,IAAI,IAAIH,SAAS;IAC1B,MAAMI,SAASN,KAAKO,MAAM,GAAGC,IAAI,GAAGC,KAAK;IACzC,IAAI,OAAOH,WAAW,UAAUN,KAAKG,MAAM,CAACG;AAC9C;AAEA,OAAO,eAAeI,qBAAqB,EACzCC,WAAW,EACXC,kBAAkB,EAClBC,WAAW,EAKZ;IACC,MAAMC,oBAAoB,MAAMjB,qBAC7BkB,IAAI,CAAC,CAAC,EAACC,WAAW,EAAC,GAAKA,aACxBC,KAAK,CAAC;QACL,MAAMC,sBACJL,YAAYM,YAAY,CAA0BrB;QACpD,OAAOoB,qBAAqBF,eAAeJ;IAC7C;IACF,OAAOE,kBAAkBM,IAAI,CAAC,CAAC,EAACC,EAAE,EAAC,GAAKA,OAAOV,cAAcW;AAC/D"}
|
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.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,19 +27,20 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@swc/helpers": "^0.5.17",
|
|
29
29
|
"@tanstack/react-form": "^1.32.0",
|
|
30
|
-
"@shipfox/api-integration-core-dto": "
|
|
31
|
-
"@shipfox/api-integration-gitea-dto": "
|
|
32
|
-
"@shipfox/api-integration-github-dto": "
|
|
33
|
-
"@shipfox/api-integration-
|
|
34
|
-
"@shipfox/api-integration-
|
|
35
|
-
"@shipfox/api-integration-
|
|
36
|
-
"@shipfox/api-integration-
|
|
30
|
+
"@shipfox/api-integration-core-dto": "12.0.0",
|
|
31
|
+
"@shipfox/api-integration-gitea-dto": "12.0.0",
|
|
32
|
+
"@shipfox/api-integration-github-dto": "12.0.0",
|
|
33
|
+
"@shipfox/api-integration-jira-dto": "12.0.0",
|
|
34
|
+
"@shipfox/api-integration-linear-dto": "12.0.0",
|
|
35
|
+
"@shipfox/api-integration-slack-dto": "12.0.0",
|
|
36
|
+
"@shipfox/api-integration-sentry-dto": "12.0.0",
|
|
37
|
+
"@shipfox/api-integration-webhook-dto": "12.0.0",
|
|
37
38
|
"@shipfox/client-api": "6.0.1",
|
|
38
|
-
"@shipfox/client-auth": "
|
|
39
|
-
"@shipfox/client-shell": "
|
|
40
|
-
"@shipfox/client-ui": "
|
|
41
|
-
"@shipfox/integration-icons": "0.
|
|
42
|
-
"@shipfox/react-ui": "0.
|
|
39
|
+
"@shipfox/client-auth": "14.0.0",
|
|
40
|
+
"@shipfox/client-shell": "14.0.0",
|
|
41
|
+
"@shipfox/client-ui": "14.0.0",
|
|
42
|
+
"@shipfox/integration-icons": "0.3.0",
|
|
43
|
+
"@shipfox/react-ui": "0.5.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
|
+
}
|
|
@@ -7,24 +7,29 @@ import {useEffect, useRef} from 'react';
|
|
|
7
7
|
export function CallbackStatusShell({
|
|
8
8
|
title,
|
|
9
9
|
message,
|
|
10
|
+
status = 'error',
|
|
10
11
|
startOver,
|
|
11
12
|
switchAccount,
|
|
12
|
-
|
|
13
|
+
workspaceSlug,
|
|
13
14
|
installPath,
|
|
14
15
|
}: {
|
|
15
16
|
title: string;
|
|
16
17
|
message: string;
|
|
18
|
+
status?: 'error' | 'success';
|
|
17
19
|
startOver?: boolean;
|
|
18
20
|
switchAccount?: boolean;
|
|
19
|
-
|
|
20
|
-
installPath:
|
|
21
|
+
workspaceSlug?: string | undefined;
|
|
22
|
+
installPath:
|
|
23
|
+
| '/w/$workspaceSlug/integrations/linear'
|
|
24
|
+
| '/w/$workspaceSlug/integrations/slack'
|
|
25
|
+
| '/w/$workspaceSlug/integrations/jira';
|
|
21
26
|
}) {
|
|
22
27
|
const headingRef = useRef<HTMLHeadingElement>(null);
|
|
23
28
|
useEffect(() => headingRef.current?.focus(), []);
|
|
24
29
|
const recoveryVariant = startOver || switchAccount ? 'muted' : 'base';
|
|
25
|
-
const settings =
|
|
30
|
+
const settings = workspaceSlug ? (
|
|
26
31
|
<ButtonLink asChild variant={recoveryVariant} className="min-h-44 w-full sm:w-fit">
|
|
27
|
-
<Link to="/
|
|
32
|
+
<Link to="/w/$workspaceSlug/settings/integrations" params={{workspaceSlug}}>
|
|
28
33
|
Back to integrations
|
|
29
34
|
</Link>
|
|
30
35
|
</ButtonLink>
|
|
@@ -33,7 +38,9 @@ export function CallbackStatusShell({
|
|
|
33
38
|
<Link to="/">Back to Shipfox</Link>
|
|
34
39
|
</ButtonLink>
|
|
35
40
|
);
|
|
36
|
-
const logoutRedirect =
|
|
41
|
+
const logoutRedirect = workspaceSlug
|
|
42
|
+
? installPath.replace('$workspaceSlug', workspaceSlug)
|
|
43
|
+
: undefined;
|
|
37
44
|
|
|
38
45
|
return (
|
|
39
46
|
<main className="flex min-h-screen bg-background-subtle-base px-16 py-32">
|
|
@@ -41,7 +48,7 @@ export function CallbackStatusShell({
|
|
|
41
48
|
<h2 ref={headingRef} tabIndex={-1} className="text-24 font-semibold outline-none">
|
|
42
49
|
{title}
|
|
43
50
|
</h2>
|
|
44
|
-
<Callout role=
|
|
51
|
+
<Callout role={status === 'error' ? 'alert' : 'status'} type={status}>
|
|
45
52
|
<Text size="sm">{message}</Text>
|
|
46
53
|
</Callout>
|
|
47
54
|
<div className="flex flex-col gap-8 sm:flex-row sm:items-center">
|
|
@@ -49,15 +56,15 @@ export function CallbackStatusShell({
|
|
|
49
56
|
<ButtonLink asChild className="min-h-44 w-full sm:w-fit">
|
|
50
57
|
<Link
|
|
51
58
|
to="/auth/logout"
|
|
52
|
-
search={
|
|
59
|
+
search={logoutRedirect ? {redirect: logoutRedirect} : undefined}
|
|
53
60
|
>
|
|
54
61
|
Switch account
|
|
55
62
|
</Link>
|
|
56
63
|
</ButtonLink>
|
|
57
64
|
) : null}
|
|
58
|
-
{startOver &&
|
|
65
|
+
{startOver && workspaceSlug ? (
|
|
59
66
|
<ButtonLink asChild className="min-h-44 w-full sm:w-fit">
|
|
60
|
-
<Link to={installPath} params={{
|
|
67
|
+
<Link to={installPath} params={{workspaceSlug}}>
|
|
61
68
|
Start over
|
|
62
69
|
</Link>
|
|
63
70
|
</ButtonLink>
|
|
@@ -18,6 +18,7 @@ import {type IntegrationConnection, isUsableConnection} from '#core/models.js';
|
|
|
18
18
|
import {usageEventsForConnection} from './integration-usage-events.js';
|
|
19
19
|
|
|
20
20
|
interface InstalledIntegrationsSectionProps {
|
|
21
|
+
workspaceSlug?: string | undefined;
|
|
21
22
|
connections: IntegrationConnection[];
|
|
22
23
|
isPending: boolean;
|
|
23
24
|
isFetching: boolean;
|
|
@@ -34,6 +35,7 @@ const INSTALLED_SURFACE_CLASS =
|
|
|
34
35
|
'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
|
|
35
36
|
|
|
36
37
|
export function InstalledIntegrationsSection({
|
|
38
|
+
workspaceSlug,
|
|
37
39
|
connections,
|
|
38
40
|
isPending,
|
|
39
41
|
isFetching,
|
|
@@ -81,6 +83,7 @@ export function InstalledIntegrationsSection({
|
|
|
81
83
|
<InstalledRow
|
|
82
84
|
key={connection.id}
|
|
83
85
|
connection={connection}
|
|
86
|
+
workspaceSlug={workspaceSlug}
|
|
84
87
|
isMutating={isMutating}
|
|
85
88
|
onUse={onUse}
|
|
86
89
|
onSetActive={(nextActive) => onSetActive(connection, nextActive)}
|
|
@@ -96,6 +99,7 @@ export function InstalledIntegrationsSection({
|
|
|
96
99
|
|
|
97
100
|
function InstalledRow({
|
|
98
101
|
connection,
|
|
102
|
+
workspaceSlug,
|
|
99
103
|
isMutating,
|
|
100
104
|
onUse,
|
|
101
105
|
onSetActive,
|
|
@@ -103,6 +107,7 @@ function InstalledRow({
|
|
|
103
107
|
providerDisplayName,
|
|
104
108
|
}: {
|
|
105
109
|
connection: IntegrationConnection;
|
|
110
|
+
workspaceSlug?: string | undefined;
|
|
106
111
|
isMutating: boolean;
|
|
107
112
|
onUse: (connectionId: string) => void;
|
|
108
113
|
onSetActive: (active: boolean) => void;
|
|
@@ -159,15 +164,17 @@ function InstalledRow({
|
|
|
159
164
|
<DropdownMenuItem onSelect={() => onUse(connection.id)}>
|
|
160
165
|
Use this integration
|
|
161
166
|
</DropdownMenuItem>
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
{workspaceSlug ? (
|
|
168
|
+
<DropdownMenuItem asChild>
|
|
169
|
+
<Link
|
|
170
|
+
to="/w/$workspaceSlug/settings/events"
|
|
171
|
+
params={{workspaceSlug}}
|
|
172
|
+
search={{source: [connection.slug], event: [recentEventsEvent]}}
|
|
173
|
+
>
|
|
174
|
+
View recent events
|
|
175
|
+
</Link>
|
|
176
|
+
</DropdownMenuItem>
|
|
177
|
+
) : null}
|
|
171
178
|
<DropdownMenuSeparator />
|
|
172
179
|
<DropdownMenuItem disabled={isMutating} onSelect={() => onSetActive(!active)}>
|
|
173
180
|
{active ? 'Disable integration' : 'Enable integration'}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {useAuthState} from '@shipfox/client-auth';
|
|
1
2
|
import {toast} from '@shipfox/react-ui/toast';
|
|
2
3
|
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
3
4
|
import {useState} from 'react';
|
|
@@ -35,6 +36,8 @@ export function IntegrationGalleryForWorkspace({
|
|
|
35
36
|
emptyProvidersMessage,
|
|
36
37
|
workspaceId,
|
|
37
38
|
}: IntegrationGalleryForWorkspaceProps) {
|
|
39
|
+
const {workspaces} = useAuthState();
|
|
40
|
+
const workspaceSlug = workspaces.find((workspace) => workspace.id === workspaceId)?.slug;
|
|
38
41
|
const [createProvider, setCreateProvider] = useState<string | undefined>();
|
|
39
42
|
const [usageConnectionId, setUsageConnectionId] = useState<string | undefined>();
|
|
40
43
|
const [createdUsageConnection, setCreatedUsageConnection] = useState<
|
|
@@ -122,6 +125,7 @@ export function IntegrationGalleryForWorkspace({
|
|
|
122
125
|
return (
|
|
123
126
|
<div className="flex flex-col gap-24">
|
|
124
127
|
<InstalledIntegrationsSection
|
|
128
|
+
workspaceSlug={workspaceSlug}
|
|
125
129
|
connections={sortedConnections}
|
|
126
130
|
isPending={connectionsQuery.isPending}
|
|
127
131
|
isFetching={connectionsQuery.isFetching}
|
|
@@ -149,16 +153,22 @@ export function IntegrationGalleryForWorkspace({
|
|
|
149
153
|
</Text>
|
|
150
154
|
</div>
|
|
151
155
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
{workspaceSlug ? (
|
|
157
|
+
<ProviderGrid
|
|
158
|
+
workspaceSlug={workspaceSlug}
|
|
159
|
+
providers={providers}
|
|
160
|
+
isPending={providersQuery.isPending}
|
|
161
|
+
isFetching={providersQuery.isFetching}
|
|
162
|
+
error={providersQuery.isError ? providersQuery.error : undefined}
|
|
163
|
+
onRetry={() => void providersQuery.refetch()}
|
|
164
|
+
emptyMessage={emptyProvidersMessage}
|
|
165
|
+
onOpenProvider={setCreateProvider}
|
|
166
|
+
/>
|
|
167
|
+
) : (
|
|
168
|
+
<Text size="sm" className="text-foreground-neutral-muted" aria-live="polite">
|
|
169
|
+
Loading workspace details…
|
|
170
|
+
</Text>
|
|
171
|
+
)}
|
|
162
172
|
</section>
|
|
163
173
|
<WebhookCreateModal
|
|
164
174
|
workspaceId={workspaceId}
|
|
@@ -20,14 +20,14 @@ import {useMemo} from 'react';
|
|
|
20
20
|
import {IntegrationGallery} from './integration-gallery.js';
|
|
21
21
|
|
|
22
22
|
const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
|
|
23
|
-
const WORKSPACE_PATH =
|
|
23
|
+
const WORKSPACE_PATH = '/w/acme/settings/integrations';
|
|
24
24
|
const SETUP_PATHS = [
|
|
25
|
-
'/
|
|
26
|
-
'/
|
|
27
|
-
'/
|
|
28
|
-
'/
|
|
29
|
-
'/
|
|
30
|
-
'/
|
|
25
|
+
'/w/$workspaceSlug/integrations/github',
|
|
26
|
+
'/w/$workspaceSlug/integrations/sentry',
|
|
27
|
+
'/w/$workspaceSlug/integrations/linear',
|
|
28
|
+
'/w/$workspaceSlug/integrations/slack',
|
|
29
|
+
'/w/$workspaceSlug/integrations/gitea',
|
|
30
|
+
'/w/$workspaceSlug/settings/events',
|
|
31
31
|
] as const;
|
|
32
32
|
|
|
33
33
|
type Scenario =
|
|
@@ -66,7 +66,7 @@ function IntegrationGalleryStory({scenario}: IntegrationGalleryStoryProps) {
|
|
|
66
66
|
const nextStore = createStore();
|
|
67
67
|
nextStore.set(authStateAtom, {
|
|
68
68
|
status: 'authenticated',
|
|
69
|
-
workspaces: [{id: WORKSPACE_ID, name: 'Acme', membershipId: 'membership-1'}],
|
|
69
|
+
workspaces: [{id: WORKSPACE_ID, name: 'Acme', slug: 'acme', membershipId: 'membership-1'}],
|
|
70
70
|
});
|
|
71
71
|
return nextStore;
|
|
72
72
|
}, []);
|
|
@@ -74,7 +74,7 @@ function IntegrationGalleryStory({scenario}: IntegrationGalleryStoryProps) {
|
|
|
74
74
|
const rootRoute = createRootRoute({component: Outlet});
|
|
75
75
|
const workspaceRoute = createRoute({
|
|
76
76
|
getParentRoute: () => rootRoute,
|
|
77
|
-
path: '/
|
|
77
|
+
path: '/w/$workspaceSlug',
|
|
78
78
|
component: Outlet,
|
|
79
79
|
});
|
|
80
80
|
const galleryRoute = createRoute({
|
|
@@ -89,7 +89,7 @@ function IntegrationGalleryStory({scenario}: IntegrationGalleryStoryProps) {
|
|
|
89
89
|
const setupRoutes = SETUP_PATHS.map((path) =>
|
|
90
90
|
createRoute({
|
|
91
91
|
getParentRoute: () => workspaceRoute,
|
|
92
|
-
path: path.replace('/
|
|
92
|
+
path: path.replace('/w/$workspaceSlug/', ''),
|
|
93
93
|
component: () => <div />,
|
|
94
94
|
}),
|
|
95
95
|
);
|
|
@@ -3,7 +3,12 @@ import '@testing-library/jest-dom/vitest';
|
|
|
3
3
|
import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
|
|
4
4
|
import {configureApiClient} from '@shipfox/client-api';
|
|
5
5
|
import {fireEvent, screen, waitFor, within} from '@testing-library/react';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
INTEGRATIONS_TEST_WID,
|
|
8
|
+
jsonResponse,
|
|
9
|
+
renderIntegrationsPage,
|
|
10
|
+
testWorkspace,
|
|
11
|
+
} from '#test/render.js';
|
|
7
12
|
import {IntegrationGallery} from './integration-gallery.js';
|
|
8
13
|
|
|
9
14
|
if (!HTMLElement.prototype.hasPointerCapture) {
|
|
@@ -28,9 +33,9 @@ if (!HTMLElement.prototype.releasePointerCapture) {
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
const SETUP_ROUTES = [
|
|
31
|
-
'/
|
|
32
|
-
'/
|
|
33
|
-
'/
|
|
36
|
+
'/w/$workspaceSlug/integrations/github',
|
|
37
|
+
'/w/$workspaceSlug/integrations/sentry',
|
|
38
|
+
'/w/$workspaceSlug/settings/events',
|
|
34
39
|
];
|
|
35
40
|
const EXTERNAL_SETTINGS_ACTION_RE = /Open.*settings/u;
|
|
36
41
|
|
|
@@ -183,16 +188,18 @@ function fetchForGallery(options: FetchOptions = {}) {
|
|
|
183
188
|
function renderGallery(
|
|
184
189
|
props: Parameters<typeof IntegrationGallery>[0] = {},
|
|
185
190
|
options: FetchOptions = {},
|
|
191
|
+
renderOptions: {workspaces?: Parameters<typeof renderIntegrationsPage>[0]['workspaces']} = {},
|
|
186
192
|
) {
|
|
187
193
|
configureApiClient({
|
|
188
194
|
baseUrl: 'https://api.example.test',
|
|
189
195
|
fetchImpl: fetchForGallery(options),
|
|
190
196
|
});
|
|
191
197
|
return renderIntegrationsPage({
|
|
192
|
-
path:
|
|
193
|
-
routePath: '/
|
|
198
|
+
path: '/w/acme/integrations',
|
|
199
|
+
routePath: '/w/$workspaceSlug/integrations',
|
|
194
200
|
element: <IntegrationGallery workspaceId={INTEGRATIONS_TEST_WID} {...props} />,
|
|
195
201
|
extraRoutes: SETUP_ROUTES,
|
|
202
|
+
...(renderOptions.workspaces ? {workspaces: renderOptions.workspaces} : {}),
|
|
196
203
|
});
|
|
197
204
|
}
|
|
198
205
|
|
|
@@ -203,11 +210,19 @@ const openActions = async (name: string) => {
|
|
|
203
210
|
};
|
|
204
211
|
|
|
205
212
|
const SORTED_NAMES_RE = /acme-(early|late)/;
|
|
206
|
-
// The meta line carries the date only
|
|
213
|
+
// The meta line carries the date only: the provider is named once, by the
|
|
207
214
|
// icon and the account name, never repeated as body text in the row.
|
|
208
215
|
const ADDED_META_RE = /^Added /;
|
|
216
|
+
const GITHUB_LINK_RE = /GitHub/u;
|
|
209
217
|
|
|
210
218
|
describe('IntegrationGallery — installed section', () => {
|
|
219
|
+
test('waits for an explicit workspace id to hydrate before showing install links', async () => {
|
|
220
|
+
renderGallery({}, {}, {workspaces: [testWorkspace({id: 'stale-workspace', slug: 'acme'})]});
|
|
221
|
+
|
|
222
|
+
expect(await screen.findByText('Loading workspace details…')).toBeVisible();
|
|
223
|
+
expect(screen.queryByRole('link', {name: GITHUB_LINK_RE})).not.toBeInTheDocument();
|
|
224
|
+
});
|
|
225
|
+
|
|
211
226
|
test('renders two distinct cards for two connections sharing one provider', async () => {
|
|
212
227
|
renderGallery(
|
|
213
228
|
{},
|
|
@@ -375,8 +390,8 @@ describe('IntegrationGallery — installed section', () => {
|
|
|
375
390
|
});
|
|
376
391
|
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
377
392
|
renderIntegrationsPage({
|
|
378
|
-
path:
|
|
379
|
-
routePath: '/
|
|
393
|
+
path: '/w/acme/integrations',
|
|
394
|
+
routePath: '/w/$workspaceSlug/integrations',
|
|
380
395
|
element: <IntegrationGallery />,
|
|
381
396
|
extraRoutes: SETUP_ROUTES,
|
|
382
397
|
});
|
|
@@ -410,8 +425,8 @@ describe('IntegrationGallery — installed section', () => {
|
|
|
410
425
|
});
|
|
411
426
|
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
412
427
|
renderIntegrationsPage({
|
|
413
|
-
path:
|
|
414
|
-
routePath: '/
|
|
428
|
+
path: '/w/acme/integrations',
|
|
429
|
+
routePath: '/w/$workspaceSlug/integrations',
|
|
415
430
|
element: <IntegrationGallery />,
|
|
416
431
|
extraRoutes: SETUP_ROUTES,
|
|
417
432
|
});
|
|
@@ -441,8 +456,8 @@ describe('IntegrationGallery — installed section', () => {
|
|
|
441
456
|
const fetchImpl = fetchForGallery({connections: [webhookConnection]});
|
|
442
457
|
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
443
458
|
renderIntegrationsPage({
|
|
444
|
-
path:
|
|
445
|
-
routePath: '/
|
|
459
|
+
path: '/w/acme/integrations',
|
|
460
|
+
routePath: '/w/$workspaceSlug/integrations',
|
|
446
461
|
element: <IntegrationGallery />,
|
|
447
462
|
extraRoutes: SETUP_ROUTES,
|
|
448
463
|
});
|
|
@@ -511,7 +526,7 @@ describe('IntegrationGallery — installed section', () => {
|
|
|
511
526
|
expect(within(installedRegion()).getByText(ADDED_META_RE)).toBeVisible();
|
|
512
527
|
});
|
|
513
528
|
|
|
514
|
-
test('names the provider once per row
|
|
529
|
+
test('names the provider once per row: not repeated in the meta line', async () => {
|
|
515
530
|
renderGallery({}, {connections: [githubConnection]});
|
|
516
531
|
|
|
517
532
|
await screen.findByText('acme-corp');
|
|
@@ -549,7 +564,7 @@ describe('IntegrationGallery — installed section', () => {
|
|
|
549
564
|
});
|
|
550
565
|
});
|
|
551
566
|
|
|
552
|
-
describe('IntegrationGallery
|
|
567
|
+
describe('IntegrationGallery: available section', () => {
|
|
553
568
|
test('lists every provider with an Install link, including installed ones', async () => {
|
|
554
569
|
renderGallery({}, {connections: [githubConnection]});
|
|
555
570
|
|
|
@@ -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,
|