@agent-native/toolkit 0.201.1 → 0.202.0-nightly-20261006033736
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/agent-native.eject.json +39 -12
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +292 -23
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/context.d.ts +5 -0
- package/dist/app/agentkit/react/context.d.ts.map +1 -1
- package/dist/app/agentkit/react/context.js +5 -0
- package/dist/app/agentkit/react/context.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +78 -3
- package/dist/app/auth/AuthPage.d.ts.map +1 -1
- package/dist/app/auth/AuthPage.js +7 -14
- package/dist/app/auth/AuthPage.js.map +1 -1
- package/dist/app/auth/server.d.ts +6 -0
- package/dist/app/auth/server.d.ts.map +1 -0
- package/dist/app/auth/server.js +19 -0
- package/dist/app/auth/server.js.map +1 -0
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +164 -83
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/AgentSidebar.d.ts +2 -1
- package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
- package/dist/app/chat/AgentSidebar.js +2 -2
- package/dist/app/chat/AgentSidebar.js.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.js +13 -3
- package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
- package/dist/app/chat/chat/surface-types.d.ts +2 -0
- package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +41 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +41 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +41 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +41 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +41 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +41 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +41 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +41 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +41 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +41 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +41 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +41 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/shared/StarfieldBackground.d.ts +1 -6
- package/dist/app/shared/StarfieldBackground.d.ts.map +1 -1
- package/dist/app/shared/StarfieldBackground.js +1 -368
- package/dist/app/shared/StarfieldBackground.js.map +1 -1
- package/dist/app/shared/WaveBackground.d.ts +5 -0
- package/dist/app/shared/WaveBackground.d.ts.map +1 -0
- package/dist/app/shared/WaveBackground.js +34 -0
- package/dist/app/shared/WaveBackground.js.map +1 -0
- package/dist/app/shared/WebGlWaveBackground.d.ts +9 -0
- package/dist/app/shared/WebGlWaveBackground.d.ts.map +1 -0
- package/dist/app/shared/WebGlWaveBackground.js +433 -0
- package/dist/app/shared/WebGlWaveBackground.js.map +1 -0
- package/dist/app/shared/index.d.ts +1 -0
- package/dist/app/shared/index.d.ts.map +1 -1
- package/dist/app/shared/index.js +1 -0
- package/dist/app/shared/index.js.map +1 -1
- package/dist/app/shared/ocean/bloom-blur.wgsl +43 -0
- package/dist/app/shared/ocean/bloom-blur.wgsl.js +1 -0
- package/dist/app/shared/ocean/bloom-bright.wgsl +21 -0
- package/dist/app/shared/ocean/bloom-bright.wgsl.js +1 -0
- package/dist/app/shared/ocean/bloom-composite.wgsl +40 -0
- package/dist/app/shared/ocean/bloom-composite.wgsl.js +1 -0
- package/dist/app/shared/ocean/brand-colors.d.ts +9 -0
- package/dist/app/shared/ocean/brand-colors.d.ts.map +1 -0
- package/dist/app/shared/ocean/brand-colors.js +45 -0
- package/dist/app/shared/ocean/brand-colors.js.map +1 -0
- package/dist/app/shared/ocean/camera.d.ts +5 -0
- package/dist/app/shared/ocean/camera.d.ts.map +1 -0
- package/dist/app/shared/ocean/camera.js +24 -0
- package/dist/app/shared/ocean/camera.js.map +1 -0
- package/dist/app/shared/ocean/hero-ocean-background.d.ts +8 -0
- package/dist/app/shared/ocean/hero-ocean-background.d.ts.map +1 -0
- package/dist/app/shared/ocean/hero-ocean-background.js +186 -0
- package/dist/app/shared/ocean/hero-ocean-background.js.map +1 -0
- package/dist/app/shared/ocean/ifft-stage.wgsl +36 -0
- package/dist/app/shared/ocean/ifft-stage.wgsl.js +1 -0
- package/dist/app/shared/ocean/initial-spectrum.wgsl +36 -0
- package/dist/app/shared/ocean/initial-spectrum.wgsl.js +1 -0
- package/dist/app/shared/ocean/noise.wgsl +24 -0
- package/dist/app/shared/ocean/noise.wgsl.js +1 -0
- package/dist/app/shared/ocean/normal-foam.wgsl +32 -0
- package/dist/app/shared/ocean/normal-foam.wgsl.js +1 -0
- package/dist/app/shared/ocean/ocean-colors.d.ts +6 -0
- package/dist/app/shared/ocean/ocean-colors.d.ts.map +1 -0
- package/dist/app/shared/ocean/ocean-colors.js +6 -0
- package/dist/app/shared/ocean/ocean-colors.js.map +1 -0
- package/dist/app/shared/ocean/ocean-common.wgsl +15 -0
- package/dist/app/shared/ocean/ocean-common.wgsl.js +1 -0
- package/dist/app/shared/ocean/ocean-graph.d.ts +12 -0
- package/dist/app/shared/ocean/ocean-graph.d.ts.map +1 -0
- package/dist/app/shared/ocean/ocean-graph.js +16 -0
- package/dist/app/shared/ocean/ocean-graph.js.map +1 -0
- package/dist/app/shared/ocean/particles.wgsl +139 -0
- package/dist/app/shared/ocean/particles.wgsl.js +1 -0
- package/dist/app/shared/ocean/present.wgsl +40 -0
- package/dist/app/shared/ocean/present.wgsl.js +1 -0
- package/dist/app/shared/ocean/renderer.d.ts +66 -0
- package/dist/app/shared/ocean/renderer.d.ts.map +1 -0
- package/dist/app/shared/ocean/renderer.js +535 -0
- package/dist/app/shared/ocean/renderer.js.map +1 -0
- package/dist/app/shared/ocean/spectrum.wgsl +35 -0
- package/dist/app/shared/ocean/spectrum.wgsl.js +1 -0
- package/dist/app/shared/ocean/tuning.d.ts +48 -0
- package/dist/app/shared/ocean/tuning.d.ts.map +1 -0
- package/dist/app/shared/ocean/tuning.js +74 -0
- package/dist/app/shared/ocean/tuning.js.map +1 -0
- package/dist/app/shared/ocean/webgpu-support.d.ts +3 -0
- package/dist/app/shared/ocean/webgpu-support.d.ts.map +1 -0
- package/dist/app/shared/ocean/webgpu-support.js +13 -0
- package/dist/app/shared/ocean/webgpu-support.js.map +1 -0
- package/dist/app/sharing/AccessRequestApprovalPage.d.ts +23 -0
- package/dist/app/sharing/AccessRequestApprovalPage.d.ts.map +1 -0
- package/dist/app/sharing/AccessRequestApprovalPage.js +95 -0
- package/dist/app/sharing/AccessRequestApprovalPage.js.map +1 -0
- package/dist/app/sharing/AccessRequestsSection.d.ts +5 -0
- package/dist/app/sharing/AccessRequestsSection.d.ts.map +1 -0
- package/dist/app/sharing/AccessRequestsSection.js +47 -0
- package/dist/app/sharing/AccessRequestsSection.js.map +1 -0
- package/dist/app/sharing/ResourceAccessScreen.d.ts +53 -0
- package/dist/app/sharing/ResourceAccessScreen.d.ts.map +1 -0
- package/dist/app/sharing/ResourceAccessScreen.js +132 -0
- package/dist/app/sharing/ResourceAccessScreen.js.map +1 -0
- package/dist/app/sharing/ShareButton.d.ts.map +1 -1
- package/dist/app/sharing/ShareButton.js +4 -2
- package/dist/app/sharing/ShareButton.js.map +1 -1
- package/dist/app/sharing/ShareDialog.d.ts.map +1 -1
- package/dist/app/sharing/ShareDialog.js +7 -5
- package/dist/app/sharing/ShareDialog.js.map +1 -1
- package/dist/app/sharing/index.d.ts +2 -0
- package/dist/app/sharing/index.d.ts.map +1 -1
- package/dist/app/sharing/index.js +2 -0
- package/dist/app/sharing/index.js.map +1 -1
- package/dist/app/styles/agent-native.css +21 -1
- package/dist/app/styles/tokens/agent-kit.css +3 -1
- package/dist/composer/AgentSuggestionBar.d.ts +1 -1
- package/dist/composer/AgentSuggestionBar.d.ts.map +1 -1
- package/dist/composer/AgentSuggestionBar.js +75 -6
- package/dist/composer/AgentSuggestionBar.js.map +1 -1
- package/package.json +14 -4
- package/src/app/agentkit/react/components.tsx +492 -60
- package/src/app/agentkit/react/context.tsx +10 -0
- package/src/app/agentkit/react/lifecycle.spec.tsx +487 -1
- package/src/app/agentkit/react/styles.css +78 -3
- package/src/app/auth/AuthPage.spec.tsx +11 -7
- package/src/app/auth/AuthPage.tsx +16 -35
- package/src/app/auth/server.spec.ts +68 -0
- package/src/app/auth/server.ts +29 -0
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +287 -290
- package/src/app/chat/AgentKitAssistantChat.tsx +219 -116
- package/src/app/chat/AgentSidebar.tsx +3 -0
- package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +85 -0
- package/src/app/chat/agentkit-chat/guided-questions.tsx +12 -5
- package/src/app/chat/chat/surface-types.ts +2 -0
- package/src/app/i18n/catalogs/ar-SA.ts +44 -0
- package/src/app/i18n/catalogs/de-DE.ts +55 -0
- package/src/app/i18n/catalogs/en-US.ts +46 -0
- package/src/app/i18n/catalogs/es-ES.ts +53 -0
- package/src/app/i18n/catalogs/fr-FR.ts +53 -0
- package/src/app/i18n/catalogs/hi-IN.ts +47 -0
- package/src/app/i18n/catalogs/ja-JP.ts +54 -0
- package/src/app/i18n/catalogs/ko-KR.ts +50 -0
- package/src/app/i18n/catalogs/pt-BR.ts +52 -0
- package/src/app/i18n/catalogs/zh-CN.ts +42 -0
- package/src/app/i18n/catalogs/zh-TW.ts +43 -0
- package/src/app/shared/StarfieldBackground.tsx +4 -423
- package/src/app/shared/WaveBackground.spec.tsx +104 -0
- package/src/app/shared/WaveBackground.tsx +60 -0
- package/src/app/shared/WebGlWaveBackground.spec.tsx +106 -0
- package/src/app/shared/WebGlWaveBackground.tsx +499 -0
- package/src/app/shared/index.ts +1 -0
- package/src/app/shared/ocean/bloom-blur.wgsl +43 -0
- package/src/app/shared/ocean/bloom-bright.wgsl +21 -0
- package/src/app/shared/ocean/bloom-composite.wgsl +40 -0
- package/src/app/shared/ocean/brand-colors.ts +49 -0
- package/src/app/shared/ocean/camera.ts +28 -0
- package/src/app/shared/ocean/hero-ocean-background.spec.tsx +369 -0
- package/src/app/shared/ocean/hero-ocean-background.tsx +238 -0
- package/src/app/shared/ocean/ifft-stage.wgsl +36 -0
- package/src/app/shared/ocean/initial-spectrum.wgsl +36 -0
- package/src/app/shared/ocean/noise.wgsl +24 -0
- package/src/app/shared/ocean/normal-foam.wgsl +32 -0
- package/src/app/shared/ocean/ocean-colors.ts +11 -0
- package/src/app/shared/ocean/ocean-common.wgsl +15 -0
- package/src/app/shared/ocean/ocean-graph.ts +29 -0
- package/src/app/shared/ocean/particles.wgsl +139 -0
- package/src/app/shared/ocean/present.wgsl +40 -0
- package/src/app/shared/ocean/renderer.spec.ts +100 -0
- package/src/app/shared/ocean/renderer.ts +693 -0
- package/src/app/shared/ocean/spectrum.wgsl +35 -0
- package/src/app/shared/ocean/tuning.ts +131 -0
- package/src/app/shared/ocean/webgpu-support.ts +12 -0
- package/src/app/sharing/AccessRequestApprovalPage.spec.tsx +276 -0
- package/src/app/sharing/AccessRequestApprovalPage.tsx +307 -0
- package/src/app/sharing/AccessRequestsSection.tsx +163 -0
- package/src/app/sharing/ResourceAccessScreen.spec.tsx +235 -0
- package/src/app/sharing/ResourceAccessScreen.tsx +331 -0
- package/src/app/sharing/ShareButton.spec.tsx +171 -5
- package/src/app/sharing/ShareButton.tsx +15 -2
- package/src/app/sharing/ShareDialog.spec.tsx +71 -2
- package/src/app/sharing/ShareDialog.tsx +24 -2
- package/src/app/sharing/index.ts +2 -0
- package/src/app/styles/agent-native.css +21 -1
- package/src/app/styles/agent-native.spec.ts +16 -3
- package/src/app/styles/tokens/agent-kit.css +3 -1
- package/src/composer/AgentSuggestionBar.overflow.spec.tsx +277 -0
- package/src/composer/AgentSuggestionBar.tsx +113 -8
- package/src/wgsl-env.d.ts +6 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { signOut, useSession } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
3
|
+
import {
|
|
4
|
+
useAccessRequestReview,
|
|
5
|
+
type AccessRequestReview,
|
|
6
|
+
type AccessRequestRole,
|
|
7
|
+
} from "@agent-native/core/client/sharing/useAccessRequestReview";
|
|
8
|
+
import {
|
|
9
|
+
ActionButton,
|
|
10
|
+
Avatar,
|
|
11
|
+
Picker,
|
|
12
|
+
Skeleton,
|
|
13
|
+
Status,
|
|
14
|
+
} from "@agent-native/toolkit/design-system";
|
|
15
|
+
import { cn } from "@agent-native/toolkit/utils";
|
|
16
|
+
import { useEffect, useId, useRef, useState, type ReactNode } from "react";
|
|
17
|
+
import { Link, useParams } from "react-router";
|
|
18
|
+
|
|
19
|
+
import { ResourceAccessScreen } from "./ResourceAccessScreen.js";
|
|
20
|
+
|
|
21
|
+
export interface AccessRequestApprovalPageProps {
|
|
22
|
+
/**
|
|
23
|
+
* The request id from the link in the owner's email or notification.
|
|
24
|
+
* Defaults to the route's `requestId` param.
|
|
25
|
+
*/
|
|
26
|
+
requestId?: string;
|
|
27
|
+
/** Replaces Switch account, which by default signs out. */
|
|
28
|
+
onSwitchAccount?: () => void;
|
|
29
|
+
/** Rendered above the page, such as a sidebar toggle. */
|
|
30
|
+
header?: ReactNode;
|
|
31
|
+
/** Render as the page's `main` landmark when the app shell has none. */
|
|
32
|
+
landmark?: boolean;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ROLES = ["viewer", "commenter", "editor", "admin"] as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Where an access request's email and notification lead: who is asking, for
|
|
40
|
+
* what, and Allow or Decline. Opening it only reads; the decision is always an
|
|
41
|
+
* explicit press. Someone who can't manage the resource sees the same
|
|
42
|
+
* unavailable screen whether or not the request exists.
|
|
43
|
+
*/
|
|
44
|
+
export function AccessRequestApprovalPage({
|
|
45
|
+
requestId,
|
|
46
|
+
onSwitchAccount,
|
|
47
|
+
header,
|
|
48
|
+
landmark = false,
|
|
49
|
+
className,
|
|
50
|
+
}: AccessRequestApprovalPageProps) {
|
|
51
|
+
const t = useT();
|
|
52
|
+
const params = useParams();
|
|
53
|
+
const { session } = useSession();
|
|
54
|
+
const controller = useAccessRequestReview(
|
|
55
|
+
requestId ?? params.requestId ?? "",
|
|
56
|
+
);
|
|
57
|
+
const Root = landmark ? "main" : "div";
|
|
58
|
+
|
|
59
|
+
if (controller.isSignedOut) {
|
|
60
|
+
return (
|
|
61
|
+
<ResourceAccessScreen
|
|
62
|
+
state="signed-out"
|
|
63
|
+
header={header}
|
|
64
|
+
landmark={landmark}
|
|
65
|
+
className={className}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (controller.isUnavailable) {
|
|
71
|
+
return (
|
|
72
|
+
<ResourceAccessScreen
|
|
73
|
+
state="denied"
|
|
74
|
+
title={t("agentChat.accessRequest.unavailableTitle", {
|
|
75
|
+
defaultValue: "You can't review this request",
|
|
76
|
+
})}
|
|
77
|
+
description={t("agentChat.accessRequest.unavailableDescription", {
|
|
78
|
+
defaultValue:
|
|
79
|
+
"It may have been withdrawn, or this account can't manage access.",
|
|
80
|
+
})}
|
|
81
|
+
signedInEmail={session?.email ?? null}
|
|
82
|
+
onSwitchAccount={onSwitchAccount ?? (() => void signOut())}
|
|
83
|
+
header={header}
|
|
84
|
+
landmark={landmark}
|
|
85
|
+
className={className}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Root className={cn("flex min-h-0 flex-1 flex-col", className)}>
|
|
92
|
+
{header}
|
|
93
|
+
<div className="flex min-h-0 flex-1 items-center justify-center bg-background px-6 py-10">
|
|
94
|
+
{controller.review ? (
|
|
95
|
+
<ReviewPanel
|
|
96
|
+
review={controller.review}
|
|
97
|
+
decisions={controller.decisions}
|
|
98
|
+
/>
|
|
99
|
+
) : controller.isError ? (
|
|
100
|
+
<section className="flex w-full max-w-sm flex-col items-center text-center">
|
|
101
|
+
<p role="alert" className="text-sm text-muted-foreground">
|
|
102
|
+
{t("agentChat.accessRequest.loadFailed", {
|
|
103
|
+
defaultValue: "Couldn't load this request.",
|
|
104
|
+
})}
|
|
105
|
+
</p>
|
|
106
|
+
<ActionButton
|
|
107
|
+
emphasis="outline"
|
|
108
|
+
className="mt-4"
|
|
109
|
+
onPress={() => void controller.refetch()}
|
|
110
|
+
>
|
|
111
|
+
{t("agentChat.accessRequest.retry", { defaultValue: "Retry" })}
|
|
112
|
+
</ActionButton>
|
|
113
|
+
</section>
|
|
114
|
+
) : (
|
|
115
|
+
<ReviewSkeleton />
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</Root>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ReviewSkeleton() {
|
|
123
|
+
return (
|
|
124
|
+
<div aria-hidden="true" className="flex w-full max-w-md flex-col gap-6">
|
|
125
|
+
<Skeleton height={32} width="80%" />
|
|
126
|
+
<div className="flex items-center gap-3">
|
|
127
|
+
<Skeleton shape="circle" width={36} height={36} />
|
|
128
|
+
<div className="flex flex-1 flex-col gap-2">
|
|
129
|
+
<Skeleton height={14} width="40%" />
|
|
130
|
+
<Skeleton height={12} width="60%" />
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<Skeleton height={14} width="50%" />
|
|
134
|
+
<div className="flex gap-2">
|
|
135
|
+
<Skeleton height={36} width={128} />
|
|
136
|
+
<Skeleton height={36} width={80} />
|
|
137
|
+
<Skeleton height={36} width={80} />
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ReviewPanel({
|
|
144
|
+
review,
|
|
145
|
+
decisions,
|
|
146
|
+
}: {
|
|
147
|
+
review: AccessRequestReview;
|
|
148
|
+
decisions: ReturnType<typeof useAccessRequestReview>["decisions"];
|
|
149
|
+
}) {
|
|
150
|
+
const t = useT();
|
|
151
|
+
const headingId = useId();
|
|
152
|
+
const headingRef = useRef<HTMLHeadingElement>(null);
|
|
153
|
+
const [role, setRole] = useState<AccessRequestRole>("viewer");
|
|
154
|
+
const name = review.requester.name?.trim() || review.requester.email;
|
|
155
|
+
const deciding = decisions.pendingId === review.id;
|
|
156
|
+
const roleLabel = (value: AccessRequestRole) =>
|
|
157
|
+
({
|
|
158
|
+
viewer: t("agentChat.share.viewer", { defaultValue: "Viewer" }),
|
|
159
|
+
commenter: t("agentChat.share.commenter", { defaultValue: "Commenter" }),
|
|
160
|
+
editor: t("agentChat.share.editor", { defaultValue: "Editor" }),
|
|
161
|
+
admin: t("agentChat.share.admin", { defaultValue: "Admin" }),
|
|
162
|
+
})[value];
|
|
163
|
+
|
|
164
|
+
// A decision removes the buttons that held focus, so the outcome is read
|
|
165
|
+
// from the heading it moves to.
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
headingRef.current?.focus({ preventScroll: true });
|
|
168
|
+
}, [review.state]);
|
|
169
|
+
|
|
170
|
+
// A failure shows as `decisions.error`, and a stale one reloads the request.
|
|
171
|
+
const decide = (run: () => Promise<void>) =>
|
|
172
|
+
void run().catch(() => undefined);
|
|
173
|
+
|
|
174
|
+
const heading =
|
|
175
|
+
review.state === "approved"
|
|
176
|
+
? t("agentChat.accessRequest.approvedTitle", {
|
|
177
|
+
defaultValue: "Access allowed",
|
|
178
|
+
})
|
|
179
|
+
: review.state === "declined"
|
|
180
|
+
? t("agentChat.accessRequest.declinedTitle", {
|
|
181
|
+
defaultValue: "Request declined",
|
|
182
|
+
})
|
|
183
|
+
: t("agentChat.accessRequest.title", {
|
|
184
|
+
name,
|
|
185
|
+
defaultValue: "{{name}} is asking for access",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const error = decisions.error;
|
|
189
|
+
const errorText = error
|
|
190
|
+
? error.errorCode === "access_request_stale"
|
|
191
|
+
? t("agentChat.accessRequest.stale", {
|
|
192
|
+
defaultValue: "Someone already handled this request, or it changed.",
|
|
193
|
+
})
|
|
194
|
+
: (error.message ??
|
|
195
|
+
t("agentChat.accessRequest.decisionFailed", {
|
|
196
|
+
defaultValue: "Couldn't save your decision. Try again.",
|
|
197
|
+
}))
|
|
198
|
+
: null;
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<section
|
|
202
|
+
aria-labelledby={headingId}
|
|
203
|
+
className="flex w-full max-w-md flex-col gap-6"
|
|
204
|
+
data-access-request-state={review.state}
|
|
205
|
+
>
|
|
206
|
+
<h1
|
|
207
|
+
ref={headingRef}
|
|
208
|
+
id={headingId}
|
|
209
|
+
tabIndex={-1}
|
|
210
|
+
className="break-words text-2xl font-semibold tracking-normal text-foreground outline-none"
|
|
211
|
+
>
|
|
212
|
+
{heading}
|
|
213
|
+
</h1>
|
|
214
|
+
|
|
215
|
+
<div className="flex items-center gap-3">
|
|
216
|
+
<Avatar name={name} size="default" className="shrink-0" />
|
|
217
|
+
<div className="min-w-0 flex-1">
|
|
218
|
+
<div className="truncate text-sm font-medium text-foreground">
|
|
219
|
+
{name}
|
|
220
|
+
</div>
|
|
221
|
+
{review.requester.name ? (
|
|
222
|
+
<div className="truncate text-sm text-muted-foreground">
|
|
223
|
+
{review.requester.email}
|
|
224
|
+
</div>
|
|
225
|
+
) : null}
|
|
226
|
+
</div>
|
|
227
|
+
{review.state === "approved" && review.grantedRole ? (
|
|
228
|
+
<Status tone="neutral" size="compact">
|
|
229
|
+
{roleLabel(review.grantedRole)}
|
|
230
|
+
</Status>
|
|
231
|
+
) : null}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div className="text-sm text-muted-foreground">
|
|
235
|
+
<span>{review.resource.label}</span>{" "}
|
|
236
|
+
{review.resource.path ? (
|
|
237
|
+
<Link
|
|
238
|
+
to={review.resource.path}
|
|
239
|
+
className="font-medium text-foreground underline decoration-border underline-offset-4 hover:decoration-foreground"
|
|
240
|
+
>
|
|
241
|
+
{review.resource.title}
|
|
242
|
+
</Link>
|
|
243
|
+
) : (
|
|
244
|
+
<span className="font-medium text-foreground">
|
|
245
|
+
{review.resource.title}
|
|
246
|
+
</span>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{review.note ? (
|
|
251
|
+
<blockquote className="whitespace-pre-wrap break-words border-s-2 border-border ps-3 text-sm text-foreground">
|
|
252
|
+
{review.note}
|
|
253
|
+
</blockquote>
|
|
254
|
+
) : null}
|
|
255
|
+
|
|
256
|
+
{review.state === "pending" ? (
|
|
257
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
258
|
+
<Picker
|
|
259
|
+
mode="select"
|
|
260
|
+
options={ROLES.map((value) => ({
|
|
261
|
+
value,
|
|
262
|
+
label: roleLabel(value),
|
|
263
|
+
}))}
|
|
264
|
+
value={role}
|
|
265
|
+
onChange={(value) => {
|
|
266
|
+
if (value && (ROLES as readonly string[]).includes(value)) {
|
|
267
|
+
setRole(value as AccessRequestRole);
|
|
268
|
+
}
|
|
269
|
+
}}
|
|
270
|
+
disabled={deciding}
|
|
271
|
+
aria-label={t("agentChat.share.role", { defaultValue: "Role" })}
|
|
272
|
+
className="w-auto"
|
|
273
|
+
/>
|
|
274
|
+
<ActionButton
|
|
275
|
+
pending={deciding}
|
|
276
|
+
disabled={deciding}
|
|
277
|
+
onPress={() => decide(() => decisions.approve(review, role))}
|
|
278
|
+
>
|
|
279
|
+
{t("agentChat.accessRequest.allow", { defaultValue: "Allow" })}
|
|
280
|
+
</ActionButton>
|
|
281
|
+
<ActionButton
|
|
282
|
+
emphasis="outline"
|
|
283
|
+
disabled={deciding}
|
|
284
|
+
onPress={() => decide(() => decisions.decline(review))}
|
|
285
|
+
>
|
|
286
|
+
{t("agentChat.accessRequest.decline", { defaultValue: "Decline" })}
|
|
287
|
+
</ActionButton>
|
|
288
|
+
</div>
|
|
289
|
+
) : null}
|
|
290
|
+
|
|
291
|
+
{decisions.unemailed?.id === review.id ? (
|
|
292
|
+
<p role="status" className="text-sm text-muted-foreground">
|
|
293
|
+
{t("agentChat.accessRequest.emailFailed", {
|
|
294
|
+
name,
|
|
295
|
+
defaultValue: "{{name}} has access, but we couldn't email them.",
|
|
296
|
+
})}
|
|
297
|
+
</p>
|
|
298
|
+
) : null}
|
|
299
|
+
|
|
300
|
+
{errorText ? (
|
|
301
|
+
<p role="alert" className="text-sm text-destructive">
|
|
302
|
+
{errorText}
|
|
303
|
+
</p>
|
|
304
|
+
) : null}
|
|
305
|
+
</section>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import {
|
|
3
|
+
useResourceAccessRequests,
|
|
4
|
+
type AccessRequestReview,
|
|
5
|
+
} from "@agent-native/core/client/sharing/useAccessRequestReview";
|
|
6
|
+
import {
|
|
7
|
+
ActionButton,
|
|
8
|
+
Avatar as DesignSystemAvatar,
|
|
9
|
+
} from "@agent-native/toolkit/design-system";
|
|
10
|
+
|
|
11
|
+
// Allow here grants Viewer; the request's own page offers every role.
|
|
12
|
+
export function AccessRequestsSection({
|
|
13
|
+
resourceType,
|
|
14
|
+
resourceId,
|
|
15
|
+
}: {
|
|
16
|
+
resourceType: string;
|
|
17
|
+
resourceId: string;
|
|
18
|
+
}) {
|
|
19
|
+
const t = useT();
|
|
20
|
+
const { requests, hasMore, isError, refetch, decisions } =
|
|
21
|
+
useResourceAccessRequests({ resourceType, resourceId });
|
|
22
|
+
const error = decisions.error;
|
|
23
|
+
const unemailed = decisions.unemailed;
|
|
24
|
+
// A decision's outcome stays readable after its row is gone, even when the
|
|
25
|
+
// list's reload fails: a stale decision, or an email that didn't send.
|
|
26
|
+
if (!isError && !requests.length && !error && !unemailed) return null;
|
|
27
|
+
// A failure shows as `decisions.error`, and a stale one reloads the list.
|
|
28
|
+
const decide = (run: () => Promise<void>) =>
|
|
29
|
+
void run().catch(() => undefined);
|
|
30
|
+
return (
|
|
31
|
+
<div className="space-y-2">
|
|
32
|
+
{isError ? (
|
|
33
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
34
|
+
<span role="alert">
|
|
35
|
+
{t("agentChat.share.accessRequestsLoadFailed", {
|
|
36
|
+
defaultValue: "Couldn't load access requests.",
|
|
37
|
+
})}
|
|
38
|
+
</span>
|
|
39
|
+
<ActionButton
|
|
40
|
+
emphasis="ghost"
|
|
41
|
+
size="compact"
|
|
42
|
+
onPress={() => void refetch()}
|
|
43
|
+
>
|
|
44
|
+
{t("agentChat.accessRequest.retry", { defaultValue: "Retry" })}
|
|
45
|
+
</ActionButton>
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
<>
|
|
49
|
+
<div className="text-sm font-semibold">
|
|
50
|
+
{t("agentChat.share.accessRequests", {
|
|
51
|
+
defaultValue: "Access requests",
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
{requests.length ? (
|
|
55
|
+
<ul className="m-0 flex list-none flex-col gap-1 p-0">
|
|
56
|
+
{requests.map((request) => (
|
|
57
|
+
<AccessRequestRow
|
|
58
|
+
key={request.id}
|
|
59
|
+
request={request}
|
|
60
|
+
deciding={decisions.pendingId === request.id}
|
|
61
|
+
onAllow={() =>
|
|
62
|
+
decide(() => decisions.approve(request, "viewer"))
|
|
63
|
+
}
|
|
64
|
+
onDecline={() => decide(() => decisions.decline(request))}
|
|
65
|
+
/>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
) : null}
|
|
69
|
+
{hasMore ? (
|
|
70
|
+
<p className="text-xs text-muted-foreground">
|
|
71
|
+
{t("agentChat.share.accessRequestsNewest", {
|
|
72
|
+
count: requests.length,
|
|
73
|
+
defaultValue: "Showing the {{count}} newest requests.",
|
|
74
|
+
})}
|
|
75
|
+
</p>
|
|
76
|
+
) : null}
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
{unemailed ? (
|
|
80
|
+
<p role="status" className="text-xs text-muted-foreground">
|
|
81
|
+
{t("agentChat.accessRequest.emailFailed", {
|
|
82
|
+
name: unemailed.requester.name?.trim() || unemailed.requester.email,
|
|
83
|
+
defaultValue: "{{name}} has access, but we couldn't email them.",
|
|
84
|
+
})}
|
|
85
|
+
</p>
|
|
86
|
+
) : null}
|
|
87
|
+
{error ? (
|
|
88
|
+
<p role="alert" className="text-xs text-destructive">
|
|
89
|
+
{error.errorCode === "access_request_stale"
|
|
90
|
+
? t("agentChat.accessRequest.stale", {
|
|
91
|
+
defaultValue:
|
|
92
|
+
"Someone already handled this request, or it changed.",
|
|
93
|
+
})
|
|
94
|
+
: (error.message ??
|
|
95
|
+
t("agentChat.accessRequest.decisionFailed", {
|
|
96
|
+
defaultValue: "Couldn't save your decision. Try again.",
|
|
97
|
+
}))}
|
|
98
|
+
</p>
|
|
99
|
+
) : null}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function AccessRequestRow({
|
|
105
|
+
request,
|
|
106
|
+
deciding,
|
|
107
|
+
onAllow,
|
|
108
|
+
onDecline,
|
|
109
|
+
}: {
|
|
110
|
+
request: AccessRequestReview;
|
|
111
|
+
deciding: boolean;
|
|
112
|
+
onAllow: () => void;
|
|
113
|
+
onDecline: () => void;
|
|
114
|
+
}) {
|
|
115
|
+
const t = useT();
|
|
116
|
+
const name = request.requester.name?.trim() || request.requester.email;
|
|
117
|
+
return (
|
|
118
|
+
<li className="flex items-start gap-3 px-1 py-1.5 text-sm">
|
|
119
|
+
<DesignSystemAvatar
|
|
120
|
+
name={name}
|
|
121
|
+
size="compact"
|
|
122
|
+
className="mt-0.5 inline-flex h-7 w-7 shrink-0 text-[11px] font-semibold"
|
|
123
|
+
/>
|
|
124
|
+
<div className="min-w-0 flex-1">
|
|
125
|
+
<div className="truncate">{name}</div>
|
|
126
|
+
{request.requester.name ? (
|
|
127
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
128
|
+
{request.requester.email}
|
|
129
|
+
</div>
|
|
130
|
+
) : null}
|
|
131
|
+
{request.note ? (
|
|
132
|
+
<div className="line-clamp-2 break-words text-xs text-muted-foreground">
|
|
133
|
+
{request.note}
|
|
134
|
+
</div>
|
|
135
|
+
) : null}
|
|
136
|
+
</div>
|
|
137
|
+
<ActionButton
|
|
138
|
+
size="compact"
|
|
139
|
+
pending={deciding}
|
|
140
|
+
disabled={deciding}
|
|
141
|
+
onPress={onAllow}
|
|
142
|
+
aria-label={t("agentChat.share.allowRequestFrom", {
|
|
143
|
+
name,
|
|
144
|
+
defaultValue: "Allow {{name}}",
|
|
145
|
+
})}
|
|
146
|
+
>
|
|
147
|
+
{t("agentChat.accessRequest.allow", { defaultValue: "Allow" })}
|
|
148
|
+
</ActionButton>
|
|
149
|
+
<ActionButton
|
|
150
|
+
size="compact"
|
|
151
|
+
emphasis="ghost"
|
|
152
|
+
disabled={deciding}
|
|
153
|
+
onPress={onDecline}
|
|
154
|
+
aria-label={t("agentChat.share.declineRequestFrom", {
|
|
155
|
+
name,
|
|
156
|
+
defaultValue: "Decline {{name}}",
|
|
157
|
+
})}
|
|
158
|
+
>
|
|
159
|
+
{t("agentChat.accessRequest.decline", { defaultValue: "Decline" })}
|
|
160
|
+
</ActionButton>
|
|
161
|
+
</li>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { createToolkitI18nCatalog } from "../i18n.js";
|
|
8
|
+
import {
|
|
9
|
+
ResourceAccessScreen,
|
|
10
|
+
type ResourceAccessScreenProps,
|
|
11
|
+
} from "./ResourceAccessScreen.js";
|
|
12
|
+
|
|
13
|
+
const signInHref = vi.hoisted(() => vi.fn(() => "/_agent-native/sign-in"));
|
|
14
|
+
|
|
15
|
+
vi.mock("@agent-native/core/client/sign-in-return", () => ({
|
|
16
|
+
buildSignInReturnHref: signInHref,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const catalog = createToolkitI18nCatalog({ messages: {} });
|
|
20
|
+
|
|
21
|
+
describe("ResourceAccessScreen", () => {
|
|
22
|
+
let container: HTMLDivElement;
|
|
23
|
+
let root: Root;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
27
|
+
container = document.createElement("div");
|
|
28
|
+
document.body.appendChild(container);
|
|
29
|
+
root = createRoot(container);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
act(() => root.unmount());
|
|
34
|
+
container.remove();
|
|
35
|
+
vi.unstubAllGlobals();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function render(props: ResourceAccessScreenProps, locale = "en-US") {
|
|
39
|
+
act(() => {
|
|
40
|
+
root.render(
|
|
41
|
+
<AgentNativeI18nProvider
|
|
42
|
+
catalog={catalog}
|
|
43
|
+
initialLocale={locale}
|
|
44
|
+
initialPreference={locale}
|
|
45
|
+
persistPreference={false}
|
|
46
|
+
>
|
|
47
|
+
<ResourceAccessScreen {...props} />
|
|
48
|
+
</AgentNativeI18nProvider>,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buttons() {
|
|
54
|
+
return [...container.querySelectorAll("button")].map(
|
|
55
|
+
(button) => button.textContent,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
it("says the viewer has no access, names their account, and focuses the heading", () => {
|
|
60
|
+
const onSwitchAccount = vi.fn();
|
|
61
|
+
render({
|
|
62
|
+
state: "denied",
|
|
63
|
+
signedInEmail: "outsider@example.test",
|
|
64
|
+
actions: <a href="/home">Go to my pages</a>,
|
|
65
|
+
onSwitchAccount,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const heading = container.querySelector("h1");
|
|
69
|
+
expect(heading?.textContent).toBe("You don't have access");
|
|
70
|
+
expect(document.activeElement).toBe(heading);
|
|
71
|
+
expect(
|
|
72
|
+
container.querySelector("section")?.getAttribute("aria-labelledby"),
|
|
73
|
+
).toBe(heading?.id);
|
|
74
|
+
expect(container.textContent).toContain(
|
|
75
|
+
"You're signed in as outsider@example.test",
|
|
76
|
+
);
|
|
77
|
+
expect(container.querySelector('[role="status"], [aria-live]')).toBeNull();
|
|
78
|
+
expect(container.querySelector("main")).toBeNull();
|
|
79
|
+
|
|
80
|
+
const switchAccount = [...container.querySelectorAll("button")].find(
|
|
81
|
+
(button) => button.textContent === "Switch account",
|
|
82
|
+
);
|
|
83
|
+
act(() => switchAccount?.click());
|
|
84
|
+
expect(onSwitchAccount).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("puts the app's actions before Switch account", () => {
|
|
88
|
+
render({
|
|
89
|
+
state: "denied",
|
|
90
|
+
actions: <button type="button">Go to my pages</button>,
|
|
91
|
+
onSwitchAccount: () => {},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(buttons()).toEqual(["Go to my pages", "Switch account"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("uses the app's heading and description when given", () => {
|
|
98
|
+
render({
|
|
99
|
+
state: "missing",
|
|
100
|
+
title: "This page doesn't exist",
|
|
101
|
+
description: "Custom description",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(container.querySelector("h1")?.textContent).toBe(
|
|
105
|
+
"This page doesn't exist",
|
|
106
|
+
);
|
|
107
|
+
expect(container.textContent).toContain("Custom description");
|
|
108
|
+
expect(buttons()).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("sends a signed-out visitor to sign-in, which returns to the link", () => {
|
|
112
|
+
const assign = vi.fn();
|
|
113
|
+
vi.stubGlobal("location", { ...window.location, assign });
|
|
114
|
+
render({ state: "signed-out" });
|
|
115
|
+
|
|
116
|
+
expect(container.querySelector("h1")?.textContent).toBe(
|
|
117
|
+
"Sign in to continue",
|
|
118
|
+
);
|
|
119
|
+
const signIn = [...container.querySelectorAll("button")].find(
|
|
120
|
+
(button) => button.textContent === "Sign in",
|
|
121
|
+
);
|
|
122
|
+
act(() => signIn?.click());
|
|
123
|
+
expect(assign).toHaveBeenCalledWith("/_agent-native/sign-in");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("can be the page's main landmark", () => {
|
|
127
|
+
render({ state: "trashed", landmark: true });
|
|
128
|
+
|
|
129
|
+
expect(container.querySelector("main")?.dataset.accessState).toBe(
|
|
130
|
+
"trashed",
|
|
131
|
+
);
|
|
132
|
+
expect(container.querySelector("h1")?.textContent).toBe(
|
|
133
|
+
"This is in the trash",
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
function typeInto(element: HTMLTextAreaElement, value: string) {
|
|
138
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
139
|
+
HTMLTextAreaElement.prototype,
|
|
140
|
+
"value",
|
|
141
|
+
)?.set;
|
|
142
|
+
act(() => {
|
|
143
|
+
setter?.call(element, value);
|
|
144
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function bodyButton(label: string) {
|
|
149
|
+
return [...document.body.querySelectorAll("button")].find(
|
|
150
|
+
(button) => button.textContent === label,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
it("offers Request access with an optional note, announced politely", async () => {
|
|
155
|
+
const onRequest = vi.fn(async () => undefined);
|
|
156
|
+
render({
|
|
157
|
+
state: "denied",
|
|
158
|
+
signedInEmail: "outsider@example.test",
|
|
159
|
+
request: { sent: false, onRequest },
|
|
160
|
+
onSwitchAccount: () => {},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(buttons()).toEqual(["Request access", "Switch account"]);
|
|
164
|
+
expect(container.querySelector('[role="status"]')?.textContent).toBe(
|
|
165
|
+
"Request access and the owner will be notified.",
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
act(() => bodyButton("Request access")?.click());
|
|
169
|
+
const note = document.body.querySelector("textarea");
|
|
170
|
+
expect(note).not.toBeNull();
|
|
171
|
+
typeInto(note!, "Need this for the launch review.");
|
|
172
|
+
await act(async () => bodyButton("Send request")?.click());
|
|
173
|
+
|
|
174
|
+
expect(onRequest).toHaveBeenCalledWith("Need this for the launch review.");
|
|
175
|
+
expect(document.body.querySelector("textarea")).toBeNull();
|
|
176
|
+
await vi.waitFor(() => {
|
|
177
|
+
expect(document.activeElement).toBe(container.querySelector("h1"));
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("says the request was sent and stops offering another", () => {
|
|
182
|
+
render({
|
|
183
|
+
state: "denied",
|
|
184
|
+
request: { sent: true, onRequest: vi.fn() },
|
|
185
|
+
onSwitchAccount: () => {},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(container.querySelector('[role="status"]')?.textContent).toBe(
|
|
189
|
+
"Request sent. The owner has been notified.",
|
|
190
|
+
);
|
|
191
|
+
expect(buttons()).toEqual(["Switch account"]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("explains a rate limit apart from other failures", async () => {
|
|
195
|
+
const request = {
|
|
196
|
+
sent: false,
|
|
197
|
+
onRequest: vi.fn(async () => {
|
|
198
|
+
throw new Error("failed");
|
|
199
|
+
}),
|
|
200
|
+
error: { errorCode: "access_request_rate_limited" },
|
|
201
|
+
};
|
|
202
|
+
render({ state: "denied", request });
|
|
203
|
+
|
|
204
|
+
act(() => bodyButton("Request access")?.click());
|
|
205
|
+
await act(async () => bodyButton("Send request")?.click());
|
|
206
|
+
expect(document.body.textContent).toContain(
|
|
207
|
+
"Too many requests right now. Try again later.",
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
render({ state: "denied", request: { ...request, error: null } });
|
|
211
|
+
await act(async () => bodyButton("Send request")?.click());
|
|
212
|
+
expect(document.body.textContent).toContain(
|
|
213
|
+
"Couldn't send your request. Try again.",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("tells a signed-out visitor that signing in is how to ask", () => {
|
|
218
|
+
render({ state: "signed-out", acceptsRequests: true });
|
|
219
|
+
|
|
220
|
+
expect(container.textContent).toContain("Sign in to request access.");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("translates its own strings", async () => {
|
|
224
|
+
render({ state: "denied", signedInEmail: "a@example.test" }, "de-DE");
|
|
225
|
+
|
|
226
|
+
await vi.waitFor(() => {
|
|
227
|
+
expect(container.querySelector("h1")?.textContent).toBe(
|
|
228
|
+
"Du hast keinen Zugriff",
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
expect(container.textContent).toContain(
|
|
232
|
+
"Du bist als a@example.test angemeldet",
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
});
|