@shipfox/client-integrations 12.0.2 → 13.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 +34 -0
- 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/feature.d.ts +7 -7
- package/dist/feature.js +7 -7
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/integrations.js +1 -1
- package/dist/hooks/api/integrations.js.map +1 -1
- package/dist/pages/gitea-install-page.js +4 -4
- package/dist/pages/gitea-install-page.js.map +1 -1
- 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 +5 -5
- package/dist/provider-catalog.js.map +1 -1
- 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 +13 -13
- package/src/components/callback-status-shell.tsx +14 -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/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/feature.ts +7 -7
- package/src/hooks/api/integrations.ts +1 -1
- package/src/pages/gitea-install-page.tsx +2 -2
- 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 +4 -4
- package/src/provider-catalog.ts +10 -10
- 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": "13.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,19 +27,19 @@
|
|
|
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-
|
|
32
|
-
"@shipfox/api-integration-
|
|
33
|
-
"@shipfox/api-integration-
|
|
34
|
-
"@shipfox/api-integration-slack-dto": "
|
|
35
|
-
"@shipfox/api-integration-
|
|
36
|
-
"@shipfox/api-integration-webhook-dto": "9.0.2",
|
|
30
|
+
"@shipfox/api-integration-core-dto": "12.0.0",
|
|
31
|
+
"@shipfox/api-integration-github-dto": "12.0.0",
|
|
32
|
+
"@shipfox/api-integration-linear-dto": "12.0.0",
|
|
33
|
+
"@shipfox/api-integration-gitea-dto": "12.0.0",
|
|
34
|
+
"@shipfox/api-integration-slack-dto": "12.0.0",
|
|
35
|
+
"@shipfox/api-integration-webhook-dto": "12.0.0",
|
|
37
36
|
"@shipfox/client-api": "6.0.1",
|
|
38
|
-
"@shipfox/client-
|
|
39
|
-
"@shipfox/
|
|
40
|
-
"@shipfox/
|
|
41
|
-
"@shipfox/
|
|
42
|
-
"@shipfox/
|
|
37
|
+
"@shipfox/client-shell": "13.0.0",
|
|
38
|
+
"@shipfox/integration-icons": "0.2.3",
|
|
39
|
+
"@shipfox/api-integration-sentry-dto": "12.0.0",
|
|
40
|
+
"@shipfox/client-ui": "13.0.0",
|
|
41
|
+
"@shipfox/client-auth": "13.0.0",
|
|
42
|
+
"@shipfox/react-ui": "0.4.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -7,24 +7,26 @@ 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: '/w/$workspaceSlug/integrations/linear' | '/w/$workspaceSlug/integrations/slack';
|
|
21
23
|
}) {
|
|
22
24
|
const headingRef = useRef<HTMLHeadingElement>(null);
|
|
23
25
|
useEffect(() => headingRef.current?.focus(), []);
|
|
24
26
|
const recoveryVariant = startOver || switchAccount ? 'muted' : 'base';
|
|
25
|
-
const settings =
|
|
27
|
+
const settings = workspaceSlug ? (
|
|
26
28
|
<ButtonLink asChild variant={recoveryVariant} className="min-h-44 w-full sm:w-fit">
|
|
27
|
-
<Link to="/
|
|
29
|
+
<Link to="/w/$workspaceSlug/settings/integrations" params={{workspaceSlug}}>
|
|
28
30
|
Back to integrations
|
|
29
31
|
</Link>
|
|
30
32
|
</ButtonLink>
|
|
@@ -33,7 +35,9 @@ export function CallbackStatusShell({
|
|
|
33
35
|
<Link to="/">Back to Shipfox</Link>
|
|
34
36
|
</ButtonLink>
|
|
35
37
|
);
|
|
36
|
-
const logoutRedirect =
|
|
38
|
+
const logoutRedirect = workspaceSlug
|
|
39
|
+
? installPath.replace('$workspaceSlug', workspaceSlug)
|
|
40
|
+
: undefined;
|
|
37
41
|
|
|
38
42
|
return (
|
|
39
43
|
<main className="flex min-h-screen bg-background-subtle-base px-16 py-32">
|
|
@@ -41,7 +45,7 @@ export function CallbackStatusShell({
|
|
|
41
45
|
<h2 ref={headingRef} tabIndex={-1} className="text-24 font-semibold outline-none">
|
|
42
46
|
{title}
|
|
43
47
|
</h2>
|
|
44
|
-
<Callout role=
|
|
48
|
+
<Callout role={status === 'error' ? 'alert' : 'status'} type={status}>
|
|
45
49
|
<Text size="sm">{message}</Text>
|
|
46
50
|
</Callout>
|
|
47
51
|
<div className="flex flex-col gap-8 sm:flex-row sm:items-center">
|
|
@@ -49,15 +53,15 @@ export function CallbackStatusShell({
|
|
|
49
53
|
<ButtonLink asChild className="min-h-44 w-full sm:w-fit">
|
|
50
54
|
<Link
|
|
51
55
|
to="/auth/logout"
|
|
52
|
-
search={
|
|
56
|
+
search={logoutRedirect ? {redirect: logoutRedirect} : undefined}
|
|
53
57
|
>
|
|
54
58
|
Switch account
|
|
55
59
|
</Link>
|
|
56
60
|
</ButtonLink>
|
|
57
61
|
) : null}
|
|
58
|
-
{startOver &&
|
|
62
|
+
{startOver && workspaceSlug ? (
|
|
59
63
|
<ButtonLink asChild className="min-h-44 w-full sm:w-fit">
|
|
60
|
-
<Link to={installPath} params={{
|
|
64
|
+
<Link to={installPath} params={{workspaceSlug}}>
|
|
61
65
|
Start over
|
|
62
66
|
</Link>
|
|
63
67
|
</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
|
|
|
@@ -11,12 +11,12 @@ import type {IntegrationProvider} from '#core/models.js';
|
|
|
11
11
|
import {PROVIDER_CATALOG} from '#provider-catalog.js';
|
|
12
12
|
|
|
13
13
|
export interface ProviderGridProps {
|
|
14
|
+
workspaceSlug: string;
|
|
14
15
|
providers: IntegrationProvider[];
|
|
15
16
|
isPending: boolean;
|
|
16
17
|
isFetching?: boolean;
|
|
17
18
|
error?: Error | null | undefined;
|
|
18
19
|
onRetry?: () => void;
|
|
19
|
-
workspaceId: string;
|
|
20
20
|
emptyMessage: string;
|
|
21
21
|
loadingLabel?: string;
|
|
22
22
|
errorSubject?: string;
|
|
@@ -29,12 +29,12 @@ export const PROVIDER_SURFACE_CLASS =
|
|
|
29
29
|
'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
|
|
30
30
|
|
|
31
31
|
export function ProviderGrid({
|
|
32
|
+
workspaceSlug,
|
|
32
33
|
providers,
|
|
33
34
|
isPending,
|
|
34
35
|
isFetching = false,
|
|
35
36
|
error,
|
|
36
37
|
onRetry,
|
|
37
|
-
workspaceId,
|
|
38
38
|
emptyMessage,
|
|
39
39
|
loadingLabel = 'Loading providers',
|
|
40
40
|
errorSubject = 'available integrations',
|
|
@@ -79,7 +79,7 @@ export function ProviderGrid({
|
|
|
79
79
|
<li key={provider.provider}>
|
|
80
80
|
<ProviderCard
|
|
81
81
|
provider={provider}
|
|
82
|
-
|
|
82
|
+
workspaceSlug={workspaceSlug}
|
|
83
83
|
onOpenProvider={onOpenProvider}
|
|
84
84
|
/>
|
|
85
85
|
</li>
|
|
@@ -90,11 +90,11 @@ export function ProviderGrid({
|
|
|
90
90
|
|
|
91
91
|
function ProviderCard({
|
|
92
92
|
provider,
|
|
93
|
-
|
|
93
|
+
workspaceSlug,
|
|
94
94
|
onOpenProvider,
|
|
95
95
|
}: {
|
|
96
96
|
provider: IntegrationProvider;
|
|
97
|
-
|
|
97
|
+
workspaceSlug: string;
|
|
98
98
|
onOpenProvider?: ((provider: string) => void) | undefined;
|
|
99
99
|
}) {
|
|
100
100
|
const catalog = PROVIDER_CATALOG[provider.provider];
|
|
@@ -116,7 +116,7 @@ function ProviderCard({
|
|
|
116
116
|
return (
|
|
117
117
|
<Link
|
|
118
118
|
to={catalog.setupPath}
|
|
119
|
-
params={{
|
|
119
|
+
params={{workspaceSlug}}
|
|
120
120
|
aria-label={`Install ${provider.displayName}`}
|
|
121
121
|
className="group flex h-full min-w-0 items-center justify-between gap-12 rounded-8 border border-border-neutral-base bg-background-neutral-base p-16 transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
|
|
122
122
|
>
|
|
@@ -11,8 +11,8 @@ function renderInstallPage(
|
|
|
11
11
|
options?: {strict?: boolean},
|
|
12
12
|
) {
|
|
13
13
|
return renderIntegrationsPage({
|
|
14
|
-
path:
|
|
15
|
-
routePath: '/
|
|
14
|
+
path: '/w/acme/integrations/github',
|
|
15
|
+
routePath: '/w/$workspaceSlug/integrations/github',
|
|
16
16
|
element: options?.strict ? (
|
|
17
17
|
<StrictMode>
|
|
18
18
|
<RedirectInstallPage {...props} />
|
|
@@ -20,7 +20,7 @@ function renderInstallPage(
|
|
|
20
20
|
) : (
|
|
21
21
|
<RedirectInstallPage {...props} />
|
|
22
22
|
),
|
|
23
|
-
extraRoutes: ['/
|
|
23
|
+
extraRoutes: ['/w/$workspaceSlug/integrations'],
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -14,7 +14,7 @@ interface RedirectInstallPageProps {
|
|
|
14
14
|
loadingLabel?: string;
|
|
15
15
|
/**
|
|
16
16
|
* Runs before leaving the app (e.g. to persist the workspace id for a
|
|
17
|
-
* state-less provider callback
|
|
17
|
+
* state-less provider callback. It must not throw. A failed side effect
|
|
18
18
|
* should never block the redirect.
|
|
19
19
|
*/
|
|
20
20
|
beforeRedirect?: (workspaceId: string) => void;
|
|
@@ -59,7 +59,7 @@ export function RedirectInstallPage({
|
|
|
59
59
|
<Text size="sm">{errorMessage}</Text>
|
|
60
60
|
</Callout>
|
|
61
61
|
<ButtonLink asChild variant="muted" className="w-fit">
|
|
62
|
-
<Link to="/
|
|
62
|
+
<Link to="/w/$workspaceSlug/integrations" params={{workspaceSlug: workspace.slug}}>
|
|
63
63
|
Back to integrations
|
|
64
64
|
</Link>
|
|
65
65
|
</ButtonLink>
|
|
@@ -176,7 +176,7 @@ function createStoryRouter(scenario: Scenario) {
|
|
|
176
176
|
const rootRoute = createRootRoute({component: Outlet});
|
|
177
177
|
const workspaceRoute = createRoute({
|
|
178
178
|
getParentRoute: () => rootRoute,
|
|
179
|
-
path: '/
|
|
179
|
+
path: '/w/$workspaceSlug',
|
|
180
180
|
component: Outlet,
|
|
181
181
|
});
|
|
182
182
|
const integrationsRoute = createRoute({
|
|
@@ -192,7 +192,7 @@ function createStoryRouter(scenario: Scenario) {
|
|
|
192
192
|
|
|
193
193
|
return createRouter({
|
|
194
194
|
history: createMemoryHistory({
|
|
195
|
-
initialEntries: [
|
|
195
|
+
initialEntries: ['/w/acme/settings/integrations'],
|
|
196
196
|
}),
|
|
197
197
|
routeTree: rootRoute.addChildren([
|
|
198
198
|
workspaceRoute.addChildren([integrationsRoute, eventsRoute]),
|
package/src/feature.ts
CHANGED
|
@@ -34,37 +34,37 @@ export const integrationsFeature = defineClientFeature({
|
|
|
34
34
|
impl: '@shipfox/client-integrations/routes/slack-callback',
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
|
-
path: '/
|
|
37
|
+
path: '/w/$workspaceSlug/integrations',
|
|
38
38
|
parent: 'workspaceLayout',
|
|
39
39
|
impl: '@shipfox/client-integrations/routes/integrations',
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
|
-
path: '/
|
|
42
|
+
path: '/w/$workspaceSlug/integrations/gitea',
|
|
43
43
|
parent: 'workspaceLayout',
|
|
44
44
|
impl: '@shipfox/client-integrations/routes/gitea',
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
|
-
path: '/
|
|
47
|
+
path: '/w/$workspaceSlug/integrations/github',
|
|
48
48
|
parent: 'workspaceLayout',
|
|
49
49
|
impl: '@shipfox/client-integrations/routes/github',
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
|
-
path: '/
|
|
52
|
+
path: '/w/$workspaceSlug/integrations/linear',
|
|
53
53
|
parent: 'workspaceLayout',
|
|
54
54
|
impl: '@shipfox/client-integrations/routes/linear',
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
|
-
path: '/
|
|
57
|
+
path: '/w/$workspaceSlug/integrations/sentry',
|
|
58
58
|
parent: 'workspaceLayout',
|
|
59
59
|
impl: '@shipfox/client-integrations/routes/sentry',
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
|
-
path: '/
|
|
62
|
+
path: '/w/$workspaceSlug/integrations/slack',
|
|
63
63
|
parent: 'workspaceLayout',
|
|
64
64
|
impl: '@shipfox/client-integrations/routes/slack',
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
|
-
path: '/
|
|
67
|
+
path: '/w/$workspaceSlug/settings/integrations',
|
|
68
68
|
parent: 'workspaceSettings',
|
|
69
69
|
impl: '@shipfox/client-integrations/routes/integrations-settings',
|
|
70
70
|
},
|
|
@@ -175,7 +175,7 @@ export async function listSourceConnections({
|
|
|
175
175
|
});
|
|
176
176
|
// The endpoint returns every lifecycle status (the settings hub needs that),
|
|
177
177
|
// but source-control consumers (onboarding redirect, project creation) only
|
|
178
|
-
// act on usable connections
|
|
178
|
+
// act on usable connections. A disabled/error one must read as "not there".
|
|
179
179
|
return connections.filter(isUsableConnection);
|
|
180
180
|
}
|
|
181
181
|
|
|
@@ -24,7 +24,7 @@ export function GiteaInstallPage() {
|
|
|
24
24
|
try {
|
|
25
25
|
await connect.mutateAsync({workspace_id: workspaceId, org: value.org.trim()});
|
|
26
26
|
toast.success('Gitea organization installed.');
|
|
27
|
-
await navigate({to: '/
|
|
27
|
+
await navigate({to: '/w/$workspaceSlug', params: {workspaceSlug: workspace.slug}});
|
|
28
28
|
} catch (error) {
|
|
29
29
|
const mapped = giteaConnectErrorToFormError(error);
|
|
30
30
|
if (mapped.kind === 'field') {
|
|
@@ -88,7 +88,7 @@ export function GiteaInstallPage() {
|
|
|
88
88
|
Install
|
|
89
89
|
</Button>
|
|
90
90
|
<ButtonLink asChild variant="muted">
|
|
91
|
-
<Link to="/
|
|
91
|
+
<Link to="/w/$workspaceSlug/integrations" params={{workspaceSlug: workspace.slug}}>
|
|
92
92
|
Cancel
|
|
93
93
|
</Link>
|
|
94
94
|
</ButtonLink>
|