@tinvar/react 0.4.12 → 0.4.14
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/package.json +5 -1
- package/src/client.d.ts +1 -0
- package/src/client.js +1 -0
- package/src/impersonation-banner.d.ts +29 -0
- package/src/impersonation-banner.js +62 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinvar/react",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -158,6 +158,10 @@
|
|
|
158
158
|
"types": "./src/when-mounted.d.ts",
|
|
159
159
|
"default": "./src/when-mounted.js"
|
|
160
160
|
},
|
|
161
|
+
"./impersonation-banner": {
|
|
162
|
+
"types": "./src/impersonation-banner.d.ts",
|
|
163
|
+
"default": "./src/impersonation-banner.js"
|
|
164
|
+
},
|
|
161
165
|
"./testing": {
|
|
162
166
|
"types": "./src/testing.d.ts",
|
|
163
167
|
"default": "./src/testing.js"
|
package/src/client.d.ts
CHANGED
package/src/client.js
CHANGED
|
@@ -40,3 +40,4 @@ export { inlinePayloadStream, payloadRowBytes } from './payload.js';
|
|
|
40
40
|
export { action, actionName, actionUrl, isActionReference, ActionError, ACTION_PREFIX } from './action.js';
|
|
41
41
|
export * from './forms.js';
|
|
42
42
|
export { WhenMounted } from './when-mounted.js';
|
|
43
|
+
export { ImpersonationBanner } from './impersonation-banner.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ActionTarget } from './action';
|
|
3
|
+
|
|
4
|
+
/** What Impersonations::banner() hands a page (ImpersonationBannerData), as it arrives. */
|
|
5
|
+
export interface ImpersonationBannerData {
|
|
6
|
+
/** The user being acted as: their name, or their address without one. */
|
|
7
|
+
actingAs: string;
|
|
8
|
+
/** Who is acting as them, the same way. */
|
|
9
|
+
impersonator: string;
|
|
10
|
+
/** Why, as the impersonator gave it. */
|
|
11
|
+
reason?: string;
|
|
12
|
+
/** When it ends by itself. */
|
|
13
|
+
until?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ImpersonationBannerProps {
|
|
17
|
+
/** The page's impersonation, from shared data; null or undefined draws nothing. */
|
|
18
|
+
impersonation: ImpersonationBannerData | null | undefined;
|
|
19
|
+
/** The app's action that calls Impersonations::stop() and answers `{ next }`. */
|
|
20
|
+
stop: ActionTarget;
|
|
21
|
+
className?: string;
|
|
22
|
+
stopLabel?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* "You are acting as Ada -- stop" (ADR 0194): a status line and a button
|
|
27
|
+
* that posts the stop action, then loads the page it names.
|
|
28
|
+
*/
|
|
29
|
+
export declare function ImpersonationBanner(props: ImpersonationBannerProps): ReactNode;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// <ImpersonationBanner>: what a page shows while staff act as its user
|
|
4
|
+
// (ADR 0194).
|
|
5
|
+
//
|
|
6
|
+
// The server hands every page the impersonation, as shared data an app
|
|
7
|
+
// makes from Impersonations::banner() -- whom the session acts as, who is
|
|
8
|
+
// acting, why and until when -- or null for every other session. This draws
|
|
9
|
+
// it: a status line naming both, and a button that posts the app's stop
|
|
10
|
+
// action and follows where it says to go, by a document load: the session
|
|
11
|
+
// the browser comes back to is another person's, and nothing the router
|
|
12
|
+
// holds for this one is theirs. Unstyled, as everything in the package is;
|
|
13
|
+
// the app styles it through className, or draws its own from the same data.
|
|
14
|
+
|
|
15
|
+
import { createElement, useState } from 'react';
|
|
16
|
+
import { action } from './action.js';
|
|
17
|
+
import { clearHistory } from './router.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{
|
|
21
|
+
* impersonation: { actingAs: string, impersonator: string, reason?: string, until?: string } | null | undefined,
|
|
22
|
+
* stop: string | { readonly action: string },
|
|
23
|
+
* className?: string,
|
|
24
|
+
* stopLabel?: string,
|
|
25
|
+
* }} props
|
|
26
|
+
*/
|
|
27
|
+
export function ImpersonationBanner({ impersonation, stop, className, stopLabel = 'Stop impersonating' }) {
|
|
28
|
+
const [busy, setBusy] = useState(false);
|
|
29
|
+
const [failed, setFailed] = useState(false);
|
|
30
|
+
if (impersonation === null || impersonation === undefined) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const leave = async () => {
|
|
35
|
+
setBusy(true);
|
|
36
|
+
setFailed(false);
|
|
37
|
+
try {
|
|
38
|
+
const result = await action(stop)({});
|
|
39
|
+
const next = /** @type {{ next?: unknown } | null} */ (result.value)?.next;
|
|
40
|
+
clearHistory();
|
|
41
|
+
window.location.assign(typeof next === 'string' ? next : '/');
|
|
42
|
+
} catch {
|
|
43
|
+
setBusy(false);
|
|
44
|
+
setFailed(true);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const by = impersonation.impersonator === '' ? '' : ` (you are ${impersonation.impersonator})`;
|
|
49
|
+
|
|
50
|
+
return createElement(
|
|
51
|
+
'div',
|
|
52
|
+
{ role: 'status', className, 'data-impersonation': '' },
|
|
53
|
+
createElement('span', null, `You are acting as ${impersonation.actingAs}${by}.`),
|
|
54
|
+
' ',
|
|
55
|
+
createElement(
|
|
56
|
+
'button',
|
|
57
|
+
{ type: 'button', disabled: busy, onClick: () => void leave() },
|
|
58
|
+
busy ? 'Stopping…' : stopLabel,
|
|
59
|
+
),
|
|
60
|
+
failed ? createElement('span', { role: 'alert' }, ' Not stopped. Try again.') : null,
|
|
61
|
+
);
|
|
62
|
+
}
|