@shipfox/client-integrations 3.0.1 → 5.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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +37 -0
  3. package/dist/feature.d.ts +11 -0
  4. package/dist/feature.d.ts.map +1 -1
  5. package/dist/feature.js +14 -0
  6. package/dist/feature.js.map +1 -1
  7. package/dist/hooks/api/integrations.d.ts +6 -1
  8. package/dist/hooks/api/integrations.d.ts.map +1 -1
  9. package/dist/hooks/api/integrations.js +15 -12
  10. package/dist/hooks/api/integrations.js.map +1 -1
  11. package/dist/pages/linear-callback-page.d.ts.map +1 -1
  12. package/dist/pages/linear-callback-page.js +9 -13
  13. package/dist/pages/linear-callback-page.js.map +1 -1
  14. package/dist/pages/sentry-callback-page.d.ts.map +1 -1
  15. package/dist/pages/sentry-callback-page.js +5 -19
  16. package/dist/pages/sentry-callback-page.js.map +1 -1
  17. package/dist/pages/slack-callback-page.d.ts.map +1 -1
  18. package/dist/pages/slack-callback-page.js +9 -10
  19. package/dist/pages/slack-callback-page.js.map +1 -1
  20. package/dist/routes/github-callback.d.ts +1 -0
  21. package/dist/routes/github-callback.d.ts.map +1 -1
  22. package/dist/routes/github-callback.js +15 -12
  23. package/dist/routes/github-callback.js.map +1 -1
  24. package/dist/routes/integrations-settings.d.ts +6 -0
  25. package/dist/routes/integrations-settings.d.ts.map +1 -0
  26. package/dist/routes/integrations-settings.js +7 -0
  27. package/dist/routes/integrations-settings.js.map +1 -0
  28. package/dist/tsconfig.test.tsbuildinfo +1 -1
  29. package/package.json +12 -45
  30. package/src/feature.ts +14 -0
  31. package/src/hooks/api/integrations.ts +32 -8
  32. package/src/pages/linear-callback-page.test.tsx +1 -1
  33. package/src/pages/linear-callback-page.tsx +14 -13
  34. package/src/pages/sentry-callback-page.test.tsx +1 -1
  35. package/src/pages/sentry-callback-page.tsx +16 -26
  36. package/src/pages/slack-callback-page.tsx +14 -10
  37. package/src/routes/github-callback.test.ts +17 -0
  38. package/src/routes/github-callback.tsx +15 -12
  39. package/src/routes/integrations-settings.tsx +4 -0
  40. package/test/render.tsx +1 -5
  41. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 48 files with swc (596.42ms)
2
+ Successfully compiled: 49 files with swc (515.65ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @shipfox/client-integrations
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8d8cdef: Extracts workspace onboarding into a dedicated coordinator and shares its feature query policies.
8
+
9
+ ### Patch Changes
10
+
11
+ - ffd727b: Converges auth session and invitation state onto shared camelCase domain types validated at the API boundary, replacing the raw snake_case DTOs previously returned by login, signup, password reset, email verification, workspace creation, and invitation preview. `AuthState.user`, `useRefreshAuth()`, and `usePreviewInvitation()` now resolve to `UserIdentity`/`AuthenticatedSession`/`InvitationPreview` shapes (for example `accessToken` instead of `token`, `workspaceName` instead of `workspace_name`). Also moves the shared `AuthShell` component and session mapping helpers into `@shipfox/client-shell`, breaking the former `client-auth` ↔ `client-invitations` circular dependency.
12
+ - f1d6465: Moves workspace-settings and project-workflow route ownership from centralized packages into each feature's own route module, so a feature package declares and ships its own settings pages.
13
+ - Updated dependencies [ffd727b]
14
+ - Updated dependencies [7f227c6]
15
+ - @shipfox/client-shell@5.0.0
16
+ - @shipfox/client-auth@5.0.0
17
+ - @shipfox/api-integration-core-dto@8.0.0
18
+ - @shipfox/api-integration-gitea-dto@8.0.0
19
+ - @shipfox/api-integration-github-dto@8.0.0
20
+ - @shipfox/api-integration-linear-dto@8.0.0
21
+ - @shipfox/api-integration-sentry-dto@8.0.0
22
+ - @shipfox/api-integration-slack-dto@8.0.0
23
+ - @shipfox/api-integration-webhook-dto@8.0.0
24
+
25
+ ## 4.0.0
26
+
27
+ ### Patch Changes
28
+
29
+ - 2e5b718: Adds safe browser persistence and bounded callback deduplication across client flows.
30
+ - Updated dependencies [2e5b718]
31
+ - Updated dependencies [6b4a575]
32
+ - Updated dependencies [20e4feb]
33
+ - Updated dependencies [11b10f7]
34
+ - Updated dependencies [781a45b]
35
+ - @shipfox/client-ui@4.0.0
36
+ - @shipfox/client-shell@4.0.0
37
+ - @shipfox/client-api@4.0.0
38
+ - @shipfox/client-auth@4.0.0
39
+
3
40
  ## 3.0.1
4
41
 
5
42
  ### Patch Changes
package/dist/feature.d.ts CHANGED
@@ -40,6 +40,17 @@ export declare const integrationsFeature: {
40
40
  readonly path: "/workspaces/$wid/integrations/slack";
41
41
  readonly parent: "workspaceLayout";
42
42
  readonly impl: "@shipfox/client-integrations/routes/slack";
43
+ }, {
44
+ readonly path: "/workspaces/$wid/settings/integrations";
45
+ readonly parent: "workspaceSettings";
46
+ readonly impl: "@shipfox/client-integrations/routes/integrations-settings";
47
+ }];
48
+ readonly settingsSections: readonly [{
49
+ readonly id: "settings.integrations";
50
+ readonly pathSegment: "integrations";
51
+ readonly label: "Integrations";
52
+ readonly icon: "plugLine";
53
+ readonly order: 700;
43
54
  }];
44
55
  };
45
56
  //# sourceMappingURL=feature.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../src/feature.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsD9B,CAAC"}
1
+ {"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../src/feature.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoE9B,CAAC"}
package/dist/feature.js CHANGED
@@ -51,6 +51,20 @@ export const integrationsFeature = defineClientFeature({
51
51
  path: '/workspaces/$wid/integrations/slack',
52
52
  parent: 'workspaceLayout',
53
53
  impl: '@shipfox/client-integrations/routes/slack'
54
+ },
55
+ {
56
+ path: '/workspaces/$wid/settings/integrations',
57
+ parent: 'workspaceSettings',
58
+ impl: '@shipfox/client-integrations/routes/integrations-settings'
59
+ }
60
+ ],
61
+ settingsSections: [
62
+ {
63
+ id: 'settings.integrations',
64
+ pathSegment: 'integrations',
65
+ label: 'Integrations',
66
+ icon: 'plugLine',
67
+ order: 700
54
68
  }
55
69
  ]
