@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
|
@@ -1,33 +1,39 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useRefreshAuth } from '@shipfox/client-auth';
|
|
2
|
+
import { useAuthState, useRefreshAuth } from '@shipfox/client-auth';
|
|
3
3
|
import { useRouteSearch } from '@shipfox/client-shell/runtime';
|
|
4
4
|
import { createSingleFlight, sessionStorageOrUndefined } from '@shipfox/client-ui';
|
|
5
5
|
import { FullPageLoader } from '@shipfox/react-ui/loader';
|
|
6
6
|
import { toast } from '@shipfox/react-ui/toast';
|
|
7
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
7
8
|
import { useNavigate } from '@tanstack/react-router';
|
|
8
9
|
import { useEffect, useMemo, useState } from 'react';
|
|
9
10
|
import { useCompleteIntegrationCallback } from '#application/complete-integration-callback.js';
|
|
10
11
|
import { CallbackStatusShell } from '#components/callback-status-shell.js';
|
|
11
12
|
import { useCompleteLinearCallbackMutation } from '#hooks/api/integrations.js';
|
|
12
13
|
import { classifyLinearCallbackError, clearLinearInstallWorkspace, parseLinearCallbackQuery, readLinearInstallWorkspace, serializeLinearCallbackQuery } from '#linear-callback.js';
|
|
14
|
+
import { rememberCallbackKey, resolveWorkspaceSlug } from '#workspace-navigation.js';
|
|
13
15
|
// Retain only recent completions: this bounds long-lived callback pages while
|
|
14
16
|
// still covering StrictMode and immediate Back/Forward remounts.
|
|
15
17
|
const callbackRequests = createSingleFlight({
|
|
16
18
|
maxTerminalResults: 32
|
|
17
19
|
});
|
|
20
|
+
const completedCallbacks = new Set();
|
|
18
21
|
// Keeps the success toast firing once per distinct callback even though the
|
|
19
22
|
// effect re-runs against the cached request as the mutation identity churns.
|
|
20
23
|
const toastedCallbacks = new Set();
|
|
21
24
|
export function LinearCallbackPage() {
|
|
22
25
|
const navigate = useNavigate();
|
|
26
|
+
const queryClient = useQueryClient();
|
|
23
27
|
const refreshAuth = useRefreshAuth();
|
|
24
28
|
const completeIntegrationCallback = useCompleteIntegrationCallback();
|
|
25
29
|
const { mutateAsync: completeLinearCallback } = useCompleteLinearCallbackMutation();
|
|
30
|
+
const { workspaces, isLoading } = useAuthState();
|
|
26
31
|
const params = useRouteSearch(parseLinearCallbackQuery);
|
|
27
32
|
const workspaceId = useMemo(()=>readLinearInstallWorkspace(sessionStorageOrUndefined()), []);
|
|
28
33
|
const [failure, setFailure] = useState();
|
|
34
|
+
const [completedWorkspace, setCompletedWorkspace] = useState();
|
|
29
35
|
useEffect(()=>{
|
|
30
|
-
if (!params) return;
|
|
36
|
+
if (!params || isLoading) return;
|
|
31
37
|
let disposed = false;
|
|
32
38
|
const key = serializeLinearCallbackQuery(params);
|
|
33
39
|
const request = callbackRequests.run(key, async ()=>await completeIntegrationCallback({
|
|
@@ -40,6 +46,18 @@ export function LinearCallbackPage() {
|
|
|
40
46
|
}));
|
|
41
47
|
request.then(async (connection)=>{
|
|
42
48
|
if (disposed) return;
|
|
49
|
+
if (completedCallbacks.has(key)) {
|
|
50
|
+
const workspaceSlug = await resolveWorkspaceSlug({
|
|
51
|
+
workspaceId: connection.workspaceId,
|
|
52
|
+
fallbackWorkspaces: workspaces,
|
|
53
|
+
queryClient
|
|
54
|
+
});
|
|
55
|
+
if (!disposed) setCompletedWorkspace(workspaceSlug ? {
|
|
56
|
+
slug: workspaceSlug
|
|
57
|
+
} : {});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
rememberCallbackKey(completedCallbacks, key);
|
|
43
61
|
try {
|
|
44
62
|
clearLinearInstallWorkspace(sessionStorageOrUndefined());
|
|
45
63
|
} catch {
|
|
@@ -47,19 +65,37 @@ export function LinearCallbackPage() {
|
|
|
47
65
|
}
|
|
48
66
|
if (disposed) return;
|
|
49
67
|
if (!toastedCallbacks.has(key)) {
|
|
50
|
-
toastedCallbacks
|
|
68
|
+
rememberCallbackKey(toastedCallbacks, key);
|
|
51
69
|
toast.success('Linear installed.');
|
|
52
70
|
}
|
|
71
|
+
let workspaceSlug;
|
|
53
72
|
try {
|
|
73
|
+
if (disposed) return;
|
|
74
|
+
workspaceSlug = await resolveWorkspaceSlug({
|
|
75
|
+
workspaceId: connection.workspaceId,
|
|
76
|
+
fallbackWorkspaces: workspaces,
|
|
77
|
+
queryClient
|
|
78
|
+
});
|
|
79
|
+
if (disposed) return;
|
|
80
|
+
if (!workspaceSlug) {
|
|
81
|
+
setCompletedWorkspace({});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
setCompletedWorkspace({
|
|
85
|
+
slug: workspaceSlug
|
|
86
|
+
});
|
|
54
87
|
await navigate({
|
|
55
|
-
to: '/
|
|
88
|
+
to: '/w/$workspaceSlug/settings/integrations',
|
|
56
89
|
params: {
|
|
57
|
-
|
|
90
|
+
workspaceSlug
|
|
58
91
|
},
|
|
59
92
|
replace: true
|
|
60
93
|
});
|
|
61
94
|
} catch {
|
|
62
|
-
|
|
95
|
+
// Keep the completed callback page visible if client navigation is interrupted.
|
|
96
|
+
if (!disposed) setCompletedWorkspace({
|
|
97
|
+
slug: workspaceSlug
|
|
98
|
+
});
|
|
63
99
|
}
|
|
64
100
|
}, (error)=>{
|
|
65
101
|
if (disposed) return;
|
|
@@ -71,9 +107,12 @@ export function LinearCallbackPage() {
|
|
|
71
107
|
}, [
|
|
72
108
|
completeIntegrationCallback,
|
|
73
109
|
completeLinearCallback,
|
|
110
|
+
isLoading,
|
|
74
111
|
navigate,
|
|
75
112
|
params,
|
|
76
|
-
|
|
113
|
+
queryClient,
|
|
114
|
+
refreshAuth,
|
|
115
|
+
workspaces
|
|
77
116
|
]);
|
|
78
117
|
if (!params) {
|
|
79
118
|
return /*#__PURE__*/ _jsx(LinearCallbackFailurePage, {
|
|
@@ -83,25 +122,32 @@ export function LinearCallbackPage() {
|
|
|
83
122
|
startOver: true,
|
|
84
123
|
signIn: false
|
|
85
124
|
},
|
|
86
|
-
|
|
125
|
+
workspaceSlug: workspaces.find(({ id })=>id === workspaceId)?.slug
|
|
87
126
|
});
|
|
88
127
|
}
|
|
128
|
+
if (completedWorkspace) return /*#__PURE__*/ _jsx(CallbackStatusShell, {
|
|
129
|
+
title: "Linear connected",
|
|
130
|
+
status: "success",
|
|
131
|
+
message: completedWorkspace.slug ? 'Linear is connected. Continue in integrations settings.' : 'Linear is connected. Return to Shipfox to continue.',
|
|
132
|
+
workspaceSlug: completedWorkspace.slug,
|
|
133
|
+
installPath: "/w/$workspaceSlug/integrations/linear"
|
|
134
|
+
});
|
|
89
135
|
if (!failure) return /*#__PURE__*/ _jsx(FullPageLoader, {
|
|
90
136
|
"aria-label": "Connecting Linear"
|
|
91
137
|
});
|
|
92
138
|
return /*#__PURE__*/ _jsx(LinearCallbackFailurePage, {
|
|
93
139
|
failure: failure,
|
|
94
|
-
|
|
140
|
+
workspaceSlug: workspaces.find(({ id })=>id === workspaceId)?.slug
|
|
95
141
|
});
|
|
96
142
|
}
|
|
97
|
-
function LinearCallbackFailurePage({ failure,
|
|
143
|
+
function LinearCallbackFailurePage({ failure, workspaceSlug }) {
|
|
98
144
|
return /*#__PURE__*/ _jsx(CallbackStatusShell, {
|
|
99
145
|
title: failure.title ?? 'Linear install could not be completed',
|
|
100
146
|
message: failure.message,
|
|
101
147
|
startOver: failure.startOver,
|
|
102
148
|
switchAccount: failure.signIn,
|
|
103
|
-
|
|
104
|
-
installPath: "/
|
|
149
|
+
workspaceSlug: workspaceSlug,
|
|
150
|
+
installPath: "/w/$workspaceSlug/integrations/linear"
|
|
105
151
|
});
|
|
106
152
|
}
|
|
107
153
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/linear-callback-page.tsx"],"sourcesContent":["import {useRefreshAuth} from '@shipfox/client-auth';\nimport {useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {createSingleFlight, sessionStorageOrUndefined} from '@shipfox/client-ui';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useNavigate} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {useCompleteIntegrationCallback} from '#application/complete-integration-callback.js';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport type {IntegrationConnection} from '#core/models.js';\nimport {useCompleteLinearCallbackMutation} from '#hooks/api/integrations.js';\nimport {\n classifyLinearCallbackError,\n clearLinearInstallWorkspace,\n type LinearCallbackFailure,\n parseLinearCallbackQuery,\n readLinearInstallWorkspace,\n serializeLinearCallbackQuery,\n} from '#linear-callback.js';\n\n// Retain only recent completions: this bounds long-lived callback pages while\n// still covering StrictMode and immediate Back/Forward remounts.\nconst callbackRequests = createSingleFlight<string, IntegrationConnection>({\n maxTerminalResults: 32,\n});\n// Keeps the success toast firing once per distinct callback even though the\n// effect re-runs against the cached request as the mutation identity churns.\nconst toastedCallbacks = new Set<string>();\n\nexport function LinearCallbackPage() {\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const completeIntegrationCallback = useCompleteIntegrationCallback();\n const {mutateAsync: completeLinearCallback} = useCompleteLinearCallbackMutation();\n const params = useRouteSearch(parseLinearCallbackQuery);\n const workspaceId = useMemo(() => readLinearInstallWorkspace(sessionStorageOrUndefined()), []);\n const [failure, setFailure] = useState<LinearCallbackFailure | undefined>();\n useEffect(() => {\n if (!params) return;\n\n let disposed = false;\n const key = serializeLinearCallbackQuery(params);\n const request = callbackRequests.run(\n key,\n async () =>\n await completeIntegrationCallback({\n input: params,\n refreshAuth,\n complete: async (query, token) => await completeLinearCallback({query, token}),\n }),\n );\n\n request.then(\n async (connection) => {\n if (disposed) return;\n try {\n clearLinearInstallWorkspace(sessionStorageOrUndefined());\n } catch {\n // The successful API response remains the source of truth for navigation.\n }\n if (disposed) return;\n if (!toastedCallbacks.has(key)) {\n toastedCallbacks.add(key);\n toast.success('Linear installed.');\n }\n try {\n await navigate({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: connection.workspaceId},\n replace: true,\n });\n } catch {\n // Keep the completed callback page visible if client navigation is interrupted.\n }\n },\n (error: unknown) => {\n if (disposed) return;\n setFailure(classifyLinearCallbackError(error));\n },\n );\n\n return () => {\n disposed = true;\n };\n }, [completeIntegrationCallback, completeLinearCallback, navigate, params, refreshAuth]);\n\n if (!params) {\n return (\n <LinearCallbackFailurePage\n failure={{\n title: 'Invalid Linear callback',\n message: 'Invalid Linear callback. Start the install again from workspace settings.',\n startOver: true,\n signIn: false,\n }}\n workspaceId={workspaceId}\n />\n );\n }\n\n if (!failure) return <FullPageLoader aria-label=\"Connecting Linear\" />;\n\n return <LinearCallbackFailurePage failure={failure} workspaceId={workspaceId} />;\n}\n\nfunction LinearCallbackFailurePage({\n failure,\n workspaceId,\n}: {\n failure: LinearCallbackFailure;\n workspaceId: string | undefined;\n}) {\n return (\n <CallbackStatusShell\n title={failure.title ?? 'Linear install could not be completed'}\n message={failure.message}\n startOver={failure.startOver}\n switchAccount={failure.signIn}\n workspaceId={workspaceId}\n installPath=\"/workspaces/$wid/integrations/linear\"\n />\n );\n}\n"],"names":["useRefreshAuth","useRouteSearch","createSingleFlight","sessionStorageOrUndefined","FullPageLoader","toast","useNavigate","useEffect","useMemo","useState","useCompleteIntegrationCallback","CallbackStatusShell","useCompleteLinearCallbackMutation","classifyLinearCallbackError","clearLinearInstallWorkspace","parseLinearCallbackQuery","readLinearInstallWorkspace","serializeLinearCallbackQuery","callbackRequests","maxTerminalResults","toastedCallbacks","Set","LinearCallbackPage","navigate","refreshAuth","completeIntegrationCallback","mutateAsync","completeLinearCallback","params","workspaceId","failure","setFailure","disposed","key","request","run","input","complete","query","token","then","connection","has","add","success","to","wid","replace","error","LinearCallbackFailurePage","title","message","startOver","signIn","aria-label","switchAccount","installPath"],"mappings":";AAAA,SAAQA,cAAc,QAAO,uBAAuB;AACpD,SAAQC,cAAc,QAAO,gCAAgC;AAC7D,SAAQC,kBAAkB,EAAEC,yBAAyB,QAAO,qBAAqB;AACjF,SAAQC,cAAc,QAAO,2BAA2B;AACxD,SAAQC,KAAK,QAAO,0BAA0B;AAC9C,SAAQC,WAAW,QAAO,yBAAyB;AACnD,SAAQC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAO,QAAQ;AACnD,SAAQC,8BAA8B,QAAO,gDAAgD;AAC7F,SAAQC,mBAAmB,QAAO,uCAAuC;AAEzE,SAAQC,iCAAiC,QAAO,6BAA6B;AAC7E,SACEC,2BAA2B,EAC3BC,2BAA2B,EAE3BC,wBAAwB,EACxBC,0BAA0B,EAC1BC,4BAA4B,QACvB,sBAAsB;AAE7B,8EAA8E;AAC9E,iEAAiE;AACjE,MAAMC,mBAAmBhB,mBAAkD;IACzEiB,oBAAoB;AACtB;AACA,4EAA4E;AAC5E,6EAA6E;AAC7E,MAAMC,mBAAmB,IAAIC;AAE7B,OAAO,SAASC;IACd,MAAMC,WAAWjB;IACjB,MAAMkB,cAAcxB;IACpB,MAAMyB,8BAA8Bf;IACpC,MAAM,EAACgB,aAAaC,sBAAsB,EAAC,GAAGf;IAC9C,MAAMgB,SAAS3B,eAAec;IAC9B,MAAMc,cAAcrB,QAAQ,IAAMQ,2BAA2Bb,8BAA8B,EAAE;IAC7F,MAAM,CAAC2B,SAASC,WAAW,GAAGtB;IAC9BF,UAAU;QACR,IAAI,CAACqB,QAAQ;QAEb,IAAII,WAAW;QACf,MAAMC,MAAMhB,6BAA6BW;QACzC,MAAMM,UAAUhB,iBAAiBiB,GAAG,CAClCF,KACA,UACE,MAAMR,4BAA4B;gBAChCW,OAAOR;gBACPJ;gBACAa,UAAU,OAAOC,OAAOC,QAAU,MAAMZ,uBAAuB;wBAACW;wBAAOC;oBAAK;YAC9E;QAGJL,QAAQM,IAAI,CACV,OAAOC;YACL,IAAIT,UAAU;YACd,IAAI;gBACFlB,4BAA4BX;YAC9B,EAAE,OAAM;YACN,0EAA0E;YAC5E;YACA,IAAI6B,UAAU;YACd,IAAI,CAACZ,iBAAiBsB,GAAG,CAACT,MAAM;gBAC9Bb,iBAAiBuB,GAAG,CAACV;gBACrB5B,MAAMuC,OAAO,CAAC;YAChB;YACA,IAAI;gBACF,MAAMrB,SAAS;oBACbsB,IAAI;oBACJjB,QAAQ;wBAACkB,KAAKL,WAAWZ,WAAW;oBAAA;oBACpCkB,SAAS;gBACX;YACF,EAAE,OAAM;YACN,gFAAgF;YAClF;QACF,GACA,CAACC;YACC,IAAIhB,UAAU;YACdD,WAAWlB,4BAA4BmC;QACzC;QAGF,OAAO;YACLhB,WAAW;QACb;IACF,GAAG;QAACP;QAA6BE;QAAwBJ;QAAUK;QAAQJ;KAAY;IAEvF,IAAI,CAACI,QAAQ;QACX,qBACE,KAACqB;YACCnB,SAAS;gBACPoB,OAAO;gBACPC,SAAS;gBACTC,WAAW;gBACXC,QAAQ;YACV;YACAxB,aAAaA;;IAGnB;IAEA,IAAI,CAACC,SAAS,qBAAO,KAAC1B;QAAekD,cAAW;;IAEhD,qBAAO,KAACL;QAA0BnB,SAASA;QAASD,aAAaA;;AACnE;AAEA,SAASoB,0BAA0B,EACjCnB,OAAO,EACPD,WAAW,EAIZ;IACC,qBACE,KAAClB;QACCuC,OAAOpB,QAAQoB,KAAK,IAAI;QACxBC,SAASrB,QAAQqB,OAAO;QACxBC,WAAWtB,QAAQsB,SAAS;QAC5BG,eAAezB,QAAQuB,MAAM;QAC7BxB,aAAaA;QACb2B,aAAY;;AAGlB"}
|
|
1
|
+
{"version":3,"sources":["../../src/pages/linear-callback-page.tsx"],"sourcesContent":["import {useAuthState, useRefreshAuth} from '@shipfox/client-auth';\nimport {useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {createSingleFlight, sessionStorageOrUndefined} from '@shipfox/client-ui';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {useNavigate} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {useCompleteIntegrationCallback} from '#application/complete-integration-callback.js';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport type {IntegrationConnection} from '#core/models.js';\nimport {useCompleteLinearCallbackMutation} from '#hooks/api/integrations.js';\nimport {\n classifyLinearCallbackError,\n clearLinearInstallWorkspace,\n type LinearCallbackFailure,\n parseLinearCallbackQuery,\n readLinearInstallWorkspace,\n serializeLinearCallbackQuery,\n} from '#linear-callback.js';\nimport {rememberCallbackKey, resolveWorkspaceSlug} from '#workspace-navigation.js';\n\n// Retain only recent completions: this bounds long-lived callback pages while\n// still covering StrictMode and immediate Back/Forward remounts.\nconst callbackRequests = createSingleFlight<string, IntegrationConnection>({\n maxTerminalResults: 32,\n});\nconst completedCallbacks = new Set<string>();\n// Keeps the success toast firing once per distinct callback even though the\n// effect re-runs against the cached request as the mutation identity churns.\nconst toastedCallbacks = new Set<string>();\n\nexport function LinearCallbackPage() {\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n const refreshAuth = useRefreshAuth();\n const completeIntegrationCallback = useCompleteIntegrationCallback();\n const {mutateAsync: completeLinearCallback} = useCompleteLinearCallbackMutation();\n const {workspaces, isLoading} = useAuthState();\n const params = useRouteSearch(parseLinearCallbackQuery);\n const workspaceId = useMemo(() => readLinearInstallWorkspace(sessionStorageOrUndefined()), []);\n const [failure, setFailure] = useState<LinearCallbackFailure | undefined>();\n const [completedWorkspace, setCompletedWorkspace] = useState<{\n slug?: string | undefined;\n }>();\n useEffect(() => {\n if (!params || isLoading) return;\n\n let disposed = false;\n const key = serializeLinearCallbackQuery(params);\n const request = callbackRequests.run(\n key,\n async () =>\n await completeIntegrationCallback({\n input: params,\n refreshAuth,\n complete: async (query, token) => await completeLinearCallback({query, token}),\n }),\n );\n\n request.then(\n async (connection) => {\n if (disposed) return;\n if (completedCallbacks.has(key)) {\n const workspaceSlug = await resolveWorkspaceSlug({\n workspaceId: connection.workspaceId,\n fallbackWorkspaces: workspaces,\n queryClient,\n });\n if (!disposed) setCompletedWorkspace(workspaceSlug ? {slug: workspaceSlug} : {});\n return;\n }\n rememberCallbackKey(completedCallbacks, key);\n try {\n clearLinearInstallWorkspace(sessionStorageOrUndefined());\n } catch {\n // The successful API response remains the source of truth for navigation.\n }\n if (disposed) return;\n if (!toastedCallbacks.has(key)) {\n rememberCallbackKey(toastedCallbacks, key);\n toast.success('Linear installed.');\n }\n let workspaceSlug: string | undefined;\n try {\n if (disposed) return;\n workspaceSlug = await resolveWorkspaceSlug({\n workspaceId: connection.workspaceId,\n fallbackWorkspaces: workspaces,\n queryClient,\n });\n if (disposed) return;\n if (!workspaceSlug) {\n setCompletedWorkspace({});\n return;\n }\n setCompletedWorkspace({slug: workspaceSlug});\n await navigate({\n to: '/w/$workspaceSlug/settings/integrations',\n params: {workspaceSlug},\n replace: true,\n });\n } catch {\n // Keep the completed callback page visible if client navigation is interrupted.\n if (!disposed) setCompletedWorkspace({slug: workspaceSlug});\n }\n },\n (error: unknown) => {\n if (disposed) return;\n setFailure(classifyLinearCallbackError(error));\n },\n );\n\n return () => {\n disposed = true;\n };\n }, [\n completeIntegrationCallback,\n completeLinearCallback,\n isLoading,\n navigate,\n params,\n queryClient,\n refreshAuth,\n workspaces,\n ]);\n\n if (!params) {\n return (\n <LinearCallbackFailurePage\n failure={{\n title: 'Invalid Linear callback',\n message: 'Invalid Linear callback. Start the install again from workspace settings.',\n startOver: true,\n signIn: false,\n }}\n workspaceSlug={workspaces.find(({id}) => id === workspaceId)?.slug}\n />\n );\n }\n\n if (completedWorkspace)\n return (\n <CallbackStatusShell\n title=\"Linear connected\"\n status=\"success\"\n message={\n completedWorkspace.slug\n ? 'Linear is connected. Continue in integrations settings.'\n : 'Linear is connected. Return to Shipfox to continue.'\n }\n workspaceSlug={completedWorkspace.slug}\n installPath=\"/w/$workspaceSlug/integrations/linear\"\n />\n );\n\n if (!failure) return <FullPageLoader aria-label=\"Connecting Linear\" />;\n\n return (\n <LinearCallbackFailurePage\n failure={failure}\n workspaceSlug={workspaces.find(({id}) => id === workspaceId)?.slug}\n />\n );\n}\n\nfunction LinearCallbackFailurePage({\n failure,\n workspaceSlug,\n}: {\n failure: LinearCallbackFailure;\n workspaceSlug: string | undefined;\n}) {\n return (\n <CallbackStatusShell\n title={failure.title ?? 'Linear install could not be completed'}\n message={failure.message}\n startOver={failure.startOver}\n switchAccount={failure.signIn}\n workspaceSlug={workspaceSlug}\n installPath=\"/w/$workspaceSlug/integrations/linear\"\n />\n );\n}\n"],"names":["useAuthState","useRefreshAuth","useRouteSearch","createSingleFlight","sessionStorageOrUndefined","FullPageLoader","toast","useQueryClient","useNavigate","useEffect","useMemo","useState","useCompleteIntegrationCallback","CallbackStatusShell","useCompleteLinearCallbackMutation","classifyLinearCallbackError","clearLinearInstallWorkspace","parseLinearCallbackQuery","readLinearInstallWorkspace","serializeLinearCallbackQuery","rememberCallbackKey","resolveWorkspaceSlug","callbackRequests","maxTerminalResults","completedCallbacks","Set","toastedCallbacks","LinearCallbackPage","navigate","queryClient","refreshAuth","completeIntegrationCallback","mutateAsync","completeLinearCallback","workspaces","isLoading","params","workspaceId","failure","setFailure","completedWorkspace","setCompletedWorkspace","disposed","key","request","run","input","complete","query","token","then","connection","has","workspaceSlug","fallbackWorkspaces","slug","success","to","replace","error","LinearCallbackFailurePage","title","message","startOver","signIn","find","id","status","installPath","aria-label","switchAccount"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentry-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/sentry-callback-page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sentry-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/sentry-callback-page.tsx"],"names":[],"mappings":"AA2BA,wBAAgB,kBAAkB,uCAyNjC"}
|
|
@@ -8,14 +8,17 @@ import { Card } from '@shipfox/react-ui/card';
|
|
|
8
8
|
import { FullPageLoader } from '@shipfox/react-ui/loader';
|
|
9
9
|
import { toast } from '@shipfox/react-ui/toast';
|
|
10
10
|
import { Header, Text } from '@shipfox/react-ui/typography';
|
|
11
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
11
12
|
import { Link, useNavigate } from '@tanstack/react-router';
|
|
12
13
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13
14
|
import { useCompleteIntegrationCallback } from '#application/complete-integration-callback.js';
|
|
14
15
|
import { connectSentry } from '#hooks/api/integrations.js';
|
|
15
16
|
import { classifySentryConnectError, clearSentryInstallWorkspace, parseSentryCallbackParams, preselectSentryWorkspace, readSentryInstallWorkspace } from '#sentry-callback.js';
|
|
17
|
+
import { resolveWorkspaceSlug } from '#workspace-navigation.js';
|
|
16
18
|
const connectRequests = createSingleFlight();
|
|
17
19
|
export function SentryCallbackPage() {
|
|
18
20
|
const navigate = useNavigate();
|
|
21
|
+
const queryClient = useQueryClient();
|
|
19
22
|
const refreshAuth = useRefreshAuth();
|
|
20
23
|
const { workspaces, isLoading } = useAuthState();
|
|
21
24
|
const completeIntegrationCallback = useCompleteIntegrationCallback();
|
|
@@ -109,12 +112,21 @@ export function SentryCallbackPage() {
|
|
|
109
112
|
clearSentryInstallWorkspace(sessionStorageOrUndefined());
|
|
110
113
|
if (disposedRef.current) return;
|
|
111
114
|
toast.success('Sentry installed.');
|
|
112
|
-
await
|
|
113
|
-
|
|
115
|
+
const workspaceSlug = await resolveWorkspaceSlug({
|
|
116
|
+
workspaceId,
|
|
117
|
+
fallbackWorkspaces: workspaces,
|
|
118
|
+
queryClient
|
|
119
|
+
});
|
|
120
|
+
if (disposedRef.current) return;
|
|
121
|
+
await navigate(workspaceSlug ? {
|
|
122
|
+
to: '/w/$workspaceSlug/settings/integrations',
|
|
114
123
|
params: {
|
|
115
|
-
|
|
124
|
+
workspaceSlug
|
|
116
125
|
},
|
|
117
126
|
replace: true
|
|
127
|
+
} : {
|
|
128
|
+
to: '/',
|
|
129
|
+
replace: true
|
|
118
130
|
});
|
|
119
131
|
}).catch((error)=>{
|
|
120
132
|
if (disposedRef.current) return;
|
|
@@ -136,6 +148,7 @@ export function SentryCallbackPage() {
|
|
|
136
148
|
...workspaces.filter((workspace)=>workspace.id !== preselectedId)
|
|
137
149
|
] : workspaces;
|
|
138
150
|
const failureWorkspaceId = failure?.workspaceId ?? preselectedId ?? workspaces[0]?.id;
|
|
151
|
+
const failureWorkspace = failureWorkspaceId ? workspaces.find(({ id })=>id === failureWorkspaceId) : undefined;
|
|
139
152
|
return /*#__PURE__*/ _jsxs(CallbackColumn, {
|
|
140
153
|
children: [
|
|
141
154
|
/*#__PURE__*/ _jsxs("header", {
|
|
@@ -170,15 +183,15 @@ export function SentryCallbackPage() {
|
|
|
170
183
|
onClick: ()=>connect(failure.workspaceId),
|
|
171
184
|
children: "Retry"
|
|
172
185
|
}) : null,
|
|
173
|
-
failure.failure.kind === 'terminal' && failure.failure.startOver ? /*#__PURE__*/ _jsx(Button, {
|
|
186
|
+
failure.failure.kind === 'terminal' && failure.failure.startOver && failureWorkspace ? /*#__PURE__*/ _jsx(Button, {
|
|
174
187
|
asChild: true,
|
|
175
188
|
size: "sm",
|
|
176
189
|
variant: "secondary",
|
|
177
190
|
className: "w-fit",
|
|
178
191
|
children: /*#__PURE__*/ _jsx(Link, {
|
|
179
|
-
to: "/
|
|
192
|
+
to: "/w/$workspaceSlug/integrations/sentry",
|
|
180
193
|
params: {
|
|
181
|
-
|
|
194
|
+
workspaceSlug: failureWorkspace.slug
|
|
182
195
|
},
|
|
183
196
|
children: "Start over"
|
|
184
197
|
})
|
|
@@ -211,14 +224,14 @@ export function SentryCallbackPage() {
|
|
|
211
224
|
})
|
|
212
225
|
}, workspace.id))
|
|
213
226
|
}),
|
|
214
|
-
|
|
227
|
+
failureWorkspace ? /*#__PURE__*/ _jsx(ButtonLink, {
|
|
215
228
|
asChild: true,
|
|
216
229
|
variant: "muted",
|
|
217
230
|
className: "w-fit",
|
|
218
231
|
children: /*#__PURE__*/ _jsx(Link, {
|
|
219
|
-
to: "/
|
|
232
|
+
to: "/w/$workspaceSlug/settings/integrations",
|
|
220
233
|
params: {
|
|
221
|
-
|
|
234
|
+
workspaceSlug: failureWorkspace.slug
|
|
222
235
|
},
|
|
223
236
|
children: "Back to settings"
|
|
224
237
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/sentry-callback-page.tsx"],"sourcesContent":["import {useAuthState, useRefreshAuth} from '@shipfox/client-auth';\nimport {useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {createSingleFlight, sessionStorageOrUndefined} from '@shipfox/client-ui';\nimport {Button, ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {Link, useNavigate} from '@tanstack/react-router';\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {useCompleteIntegrationCallback} from '#application/complete-integration-callback.js';\nimport type {IntegrationConnection} from '#core/models.js';\nimport {connectSentry} from '#hooks/api/integrations.js';\nimport {\n classifySentryConnectError,\n clearSentryInstallWorkspace,\n parseSentryCallbackParams,\n preselectSentryWorkspace,\n readSentryInstallWorkspace,\n type SentryConnectFailure,\n} from '#sentry-callback.js';\n\nconst connectRequests = createSingleFlight<string, IntegrationConnection>();\n\nexport function SentryCallbackPage() {\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const {workspaces, isLoading} = useAuthState();\n const completeIntegrationCallback = useCompleteIntegrationCallback();\n\n const params = useRouteSearch(parseSentryCallbackParams);\n const storedWorkspaceId = useMemo(\n () => readSentryInstallWorkspace(sessionStorageOrUndefined()),\n [],\n );\n const preselection = preselectSentryWorkspace(storedWorkspaceId, workspaces);\n const preselectedId = preselection.kind === 'pick' ? preselection.preselectedId : undefined;\n\n const [connectingId, setConnectingId] = useState<string | undefined>();\n const [failure, setFailure] = useState<\n {workspaceId: string; failure: SentryConnectFailure} | undefined\n >();\n const [retryLocked, setRetryLocked] = useState(false);\n const disposedRef = useRef(false);\n\n useEffect(() => {\n disposedRef.current = false;\n return () => {\n disposedRef.current = true;\n };\n }, []);\n\n // Wait for auth before treating an empty workspace list as \"no workspace\": on\n // a cold return from Sentry, `workspaces` is [] until the session refresh\n // resolves, and bouncing to `/` then would discard the one-shot grant code.\n const noWorkspace = !isLoading && preselection.kind === 'none';\n useEffect(() => {\n if (!noWorkspace) return;\n toast.error('You need a workspace before installing Sentry.');\n navigate({to: '/', replace: true});\n }, [noWorkspace, navigate]);\n\n const retryAfterSeconds =\n failure?.failure.kind === 'retryable' ? failure.failure.retryAfterSeconds : undefined;\n useEffect(() => {\n // No backoff window (any non-rate-limited failure) re-enables Retry, so a\n // prior rate-limit lock can never strand the button in a disabled state.\n if (!retryAfterSeconds) {\n setRetryLocked(false);\n return;\n }\n setRetryLocked(true);\n const timer = setTimeout(() => setRetryLocked(false), retryAfterSeconds * 1000);\n return () => clearTimeout(timer);\n }, [retryAfterSeconds]);\n\n if (!params) {\n return (\n <CallbackColumn>\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">\n This Sentry link is missing required parameters. Start the install again from your\n workspace settings.\n </Text>\n </Callout>\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link to=\"/\">Back to Shipfox</Link>\n </ButtonLink>\n </CallbackColumn>\n );\n }\n\n // Hold the loader until auth resolves; the workspace list and the\n // noWorkspace redirect both depend on a settled `workspaces`.\n if (isLoading) return <FullPageLoader />;\n\n if (noWorkspace) return null;\n\n const connect = (workspaceId: string) => {\n setConnectingId(workspaceId);\n setFailure(undefined);\n const key = `${params.installationId}|${workspaceId}|${params.code}`;\n const request = connectRequests.run(\n key,\n async () =>\n await completeIntegrationCallback({\n input: workspaceId,\n refreshAuth,\n complete: async (selectedWorkspaceId, token) =>\n await connectSentry({\n body: {\n workspace_id: selectedWorkspaceId,\n code: params.code,\n installation_id: params.installationId,\n },\n token,\n }),\n }),\n );\n\n request\n .then(async () => {\n clearSentryInstallWorkspace(sessionStorageOrUndefined());\n if (disposedRef.current) return;\n toast.success('Sentry installed.');\n await navigate({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: workspaceId},\n replace: true,\n });\n })\n .catch((error: unknown) => {\n if (disposedRef.current) return;\n const classified = classifySentryConnectError(error);\n if (classified.kind === 'terminal') {\n // Only a fresh install (new grant code) can recover; the stored\n // handoff has served its purpose either way.\n clearSentryInstallWorkspace(sessionStorageOrUndefined());\n }\n setConnectingId(undefined);\n setFailure({workspaceId, failure: classified});\n });\n };\n\n const orderedWorkspaces = preselectedId\n ? [\n ...workspaces.filter((workspace) => workspace.id === preselectedId),\n ...workspaces.filter((workspace) => workspace.id !== preselectedId),\n ]\n : workspaces;\n\n const failureWorkspaceId = failure?.workspaceId ?? preselectedId ?? workspaces[0]?.id;\n\n return (\n <CallbackColumn>\n <header className=\"flex flex-col gap-8\">\n <Header variant=\"h2\">Install Sentry</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n {params.orgSlug\n ? `Install the Sentry org \"${params.orgSlug}\" in a workspace.`\n : 'Install this Sentry integration in a workspace.'}\n </Text>\n </header>\n\n {failure ? (\n <Callout role=\"alert\" type=\"error\">\n <div className=\"flex flex-col gap-8\">\n <Text size=\"sm\">{failure.failure.message}</Text>\n {failure.failure.kind === 'retryable' ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n disabled={retryLocked || connectingId !== undefined}\n onClick={() => connect(failure.workspaceId)}\n >\n Retry\n </Button>\n ) : null}\n {failure.failure.kind === 'terminal' && failure.failure.startOver ? (\n <Button asChild size=\"sm\" variant=\"secondary\" className=\"w-fit\">\n <Link to=\"/workspaces/$wid/integrations/sentry\" params={{wid: failure.workspaceId}}>\n Start over\n </Link>\n </Button>\n ) : null}\n </div>\n </Callout>\n ) : null}\n\n <section className=\"flex flex-col gap-8\" aria-label=\"Choose a workspace\">\n {orderedWorkspaces.map((workspace) => (\n <Card key={workspace.id} className=\"p-16\">\n <div className=\"flex items-center justify-between gap-12\">\n <Text size=\"md\" bold className=\"truncate\">\n {workspace.name}\n </Text>\n <Button\n variant=\"secondary\"\n disabled={connectingId !== undefined}\n isLoading={connectingId === workspace.id}\n onClick={() => connect(workspace.id)}\n >\n Install\n </Button>\n </div>\n </Card>\n ))}\n </section>\n\n {failureWorkspaceId ? (\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link to=\"/workspaces/$wid/settings/integrations\" params={{wid: failureWorkspaceId}}>\n Back to settings\n </Link>\n </ButtonLink>\n ) : null}\n </CallbackColumn>\n );\n}\n\nfunction CallbackColumn({children}: {children: React.ReactNode}) {\n return (\n <main className=\"flex min-h-screen bg-background-subtle-base px-16 py-32\">\n <div className=\"mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20\">\n {children}\n </div>\n </main>\n );\n}\n"],"names":["useAuthState","useRefreshAuth","useRouteSearch","createSingleFlight","sessionStorageOrUndefined","Button","ButtonLink","Callout","Card","FullPageLoader","toast","Header","Text","Link","useNavigate","useEffect","useMemo","useRef","useState","useCompleteIntegrationCallback","connectSentry","classifySentryConnectError","clearSentryInstallWorkspace","parseSentryCallbackParams","preselectSentryWorkspace","readSentryInstallWorkspace","connectRequests","SentryCallbackPage","navigate","refreshAuth","workspaces","isLoading","completeIntegrationCallback","params","storedWorkspaceId","preselection","preselectedId","kind","undefined","connectingId","setConnectingId","failure","setFailure","retryLocked","setRetryLocked","disposedRef","current","noWorkspace","error","to","replace","retryAfterSeconds","timer","setTimeout","clearTimeout","CallbackColumn","role","type","size","asChild","variant","className","connect","workspaceId","key","installationId","code","request","run","input","complete","selectedWorkspaceId","token","body","workspace_id","installation_id","then","success","wid","catch","classified","orderedWorkspaces","filter","workspace","id","failureWorkspaceId","header","orgSlug","div","message","disabled","onClick","startOver","section","aria-label","map","bold","name","children","main"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/pages/sentry-callback-page.tsx"],"sourcesContent":["import {useAuthState, useRefreshAuth} from '@shipfox/client-auth';\nimport {useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {createSingleFlight, sessionStorageOrUndefined} from '@shipfox/client-ui';\nimport {Button, ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {Link, useNavigate} from '@tanstack/react-router';\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {useCompleteIntegrationCallback} from '#application/complete-integration-callback.js';\nimport type {IntegrationConnection} from '#core/models.js';\nimport {connectSentry} from '#hooks/api/integrations.js';\nimport {\n classifySentryConnectError,\n clearSentryInstallWorkspace,\n parseSentryCallbackParams,\n preselectSentryWorkspace,\n readSentryInstallWorkspace,\n type SentryConnectFailure,\n} from '#sentry-callback.js';\nimport {resolveWorkspaceSlug} from '#workspace-navigation.js';\n\nconst connectRequests = createSingleFlight<string, IntegrationConnection>();\n\nexport function SentryCallbackPage() {\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n const refreshAuth = useRefreshAuth();\n const {workspaces, isLoading} = useAuthState();\n const completeIntegrationCallback = useCompleteIntegrationCallback();\n\n const params = useRouteSearch(parseSentryCallbackParams);\n const storedWorkspaceId = useMemo(\n () => readSentryInstallWorkspace(sessionStorageOrUndefined()),\n [],\n );\n const preselection = preselectSentryWorkspace(storedWorkspaceId, workspaces);\n const preselectedId = preselection.kind === 'pick' ? preselection.preselectedId : undefined;\n\n const [connectingId, setConnectingId] = useState<string | undefined>();\n const [failure, setFailure] = useState<\n {workspaceId: string; failure: SentryConnectFailure} | undefined\n >();\n const [retryLocked, setRetryLocked] = useState(false);\n const disposedRef = useRef(false);\n\n useEffect(() => {\n disposedRef.current = false;\n return () => {\n disposedRef.current = true;\n };\n }, []);\n\n // Wait for auth before treating an empty workspace list as \"no workspace\": on\n // a cold return from Sentry, `workspaces` is [] until the session refresh\n // resolves, and bouncing to `/` then would discard the one-shot grant code.\n const noWorkspace = !isLoading && preselection.kind === 'none';\n useEffect(() => {\n if (!noWorkspace) return;\n toast.error('You need a workspace before installing Sentry.');\n navigate({to: '/', replace: true});\n }, [noWorkspace, navigate]);\n\n const retryAfterSeconds =\n failure?.failure.kind === 'retryable' ? failure.failure.retryAfterSeconds : undefined;\n useEffect(() => {\n // No backoff window (any non-rate-limited failure) re-enables Retry, so a\n // prior rate-limit lock can never strand the button in a disabled state.\n if (!retryAfterSeconds) {\n setRetryLocked(false);\n return;\n }\n setRetryLocked(true);\n const timer = setTimeout(() => setRetryLocked(false), retryAfterSeconds * 1000);\n return () => clearTimeout(timer);\n }, [retryAfterSeconds]);\n\n if (!params) {\n return (\n <CallbackColumn>\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">\n This Sentry link is missing required parameters. Start the install again from your\n workspace settings.\n </Text>\n </Callout>\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link to=\"/\">Back to Shipfox</Link>\n </ButtonLink>\n </CallbackColumn>\n );\n }\n\n // Hold the loader until auth resolves; the workspace list and the\n // noWorkspace redirect both depend on a settled `workspaces`.\n if (isLoading) return <FullPageLoader />;\n\n if (noWorkspace) return null;\n\n const connect = (workspaceId: string) => {\n setConnectingId(workspaceId);\n setFailure(undefined);\n const key = `${params.installationId}|${workspaceId}|${params.code}`;\n const request = connectRequests.run(\n key,\n async () =>\n await completeIntegrationCallback({\n input: workspaceId,\n refreshAuth,\n complete: async (selectedWorkspaceId, token) =>\n await connectSentry({\n body: {\n workspace_id: selectedWorkspaceId,\n code: params.code,\n installation_id: params.installationId,\n },\n token,\n }),\n }),\n );\n\n request\n .then(async () => {\n clearSentryInstallWorkspace(sessionStorageOrUndefined());\n if (disposedRef.current) return;\n toast.success('Sentry installed.');\n const workspaceSlug = await resolveWorkspaceSlug({\n workspaceId,\n fallbackWorkspaces: workspaces,\n queryClient,\n });\n if (disposedRef.current) return;\n await navigate(\n workspaceSlug\n ? {\n to: '/w/$workspaceSlug/settings/integrations',\n params: {workspaceSlug},\n replace: true,\n }\n : {to: '/', replace: true},\n );\n })\n .catch((error: unknown) => {\n if (disposedRef.current) return;\n const classified = classifySentryConnectError(error);\n if (classified.kind === 'terminal') {\n // Only a fresh install (new grant code) can recover; the stored\n // handoff has served its purpose either way.\n clearSentryInstallWorkspace(sessionStorageOrUndefined());\n }\n setConnectingId(undefined);\n setFailure({workspaceId, failure: classified});\n });\n };\n\n const orderedWorkspaces = preselectedId\n ? [\n ...workspaces.filter((workspace) => workspace.id === preselectedId),\n ...workspaces.filter((workspace) => workspace.id !== preselectedId),\n ]\n : workspaces;\n\n const failureWorkspaceId = failure?.workspaceId ?? preselectedId ?? workspaces[0]?.id;\n const failureWorkspace = failureWorkspaceId\n ? workspaces.find(({id}) => id === failureWorkspaceId)\n : undefined;\n\n return (\n <CallbackColumn>\n <header className=\"flex flex-col gap-8\">\n <Header variant=\"h2\">Install Sentry</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n {params.orgSlug\n ? `Install the Sentry org \"${params.orgSlug}\" in a workspace.`\n : 'Install this Sentry integration in a workspace.'}\n </Text>\n </header>\n\n {failure ? (\n <Callout role=\"alert\" type=\"error\">\n <div className=\"flex flex-col gap-8\">\n <Text size=\"sm\">{failure.failure.message}</Text>\n {failure.failure.kind === 'retryable' ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n disabled={retryLocked || connectingId !== undefined}\n onClick={() => connect(failure.workspaceId)}\n >\n Retry\n </Button>\n ) : null}\n {failure.failure.kind === 'terminal' &&\n failure.failure.startOver &&\n failureWorkspace ? (\n <Button asChild size=\"sm\" variant=\"secondary\" className=\"w-fit\">\n <Link\n to=\"/w/$workspaceSlug/integrations/sentry\"\n params={{workspaceSlug: failureWorkspace.slug}}\n >\n Start over\n </Link>\n </Button>\n ) : null}\n </div>\n </Callout>\n ) : null}\n\n <section className=\"flex flex-col gap-8\" aria-label=\"Choose a workspace\">\n {orderedWorkspaces.map((workspace) => (\n <Card key={workspace.id} className=\"p-16\">\n <div className=\"flex items-center justify-between gap-12\">\n <Text size=\"md\" bold className=\"truncate\">\n {workspace.name}\n </Text>\n <Button\n variant=\"secondary\"\n disabled={connectingId !== undefined}\n isLoading={connectingId === workspace.id}\n onClick={() => connect(workspace.id)}\n >\n Install\n </Button>\n </div>\n </Card>\n ))}\n </section>\n\n {failureWorkspace ? (\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link\n to=\"/w/$workspaceSlug/settings/integrations\"\n params={{workspaceSlug: failureWorkspace.slug}}\n >\n Back to settings\n </Link>\n </ButtonLink>\n ) : null}\n </CallbackColumn>\n );\n}\n\nfunction CallbackColumn({children}: {children: React.ReactNode}) {\n return (\n <main className=\"flex min-h-screen bg-background-subtle-base px-16 py-32\">\n <div className=\"mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20\">\n {children}\n </div>\n </main>\n );\n}\n"],"names":["useAuthState","useRefreshAuth","useRouteSearch","createSingleFlight","sessionStorageOrUndefined","Button","ButtonLink","Callout","Card","FullPageLoader","toast","Header","Text","useQueryClient","Link","useNavigate","useEffect","useMemo","useRef","useState","useCompleteIntegrationCallback","connectSentry","classifySentryConnectError","clearSentryInstallWorkspace","parseSentryCallbackParams","preselectSentryWorkspace","readSentryInstallWorkspace","resolveWorkspaceSlug","connectRequests","SentryCallbackPage","navigate","queryClient","refreshAuth","workspaces","isLoading","completeIntegrationCallback","params","storedWorkspaceId","preselection","preselectedId","kind","undefined","connectingId","setConnectingId","failure","setFailure","retryLocked","setRetryLocked","disposedRef","current","noWorkspace","error","to","replace","retryAfterSeconds","timer","setTimeout","clearTimeout","CallbackColumn","role","type","size","asChild","variant","className","connect","workspaceId","key","installationId","code","request","run","input","complete","selectedWorkspaceId","token","body","workspace_id","installation_id","then","success","workspaceSlug","fallbackWorkspaces","catch","classified","orderedWorkspaces","filter","workspace","id","failureWorkspaceId","failureWorkspace","find","header","orgSlug","div","message","disabled","onClick","startOver","slug","section","aria-label","map","bold","name","children","main"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/slack-callback-page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"slack-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/slack-callback-page.tsx"],"names":[],"mappings":"AA8BA,wBAAgB,iBAAiB,gCA6HhC"}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useRefreshAuth } from '@shipfox/client-auth';
|
|
2
|
+
import { useAuthState, useRefreshAuth } from '@shipfox/client-auth';
|
|
3
3
|
import { useRouteSearch } from '@shipfox/client-shell/runtime';
|
|
4
4
|
import { createSingleFlight, sessionStorageOrUndefined } from '@shipfox/client-ui';
|
|
5
5
|
import { FullPageLoader } from '@shipfox/react-ui/loader';
|
|
6
6
|
import { toast } from '@shipfox/react-ui/toast';
|
|
7
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
7
8
|
import { useNavigate } from '@tanstack/react-router';
|
|
8
9
|
import { useEffect, useMemo, useState } from 'react';
|
|
9
10
|
import { useCompleteIntegrationCallback } from '#application/complete-integration-callback.js';
|
|
10
11
|
import { CallbackStatusShell } from '#components/callback-status-shell.js';
|
|
11
12
|
import { useCompleteSlackCallbackMutation } from '#hooks/api/integrations.js';
|
|
12
13
|
import { classifySlackCallbackError, clearSlackInstallWorkspace, parseSlackCallbackQuery, readSlackInstallWorkspace, serializeSlackCallbackQuery } from '#slack-callback.js';
|
|
14
|
+
import { rememberCallbackKey, resolveWorkspaceSlug } from '#workspace-navigation.js';
|
|
13
15
|
// Retain only recent completions: this bounds long-lived callback pages while
|
|
14
16
|
// still covering StrictMode and immediate Back/Forward remounts.
|
|
15
17
|
const callbackRequests = createSingleFlight({
|
|
@@ -19,15 +21,17 @@ const completedCallbacks = new Set();
|
|
|
19
21
|
const toastedCallbacks = new Set();
|
|
20
22
|
export function SlackCallbackPage() {
|
|
21
23
|
const navigate = useNavigate();
|
|
24
|
+
const queryClient = useQueryClient();
|
|
22
25
|
const refreshAuth = useRefreshAuth();
|
|
23
26
|
const completeIntegrationCallback = useCompleteIntegrationCallback();
|
|
24
27
|
const { mutateAsync: completeSlackCallback } = useCompleteSlackCallbackMutation();
|
|
28
|
+
const { workspaces, isLoading } = useAuthState();
|
|
25
29
|
const params = useRouteSearch(parseSlackCallbackQuery);
|
|
26
30
|
const workspaceId = useMemo(()=>readSlackInstallWorkspace(sessionStorageOrUndefined()), []);
|
|
27
31
|
const [failure, setFailure] = useState();
|
|
28
|
-
const [
|
|
32
|
+
const [completedWorkspace, setCompletedWorkspace] = useState();
|
|
29
33
|
useEffect(()=>{
|
|
30
|
-
if (!params) return;
|
|
34
|
+
if (!params || isLoading) return;
|
|
31
35
|
let disposed = false;
|
|
32
36
|
const key = serializeSlackCallbackQuery(params);
|
|
33
37
|
const request = callbackRequests.run(key, async ()=>await completeIntegrationCallback({
|
|
@@ -41,10 +45,17 @@ export function SlackCallbackPage() {
|
|
|
41
45
|
request.then(async (connection)=>{
|
|
42
46
|
if (disposed) return;
|
|
43
47
|
if (completedCallbacks.has(key)) {
|
|
44
|
-
|
|
48
|
+
const workspaceSlug = await resolveWorkspaceSlug({
|
|
49
|
+
workspaceId: connection.workspaceId,
|
|
50
|
+
fallbackWorkspaces: workspaces,
|
|
51
|
+
queryClient
|
|
52
|
+
});
|
|
53
|
+
if (!disposed) setCompletedWorkspace(workspaceSlug ? {
|
|
54
|
+
slug: workspaceSlug
|
|
55
|
+
} : {});
|
|
45
56
|
return;
|
|
46
57
|
}
|
|
47
|
-
completedCallbacks
|
|
58
|
+
rememberCallbackKey(completedCallbacks, key);
|
|
48
59
|
try {
|
|
49
60
|
clearSlackInstallWorkspace(sessionStorageOrUndefined());
|
|
50
61
|
} catch {
|
|
@@ -52,19 +63,36 @@ export function SlackCallbackPage() {
|
|
|
52
63
|
}
|
|
53
64
|
if (disposed) return;
|
|
54
65
|
if (!toastedCallbacks.has(key)) {
|
|
55
|
-
toastedCallbacks
|
|
66
|
+
rememberCallbackKey(toastedCallbacks, key);
|
|
56
67
|
toast.success('Slack installed.');
|
|
57
68
|
}
|
|
69
|
+
let workspaceSlug;
|
|
58
70
|
try {
|
|
71
|
+
if (disposed) return;
|
|
72
|
+
workspaceSlug = await resolveWorkspaceSlug({
|
|
73
|
+
workspaceId: connection.workspaceId,
|
|
74
|
+
fallbackWorkspaces: workspaces,
|
|
75
|
+
queryClient
|
|
76
|
+
});
|
|
77
|
+
if (disposed) return;
|
|
78
|
+
if (!workspaceSlug) {
|
|
79
|
+
setCompletedWorkspace({});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setCompletedWorkspace({
|
|
83
|
+
slug: workspaceSlug
|
|
84
|
+
});
|
|
59
85
|
await navigate({
|
|
60
|
-
to: '/
|
|
86
|
+
to: '/w/$workspaceSlug/settings/integrations',
|
|
61
87
|
params: {
|
|
62
|
-
|
|
88
|
+
workspaceSlug
|
|
63
89
|
},
|
|
64
90
|
replace: true
|
|
65
91
|
});
|
|
66
92
|
} catch {
|
|
67
|
-
if (!disposed)
|
|
93
|
+
if (!disposed) setCompletedWorkspace({
|
|
94
|
+
slug: workspaceSlug
|
|
95
|
+
});
|
|
68
96
|
}
|
|
69
97
|
}, (error)=>{
|
|
70
98
|
if (!disposed) setFailure(classifySlackCallbackError(error));
|
|
@@ -75,28 +103,32 @@ export function SlackCallbackPage() {
|
|
|
75
103
|
}, [
|
|
76
104
|
completeIntegrationCallback,
|
|
77
105
|
completeSlackCallback,
|
|
106
|
+
isLoading,
|
|
78
107
|
navigate,
|
|
79
108
|
params,
|
|
80
|
-
|
|
109
|
+
queryClient,
|
|
110
|
+
refreshAuth,
|
|
111
|
+
workspaces
|
|
81
112
|
]);
|
|
82
113
|
if (!params) return /*#__PURE__*/ _jsx(CallbackStatusShell, {
|
|
83
114
|
title: "Invalid Slack callback",
|
|
84
115
|
message: "This Slack link is missing required parameters. Start the install again from workspace settings.",
|
|
85
116
|
startOver: true,
|
|
86
|
-
|
|
87
|
-
installPath: "/
|
|
117
|
+
workspaceSlug: workspaces.find(({ id })=>id === workspaceId)?.slug,
|
|
118
|
+
installPath: "/w/$workspaceSlug/integrations/slack"
|
|
88
119
|
});
|
|
89
|
-
if (
|
|
120
|
+
if (completedWorkspace) return /*#__PURE__*/ _jsx(CallbackStatusShell, {
|
|
90
121
|
title: "Slack connected",
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
122
|
+
status: "success",
|
|
123
|
+
message: completedWorkspace.slug ? 'Slack is connected. Continue in integrations settings.' : 'Slack is connected. Return to Shipfox to continue.',
|
|
124
|
+
workspaceSlug: completedWorkspace.slug,
|
|
125
|
+
installPath: "/w/$workspaceSlug/integrations/slack"
|
|
94
126
|
});
|
|
95
127
|
if (failure) return /*#__PURE__*/ _jsx(CallbackStatusShell, {
|
|
96
128
|
...failure,
|
|
97
|
-
|
|
129
|
+
workspaceSlug: workspaces.find(({ id })=>id === workspaceId)?.slug,
|
|
98
130
|
switchAccount: failure.signIn,
|
|
99
|
-
installPath: "/
|
|
131
|
+
installPath: "/w/$workspaceSlug/integrations/slack"
|
|
100
132
|
});
|
|
101
133
|
return /*#__PURE__*/ _jsx(FullPageLoader, {
|
|
102
134
|
"aria-label": "Connecting Slack"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/slack-callback-page.tsx"],"sourcesContent":["import {useRefreshAuth} from '@shipfox/client-auth';\nimport {useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {createSingleFlight, sessionStorageOrUndefined} from '@shipfox/client-ui';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useNavigate} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {useCompleteIntegrationCallback} from '#application/complete-integration-callback.js';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport type {IntegrationConnection} from '#core/models.js';\nimport {useCompleteSlackCallbackMutation} from '#hooks/api/integrations.js';\nimport {\n classifySlackCallbackError,\n clearSlackInstallWorkspace,\n parseSlackCallbackQuery,\n readSlackInstallWorkspace,\n type SlackCallbackFailure,\n serializeSlackCallbackQuery,\n} from '#slack-callback.js';\n\n// Retain only recent completions: this bounds long-lived callback pages while\n// still covering StrictMode and immediate Back/Forward remounts.\nconst callbackRequests = createSingleFlight<string, IntegrationConnection>({\n maxTerminalResults: 32,\n});\nconst completedCallbacks = new Set<string>();\nconst toastedCallbacks = new Set<string>();\n\nexport function SlackCallbackPage() {\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const completeIntegrationCallback = useCompleteIntegrationCallback();\n const {mutateAsync: completeSlackCallback} = useCompleteSlackCallbackMutation();\n const params = useRouteSearch(parseSlackCallbackQuery);\n const workspaceId = useMemo(() => readSlackInstallWorkspace(sessionStorageOrUndefined()), []);\n const [failure, setFailure] = useState<SlackCallbackFailure>();\n const [
|
|
1
|
+
{"version":3,"sources":["../../src/pages/slack-callback-page.tsx"],"sourcesContent":["import {useAuthState, useRefreshAuth} from '@shipfox/client-auth';\nimport {useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {createSingleFlight, sessionStorageOrUndefined} from '@shipfox/client-ui';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {useNavigate} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {useCompleteIntegrationCallback} from '#application/complete-integration-callback.js';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport type {IntegrationConnection} from '#core/models.js';\nimport {useCompleteSlackCallbackMutation} from '#hooks/api/integrations.js';\nimport {\n classifySlackCallbackError,\n clearSlackInstallWorkspace,\n parseSlackCallbackQuery,\n readSlackInstallWorkspace,\n type SlackCallbackFailure,\n serializeSlackCallbackQuery,\n} from '#slack-callback.js';\nimport {rememberCallbackKey, resolveWorkspaceSlug} from '#workspace-navigation.js';\n\n// Retain only recent completions: this bounds long-lived callback pages while\n// still covering StrictMode and immediate Back/Forward remounts.\nconst callbackRequests = createSingleFlight<string, IntegrationConnection>({\n maxTerminalResults: 32,\n});\nconst completedCallbacks = new Set<string>();\nconst toastedCallbacks = new Set<string>();\n\nexport function SlackCallbackPage() {\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n const refreshAuth = useRefreshAuth();\n const completeIntegrationCallback = useCompleteIntegrationCallback();\n const {mutateAsync: completeSlackCallback} = useCompleteSlackCallbackMutation();\n const {workspaces, isLoading} = useAuthState();\n const params = useRouteSearch(parseSlackCallbackQuery);\n const workspaceId = useMemo(() => readSlackInstallWorkspace(sessionStorageOrUndefined()), []);\n const [failure, setFailure] = useState<SlackCallbackFailure>();\n const [completedWorkspace, setCompletedWorkspace] = useState<{\n slug?: string | undefined;\n }>();\n\n useEffect(() => {\n if (!params || isLoading) return;\n let disposed = false;\n const key = serializeSlackCallbackQuery(params);\n const request = callbackRequests.run(\n key,\n async () =>\n await completeIntegrationCallback({\n input: params,\n refreshAuth,\n complete: async (query, token) => await completeSlackCallback({query, token}),\n }),\n );\n request.then(\n async (connection) => {\n if (disposed) return;\n if (completedCallbacks.has(key)) {\n const workspaceSlug = await resolveWorkspaceSlug({\n workspaceId: connection.workspaceId,\n fallbackWorkspaces: workspaces,\n queryClient,\n });\n if (!disposed) setCompletedWorkspace(workspaceSlug ? {slug: workspaceSlug} : {});\n return;\n }\n rememberCallbackKey(completedCallbacks, key);\n try {\n clearSlackInstallWorkspace(sessionStorageOrUndefined());\n } catch {\n // The successful API response remains the source of truth.\n }\n if (disposed) return;\n if (!toastedCallbacks.has(key)) {\n rememberCallbackKey(toastedCallbacks, key);\n toast.success('Slack installed.');\n }\n let workspaceSlug: string | undefined;\n try {\n if (disposed) return;\n workspaceSlug = await resolveWorkspaceSlug({\n workspaceId: connection.workspaceId,\n fallbackWorkspaces: workspaces,\n queryClient,\n });\n if (disposed) return;\n if (!workspaceSlug) {\n setCompletedWorkspace({});\n return;\n }\n setCompletedWorkspace({slug: workspaceSlug});\n await navigate({\n to: '/w/$workspaceSlug/settings/integrations',\n params: {workspaceSlug},\n replace: true,\n });\n } catch {\n if (!disposed) setCompletedWorkspace({slug: workspaceSlug});\n }\n },\n (error: unknown) => {\n if (!disposed) setFailure(classifySlackCallbackError(error));\n },\n );\n return () => {\n disposed = true;\n };\n }, [\n completeIntegrationCallback,\n completeSlackCallback,\n isLoading,\n navigate,\n params,\n queryClient,\n refreshAuth,\n workspaces,\n ]);\n\n if (!params)\n return (\n <CallbackStatusShell\n title=\"Invalid Slack callback\"\n message=\"This Slack link is missing required parameters. Start the install again from workspace settings.\"\n startOver\n workspaceSlug={workspaces.find(({id}) => id === workspaceId)?.slug}\n installPath=\"/w/$workspaceSlug/integrations/slack\"\n />\n );\n if (completedWorkspace)\n return (\n <CallbackStatusShell\n title=\"Slack connected\"\n status=\"success\"\n message={\n completedWorkspace.slug\n ? 'Slack is connected. Continue in integrations settings.'\n : 'Slack is connected. Return to Shipfox to continue.'\n }\n workspaceSlug={completedWorkspace.slug}\n installPath=\"/w/$workspaceSlug/integrations/slack\"\n />\n );\n if (failure)\n return (\n <CallbackStatusShell\n {...failure}\n workspaceSlug={workspaces.find(({id}) => id === workspaceId)?.slug}\n switchAccount={failure.signIn}\n installPath=\"/w/$workspaceSlug/integrations/slack\"\n />\n );\n return <FullPageLoader aria-label=\"Connecting Slack\" />;\n}\n"],"names":["useAuthState","useRefreshAuth","useRouteSearch","createSingleFlight","sessionStorageOrUndefined","FullPageLoader","toast","useQueryClient","useNavigate","useEffect","useMemo","useState","useCompleteIntegrationCallback","CallbackStatusShell","useCompleteSlackCallbackMutation","classifySlackCallbackError","clearSlackInstallWorkspace","parseSlackCallbackQuery","readSlackInstallWorkspace","serializeSlackCallbackQuery","rememberCallbackKey","resolveWorkspaceSlug","callbackRequests","maxTerminalResults","completedCallbacks","Set","toastedCallbacks","SlackCallbackPage","navigate","queryClient","refreshAuth","completeIntegrationCallback","mutateAsync","completeSlackCallback","workspaces","isLoading","params","workspaceId","failure","setFailure","completedWorkspace","setCompletedWorkspace","disposed","key","request","run","input","complete","query","token","then","connection","has","workspaceSlug","fallbackWorkspaces","slug","success","to","replace","error","title","message","startOver","find","id","installPath","status","switchAccount","signIn","aria-label"],"mappings":"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"}
|
|
@@ -26,12 +26,12 @@ export function SourceControlOnboardingPage() {
|
|
|
26
26
|
]
|
|
27
27
|
}),
|
|
28
28
|
/*#__PURE__*/ _jsx(ProviderGrid, {
|
|
29
|
+
workspaceSlug: workspace.slug,
|
|
29
30
|
providers: providersQuery.data ?? [],
|
|
30
31
|
isPending: providersQuery.isPending,
|
|
31
32
|
isFetching: providersQuery.isFetching,
|
|
32
33
|
error: providersQuery.isError ? providersQuery.error : undefined,
|
|
33
34
|
onRetry: ()=>void providersQuery.refetch(),
|
|
34
|
-
workspaceId: workspace.id,
|
|
35
35
|
emptyMessage: "Enable at least one source-control provider in the application settings."
|
|
36
36
|
})
|
|
37
37
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/source-control-onboarding-page.tsx"],"sourcesContent":["import {useActiveWorkspace} from '@shipfox/client-auth';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {ProviderGrid} from '#components/provider-grid.js';\nimport {useIntegrationProvidersQuery} from '#hooks/api/integrations.js';\n\nexport function SourceControlOnboardingPage() {\n const workspace = useActiveWorkspace();\n const providersQuery = useIntegrationProvidersQuery({capability: 'source_control'});\n\n return (\n <div className=\"mx-auto flex w-full max-w-[640px] flex-col gap-20\">\n <header className=\"flex flex-col gap-8\">\n <Header variant=\"h1\">Install source control</Header>\n <Text size=\"md\" className=\"text-foreground-neutral-muted\">\n Shipfox needs a source control integration to import your repositories.\n </Text>\n </header>\n\n <ProviderGrid\n providers={providersQuery.data ?? []}\n isPending={providersQuery.isPending}\n isFetching={providersQuery.isFetching}\n error={providersQuery.isError ? providersQuery.error : undefined}\n onRetry={() => void providersQuery.refetch()}\n
|
|
1
|
+
{"version":3,"sources":["../../src/pages/source-control-onboarding-page.tsx"],"sourcesContent":["import {useActiveWorkspace} from '@shipfox/client-auth';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {ProviderGrid} from '#components/provider-grid.js';\nimport {useIntegrationProvidersQuery} from '#hooks/api/integrations.js';\n\nexport function SourceControlOnboardingPage() {\n const workspace = useActiveWorkspace();\n const providersQuery = useIntegrationProvidersQuery({capability: 'source_control'});\n\n return (\n <div className=\"mx-auto flex w-full max-w-[640px] flex-col gap-20\">\n <header className=\"flex flex-col gap-8\">\n <Header variant=\"h1\">Install source control</Header>\n <Text size=\"md\" className=\"text-foreground-neutral-muted\">\n Shipfox needs a source control integration to import your repositories.\n </Text>\n </header>\n\n <ProviderGrid\n workspaceSlug={workspace.slug}\n providers={providersQuery.data ?? []}\n isPending={providersQuery.isPending}\n isFetching={providersQuery.isFetching}\n error={providersQuery.isError ? providersQuery.error : undefined}\n onRetry={() => void providersQuery.refetch()}\n emptyMessage=\"Enable at least one source-control provider in the application settings.\"\n />\n </div>\n );\n}\n"],"names":["useActiveWorkspace","Header","Text","ProviderGrid","useIntegrationProvidersQuery","SourceControlOnboardingPage","workspace","providersQuery","capability","div","className","header","variant","size","workspaceSlug","slug","providers","data","isPending","isFetching","error","isError","undefined","onRetry","refetch","emptyMessage"],"mappings":";AAAA,SAAQA,kBAAkB,QAAO,uBAAuB;AACxD,SAAQC,MAAM,EAAEC,IAAI,QAAO,+BAA+B;AAC1D,SAAQC,YAAY,QAAO,+BAA+B;AAC1D,SAAQC,4BAA4B,QAAO,6BAA6B;AAExE,OAAO,SAASC;IACd,MAAMC,YAAYN;IAClB,MAAMO,iBAAiBH,6BAA6B;QAACI,YAAY;IAAgB;IAEjF,qBACE,MAACC;QAAIC,WAAU;;0BACb,MAACC;gBAAOD,WAAU;;kCAChB,KAACT;wBAAOW,SAAQ;kCAAK;;kCACrB,KAACV;wBAAKW,MAAK;wBAAKH,WAAU;kCAAgC;;;;0BAK5D,KAACP;gBACCW,eAAeR,UAAUS,IAAI;gBAC7BC,WAAWT,eAAeU,IAAI,IAAI,EAAE;gBACpCC,WAAWX,eAAeW,SAAS;gBACnCC,YAAYZ,eAAeY,UAAU;gBACrCC,OAAOb,eAAec,OAAO,GAAGd,eAAea,KAAK,GAAGE;gBACvDC,SAAS,IAAM,KAAKhB,eAAeiB,OAAO;gBAC1CC,cAAa;;;;AAIrB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IconName } from '@shipfox/react-ui/icon';
|
|
2
|
-
export type ProviderSetupPath = '/
|
|
2
|
+
export type ProviderSetupPath = '/w/$workspaceSlug/integrations/github' | '/w/$workspaceSlug/integrations/gitea' | '/w/$workspaceSlug/integrations/sentry' | '/w/$workspaceSlug/integrations/linear' | '/w/$workspaceSlug/integrations/slack' | '/w/$workspaceSlug/integrations/jira';
|
|
3
3
|
interface RouteProviderCatalogEntry {
|
|
4
4
|
kind: 'redirect-install' | 'direct-connect';
|
|
5
5
|
iconName: IconName;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-catalog.d.ts","sourceRoot":"","sources":["../src/provider-catalog.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AAIrD,MAAM,MAAM,iBAAiB,GACzB,sCAAsC,GACtC,
|
|
1
|
+
{"version":3,"file":"provider-catalog.d.ts","sourceRoot":"","sources":["../src/provider-catalog.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AAIrD,MAAM,MAAM,iBAAiB,GACzB,uCAAuC,GACvC,sCAAsC,GACtC,uCAAuC,GACvC,uCAAuC,GACvC,sCAAsC,GACtC,qCAAqC,CAAC;AAE1C,UAAU,yBAAyB;IACjC,IAAI,EAAE,kBAAkB,GAAG,gBAAgB,CAAC;IAC5C,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,iBAAiB,CAAC;CAC9B;AAED,UAAU,yBAAyB;IACjC,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,MAAM,MAAM,oBAAoB,GAAG,yBAAyB,GAAG,yBAAyB,CAAC;AAEzF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAmCjE,CAAC"}
|