@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 +6 -1
- package/dist/src/app.js +13 -4
- package/dist/src/location.d.ts +2 -0
- package/dist/src/location.js +2 -0
- package/dist/src/ports.d.ts +6 -0
- package/package.json +5 -5
- package/src/app.tsx +12 -3
- package/src/location.ts +2 -0
- package/src/ports.ts +6 -0
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
|
-
/**
|
|
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
|
-
|
|
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 });
|
package/dist/src/ports.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
53
|
-
"@loopbacktools/sdk": "0.4.
|
|
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.
|
|
63
|
-
"@loopbacktools/sdk": "^0.4.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
}
|
package/src/location.ts
ADDED
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. */
|