56
70
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/feature.ts"],"sourcesContent":["import {defineClientFeature} from '@shipfox/client-shell';\n\nexport const integrationsFeature = defineClientFeature({\n id: 'shipfox.integrations',\n routes: [\n {\n path: '/integrations/github/callback',\n parent: 'root',\n impl: '@shipfox/client-integrations/routes/github-callback',\n },\n {\n path: '/integrations/linear/callback',\n parent: 'root',\n impl: '@shipfox/client-integrations/routes/linear-callback',\n },\n {\n path: '/integrations/sentry/callback',\n parent: 'root',\n impl: '@shipfox/client-integrations/routes/sentry-callback',\n },\n {\n path: '/integrations/slack/callback',\n parent: 'root',\n impl: '@shipfox/client-integrations/routes/slack-callback',\n },\n {\n path: '/workspaces/$wid/integrations',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/integrations',\n },\n {\n path: '/workspaces/$wid/integrations/gitea',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/gitea',\n },\n {\n path: '/workspaces/$wid/integrations/github',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/github',\n },\n {\n path: '/workspaces/$wid/integrations/linear',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/linear',\n },\n {\n path: '/workspaces/$wid/integrations/sentry',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/sentry',\n },\n {\n path: '/workspaces/$wid/integrations/slack',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/slack',\n },\n ],\n});\n"],"names":["defineClientFeature","integrationsFeature","id","routes","path","parent","impl"],"mappings":"AAAA,SAAQA,mBAAmB,QAAO,wBAAwB;AAE1D,OAAO,MAAMC,sBAAsBD,oBAAoB;IACrDE,IAAI;IACJC,QAAQ;QACN;YACEC,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;KACD;AACH,GAAG"}
1
+ {"version":3,"sources":["../src/feature.ts"],"sourcesContent":["import {defineClientFeature} from '@shipfox/client-shell';\n\nexport const integrationsFeature = defineClientFeature({\n id: 'shipfox.integrations',\n routes: [\n {\n path: '/integrations/github/callback',\n parent: 'root',\n impl: '@shipfox/client-integrations/routes/github-callback',\n },\n {\n path: '/integrations/linear/callback',\n parent: 'root',\n impl: '@shipfox/client-integrations/routes/linear-callback',\n },\n {\n path: '/integrations/sentry/callback',\n parent: 'root',\n impl: '@shipfox/client-integrations/routes/sentry-callback',\n },\n {\n path: '/integrations/slack/callback',\n parent: 'root',\n impl: '@shipfox/client-integrations/routes/slack-callback',\n },\n {\n path: '/workspaces/$wid/integrations',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/integrations',\n },\n {\n path: '/workspaces/$wid/integrations/gitea',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/gitea',\n },\n {\n path: '/workspaces/$wid/integrations/github',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/github',\n },\n {\n path: '/workspaces/$wid/integrations/linear',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/linear',\n },\n {\n path: '/workspaces/$wid/integrations/sentry',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/sentry',\n },\n {\n path: '/workspaces/$wid/integrations/slack',\n parent: 'workspaceLayout',\n impl: '@shipfox/client-integrations/routes/slack',\n },\n {\n path: '/workspaces/$wid/settings/integrations',\n parent: 'workspaceSettings',\n impl: '@shipfox/client-integrations/routes/integrations-settings',\n },\n ],\n settingsSections: [\n {\n id: 'settings.integrations',\n pathSegment: 'integrations',\n label: 'Integrations',\n icon: 'plugLine',\n order: 700,\n },\n ],\n});\n"],"names":["defineClientFeature","integrationsFeature","id","routes","path","parent","impl","settingsSections","pathSegment","label","icon","order"],"mappings":"AAAA,SAAQA,mBAAmB,QAAO,wBAAwB;AAE1D,OAAO,MAAMC,sBAAsBD,oBAAoB;IACrDE,IAAI;IACJC,QAAQ;QACN;YACEC,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;QACA;YACEF,MAAM;YACNC,QAAQ;YACRC,MAAM;QACR;KACD;IACDC,kBAAkB;QAChB;YACEL,IAAI;YACJM,aAAa;YACbC,OAAO;YACPC,MAAM;YACNC,OAAO;QACT;KACD;AACH,GAAG"}
@@ -1,9 +1,10 @@
1
- import type { IntegrationCapabilityDto, UpdateIntegrationConnectionBodyDto } from '@shipfox/api-integration-core-dto';
1
+ import type { IntegrationCapabilityDto, ListIntegrationConnectionsResponseDto, UpdateIntegrationConnectionBodyDto } from '@shipfox/api-integration-core-dto';
2
2
  import type { CreateGiteaConnectionBodyDto } from '@shipfox/api-integration-gitea-dto';
3
3
  import type { CreateGithubInstallBodyDto } from '@shipfox/api-integration-github-dto';
4
4
  import type { CreateLinearInstallBodyDto, LinearCallbackQueryDto } from '@shipfox/api-integration-linear-dto';
5
5
  import type { CreateSentryInstallBodyDto, SentryConnectBodyDto } from '@shipfox/api-integration-sentry-dto';
6
6
  import type { CreateSlackInstallBodyDto, SlackCallbackQueryDto } from '@shipfox/api-integration-slack-dto';
