@loopbacktools/app 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,6 +28,9 @@ const App = createLoopbackApp({
28
28
  },
29
29
  // Shown at /sign-in; once someone is signed in, the app sends them on to `next`.
30
30
  SignIn: ({ next }) => <YourSignIn returnTo={next} />,
31
+ // Optional: a page of the server's that signing in continues to, loaded in full.
32
+ continueAt: (search) =>
33
+ search.has("client_id") ? `/api/auth/oauth2/authorize?${search}` : undefined,
31
34
  },
32
35
  workspaces: {
33
36
  useList: () => useYourWorkspaces(), // [{ id, name }], or undefined while loading
@@ -63,7 +66,9 @@ it keeps them. The app imports no sign-in library; a test keeps it that way.
63
66
  `useClient` gives for it. The server's links to asks are of this form.
64
67
  - `/w/:workspace/connect`: how to connect an agent. Whether agents sign in with OAuth or send a token
65
68
  comes from the server itself.
66
- - `/sign-in`: the deployment's own sign-in page.
69
+ - `/sign-in`: the deployment's own sign-in page. Once someone is signed in, it sends them on to
70
+ `next`, a path in the app, or to the page `continueAt` names, loaded in full: an OAuth
71
+ authorization an agent started, say.
67
72
  - `/`: the workspace the person prefers (`usePreferred`), or their first. An old link, `/#/<id>`, is
68
73
  followed to that ask.
69
74
  - The deployment's pages. They stand alone, without the inbox's header; `Page` and `Section` give
package/dist/src/app.js CHANGED
@@ -4,9 +4,10 @@ import { useAtomValue } from "@effect/atom-react";
4
4
  import { Inbox, InboxProvider } from "@loopbacktools/inbox";
5
5
  import { Plus, Tray } from "@phosphor-icons/react";
6
6
  import { AsyncResult, Atom } from "effect/reactivity";
7
- import { createContext, forwardRef, use, useCallback, } from "react";
7
+ import { createContext, forwardRef, use, useCallback, useEffect, } from "react";
8
8
  import { BrowserRouter, Link, Navigate, Route, Routes, useLocation, useNavigate, useParams, useSearchParams, } from "react-router";
9
9
  import { Header } from "./header.js";
10
+ import { loadPage } from "./location.js";
10
11
  import { Page, Section } from "./page.js";
11
12
  import { askInOldLink, safeNext, workspacePath } from "./paths.js";
12
13
  function Waiting() {
@@ -52,7 +53,7 @@ const noPreference = () => undefined;
52
53
  * The deployment's hooks are bound here, once: the app's pages call the same ones on every render.
53
54
  */
54
55
  export const createLoopbackApp = (options) => {
55
- const { SignIn } = options.session;
56
+ const { SignIn, continueAt } = options.session;
56
57
  const useSignedIn = options.session.useSignedIn;
57
58
  const useList = options.workspaces.useList;
58
59
  const usePreferred = options.workspaces.usePreferred ?? noPreference;
@@ -71,12 +72,20 @@ export const createLoopbackApp = (options) => {
71
72
  }
72
73
  return _jsx(SignedInContext, { value: signedIn, children: children });
73
74
  }
74
- /** The deployment's sign-in page, until the person is signed in; then where they were going. */
75
+ /**
76
+ * The deployment's sign-in page, until the person is signed in; then where they were going: a
77
+ * page of the server's the deployment names, loaded in full, or else `next`.
78
+ */
75
79
  function SignInPage() {
76
80
  const signedIn = useSignedIn();
77
81
  const [search] = useSearchParams();
78
82
  const next = safeNext(search.get("next"));
79
- if (signedIn === undefined)
83
+ const onward = signedIn == null ? undefined : continueAt?.(search);
84
+ useEffect(() => {
85
+ if (onward !== undefined)
86
+ loadPage(onward);
87
+ }, [onward]);
88
+ if (signedIn === undefined || onward !== undefined)
80
89
  return _jsx(Waiting, {});
81
90
  if (signedIn !== null)
82
91
  return _jsx(Navigate, { to: next, replace: true });
@@ -0,0 +1,2 @@
1
+ /** Loads a page in full, as one of the server's own pages needs, rather than routing to it. */
2
+ export declare const loadPage: (url: string) => void;
@@ -0,0 +1,2 @@
1
+ /** Loads a page in full, as one of the server's own pages needs, rather than routing to it. */
2
+ export const loadPage = (url) => window.location.assign(url);
@@ -21,6 +21,12 @@ export interface Session {
21
21
  readonly SignIn: ComponentType<{
22
22
  readonly next: string;
23
23
  }>;
24
+ /**
25
+ * Where someone at the sign-in page goes once signed in, when it is a page of the server's
26
+ * rather than the app's, as an authorization an agent started is. The app loads it in full, in
27
+ * place of routing to `next`. Given the sign-in page's query; `undefined` for no such page.
28
+ */
29
+ readonly continueAt?: (search: URLSearchParams) => string | undefined;
24
30
  }
25
31
  /** A workspace someone can open: one Loopback space. */
26
32
  export interface WorkspaceSummary {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopbacktools/app",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "The Loopback app: the inbox, routed, with its header and pages, for a deployment to compose with its own sign-in and workspaces.",
5
5
  "homepage": "https://github.com/LoopBackTools/loopback/tree/main/packages/app#readme",
6
6
  "license": "MIT",
@@ -49,8 +49,8 @@
49
49
  "typescript": "7.0.2",
50
50
  "vite": "8.3.5",
51
51
  "vitest": "5.0.5",
52
- "@loopbacktools/inbox": "0.4.0",
53
- "@loopbacktools/sdk": "0.4.0"
52
+ "@loopbacktools/inbox": "0.4.1",
53
+ "@loopbacktools/sdk": "0.4.1"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@cloudflare/kumo": "^2.15.0",
@@ -59,8 +59,8 @@
59
59
  "react": "^19.2.0",
60
60
  "react-dom": "^19.2.0",
61
61
  "react-router": "^8.3.0",
62
- "@loopbacktools/inbox": "^0.4.0",
63
- "@loopbacktools/sdk": "^0.4.0"
62
+ "@loopbacktools/inbox": "^0.4.1",
63
+ "@loopbacktools/sdk": "^0.4.1"
64
64
  },
65
65
  "scripts": {
66
66
  "typecheck": "tsc --noEmit",
package/src/app.tsx CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  type ReactNode,
21
21
  use,
22
22
  useCallback,
23
+ useEffect,
23
24
  } from "react";
24
25
  import {
25
26
  BrowserRouter,
@@ -33,6 +34,7 @@ import {
33
34
  useSearchParams,
34
35
  } from "react-router";
35
36
  import { Header } from "./header.tsx";
37
+ import { loadPage } from "./location.ts";
36
38
  import { Page, Section } from "./page.tsx";
37
39
  import { askInOldLink, safeNext, workspacePath } from "./paths.ts";
38
40
  import type { LoopbackAppOptions, SignedIn as Signed } from "./ports.ts";
@@ -136,7 +138,7 @@ const noPreference = (): string | undefined => undefined;
136
138
  * The deployment's hooks are bound here, once: the app's pages call the same ones on every render.
137
139
  */
138
140
  export const createLoopbackApp = (options: LoopbackAppOptions): ComponentType => {
139
- const { SignIn } = options.session;
141
+ const { SignIn, continueAt } = options.session;
140
142
  const useSignedIn = options.session.useSignedIn;
141
143
  const useList = options.workspaces.useList;
142
144
  const usePreferred = options.workspaces.usePreferred ?? noPreference;
@@ -156,12 +158,19 @@ export const createLoopbackApp = (options: LoopbackAppOptions): ComponentType =>
156
158
  return <SignedInContext value={signedIn}>{children}</SignedInContext>;
157
159
  }
158
160
 
159
- /** The deployment's sign-in page, until the person is signed in; then where they were going. */
161
+ /**
162
+ * The deployment's sign-in page, until the person is signed in; then where they were going: a
163
+ * page of the server's the deployment names, loaded in full, or else `next`.
164
+ */
160
165
  function SignInPage() {
161
166
  const signedIn = useSignedIn();
162
167
  const [search] = useSearchParams();
163
168
  const next = safeNext(search.get("next"));
164
- if (signedIn === undefined) return <Waiting />;
169
+ const onward = signedIn == null ? undefined : continueAt?.(search);
170
+ useEffect(() => {
171
+ if (onward !== undefined) loadPage(onward);
172
+ }, [onward]);
173
+ if (signedIn === undefined || onward !== undefined) return <Waiting />;
165
174
  if (signedIn !== null) return <Navigate to={next} replace />;
166
175
  return <SignIn next={next} />;
167
176
  }
@@ -0,0 +1,2 @@
1
+ /** Loads a page in full, as one of the server's own pages needs, rather than routing to it. */
2
+ export const loadPage = (url: string): void => window.location.assign(url);
package/src/ports.ts CHANGED
@@ -25,6 +25,12 @@ export interface Session {
25
25
  * `next`, the path they were going to.
26
26
  */
27
27
  readonly SignIn: ComponentType<{ readonly next: string }>;
28
+ /**
29
+ * Where someone at the sign-in page goes once signed in, when it is a page of the server's
30
+ * rather than the app's, as an authorization an agent started is. The app loads it in full, in
31
+ * place of routing to `next`. Given the sign-in page's query; `undefined` for no such page.
32
+ */
33
+ readonly continueAt?: (search: URLSearchParams) => string | undefined;
28
34
  }
29
35
 
30
36
  /** A workspace someone can open: one Loopback space. */