@vertesia/ui 1.6.0-dev.20261008.064737Z → 1.6.0-dev.20261014.015055Z
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 +24 -0
- package/lib/boot/index.d.ts +12 -1
- package/lib/boot/index.d.ts.map +1 -1
- package/lib/boot/index.js +65 -10
- package/lib/boot/index.js.map +1 -1
- package/lib/boot/text.d.ts +13 -0
- package/lib/boot/text.d.ts.map +1 -0
- package/lib/boot/text.js +68 -0
- package/lib/boot/text.js.map +1 -0
- package/lib/core/components/shadcn/button.d.ts +1 -0
- package/lib/core/components/shadcn/button.d.ts.map +1 -1
- package/lib/core/components/shadcn/button.js +2 -2
- package/lib/core/components/shadcn/button.js.map +1 -1
- package/lib/core/components/shadcn/selectBox.d.ts.map +1 -1
- package/lib/core/components/shadcn/selectBox.js +2 -2
- package/lib/core/components/shadcn/selectBox.js.map +1 -1
- package/lib/env/index.d.ts +1 -1
- package/lib/env/index.d.ts.map +1 -1
- package/lib/env/index.js +1 -1
- package/lib/env/index.js.map +1 -1
- package/lib/features/agent/PayloadBuilder.d.ts +6 -0
- package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.js +2 -1
- package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +6 -4
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +2 -253
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts +1 -3
- package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js +2 -8
- package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js +58 -0
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.d.ts +4 -0
- package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.d.ts.map +1 -0
- package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.js +248 -0
- package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.js.map +1 -0
- package/lib/features/agent/chat/SkillWidgetProvider.d.ts.map +1 -1
- package/lib/features/agent/chat/SkillWidgetProvider.js +3 -2
- package/lib/features/agent/chat/SkillWidgetProvider.js.map +1 -1
- package/lib/i18n/LanguageProvider.d.ts +9 -1
- package/lib/i18n/LanguageProvider.d.ts.map +1 -1
- package/lib/i18n/LanguageProvider.js +7 -4
- package/lib/i18n/LanguageProvider.js.map +1 -1
- package/lib/i18n/locales/ar.json +6 -1
- package/lib/i18n/locales/de.json +6 -1
- package/lib/i18n/locales/en.json +6 -1
- package/lib/i18n/locales/es.json +6 -1
- package/lib/i18n/locales/fr.json +6 -1
- package/lib/i18n/locales/it.json +6 -1
- package/lib/i18n/locales/ja.json +6 -1
- package/lib/i18n/locales/ko.json +6 -1
- package/lib/i18n/locales/pt.json +6 -1
- package/lib/i18n/locales/ru.json +6 -1
- package/lib/i18n/locales/tr.json +6 -1
- package/lib/i18n/locales/zh-TW.json +6 -1
- package/lib/i18n/locales/zh.json +6 -1
- package/lib/i18n/rtl.d.ts +4 -2
- package/lib/i18n/rtl.d.ts.map +1 -1
- package/lib/i18n/rtl.js +4 -2
- package/lib/i18n/rtl.js.map +1 -1
- package/lib/session/UserSessionProvider.d.ts.map +1 -1
- package/lib/session/UserSessionProvider.js +10 -4
- package/lib/session/UserSessionProvider.js.map +1 -1
- package/lib/session/auth/composable.d.ts +1 -5
- package/lib/session/auth/composable.d.ts.map +1 -1
- package/lib/session/auth/composable.js +1 -10
- package/lib/session/auth/composable.js.map +1 -1
- package/lib/session/auth/firebase.d.ts.map +1 -1
- package/lib/session/auth/firebase.js +8 -5
- package/lib/session/auth/firebase.js.map +1 -1
- package/lib/session/auth/oauth.d.ts.map +1 -1
- package/lib/session/auth/oauth.js +53 -25
- package/lib/session/auth/oauth.js.map +1 -1
- package/lib/session/auth/vertesiaHosts.d.ts +0 -40
- package/lib/session/auth/vertesiaHosts.d.ts.map +1 -1
- package/lib/session/auth/vertesiaHosts.js +2 -45
- package/lib/session/auth/vertesiaHosts.js.map +1 -1
- package/lib/session/index.d.ts +1 -1
- package/lib/session/index.d.ts.map +1 -1
- package/lib/session/index.js +1 -1
- package/lib/session/index.js.map +1 -1
- package/lib/session/scopeSelection.d.ts +4 -0
- package/lib/session/scopeSelection.d.ts.map +1 -1
- package/lib/session/scopeSelection.js +11 -0
- package/lib/session/scopeSelection.js.map +1 -1
- package/lib/shell/VertesiaShell.d.ts +7 -2
- package/lib/shell/VertesiaShell.d.ts.map +1 -1
- package/lib/shell/VertesiaShell.js +2 -2
- package/lib/shell/VertesiaShell.js.map +1 -1
- package/lib/shell/login/InviteAcceptModal.d.ts.map +1 -1
- package/lib/shell/login/InviteAcceptModal.js +83 -42
- package/lib/shell/login/InviteAcceptModal.js.map +1 -1
- package/lib/vertesia-ui-boot.js +52 -15
- package/lib/vertesia-ui-boot.js.map +1 -1
- package/lib/vertesia-ui-core.js +44 -44
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-env.js +1 -1
- package/lib/vertesia-ui-env.js.map +1 -1
- package/lib/vertesia-ui-features.js +570 -585
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.B1Nd8-dE.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B1Nd8-dE.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.B2Z7mx1x.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B2Z7mx1x.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.B756-YtS.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B756-YtS.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BICdgAEn.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BICdgAEn.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.B_yx0c2A.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B_yx0c2A.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BwPrGiQn.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BwPrGiQn.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CfQbOHm3.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CfQbOHm3.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DMISlF16.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DMISlF16.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.D_WA36Yq.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.D_WA36Yq.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Dp3KuMs-.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Dp3KuMs-.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DsioKoQ3.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DsioKoQ3.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.QEIqEPlv.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.QEIqEPlv.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +2 -2
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-layout.js +15 -15
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-rich-text.js +44 -44
- package/lib/vertesia-ui-rich-text.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +51 -51
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +51 -51
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/package.json +20 -20
- package/src/boot/index.ts +74 -11
- package/src/boot/text.ts +80 -0
- package/src/core/components/shadcn/button.tsx +3 -2
- package/src/core/components/shadcn/selectBox.tsx +12 -4
- package/src/css/index.css +1 -1
- package/src/css/typography.css +111 -88
- package/src/env/index.ts +0 -2
- package/src/features/agent/chat/AskUserWidget.tsx +3 -1
- package/src/features/agent/chat/ModernAgentConversation.tsx +7 -4
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +2 -253
- package/src/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.tsx +2 -8
- package/src/features/agent/chat/ModernAgentOutput/SummaryConversation.ts +60 -0
- package/src/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.ts +248 -0
- package/src/features/agent/chat/SkillWidgetProvider.tsx +3 -2
- package/src/i18n/LanguageProvider.tsx +16 -4
- package/src/i18n/locales/ar.json +6 -1
- package/src/i18n/locales/de.json +6 -1
- package/src/i18n/locales/en.json +6 -1
- package/src/i18n/locales/es.json +6 -1
- package/src/i18n/locales/fr.json +6 -1
- package/src/i18n/locales/it.json +6 -1
- package/src/i18n/locales/ja.json +6 -1
- package/src/i18n/locales/ko.json +6 -1
- package/src/i18n/locales/pt.json +6 -1
- package/src/i18n/locales/ru.json +6 -1
- package/src/i18n/locales/tr.json +6 -1
- package/src/i18n/locales/zh-TW.json +6 -1
- package/src/i18n/locales/zh.json +6 -1
- package/src/i18n/rtl.ts +5 -2
- package/src/session/UserSessionProvider.tsx +11 -4
- package/src/session/auth/composable.ts +1 -11
- package/src/session/auth/firebase.ts +9 -6
- package/src/session/auth/oauth.ts +65 -25
- package/src/session/auth/vertesiaHosts.ts +2 -47
- package/src/session/index.ts +1 -1
- package/src/session/scopeSelection.ts +13 -0
- package/src/shell/VertesiaShell.tsx +8 -2
- package/src/shell/login/InviteAcceptModal.tsx +112 -65
- package/lib/vertesia-ui-i18n.B0orbJb6.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.B0orbJb6.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BChwIQVA.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BChwIQVA.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BTEcAVQm.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BTEcAVQm.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BeJQKbv7.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BeJQKbv7.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BpGr0-5g.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BpGr0-5g.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.Bs8x-1wI.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.Bs8x-1wI.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BymQoOm0.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BymQoOm0.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CNT_wabV.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CNT_wabV.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.Cxh1iVk0.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.Cxh1iVk0.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DLYCM9fV.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DLYCM9fV.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.QvAVguDs.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.QvAVguDs.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.smozeSmI.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.smozeSmI.chunk.js.map +0 -1
|
@@ -1,54 +1,9 @@
|
|
|
1
1
|
const LOOPBACK_IPV4 = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
|
2
2
|
/** A single DNS label: alphanumeric, inner hyphens allowed. */
|
|
3
|
-
|
|
3
|
+
const LABEL = '[a-z0-9](?:[a-z0-9-]*[a-z0-9])?';
|
|
4
4
|
|
|
5
5
|
/** The region label is optional throughout, because each regional host has a global US alias. */
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Hostnames served by the UI backend — Vertesia's own code, not merely a Vertesia-owned name:
|
|
10
|
-
* `cloud.vertesia.io`, `cloud.us1.vertesia.io` — the Cloud UI
|
|
11
|
-
* `preprod.cloud.vertesia.io`, `preview.cloud.us1.vertesia.io` — its preview/preprod tiers
|
|
12
|
-
* `acme.cloud.us1.vertesia.io` — per-IdP-tenant branded hosts
|
|
13
|
-
* `dev-feat-x.ui.dev1.vertesia.io` — dynamic per-branch dev envs
|
|
14
|
-
*
|
|
15
|
-
* Mirrors the load-balancer host rules (region-lb/main.tf: `cloud.${var.domain}`,
|
|
16
|
-
* `*.cloud.${var.domain}`, `*.ui.${var.domain}`) rather than the DNS zone, so a name we merely own
|
|
17
|
-
* is not trusted for what serves it.
|
|
18
|
-
*/
|
|
19
|
-
export const FIRST_PARTY_HOST_PATTERNS: readonly RegExp[] = [
|
|
20
|
-
new RegExp(`^cloud\\.${REGION}vertesia\\.io$`),
|
|
21
|
-
new RegExp(`^${LABEL}\\.cloud\\.${REGION}vertesia\\.io$`),
|
|
22
|
-
new RegExp(`^${LABEL}\\.ui\\.${LABEL}\\.vertesia\\.io$`),
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Public app-gateway origins. Stable tiers use the `apps*` browser-facing host, while development
|
|
27
|
-
* environments reach the gateway through its `*.api.*` Cloud Run route:
|
|
28
|
-
* `apps.us1.vertesia.io`, `apps-preview.us1.vertesia.io`
|
|
29
|
-
* `app-gateway-dev1.api.dev1.vertesia.io`, `app-gateway-us3.api.us3.vertesia.io`
|
|
30
|
-
*
|
|
31
|
-
* The second label is deliberately unconstrained. Branch environments do NOT each get a gateway —
|
|
32
|
-
* they share their region's (`detectGatewayUrl` in packages/server-common/src/env/ServerEnv.ts:444
|
|
33
|
-
* and `getAppgenServerBaseUrl` in packages/workflows/.../app_workspace/workspace-utils.ts), so the
|
|
34
|
-
* host published to browsers is region-scoped, while the per-branch `app-gateway-dev-<branch>`
|
|
35
|
-
* shape in the infrastructure README also exists. Matching `app-gateway-<anything>` covers both,
|
|
36
|
-
* and every Cloud Run service named that way *is* a gateway (region-compute/main.tf:373).
|
|
37
|
-
*
|
|
38
|
-
* These serve every tenant's published application from one shared origin, and published assets
|
|
39
|
-
* need no authentication — so this is generated, low-trust code sitting on a Vertesia-owned
|
|
40
|
-
* hostname. Owning the domain is not the test; which backend serves the origin is.
|
|
41
|
-
*
|
|
42
|
-
* Two load-balancer variables can map further hostnames onto the same backend —
|
|
43
|
-
* `appgen_host_aliases` and `appgen_preview_host_aliases` in
|
|
44
|
-
* infrastructure/terraform/modules/region-lb/variables.tf. Both are empty in every region today; if
|
|
45
|
-
* one is ever set, add the alias here too, or it silently falls through to the permissive branch of
|
|
46
|
-
* Policy L and becomes an origin that may receive a token at any path.
|
|
47
|
-
*/
|
|
48
|
-
export const GATEWAY_HOST_PATTERNS: readonly RegExp[] = [
|
|
49
|
-
new RegExp(`^apps(?:-preview|-preprod)?\\.${REGION}vertesia\\.io$`),
|
|
50
|
-
new RegExp(`^app-gateway-${LABEL}\\.api\\.${LABEL}\\.vertesia\\.io$`),
|
|
51
|
-
];
|
|
6
|
+
const REGION = `(?:${LABEL}\\.)?`;
|
|
52
7
|
|
|
53
8
|
export function isLoopbackHostname(hostname: string): boolean {
|
|
54
9
|
if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
|
package/src/session/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export {
|
|
|
8
8
|
redirectToCentralAuth,
|
|
9
9
|
} from './auth/domainRouting';
|
|
10
10
|
export * from './auth/firebase';
|
|
11
|
-
export { isAppOAuthLoginPending, startAppOAuthLogin } from './auth/oauth';
|
|
11
|
+
export { isAppOAuthLoginPending, revokeAppOAuthSession, startAppOAuthLogin } from './auth/oauth';
|
|
12
12
|
export * from './auth/useAuthState';
|
|
13
13
|
export * from './auth/useCurrentTenant';
|
|
14
14
|
export * from './DevSessionProvider';
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* another tab's next token refresh. `localStorage` holds the last selection made in any tab, and
|
|
7
7
|
* only seeds a tab that has no scope of its own yet (a new tab, or the app after a restart).
|
|
8
8
|
*/
|
|
9
|
+
import { Env } from '@vertesia/ui/env';
|
|
9
10
|
import { LastSelectedAccountId_KEY, LastSelectedProjectId_KEY } from './constants';
|
|
10
11
|
|
|
11
12
|
const TabAccountId_KEY = 'composableai.tabAccountId';
|
|
@@ -52,6 +53,18 @@ export function readScopeSelection(accountId?: string): ScopeSelection {
|
|
|
52
53
|
return { accountId: account, projectId: projectId || undefined };
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
export function resolveAuthSelection(currentUrl: URL): { accountId?: string; projectId?: string } {
|
|
57
|
+
// A URL selection owns the whole pair: never attach an unrelated configured account/project.
|
|
58
|
+
const hasUrlScope = currentUrl.searchParams.has('a') || currentUrl.searchParams.has('p');
|
|
59
|
+
const defaults = hasUrlScope ? undefined : Env.defaultAuthSelection;
|
|
60
|
+
const urlAccount = currentUrl.searchParams.get('a') ?? defaults?.accountId;
|
|
61
|
+
const urlProject = currentUrl.searchParams.get('p') ?? defaults?.projectId;
|
|
62
|
+
// A project alone identifies its account, so the stored selection only fills in a missing project.
|
|
63
|
+
const stored = urlProject === undefined ? readScopeSelection(urlAccount) : undefined;
|
|
64
|
+
|
|
65
|
+
return { accountId: urlAccount ?? stored?.accountId, projectId: urlProject ?? stored?.projectId };
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
/** Make this the tab's scope and the default for new tabs. */
|
|
56
69
|
export function rememberScopeSelection(accountId: string, projectId?: string): void {
|
|
57
70
|
sessionSet(TabAccountId_KEY, accountId);
|
|
@@ -28,8 +28,13 @@ export interface VertesiaShellProps {
|
|
|
28
28
|
suppressSignInErrorPrefixes?: string | string[];
|
|
29
29
|
/** Use an already-issued Vertesia token instead of starting the normal sign-in flow. */
|
|
30
30
|
authToken?: string;
|
|
31
|
-
/**
|
|
31
|
+
/** Language used only when there is neither a saved choice nor a browser language. */
|
|
32
32
|
defaultLanguage?: SupportedLanguage;
|
|
33
|
+
/**
|
|
34
|
+
* Pin the language the shell renders in, whatever the saved choice or browser language.
|
|
35
|
+
* Embedded apps still receive the user's own language (`useLanguage().preferredLanguage`).
|
|
36
|
+
*/
|
|
37
|
+
language?: SupportedLanguage;
|
|
33
38
|
}
|
|
34
39
|
export function VertesiaShell({
|
|
35
40
|
children,
|
|
@@ -41,6 +46,7 @@ export function VertesiaShell({
|
|
|
41
46
|
suppressSignInErrorPrefixes,
|
|
42
47
|
authToken,
|
|
43
48
|
defaultLanguage,
|
|
49
|
+
language,
|
|
44
50
|
authScreens,
|
|
45
51
|
branding,
|
|
46
52
|
}: VertesiaShellProps) {
|
|
@@ -48,7 +54,7 @@ export function VertesiaShell({
|
|
|
48
54
|
const brandedContent = (
|
|
49
55
|
<TypeRegistryProvider>
|
|
50
56
|
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
|
51
|
-
<LanguageProvider defaultLanguage={defaultLanguage}>
|
|
57
|
+
<LanguageProvider defaultLanguage={defaultLanguage} language={language}>
|
|
52
58
|
<LanguageBoundI18nProvider>
|
|
53
59
|
<AppBrandingBoundary branding={branding}>
|
|
54
60
|
<IframeAppContextSync />
|
|
@@ -2,7 +2,12 @@ import type { UserInviteToken } from '@vertesia/common';
|
|
|
2
2
|
import { Button, Modal, ModalBody, ModalTitle } from '@vertesia/ui/core';
|
|
3
3
|
import { useUITranslation } from '@vertesia/ui/i18n';
|
|
4
4
|
import { useUserSession } from '@vertesia/ui/session';
|
|
5
|
-
import { useEffect, useState } from 'react';
|
|
5
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
// Legacy invites have no account data and cannot be shown or accepted here.
|
|
8
|
+
function withAccount(invites: UserInviteToken[]) {
|
|
9
|
+
return invites.filter((i) => i.data.account);
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
export function InviteAcceptModal() {
|
|
8
13
|
const session = useUserSession();
|
|
@@ -10,95 +15,137 @@ export function InviteAcceptModal() {
|
|
|
10
15
|
const { t } = useUITranslation();
|
|
11
16
|
const [showModal, setShowModal] = useState(false);
|
|
12
17
|
const [invites, setInvites] = useState<UserInviteToken[]>([]);
|
|
18
|
+
const [pendingId, setPendingId] = useState<string>();
|
|
19
|
+
const [error, setError] = useState<string>();
|
|
20
|
+
const [acceptedAccountIds, setAcceptedAccountIds] = useState<string[]>([]);
|
|
21
|
+
const isOpen = showModal && invites.length > 0;
|
|
22
|
+
|
|
23
|
+
const loadInvites = useCallback(async () => {
|
|
24
|
+
const valid = withAccount(await client.account.listInvites());
|
|
25
|
+
setInvites(valid);
|
|
26
|
+
return valid;
|
|
27
|
+
}, [client]);
|
|
13
28
|
|
|
14
29
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Filter out legacy invites that do not have account data
|
|
20
|
-
const notLegacyInvites = invites.filter((i) => i.data.account);
|
|
21
|
-
if (notLegacyInvites.length === 0) {
|
|
22
|
-
console.log('No valid invites found, closing modal');
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
// If we have valid invites, show the modal
|
|
26
|
-
console.log('Found valid invites', notLegacyInvites.length);
|
|
30
|
+
loadInvites()
|
|
31
|
+
.then((valid) => {
|
|
32
|
+
if (valid.length > 0) {
|
|
33
|
+
console.log('Found valid invites', valid.length);
|
|
27
34
|
setShowModal(true);
|
|
28
|
-
setInvites(notLegacyInvites);
|
|
29
|
-
} else {
|
|
30
|
-
console.log('No invites found, closing modal');
|
|
31
|
-
setShowModal(false);
|
|
32
35
|
}
|
|
33
36
|
})
|
|
34
37
|
.catch((err) => {
|
|
35
38
|
console.error('Error fetching invites', err);
|
|
36
39
|
});
|
|
37
|
-
}, [
|
|
40
|
+
}, [loadInvites]);
|
|
38
41
|
|
|
39
42
|
const closeModal = () => setShowModal(false);
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
// Refreshing the session re-renders the app around the modal, so it waits until the modal has closed and
|
|
45
|
+
// then picks up every accepted invite at once.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (isOpen || acceptedAccountIds.length === 0) return;
|
|
48
|
+
setAcceptedAccountIds([]);
|
|
49
|
+
void (async () => {
|
|
50
|
+
try {
|
|
51
|
+
// The current token predates the new memberships and does not list the new accounts, so the
|
|
52
|
+
// server rejects reads of them until a fresh token is minted.
|
|
53
|
+
await session.refreshAuthToken();
|
|
54
|
+
await session.fetchAccounts();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('Failed to refresh the session after accepting invites', err);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Each account loads on its own, so one failure does not hide the others.
|
|
60
|
+
for (const accountId of new Set(acceptedAccountIds)) {
|
|
61
|
+
await session.fetchProjects(accountId).catch((err) => {
|
|
62
|
+
console.error('Failed to load projects for an accepted invite', err);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
}, [isOpen, acceptedAccountIds, session]);
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
const removeInvite = (inviteId: string) => setInvites((current) => current.filter((i) => i.id !== inviteId));
|
|
69
|
+
|
|
70
|
+
// An accept or reject can fail because the invite was already used, e.g. from another tab: show the server's list.
|
|
71
|
+
const recoverFromFailure = async (action: string, err: unknown) => {
|
|
72
|
+
console.error(`Failed to ${action} invite`, err);
|
|
73
|
+
setError(t('login.inviteActionFailed'));
|
|
74
|
+
return loadInvites().catch((reloadErr) => {
|
|
75
|
+
console.error('Error fetching invites', reloadErr);
|
|
76
|
+
return undefined;
|
|
77
|
+
});
|
|
54
78
|
};
|
|
55
79
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
const accept = async (invite: UserInviteToken) => {
|
|
81
|
+
setPendingId(invite.id);
|
|
82
|
+
setError(undefined);
|
|
83
|
+
try {
|
|
84
|
+
await client.account.acceptInvite(invite.id);
|
|
85
|
+
removeInvite(invite.id);
|
|
86
|
+
setAcceptedAccountIds((current) => [...current, invite.data.account.id]);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const remaining = await recoverFromFailure('accept', err);
|
|
89
|
+
// A gone invite was most likely accepted after all (e.g. the response was lost), so its account
|
|
90
|
+
// still needs loading. If it was rejected elsewhere instead, that load fails harmlessly.
|
|
91
|
+
if (remaining && !remaining.some((i) => i.id === invite.id)) {
|
|
92
|
+
setAcceptedAccountIds((current) => [...current, invite.data.account.id]);
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
setPendingId(undefined);
|
|
62
96
|
}
|
|
63
97
|
};
|
|
64
98
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
const reject = async (invite: UserInviteToken) => {
|
|
100
|
+
setPendingId(invite.id);
|
|
101
|
+
setError(undefined);
|
|
102
|
+
try {
|
|
103
|
+
await client.account.rejectInvite(invite.id);
|
|
104
|
+
removeInvite(invite.id);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
await recoverFromFailure('reject', err);
|
|
107
|
+
} finally {
|
|
108
|
+
setPendingId(undefined);
|
|
69
109
|
}
|
|
110
|
+
};
|
|
70
111
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
</div>
|
|
81
|
-
<div className="flex flex-col gap-4">
|
|
82
|
-
<Button size={'xs'} onClick={() => accept(invite)}>
|
|
83
|
-
{t('login.accept')}
|
|
84
|
-
</Button>{' '}
|
|
85
|
-
<Button size={'xs'} variant="secondary" onClick={() => reject(invite)}>
|
|
86
|
-
{t('login.reject')}
|
|
87
|
-
</Button>
|
|
88
|
-
</div>
|
|
112
|
+
const inviteList = invites.map((invite) => (
|
|
113
|
+
<div key={invite.id} className="flex flex-row w-full justify-between border rounded-sm px-2 py-2 ">
|
|
114
|
+
<div className="flex flex-col">
|
|
115
|
+
<div className="w-full font-semibold">{invite.data.account.name}</div>
|
|
116
|
+
{invite.data.project && <div className="w-full text-base">- {invite.data.project.name}</div>}
|
|
117
|
+
<div className="text-xs">{t('login.inviteRole', { role: invite.data.role })}</div>
|
|
118
|
+
{invite.data.invited_by && (
|
|
119
|
+
<div className="text-xs">{t('login.invitedBy', { name: invite.data.invited_by.name })}</div>
|
|
120
|
+
)}
|
|
89
121
|
</div>
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
<div className="flex flex-col gap-4">
|
|
123
|
+
<Button
|
|
124
|
+
size={'xs'}
|
|
125
|
+
isDisabled={!!pendingId}
|
|
126
|
+
isLoading={pendingId === invite.id}
|
|
127
|
+
onClick={() => void accept(invite)}
|
|
128
|
+
>
|
|
129
|
+
{t('login.accept')}
|
|
130
|
+
</Button>{' '}
|
|
131
|
+
<Button size={'xs'} variant="secondary" isDisabled={!!pendingId} onClick={() => void reject(invite)}>
|
|
132
|
+
{t('login.reject')}
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
));
|
|
92
137
|
|
|
93
138
|
return (
|
|
94
139
|
<div>
|
|
95
|
-
<Modal isOpen={
|
|
140
|
+
<Modal isOpen={isOpen} onClose={closeModal}>
|
|
96
141
|
<ModalTitle>{t('login.reviewInvites')}</ModalTitle>
|
|
97
142
|
<ModalBody>
|
|
98
|
-
<div className="text-sm pb-4">
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
143
|
+
<div className="text-sm pb-4">{t('login.invitesIntro')}</div>
|
|
144
|
+
{error && (
|
|
145
|
+
<div role="alert" className="text-sm text-destructive pb-4">
|
|
146
|
+
{error}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
102
149
|
{inviteList}
|
|
103
150
|
</ModalBody>
|
|
104
151
|
</Modal>
|