7
+ import { type FetchQueryOptions } from '@tanstack/react-query';
7
8
  export declare const integrationsQueryKeys: {
8
9
  all: readonly ["integrations"];
9
10
  providers: (capability: IntegrationCapabilityDto | "all") => readonly ["integrations", "providers", "source_control" | "agent_tools" | "all"];
@@ -12,6 +13,8 @@ export declare const integrationsQueryKeys: {
12
13
  sourceConnections: (workspaceId: string) => readonly ["integrations", "connections", string, "source_control" | "agent_tools" | "all"];
13
14
  repositories: (connectionId: string, search: string) => readonly ["integrations", "repositories", string, string];
14
15
  };
16
+ type SourceConnectionsQueryKey = ReturnType<typeof integrationsQueryKeys.sourceConnections> | readonly ['integrations', 'source-connections'];
17
+ type SourceConnectionsQueryOptions = FetchQueryOptions<ListIntegrationConnectionsResponseDto, Error, ListIntegrationConnectionsResponseDto, SourceConnectionsQueryKey>;
15
18
  export declare function listIntegrationProviders({ capability, signal, }: {
16
19
  capability?: IntegrationCapabilityDto;
17
20
  signal?: AbortSignal;
@@ -59,6 +62,7 @@ export declare function listSourceConnections({ workspaceId, signal, }: {
59
62
  external_url?: string | undefined;
60
63
  }[];
61
64
  }>;
65
+ export declare function sourceConnectionsQueryOptions(workspaceId: string | undefined): SourceConnectionsQueryOptions;
62
66
  export declare function createGiteaConnection(body: CreateGiteaConnectionBodyDto): Promise<{
63
67
  id: string;
64
68
  workspace_id: string;
@@ -303,4 +307,5 @@ export declare function useDeleteIntegrationConnectionMutation(): import("@tanst
303
307
  workspaceId: string;
304
308
  connectionId: string;
305
309
  }, unknown>;
310
+ export {};
306
311
  //# sourceMappingURL=integrations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EAKxB,kCAAkC,EACnC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EACV,4BAA4B,EAE7B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EACV,0BAA0B,EAE3B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EACV,0BAA0B,EAE1B,sBAAsB,EAEvB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EACV,0BAA0B,EAE1B,oBAAoB,EAErB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EACV,yBAAyB,EAEzB,qBAAqB,EAEtB,MAAM,oCAAoC,CAAC;AAM5C,eAAO,MAAM,qBAAqB;;4BAER,wBAAwB,GAAG,KAAK;+BAE7B,MAAM,cAAc,wBAAwB,GAAG,KAAK;0CAKzC,MAAM;qCAEX,MAAM;iCAEV,MAAM,UAAU,MAAM;CAEpD,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,EAC7C,UAAU,EACV,MAAM,GACP,EAAE;IACD,UAAU,CAAC,EAAE,wBAAwB,CAAC;IACtC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;;;;;;GAMA;AAED,wBAAsB,0BAA0B,CAAC,EAC/C,WAAW,EACX,UAAU,EACV,MAAM,GACP,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,wBAAwB,GAAG,SAAS,CAAC;IAClD,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAClC;;;;;;;;;;;;;;GAOA;AAED,wBAAsB,qBAAqB,CAAC,EAC1C,WAAW,EACX,MAAM,GACP,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;;;;;;;;;;;;;;GAeA;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,4BAA4B;;;;;;;;;;;;GAK7E;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B;;GAKzE;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B;;GAKzE;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B;;GAKzE;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB;;GAKvE;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,EAAE,sBAAsB,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;;;;;;;;;;;;GAKA;AAED,wBAAsB,qBAAqB,CAAC,EAC1C,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,EAAE,qBAAqB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf;;;;;;;;;;;;GAKA;AAID,wBAAsB,aAAa,CAAC,EAAC,IAAI,EAAE,KAAK,EAAC,EAAE;IAAC,IAAI,EAAE,oBAAoB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC;;;;;;;;;;;;GAM7F;AAED,wBAAsB,gBAAgB,CAAC,EACrC,YAAY,EACZ,MAAM,EACN,MAAM,EACN,MAAM,GACP,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;;;;;;;;;;;;;GASA;AAED,wBAAsB,2BAA2B,CAAC,EAChD,YAAY,EACZ,IAAI,GACL,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,kCAAkC,CAAC;CAC1C;;;;;;;;;;;;GAKA;AAED,wBAAsB,2BAA2B,CAAC,EAAC,YAAY,EAAC,EAAE;IAAC,YAAY,EAAE,MAAM,CAAA;CAAC,iBAIvF;AAED,wBAAgB,4BAA4B,CAAC,MAAM,CAAC,EAAE;IAAC,UAAU,CAAC,EAAE,wBAAwB,CAAA;CAAC;;;;;;WAM5F;AAED,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS;;;;;;;;;;;;;;WAQxE;AAED,wBAAgB,8BAA8B,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS;;;;;;;;;;;;;;WAQ7E;AAED,wBAAgB,4BAA4B,CAC1C,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAC;;;;;;;;;;;;;oBAoB5B;AAED,wBAAgB,gCAAgC;;;;;;;;;;;;;;;YAE/C;AAED,wBAAgB,8BAA8B;;;;YAE7C;AAED,wBAAgB,6BAA6B;;;;YAE5C;AAED,wBAAgB,iCAAiC;;;;;;;;;;;;;WAxIxC,sBAAsB;WACtB,MAAM;YAyId;AAED,wBAAgB,gCAAgC;;;;;;;;;;;;;WA/HvC,qBAAqB;WACrB,MAAM;YAgId;AAED,wBAAgB,sCAAsC;;;;;;;;;;;;;kBAvFtC,MAAM;UACd,kCAAkC;YAgGzC;AAED,wBAAgB,sCAAsC;iBAOnC,MAAM;kBACL,MAAM;YAQzB"}
1
+ {"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/integrations.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { apiRequest } from '@shipfox/client-api';
2
- import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { queryOptions, useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
3
3
  import { serializeLinearCallbackQuery } from '#linear-callback.js';
4
4
  import { serializeSlackCallbackQuery } from '#slack-callback.js';
5
5
  export const integrationsQueryKeys = {
@@ -65,6 +65,19 @@ export async function listSourceConnections({ workspaceId, signal }) {
65
65
  connections: result.connections.filter((connection)=>connection.lifecycle_status === 'active')
66
66
  };
67
67
  }
68
+ export function sourceConnectionsQueryOptions(workspaceId) {
69
+ return queryOptions({
70
+ queryKey: workspaceId ? integrationsQueryKeys.sourceConnections(workspaceId) : [
71
+ ...integrationsQueryKeys.all,
72
+ 'source-connections'
73
+ ],
74
+ enabled: Boolean(workspaceId),
75
+ queryFn: ({ signal })=>listSourceConnections({
76
+ workspaceId: workspaceId ?? '',
77
+ signal
78
+ })
79
+ });
80
+ }
68
81
  export async function createGiteaConnection(body) {
69
82
  return await apiRequest('/integrations/gitea/connections', {
70
83
  method: 'POST',
@@ -154,17 +167,7 @@ export function useIntegrationProvidersQuery(params) {
154
167
  });
155
168
  }
156
169
  export function useSourceConnectionsQuery(workspaceId) {
157
- return useQuery({
158
- queryKey: workspaceId ? integrationsQueryKeys.sourceConnections(workspaceId) : [
159
- ...integrationsQueryKeys.all,
160
- 'source-connections'
161
- ],
162
- enabled: Boolean(workspaceId),
163
- queryFn: ({ signal })=>listSourceConnections({
164
- workspaceId: workspaceId ?? '',
165
- signal
166
- })
167
- });
170
+ return useQuery(sourceConnectionsQueryOptions(workspaceId));
168
171
  }
169
172
  export function useIntegrationConnectionsQuery(workspaceId) {
170
173
  return useQuery({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/api/integrations.ts"],"sourcesContent":["import type {\n IntegrationCapabilityDto,\n IntegrationConnectionDto,\n ListIntegrationConnectionsResponseDto,\n ListIntegrationProvidersResponseDto,\n ListRepositoriesResponseDto,\n UpdateIntegrationConnectionBodyDto,\n} from '@shipfox/api-integration-core-dto';\nimport type {\n CreateGiteaConnectionBodyDto,\n CreateGiteaConnectionResponseDto,\n} from '@shipfox/api-integration-gitea-dto';\nimport type {\n CreateGithubInstallBodyDto,\n CreateGithubInstallResponseDto,\n} from '@shipfox/api-integration-github-dto';\nimport type {\n CreateLinearInstallBodyDto,\n CreateLinearInstallResponseDto,\n LinearCallbackQueryDto,\n LinearCallbackResponseDto,\n} from '@shipfox/api-integration-linear-dto';\nimport type {\n CreateSentryInstallBodyDto,\n CreateSentryInstallResponseDto,\n SentryConnectBodyDto,\n SentryConnectResponseDto,\n} from '@shipfox/api-integration-sentry-dto';\nimport type {\n CreateSlackInstallBodyDto,\n CreateSlackInstallResponseDto,\n SlackCallbackQueryDto,\n SlackCallbackResponseDto,\n} from '@shipfox/api-integration-slack-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query';\nimport {serializeLinearCallbackQuery} from '#linear-callback.js';\nimport {serializeSlackCallbackQuery} from '#slack-callback.js';\n\nexport const integrationsQueryKeys = {\n all: ['integrations'] as const,\n providers: (capability: IntegrationCapabilityDto | 'all') =>\n [...integrationsQueryKeys.all, 'providers', capability] as const,\n connections: (workspaceId: string, capability: IntegrationCapabilityDto | 'all') =>\n [...integrationsQueryKeys.all, 'connections', workspaceId, capability] as const,\n // Prefix matching every per-capability connections query for a workspace, so\n // a mutation can refresh all connection views without touching providers or\n // repositories.\n connectionsByWorkspace: (workspaceId: string) =>\n [...integrationsQueryKeys.all, 'connections', workspaceId] as const,\n sourceConnections: (workspaceId: string) =>\n integrationsQueryKeys.connections(workspaceId, 'source_control'),\n repositories: (connectionId: string, search: string) =>\n [...integrationsQueryKeys.all, 'repositories', connectionId, search] as const,\n};\n\nexport async function listIntegrationProviders({\n capability,\n signal,\n}: {\n capability?: IntegrationCapabilityDto;\n signal?: AbortSignal;\n}) {\n const search = new URLSearchParams();\n if (capability) search.set('capability', capability);\n const query = search.toString();\n const path = query ? `/integration-providers?${query}` : '/integration-providers';\n return await apiRequest<ListIntegrationProvidersResponseDto>(path, {signal});\n}\n\nexport async function listIntegrationConnections({\n workspaceId,\n capability,\n signal,\n}: {\n workspaceId: string;\n capability?: IntegrationCapabilityDto | undefined;\n signal?: AbortSignal | undefined;\n}) {\n const search = new URLSearchParams({workspace_id: workspaceId});\n if (capability) search.set('capability', capability);\n return await apiRequest<ListIntegrationConnectionsResponseDto>(\n `/integration-connections?${search.toString()}`,\n {signal},\n );\n}\n\nexport async function listSourceConnections({\n workspaceId,\n signal,\n}: {\n workspaceId: string;\n signal?: AbortSignal;\n}) {\n const result = await listIntegrationConnections({\n workspaceId,\n capability: 'source_control',\n signal,\n });\n // The endpoint returns every lifecycle status (the settings hub needs that),\n // but source-control consumers (onboarding redirect, project creation) only\n // act on usable connections — a disabled/error one must read as \"not there\".\n return {\n ...result,\n connections: result.connections.filter(\n (connection) => connection.lifecycle_status === 'active',\n ),\n };\n}\n\nexport async function createGiteaConnection(body: CreateGiteaConnectionBodyDto) {\n return await apiRequest<CreateGiteaConnectionResponseDto>('/integrations/gitea/connections', {\n method: 'POST',\n body,\n });\n}\n\nexport async function createGithubInstall(body: CreateGithubInstallBodyDto) {\n return await apiRequest<CreateGithubInstallResponseDto>('/integrations/github/install', {\n method: 'POST',\n body,\n });\n}\n\nexport async function createSentryInstall(body: CreateSentryInstallBodyDto) {\n return await apiRequest<CreateSentryInstallResponseDto>('/integrations/sentry/install', {\n method: 'POST',\n body,\n });\n}\n\nexport async function createLinearInstall(body: CreateLinearInstallBodyDto) {\n return await apiRequest<CreateLinearInstallResponseDto>('/integrations/linear/install', {\n method: 'POST',\n body,\n });\n}\n\nexport async function createSlackInstall(body: CreateSlackInstallBodyDto) {\n return await apiRequest<CreateSlackInstallResponseDto>('/integrations/slack/install', {\n method: 'POST',\n body,\n });\n}\n\nexport async function completeLinearCallback({\n query,\n token,\n}: {\n query: LinearCallbackQueryDto;\n token: string;\n}) {\n return await apiRequest<LinearCallbackResponseDto>(\n `/integrations/linear/callback/api?${serializeLinearCallbackQuery(query)}`,\n {headers: {authorization: `Bearer ${token}`}},\n );\n}\n\nexport async function completeSlackCallback({\n query,\n token,\n}: {\n query: SlackCallbackQueryDto;\n token: string;\n}) {\n return await apiRequest<SlackCallbackResponseDto>(\n `/integrations/slack/callback/api?${serializeSlackCallbackQuery(query)}`,\n {headers: {authorization: `Bearer ${token}`}},\n );\n}\n\n// Called from the callback route with an explicit bearer (same as the GitHub\n// callback): the route refreshes auth itself before forwarding the grant code.\nexport async function connectSentry({body, token}: {body: SentryConnectBodyDto; token: string}) {\n return await apiRequest<SentryConnectResponseDto>('/integrations/sentry/connect', {\n method: 'POST',\n body,\n headers: {authorization: `Bearer ${token}`},\n });\n}\n\nexport async function listRepositories({\n connectionId,\n cursor,\n search,\n signal,\n}: {\n connectionId: string;\n cursor?: string;\n search?: string;\n signal?: AbortSignal;\n}) {\n const params = new URLSearchParams();\n if (cursor) params.set('cursor', cursor);\n if (search) params.set('search', search);\n const query = params.toString();\n const path = query\n ? `/integration-connections/${connectionId}/repositories?${query}`\n : `/integration-connections/${connectionId}/repositories`;\n return await apiRequest<ListRepositoriesResponseDto>(path, {signal});\n}\n\nexport async function updateIntegrationConnection({\n connectionId,\n body,\n}: {\n connectionId: string;\n body: UpdateIntegrationConnectionBodyDto;\n}) {\n return await apiRequest<IntegrationConnectionDto>(\n `/integration-connections/${encodeURIComponent(connectionId)}`,\n {method: 'PATCH', body},\n );\n}\n\nexport async function deleteIntegrationConnection({connectionId}: {connectionId: string}) {\n await apiRequest<void>(`/integration-connections/${encodeURIComponent(connectionId)}`, {\n method: 'DELETE',\n });\n}\n\nexport function useIntegrationProvidersQuery(params?: {capability?: IntegrationCapabilityDto}) {\n const capability = params?.capability;\n return useQuery({\n queryKey: integrationsQueryKeys.providers(capability ?? 'all'),\n queryFn: ({signal}) => listIntegrationProviders(capability ? {capability, signal} : {signal}),\n });\n}\n\nexport function useSourceConnectionsQuery(workspaceId: string | undefined) {\n return useQuery({\n queryKey: workspaceId\n ? integrationsQueryKeys.sourceConnections(workspaceId)\n : [...integrationsQueryKeys.all, 'source-connections'],\n enabled: Boolean(workspaceId),\n queryFn: ({signal}) => listSourceConnections({workspaceId: workspaceId ?? '', signal}),\n });\n}\n\nexport function useIntegrationConnectionsQuery(workspaceId: string | undefined) {\n return useQuery({\n queryKey: workspaceId\n ? integrationsQueryKeys.connections(workspaceId, 'all')\n : [...integrationsQueryKeys.all, 'connections'],\n enabled: Boolean(workspaceId),\n queryFn: ({signal}) => listIntegrationConnections({workspaceId: workspaceId ?? '', signal}),\n });\n}\n\nexport function useRepositoriesInfiniteQuery(\n connectionId: string | undefined,\n options?: {search?: string},\n) {\n const trimmedSearch = options?.search?.trim() ?? '';\n return useInfiniteQuery({\n queryKey: connectionId\n ? integrationsQueryKeys.repositories(connectionId, trimmedSearch)\n : [...integrationsQueryKeys.all, 'repositories'],\n enabled: Boolean(connectionId),\n initialPageParam: undefined as string | undefined,\n queryFn: ({pageParam, signal}) => {\n const args: {connectionId: string; cursor?: string; search?: string; signal: AbortSignal} = {\n connectionId: connectionId ?? '',\n signal,\n };\n if (pageParam) args.cursor = pageParam;\n if (trimmedSearch) args.search = trimmedSearch;\n return listRepositories(args);\n },\n getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,\n });\n}\n\nexport function useCreateGiteaConnectionMutation() {\n return useMutation({mutationFn: createGiteaConnection});\n}\n\nexport function useCreateLinearInstallMutation() {\n return useMutation({mutationFn: createLinearInstall});\n}\n\nexport function useCreateSlackInstallMutation() {\n return useMutation({mutationFn: createSlackInstall});\n}\n\nexport function useCompleteLinearCallbackMutation() {\n return useMutation({mutationFn: completeLinearCallback});\n}\n\nexport function useCompleteSlackCallbackMutation() {\n return useMutation({mutationFn: completeSlackCallback});\n}\n\nexport function useUpdateIntegrationConnectionMutation() {\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: updateIntegrationConnection,\n onSuccess: async (connection) => {\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),\n });\n },\n });\n}\n\nexport function useDeleteIntegrationConnectionMutation() {\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: ({\n workspaceId: _workspaceId,\n connectionId,\n }: {\n workspaceId: string;\n connectionId: string;\n }) => deleteIntegrationConnection({connectionId}),\n onSuccess: async (_result, variables) => {\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(variables.workspaceId),\n });\n },\n });\n}\n"],"names":["apiRequest","useInfiniteQuery","useMutation","useQuery","useQueryClient","serializeLinearCallbackQuery","serializeSlackCallbackQuery","integrationsQueryKeys","all","providers","capability","connections","workspaceId","connectionsByWorkspace","sourceConnections","repositories","connectionId","search","listIntegrationProviders","signal","URLSearchParams","set","query","toString","path","listIntegrationConnections","workspace_id","listSourceConnections","result","filter","connection","lifecycle_status","createGiteaConnection","body","method","createGithubInstall","createSentryInstall","createLinearInstall","createSlackInstall","completeLinearCallback","token","headers","authorization","completeSlackCallback","connectSentry","listRepositories","cursor","params","updateIntegrationConnection","encodeURIComponent","deleteIntegrationConnection","useIntegrationProvidersQuery","queryKey","queryFn","useSourceConnectionsQuery","enabled","Boolean","useIntegrationConnectionsQuery","useRepositoriesInfiniteQuery","options","trimmedSearch","trim","initialPageParam","undefined","pageParam","args","getNextPageParam","lastPage","next_cursor","useCreateGiteaConnectionMutation","mutationFn","useCreateLinearInstallMutation","useCreateSlackInstallMutation","useCompleteLinearCallbackMutation","useCompleteSlackCallbackMutation","useUpdateIntegrationConnectionMutation","queryClient","onSuccess","invalidateQueries","useDeleteIntegrationConnectionMutation","_workspaceId","_result","variables"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/hooks/api/integrations.ts"],"sourcesContent":["import type {\n IntegrationCapabilityDto,\n IntegrationConnectionDto,\n ListIntegrationConnectionsResponseDto,\n ListIntegrationProvidersResponseDto,\n ListRepositoriesResponseDto,\n UpdateIntegrationConnectionBodyDto,\n} from '@shipfox/api-integration-core-dto';\nimport type {\n CreateGiteaConnectionBodyDto,\n CreateGiteaConnectionResponseDto,\n} from '@shipfox/api-integration-gitea-dto';\nimport type {\n CreateGithubInstallBodyDto,\n CreateGithubInstallResponseDto,\n} from '@shipfox/api-integration-github-dto';\nimport type {\n CreateLinearInstallBodyDto,\n CreateLinearInstallResponseDto,\n LinearCallbackQueryDto,\n LinearCallbackResponseDto,\n} from '@shipfox/api-integration-linear-dto';\nimport type {\n CreateSentryInstallBodyDto,\n CreateSentryInstallResponseDto,\n SentryConnectBodyDto,\n SentryConnectResponseDto,\n} from '@shipfox/api-integration-sentry-dto';\nimport type {\n CreateSlackInstallBodyDto,\n CreateSlackInstallResponseDto,\n SlackCallbackQueryDto,\n SlackCallbackResponseDto,\n} from '@shipfox/api-integration-slack-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {\n type FetchQueryOptions,\n queryOptions,\n useInfiniteQuery,\n useMutation,\n useQuery,\n useQueryClient,\n} from '@tanstack/react-query';\nimport {serializeLinearCallbackQuery} from '#linear-callback.js';\nimport {serializeSlackCallbackQuery} from '#slack-callback.js';\n\nexport const integrationsQueryKeys = {\n all: ['integrations'] as const,\n providers: (capability: IntegrationCapabilityDto | 'all') =>\n [...integrationsQueryKeys.all, 'providers', capability] as const,\n connections: (workspaceId: string, capability: IntegrationCapabilityDto | 'all') =>\n [...integrationsQueryKeys.all, 'connections', workspaceId, capability] as const,\n // Prefix matching every per-capability connections query for a workspace, so\n // a mutation can refresh all connection views without touching providers or\n // repositories.\n connectionsByWorkspace: (workspaceId: string) =>\n [...integrationsQueryKeys.all, 'connections', workspaceId] as const,\n sourceConnections: (workspaceId: string) =>\n integrationsQueryKeys.connections(workspaceId, 'source_control'),\n repositories: (connectionId: string, search: string) =>\n [...integrationsQueryKeys.all, 'repositories', connectionId, search] as const,\n};\n\ntype SourceConnectionsQueryKey =\n | ReturnType<typeof integrationsQueryKeys.sourceConnections>\n | readonly ['integrations', 'source-connections'];\n\ntype SourceConnectionsQueryOptions = FetchQueryOptions<\n ListIntegrationConnectionsResponseDto,\n Error,\n ListIntegrationConnectionsResponseDto,\n SourceConnectionsQueryKey\n>;\n\nexport async function listIntegrationProviders({\n capability,\n signal,\n}: {\n capability?: IntegrationCapabilityDto;\n signal?: AbortSignal;\n}) {\n const search = new URLSearchParams();\n if (capability) search.set('capability', capability);\n const query = search.toString();\n const path = query ? `/integration-providers?${query}` : '/integration-providers';\n return await apiRequest<ListIntegrationProvidersResponseDto>(path, {signal});\n}\n\nexport async function listIntegrationConnections({\n workspaceId,\n capability,\n signal,\n}: {\n workspaceId: string;\n capability?: IntegrationCapabilityDto | undefined;\n signal?: AbortSignal | undefined;\n}) {\n const search = new URLSearchParams({workspace_id: workspaceId});\n if (capability) search.set('capability', capability);\n return await apiRequest<ListIntegrationConnectionsResponseDto>(\n `/integration-connections?${search.toString()}`,\n {signal},\n );\n}\n\nexport async function listSourceConnections({\n workspaceId,\n signal,\n}: {\n workspaceId: string;\n signal?: AbortSignal;\n}) {\n const result = await listIntegrationConnections({\n workspaceId,\n capability: 'source_control',\n signal,\n });\n // The endpoint returns every lifecycle status (the settings hub needs that),\n // but source-control consumers (onboarding redirect, project creation) only\n // act on usable connections — a disabled/error one must read as \"not there\".\n return {\n ...result,\n connections: result.connections.filter(\n (connection) => connection.lifecycle_status === 'active',\n ),\n };\n}\n\nexport function sourceConnectionsQueryOptions(\n workspaceId: string | undefined,\n): SourceConnectionsQueryOptions {\n return queryOptions({\n queryKey: workspaceId\n ? integrationsQueryKeys.sourceConnections(workspaceId)\n : ([...integrationsQueryKeys.all, 'source-connections'] as const),\n enabled: Boolean(workspaceId),\n queryFn: ({signal}) => listSourceConnections({workspaceId: workspaceId ?? '', signal}),\n });\n}\n\nexport async function createGiteaConnection(body: CreateGiteaConnectionBodyDto) {\n return await apiRequest<CreateGiteaConnectionResponseDto>('/integrations/gitea/connections', {\n method: 'POST',\n body,\n });\n}\n\nexport async function createGithubInstall(body: CreateGithubInstallBodyDto) {\n return await apiRequest<CreateGithubInstallResponseDto>('/integrations/github/install', {\n method: 'POST',\n body,\n });\n}\n\nexport async function createSentryInstall(body: CreateSentryInstallBodyDto) {\n return await apiRequest<CreateSentryInstallResponseDto>('/integrations/sentry/install', {\n method: 'POST',\n body,\n });\n}\n\nexport async function createLinearInstall(body: CreateLinearInstallBodyDto) {\n return await apiRequest<CreateLinearInstallResponseDto>('/integrations/linear/install', {\n method: 'POST',\n body,\n });\n}\n\nexport async function createSlackInstall(body: CreateSlackInstallBodyDto) {\n return await apiRequest<CreateSlackInstallResponseDto>('/integrations/slack/install', {\n method: 'POST',\n body,\n });\n}\n\nexport async function completeLinearCallback({\n query,\n token,\n}: {\n query: LinearCallbackQueryDto;\n token: string;\n}) {\n return await apiRequest<LinearCallbackResponseDto>(\n `/integrations/linear/callback/api?${serializeLinearCallbackQuery(query)}`,\n {headers: {authorization: `Bearer ${token}`}},\n );\n}\n\nexport async function completeSlackCallback({\n query,\n token,\n}: {\n query: SlackCallbackQueryDto;\n token: string;\n}) {\n return await apiRequest<SlackCallbackResponseDto>(\n `/integrations/slack/callback/api?${serializeSlackCallbackQuery(query)}`,\n {headers: {authorization: `Bearer ${token}`}},\n );\n}\n\n// Called from the callback route with an explicit bearer (same as the GitHub\n// callback): the route refreshes auth itself before forwarding the grant code.\nexport async function connectSentry({body, token}: {body: SentryConnectBodyDto; token: string}) {\n return await apiRequest<SentryConnectResponseDto>('/integrations/sentry/connect', {\n method: 'POST',\n body,\n headers: {authorization: `Bearer ${token}`},\n });\n}\n\nexport async function listRepositories({\n connectionId,\n cursor,\n search,\n signal,\n}: {\n connectionId: string;\n cursor?: string;\n search?: string;\n signal?: AbortSignal;\n}) {\n const params = new URLSearchParams();\n if (cursor) params.set('cursor', cursor);\n if (search) params.set('search', search);\n const query = params.toString();\n const path = query\n ? `/integration-connections/${connectionId}/repositories?${query}`\n : `/integration-connections/${connectionId}/repositories`;\n return await apiRequest<ListRepositoriesResponseDto>(path, {signal});\n}\n\nexport async function updateIntegrationConnection({\n connectionId,\n body,\n}: {\n connectionId: string;\n body: UpdateIntegrationConnectionBodyDto;\n}) {\n return await apiRequest<IntegrationConnectionDto>(\n `/integration-connections/${encodeURIComponent(connectionId)}`,\n {method: 'PATCH', body},\n );\n}\n\nexport async function deleteIntegrationConnection({connectionId}: {connectionId: string}) {\n await apiRequest<void>(`/integration-connections/${encodeURIComponent(connectionId)}`, {\n method: 'DELETE',\n });\n}\n\nexport function useIntegrationProvidersQuery(params?: {capability?: IntegrationCapabilityDto}) {\n const capability = params?.capability;\n return useQuery({\n queryKey: integrationsQueryKeys.providers(capability ?? 'all'),\n queryFn: ({signal}) => listIntegrationProviders(capability ? {capability, signal} : {signal}),\n });\n}\n\nexport function useSourceConnectionsQuery(workspaceId: string | undefined) {\n return useQuery(sourceConnectionsQueryOptions(workspaceId));\n}\n\nexport function useIntegrationConnectionsQuery(workspaceId: string | undefined) {\n return useQuery({\n queryKey: workspaceId\n ? integrationsQueryKeys.connections(workspaceId, 'all')\n : [...integrationsQueryKeys.all, 'connections'],\n enabled: Boolean(workspaceId),\n queryFn: ({signal}) => listIntegrationConnections({workspaceId: workspaceId ?? '', signal}),\n });\n}\n\nexport function useRepositoriesInfiniteQuery(\n connectionId: string | undefined,\n options?: {search?: string},\n) {\n const trimmedSearch = options?.search?.trim() ?? '';\n return useInfiniteQuery({\n queryKey: connectionId\n ? integrationsQueryKeys.repositories(connectionId, trimmedSearch)\n : [...integrationsQueryKeys.all, 'repositories'],\n enabled: Boolean(connectionId),\n initialPageParam: undefined as string | undefined,\n queryFn: ({pageParam, signal}) => {\n const args: {connectionId: string; cursor?: string; search?: string; signal: AbortSignal} = {\n connectionId: connectionId ?? '',\n signal,\n };\n if (pageParam) args.cursor = pageParam;\n if (trimmedSearch) args.search = trimmedSearch;\n return listRepositories(args);\n },\n getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,\n });\n}\n\nexport function useCreateGiteaConnectionMutation() {\n return useMutation({mutationFn: createGiteaConnection});\n}\n\nexport function useCreateLinearInstallMutation() {\n return useMutation({mutationFn: createLinearInstall});\n}\n\nexport function useCreateSlackInstallMutation() {\n return useMutation({mutationFn: createSlackInstall});\n}\n\nexport function useCompleteLinearCallbackMutation() {\n return useMutation({mutationFn: completeLinearCallback});\n}\n\nexport function useCompleteSlackCallbackMutation() {\n return useMutation({mutationFn: completeSlackCallback});\n}\n\nexport function useUpdateIntegrationConnectionMutation() {\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: updateIntegrationConnection,\n onSuccess: async (connection) => {\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),\n });\n },\n });\n}\n\nexport function useDeleteIntegrationConnectionMutation() {\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: ({\n workspaceId: _workspaceId,\n connectionId,\n }: {\n workspaceId: string;\n connectionId: string;\n }) => deleteIntegrationConnection({connectionId}),\n onSuccess: async (_result, variables) => {\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(variables.workspaceId),\n });\n },\n });\n}\n"],"names":["apiRequest","queryOptions","useInfiniteQuery","useMutation","useQuery","useQueryClient","serializeLinearCallbackQuery","serializeSlackCallbackQuery","integrationsQueryKeys","all","providers","capability","connections","workspaceId","connectionsByWorkspace","sourceConnections","repositories","connectionId","search","listIntegrationProviders","signal","URLSearchParams","set","query","toString","path","listIntegrationConnections","workspace_id","listSourceConnections","result","filter","connection","lifecycle_status","sourceConnectionsQueryOptions","queryKey","enabled","Boolean","queryFn","createGiteaConnection","body","method","createGithubInstall","createSentryInstall","createLinearInstall","createSlackInstall","completeLinearCallback","token","headers","authorization","completeSlackCallback","connectSentry","listRepositories","cursor","params","updateIntegrationConnection","encodeURIComponent","deleteIntegrationConnection","useIntegrationProvidersQuery","useSourceConnectionsQuery","useIntegrationConnectionsQuery","useRepositoriesInfiniteQuery","options","trimmedSearch","trim","initialPageParam","undefined","pageParam","args","getNextPageParam","lastPage","next_cursor","useCreateGiteaConnectionMutation","mutationFn","useCreateLinearInstallMutation","useCreateSlackInstallMutation","useCompleteLinearCallbackMutation","useCompleteSlackCallbackMutation","useUpdateIntegrationConnectionMutation","queryClient","onSuccess","invalidateQueries","useDeleteIntegrationConnectionMutation","_workspaceId","_result","variables"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"linear-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/linear-callback-page.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,kBAAkB,gCAsFjC"}
1
+ {"version":3,"file":"linear-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/linear-callback-page.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,kBAAkB,gCAuFjC"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRefreshAuth } from '@shipfox/client-auth';
3
+ import { createSingleFlight } from '@shipfox/client-ui';
3
4
  import { FullPageLoader } from '@shipfox/react-ui/loader';
4
5
  import { toast } from '@shipfox/react-ui/toast';
5
6
  import { useQueryClient } from '@tanstack/react-query';
@@ -8,12 +9,11 @@ import { useEffect, useMemo, useState } from 'react';
8
9
  import { CallbackStatusShell } from '#components/callback-status-shell.js';
9
10
  import { integrationsQueryKeys, useCompleteLinearCallbackMutation } from '#hooks/api/integrations.js';
10
11
  import { classifyLinearCallbackError, clearLinearInstallWorkspace, parseLinearCallbackQuery, readLinearInstallWorkspace, serializeLinearCallbackQuery } from '#linear-callback.js';
11
- // Keyed by the callback's code+state. This page has no in-place Retry (failures
12
- // route to "Start over", which begins a fresh install with a new state+code), so
13
- // entries are intentionally never evicted: retaining them dedupes StrictMode and
14
- // remount re-runs so the single-use grant code is exchanged at most once. The
15
- // sibling Sentry page evicts on settle only because its Retry replays the code.
16
- const callbackRequests = new Map();
12
+ // Retain only recent completions: this bounds long-lived callback pages while
13
+ // still covering StrictMode and immediate Back/Forward remounts.
14
+ const callbackRequests = createSingleFlight({
15
+ maxTerminalResults: 32
16
+ });
17
17
  // Keeps the success toast firing once per distinct callback even though the
18
18
  // effect re-runs against the cached request as the mutation identity churns.
19
19
  const toastedCallbacks = new Set();
@@ -40,14 +40,10 @@ export function LinearCallbackPage() {
40
40
  if (!params) return;
41
41
  let disposed = false;
42
42
  const key = serializeLinearCallbackQuery(params);
43
- let request = callbackRequests.get(key);
44
- if (!request) {
45
- request = refreshAuth().then(async (session)=>await completeLinearCallback({
43
+ const request = callbackRequests.run(key, async ()=>await refreshAuth().then(async (session)=>await completeLinearCallback({
46
44
  query: params,
47
- token: session.token
48
- }));
49
- callbackRequests.set(key, request);
50
- }
45
+ token: session.accessToken
46
+ })));
51
47
  request.then(async (connection)=>{
52
48
  if (disposed) return;
53
49
  try {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pages/linear-callback-page.tsx"],"sourcesContent":["import type {LinearCallbackResponseDto} from '@shipfox/api-integration-linear-dto';\nimport {useRefreshAuth} from '@shipfox/client-auth';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport {integrationsQueryKeys, 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// Keyed by the callback's code+state. This page has no in-place Retry (failures\n// route to \"Start over\", which begins a fresh install with a new state+code), so\n// entries are intentionally never evicted: retaining them dedupes StrictMode and\n// remount re-runs so the single-use grant code is exchanged at most once. The\n// sibling Sentry page evicts on settle only because its Retry replays the code.\nconst callbackRequests = new Map<string, Promise<LinearCallbackResponseDto>>();\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 search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const queryClient = useQueryClient();\n const {mutateAsync: completeLinearCallback} = useCompleteLinearCallbackMutation();\n const params = useMemo(() => parseLinearCallbackQuery(search), [search]);\n const workspaceId = useMemo(() => {\n try {\n return readLinearInstallWorkspace(window.sessionStorage);\n } catch {\n return undefined;\n }\n }, []);\n const [failure, setFailure] = useState<LinearCallbackFailure | undefined>();\n useEffect(() => {\n if (!params) return;\n\n let disposed = false;\n const key = serializeLinearCallbackQuery(params);\n let request = callbackRequests.get(key);\n if (!request) {\n request = refreshAuth().then(\n async (session) => await completeLinearCallback({query: params, token: session.token}),\n );\n callbackRequests.set(key, request);\n }\n\n request.then(\n async (connection) => {\n if (disposed) return;\n try {\n clearLinearInstallWorkspace(window.sessionStorage);\n } catch {\n // The successful API response remains the source of truth for navigation.\n }\n try {\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),\n });\n } catch {\n // Cache refresh is best effort: the successful callback is already committed server-side.\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.workspace_id},\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 }, [completeLinearCallback, navigate, params, queryClient, 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","FullPageLoader","toast","useQueryClient","useNavigate","useSearch","useEffect","useMemo","useState","CallbackStatusShell","integrationsQueryKeys","useCompleteLinearCallbackMutation","classifyLinearCallbackError","clearLinearInstallWorkspace","parseLinearCallbackQuery","readLinearInstallWorkspace","serializeLinearCallbackQuery","callbackRequests","Map","toastedCallbacks","Set","LinearCallbackPage","search","strict","navigate","refreshAuth","queryClient","mutateAsync","completeLinearCallback","params","workspaceId","window","sessionStorage","undefined","failure","setFailure","disposed","key","request","get","then","session","query","token","set","connection","invalidateQueries","queryKey","connectionsByWorkspace","workspace_id","has","add","success","to","wid","replace","error","LinearCallbackFailurePage","title","message","startOver","signIn","aria-label","switchAccount","installPath"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/pages/linear-callback-page.tsx"],"sourcesContent":["import type {LinearCallbackResponseDto} from '@shipfox/api-integration-linear-dto';\nimport {useRefreshAuth} from '@shipfox/client-auth';\nimport {createSingleFlight} 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, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport {integrationsQueryKeys, 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, LinearCallbackResponseDto>({\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 search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const queryClient = useQueryClient();\n const {mutateAsync: completeLinearCallback} = useCompleteLinearCallbackMutation();\n const params = useMemo(() => parseLinearCallbackQuery(search), [search]);\n const workspaceId = useMemo(() => {\n try {\n return readLinearInstallWorkspace(window.sessionStorage);\n } catch {\n return undefined;\n }\n }, []);\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 refreshAuth().then(\n async (session) =>\n await completeLinearCallback({query: params, token: session.accessToken}),\n ),\n );\n\n request.then(\n async (connection) => {\n if (disposed) return;\n try {\n clearLinearInstallWorkspace(window.sessionStorage);\n } catch {\n // The successful API response remains the source of truth for navigation.\n }\n try {\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),\n });\n } catch {\n // Cache refresh is best effort: the successful callback is already committed server-side.\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.workspace_id},\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 }, [completeLinearCallback, navigate, params, queryClient, 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","createSingleFlight","FullPageLoader","toast","useQueryClient","useNavigate","useSearch","useEffect","useMemo","useState","CallbackStatusShell","integrationsQueryKeys","useCompleteLinearCallbackMutation","classifyLinearCallbackError","clearLinearInstallWorkspace","parseLinearCallbackQuery","readLinearInstallWorkspace","serializeLinearCallbackQuery","callbackRequests","maxTerminalResults","toastedCallbacks","Set","LinearCallbackPage","search","strict","navigate","refreshAuth","queryClient","mutateAsync","completeLinearCallback","params","workspaceId","window","sessionStorage","undefined","failure","setFailure","disposed","key","request","run","then","session","query","token","accessToken","connection","invalidateQueries","queryKey","connectionsByWorkspace","workspace_id","has","add","success","to","wid","replace","error","LinearCallbackFailurePage","title","message","startOver","signIn","aria-label","switchAccount","installPath"],"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":"AA4BA,wBAAgB,kBAAkB,uCAuMjC"}
1
+ {"version":3,"file":"sentry-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/sentry-callback-page.tsx"],"names":[],"mappings":"AAwBA,wBAAgB,kBAAkB,uCAiMjC"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useAuthState, useRefreshAuth } from '@shipfox/client-auth';
3
+ import { createSingleFlight } from '@shipfox/client-ui';
3
4
  import { Button, ButtonLink } from '@shipfox/react-ui/button';
4
5
  import { Callout } from '@shipfox/react-ui/callout';
5
6
  import { Card } from '@shipfox/react-ui/card';
@@ -11,12 +12,7 @@ import { Link, useNavigate, useSearch } from '@tanstack/react-router';
11
12
  import { useEffect, useMemo, useRef, useState } from 'react';
12
13
  import { connectSentry, integrationsQueryKeys } from '#hooks/api/integrations.js';
13
14
  import { classifySentryConnectError, clearSentryInstallWorkspace, parseSentryCallbackParams, preselectSentryWorkspace, readSentryInstallWorkspace } from '#sentry-callback.js';
14
- // De-dupes concurrent attempts for the same installation+workspace+code
15
- // (double click, remount while in flight). Entries are evicted once the
16
- // request settles, so Retry, and any later attempt carrying a fresh grant
17
- // code, always issues a genuinely new request instead of replaying a cached
18
- // outcome.
19
- const connectRequests = new Map();
15
+ const connectRequests = createSingleFlight();
20
16
  export function SentryCallbackPage() {
21
17
  const search = useSearch({
22
18
  strict: false
@@ -101,24 +97,14 @@ export function SentryCallbackPage() {
101
97
  setConnectingId(workspaceId);
102
98
  setFailure(undefined);
103
99
  const key = `${params.installationId}|${workspaceId}|${params.code}`;
104
- let request = connectRequests.get(key);
105
- if (!request) {
106
- request = refreshAuth().then((session)=>connectSentry({
100
+ const request = connectRequests.run(key, async ()=>await refreshAuth().then((session)=>connectSentry({
107
101
  body: {
108
102
  workspace_id: workspaceId,
109
103
  code: params.code,
110
104
  installation_id: params.installationId
111
105
  },
112
- token: session.token
113
- }));
114
- // Evict on settle (success or failure) so the key never replays a stale
115
- // outcome. Two-arg `then` keeps this cleanup branch from surfacing the
116
- // rejection as unhandled — the main chain below owns the user-facing
117
- // failure handling.
118
- const evict = ()=>connectRequests.delete(key);
119
- request.then(evict, evict);
120
- connectRequests.set(key, request);
121
- }
106
+ token: session.accessToken
107
+ })));
122
108
  request.then(async ()=>{
123
109
  clearSentryInstallWorkspace(window.sessionStorage);
124
110
  await queryClient.invalidateQueries({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pages/sentry-callback-page.tsx"],"sourcesContent":["import type {SentryConnectResponseDto} from '@shipfox/api-integration-sentry-dto';\nimport {useAuthState, useRefreshAuth} from '@shipfox/client-auth';\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, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {connectSentry, integrationsQueryKeys} 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\n// De-dupes concurrent attempts for the same installation+workspace+code\n// (double click, remount while in flight). Entries are evicted once the\n// request settles, so Retry, and any later attempt carrying a fresh grant\n// code, always issues a genuinely new request instead of replaying a cached\n// outcome.\nconst connectRequests = new Map<string, Promise<SentryConnectResponseDto>>();\n\nexport function SentryCallbackPage() {\n const search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const {workspaces, isLoading} = useAuthState();\n const queryClient = useQueryClient();\n\n const params = useMemo(() => parseSentryCallbackParams(search), [search]);\n const storedWorkspaceId = useMemo(() => readSentryInstallWorkspace(window.sessionStorage), []);\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 let request = connectRequests.get(key);\n if (!request) {\n request = refreshAuth().then((session) =>\n connectSentry({\n body: {\n workspace_id: workspaceId,\n code: params.code,\n installation_id: params.installationId,\n },\n token: session.token,\n }),\n );\n // Evict on settle (success or failure) so the key never replays a stale\n // outcome. Two-arg `then` keeps this cleanup branch from surfacing the\n // rejection as unhandled — the main chain below owns the user-facing\n // failure handling.\n const evict = () => connectRequests.delete(key);\n request.then(evict, evict);\n connectRequests.set(key, request);\n }\n\n request\n .then(async () => {\n clearSentryInstallWorkspace(window.sessionStorage);\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(workspaceId),\n });\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(window.sessionStorage);\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","Button","ButtonLink","Callout","Card","FullPageLoader","toast","Header","Text","useQueryClient","Link","useNavigate","useSearch","useEffect","useMemo","useRef","useState","connectSentry","integrationsQueryKeys","classifySentryConnectError","clearSentryInstallWorkspace","parseSentryCallbackParams","preselectSentryWorkspace","readSentryInstallWorkspace","connectRequests","Map","SentryCallbackPage","search","strict","navigate","refreshAuth","workspaces","isLoading","queryClient","params","storedWorkspaceId","window","sessionStorage","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","get","then","session","body","workspace_id","installation_id","token","evict","delete","set","invalidateQueries","queryKey","connectionsByWorkspace","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 type {SentryConnectResponseDto} from '@shipfox/api-integration-sentry-dto';\nimport {useAuthState, useRefreshAuth} from '@shipfox/client-auth';\nimport {createSingleFlight} 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, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {connectSentry, integrationsQueryKeys} 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, SentryConnectResponseDto>();\n\nexport function SentryCallbackPage() {\n const search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const {workspaces, isLoading} = useAuthState();\n const queryClient = useQueryClient();\n\n const params = useMemo(() => parseSentryCallbackParams(search), [search]);\n const storedWorkspaceId = useMemo(() => readSentryInstallWorkspace(window.sessionStorage), []);\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 refreshAuth().then((session) =>\n connectSentry({\n body: {\n workspace_id: workspaceId,\n code: params.code,\n installation_id: params.installationId,\n },\n token: session.accessToken,\n }),\n ),\n );\n\n request\n .then(async () => {\n clearSentryInstallWorkspace(window.sessionStorage);\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(workspaceId),\n });\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(window.sessionStorage);\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","createSingleFlight","Button","ButtonLink","Callout","Card","FullPageLoader","toast","Header","Text","useQueryClient","Link","useNavigate","useSearch","useEffect","useMemo","useRef","useState","connectSentry","integrationsQueryKeys","classifySentryConnectError","clearSentryInstallWorkspace","parseSentryCallbackParams","preselectSentryWorkspace","readSentryInstallWorkspace","connectRequests","SentryCallbackPage","search","strict","navigate","refreshAuth","workspaces","isLoading","queryClient","params","storedWorkspaceId","window","sessionStorage","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","then","session","body","workspace_id","installation_id","token","accessToken","invalidateQueries","queryKey","connectionsByWorkspace","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 +1 @@
1
- {"version":3,"file":"slack-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/slack-callback-page.tsx"],"names":[],"mappings":"AAwBA,wBAAgB,iBAAiB,gCA+FhC"}
1
+ {"version":3,"file":"slack-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/slack-callback-page.tsx"],"names":[],"mappings":"AA2BA,wBAAgB,iBAAiB,gCAgGhC"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRefreshAuth } from '@shipfox/client-auth';
3
+ import { createSingleFlight } from '@shipfox/client-ui';
3
4
  import { FullPageLoader } from '@shipfox/react-ui/loader';
4
5
  import { toast } from '@shipfox/react-ui/toast';
5
6
  import { useQueryClient } from '@tanstack/react-query';
@@ -8,9 +9,11 @@ import { useEffect, useMemo, useState } from 'react';
8
9
  import { CallbackStatusShell } from '#components/callback-status-shell.js';
9
10
  import { integrationsQueryKeys, useCompleteSlackCallbackMutation } from '#hooks/api/integrations.js';
10
11
  import { classifySlackCallbackError, clearSlackInstallWorkspace, parseSlackCallbackQuery, readSlackInstallWorkspace, serializeSlackCallbackQuery } from '#slack-callback.js';
11
- // Requests and committed keys remain for the document lifetime so Strict Mode,
12
- // browser Back, and remounts never replay Slack's single-use grant code.
13
- const callbackRequests = new Map();
12
+ // Retain only recent completions: this bounds long-lived callback pages while
13
+ // still covering StrictMode and immediate Back/Forward remounts.
14
+ const callbackRequests = createSingleFlight({
15
+ maxTerminalResults: 32
16
+ });
14
17
  const completedCallbacks = new Set();
15
18
  const toastedCallbacks = new Set();
16
19
  export function SlackCallbackPage() {
@@ -31,14 +34,10 @@ export function SlackCallbackPage() {
31
34
  if (!params) return;
32
35
  let disposed = false;
33
36
  const key = serializeSlackCallbackQuery(params);
34
- let request = callbackRequests.get(key);
35
- if (!request) {
36
- request = refreshAuth().then(async (session)=>await completeSlackCallback({
37
+ const request = callbackRequests.run(key, async ()=>await refreshAuth().then(async (session)=>await completeSlackCallback({
37
38
  query: params,
38
- token: session.token
39
- }));
40
- callbackRequests.set(key, request);
41
- }
39
+ token: session.accessToken
40
+ })));
42
41
  request.then(async (connection)=>{
43
42
  if (disposed) return;
44
43
  if (completedCallbacks.has(key)) {