@agent-native/toolkit 0.207.1 → 0.207.2-nightly-20261013011333
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/dist/app/blocks/BlockRenderErrorBoundary.d.ts +34 -0
- package/dist/app/blocks/BlockRenderErrorBoundary.d.ts.map +1 -0
- package/dist/app/blocks/BlockRenderErrorBoundary.js +52 -0
- package/dist/app/blocks/BlockRenderErrorBoundary.js.map +1 -0
- package/dist/app/blocks/BlockView.d.ts +2 -1
- package/dist/app/blocks/BlockView.d.ts.map +1 -1
- package/dist/app/blocks/BlockView.js +5 -4
- package/dist/app/blocks/BlockView.js.map +1 -1
- package/dist/app/blocks/RegistryBlockDataProvider.d.ts +2 -1
- package/dist/app/blocks/RegistryBlockDataProvider.d.ts.map +1 -1
- package/dist/app/blocks/RegistryBlockDataProvider.js +5 -3
- package/dist/app/blocks/RegistryBlockDataProvider.js.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +2 -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 +2 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +2 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +2 -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 +2 -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 +2 -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 +2 -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 +2 -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 +2 -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 +2 -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 +2 -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 +2 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/app/org/OrgSwitcher.js +65 -8
- package/dist/app/org/OrgSwitcher.js.map +1 -1
- package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/editor/RegistryBlockNode.js +5 -4
- package/dist/editor/RegistryBlockNode.js.map +1 -1
- package/dist/ui/slider.d.ts +3 -1
- package/dist/ui/slider.d.ts.map +1 -1
- package/dist/ui/slider.js +1 -1
- package/dist/ui/slider.js.map +1 -1
- package/package.json +4 -4
- package/src/app/blocks/BlockRenderErrorBoundary.spec.tsx +225 -0
- package/src/app/blocks/BlockRenderErrorBoundary.tsx +162 -0
- package/src/app/blocks/BlockView.tsx +66 -26
- package/src/app/blocks/RegistryBlockDataProvider.spec.tsx +67 -0
- package/src/app/blocks/RegistryBlockDataProvider.tsx +65 -24
- package/src/app/i18n/catalogs/ar-SA.ts +2 -0
- package/src/app/i18n/catalogs/de-DE.ts +2 -0
- package/src/app/i18n/catalogs/en-US.ts +2 -0
- package/src/app/i18n/catalogs/es-ES.ts +2 -0
- package/src/app/i18n/catalogs/fr-FR.ts +2 -0
- package/src/app/i18n/catalogs/hi-IN.ts +2 -0
- package/src/app/i18n/catalogs/ja-JP.ts +2 -0
- package/src/app/i18n/catalogs/ko-KR.ts +2 -0
- package/src/app/i18n/catalogs/pt-BR.ts +2 -0
- package/src/app/i18n/catalogs/zh-CN.ts +2 -0
- package/src/app/i18n/catalogs/zh-TW.ts +2 -0
- package/src/app/org/OrgSwitcher.spec.tsx +103 -2
- package/src/app/org/OrgSwitcher.tsx +73 -8
- package/src/editor/RegistryBlockNode.tsx +33 -17
- package/src/ui/slider.tsx +8 -3
|
@@ -97,4 +97,71 @@ describe("RegistryBlockDataProvider", () => {
|
|
|
97
97
|
"Portable UI, Core registry",
|
|
98
98
|
);
|
|
99
99
|
});
|
|
100
|
+
|
|
101
|
+
it("contains a registered block renderer failure to its block", () => {
|
|
102
|
+
const registry = new BlockRegistry();
|
|
103
|
+
registry.register(
|
|
104
|
+
defineBlock({
|
|
105
|
+
type: "note",
|
|
106
|
+
schema: z.object({ body: z.string() }),
|
|
107
|
+
mdx: {
|
|
108
|
+
tag: "Note",
|
|
109
|
+
toAttrs: (data) => ({ body: data.body }),
|
|
110
|
+
fromAttrs: (attrs) => ({ body: attrs.string("body") ?? "" }),
|
|
111
|
+
},
|
|
112
|
+
Read: () => {
|
|
113
|
+
throw new Error("renderer failed");
|
|
114
|
+
},
|
|
115
|
+
placement: ["block"],
|
|
116
|
+
label: "Note",
|
|
117
|
+
description: "A note.",
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const block: TestBlock = {
|
|
122
|
+
id: "note-2",
|
|
123
|
+
type: "note",
|
|
124
|
+
data: { body: "unreadable" },
|
|
125
|
+
};
|
|
126
|
+
const value: RegistryBlockDataValue<TestBlock> = {
|
|
127
|
+
editable: false,
|
|
128
|
+
getBlock: (id) => (id === block.id ? block : undefined),
|
|
129
|
+
onBlockDataChange: vi.fn(),
|
|
130
|
+
};
|
|
131
|
+
const logError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
132
|
+
|
|
133
|
+
function Probe() {
|
|
134
|
+
const data = useRegistryBlockData<TestBlock>();
|
|
135
|
+
return (
|
|
136
|
+
data?.renderRegisteredBlock?.(block, {
|
|
137
|
+
blockType: "note",
|
|
138
|
+
editable: false,
|
|
139
|
+
selected: false,
|
|
140
|
+
shellHovered: false,
|
|
141
|
+
panelOpen: false,
|
|
142
|
+
setPanelOpen: vi.fn(),
|
|
143
|
+
onChange: vi.fn(),
|
|
144
|
+
})?.body ?? null
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
act(() => {
|
|
150
|
+
root.render(
|
|
151
|
+
<BlockRegistryProvider registry={registry} ctx={{}}>
|
|
152
|
+
<RegistryBlockDataProvider value={value} interactive>
|
|
153
|
+
<Probe />
|
|
154
|
+
</RegistryBlockDataProvider>
|
|
155
|
+
</BlockRegistryProvider>,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const fallback = container.querySelector("[data-block-render-error]");
|
|
160
|
+
expect(fallback).not.toBeNull();
|
|
161
|
+
expect(fallback?.textContent).not.toContain("renderer failed");
|
|
162
|
+
expect(fallback?.hasAttribute("data-plan-interactive")).toBe(true);
|
|
163
|
+
} finally {
|
|
164
|
+
logError.mockRestore();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
100
167
|
});
|
|
@@ -5,8 +5,13 @@ import {
|
|
|
5
5
|
type RegistryBlockSideMapBlock,
|
|
6
6
|
} from "@agent-native/toolkit/editor/RegistryBlockContext";
|
|
7
7
|
import { IconPencil } from "@tabler/icons-react";
|
|
8
|
+
import { Suspense } from "react";
|
|
8
9
|
import { useCallback, useMemo, type ReactNode } from "react";
|
|
9
10
|
|
|
11
|
+
import {
|
|
12
|
+
BlockRenderErrorBoundary,
|
|
13
|
+
BlockRenderSuspenseFallback,
|
|
14
|
+
} from "./BlockRenderErrorBoundary.js";
|
|
10
15
|
import { blockEditSurface } from "./BlockView.js";
|
|
11
16
|
import { useOptionalBlockRegistry } from "./provider.js";
|
|
12
17
|
import { SchemaBlockEditor } from "./SchemaBlockEditor.js";
|
|
@@ -16,9 +21,11 @@ export function RegistryBlockDataProvider<
|
|
|
16
21
|
>({
|
|
17
22
|
value,
|
|
18
23
|
children,
|
|
24
|
+
interactive = false,
|
|
19
25
|
}: {
|
|
20
26
|
value: RegistryBlockDataValue<TBlock>;
|
|
21
27
|
children: ReactNode;
|
|
28
|
+
interactive?: boolean;
|
|
22
29
|
}) {
|
|
23
30
|
const registryValue = useOptionalBlockRegistry();
|
|
24
31
|
const renderRegisteredBlock = useCallback(
|
|
@@ -35,37 +42,71 @@ export function RegistryBlockDataProvider<
|
|
|
35
42
|
const blockData = block.data;
|
|
36
43
|
const Read = spec.Read;
|
|
37
44
|
let body: ReactNode = (
|
|
38
|
-
<
|
|
39
|
-
data={blockData}
|
|
45
|
+
<BlockRenderErrorBoundary
|
|
40
46
|
blockId={block.id}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
blockType={blockType}
|
|
48
|
+
resetKey={blockData}
|
|
49
|
+
interactive={interactive}
|
|
50
|
+
>
|
|
51
|
+
<Suspense
|
|
52
|
+
fallback={
|
|
53
|
+
<BlockRenderSuspenseFallback
|
|
54
|
+
blockId={block.id}
|
|
55
|
+
blockType={blockType}
|
|
56
|
+
/>
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<Read
|
|
60
|
+
data={blockData}
|
|
61
|
+
blockId={block.id}
|
|
62
|
+
title={block.title}
|
|
63
|
+
summary={block.summary}
|
|
64
|
+
ctx={registryValue.ctx}
|
|
65
|
+
/>
|
|
66
|
+
</Suspense>
|
|
67
|
+
</BlockRenderErrorBoundary>
|
|
45
68
|
);
|
|
46
69
|
let editSurface: ReactNode = null;
|
|
47
70
|
const canEditBlock = options.editable && spec.placement.includes("block");
|
|
48
71
|
if (canEditBlock) {
|
|
49
72
|
const Edit = spec.Edit;
|
|
50
|
-
const editorNode =
|
|
51
|
-
<
|
|
52
|
-
data={blockData}
|
|
53
|
-
onChange={options.onChange}
|
|
54
|
-
editable
|
|
55
|
-
blockId={block.id}
|
|
56
|
-
title={block.title}
|
|
57
|
-
summary={block.summary}
|
|
58
|
-
ctx={registryValue.ctx}
|
|
59
|
-
/>
|
|
60
|
-
) : (
|
|
61
|
-
<SchemaBlockEditor
|
|
62
|
-
data={blockData}
|
|
63
|
-
onChange={(nextData) => options.onChange(nextData)}
|
|
64
|
-
schema={spec.schema}
|
|
65
|
-
editable
|
|
73
|
+
const editorNode = (
|
|
74
|
+
<BlockRenderErrorBoundary
|
|
66
75
|
blockId={block.id}
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
blockType={blockType}
|
|
77
|
+
resetKey={blockData}
|
|
78
|
+
interactive={interactive}
|
|
79
|
+
>
|
|
80
|
+
<Suspense
|
|
81
|
+
fallback={
|
|
82
|
+
<BlockRenderSuspenseFallback
|
|
83
|
+
blockId={block.id}
|
|
84
|
+
blockType={blockType}
|
|
85
|
+
/>
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
{Edit ? (
|
|
89
|
+
<Edit
|
|
90
|
+
data={blockData}
|
|
91
|
+
onChange={options.onChange}
|
|
92
|
+
editable
|
|
93
|
+
blockId={block.id}
|
|
94
|
+
title={block.title}
|
|
95
|
+
summary={block.summary}
|
|
96
|
+
ctx={registryValue.ctx}
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<SchemaBlockEditor
|
|
100
|
+
data={blockData}
|
|
101
|
+
onChange={(nextData) => options.onChange(nextData)}
|
|
102
|
+
schema={spec.schema}
|
|
103
|
+
editable
|
|
104
|
+
blockId={block.id}
|
|
105
|
+
ctx={registryValue.ctx}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</Suspense>
|
|
109
|
+
</BlockRenderErrorBoundary>
|
|
69
110
|
);
|
|
70
111
|
const surface = blockEditSurface(spec);
|
|
71
112
|
if (surface === "panel" && registryValue.ctx.renderEditSurface) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "هذه الكتلة غير متاحة مؤقتًا.",
|
|
5
|
+
"block.loading": "جارٍ تحميل هذه الكتلة.",
|
|
4
6
|
"composer.contextUrlLabel": "عنوان URL",
|
|
5
7
|
"composer.contextInvalidUrl": "أدخل عنوان URL صالحًا يبدأ بـ HTTP أو HTTPS.",
|
|
6
8
|
"composer.contextAttach": "إرفاق",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "Dieser Block ist vorübergehend nicht verfügbar.",
|
|
5
|
+
"block.loading": "Block wird geladen.",
|
|
4
6
|
"composer.contextUrlLabel": "URL-Adresse",
|
|
5
7
|
"composer.contextInvalidUrl": "Gib eine gültige HTTP- oder HTTPS-URL ein.",
|
|
6
8
|
"composer.contextAttach": "Anhängen",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "Este bloque no está disponible temporalmente.",
|
|
5
|
+
"block.loading": "Cargando bloque.",
|
|
4
6
|
"composer.contextUrlLabel": "Dirección URL",
|
|
5
7
|
"composer.contextInvalidUrl": "Introduce una URL HTTP o HTTPS válida.",
|
|
6
8
|
"composer.contextAttach": "Adjuntar",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "Ce bloc est temporairement indisponible.",
|
|
5
|
+
"block.loading": "Chargement du bloc.",
|
|
4
6
|
"composer.contextUrlLabel": "Adresse URL",
|
|
5
7
|
"composer.contextInvalidUrl": "Saisissez une URL HTTP ou HTTPS valide.",
|
|
6
8
|
"composer.contextAttach": "Joindre",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "यह ब्लॉक अस्थायी रूप से उपलब्ध नहीं है।",
|
|
5
|
+
"block.loading": "ब्लॉक लोड हो रहा है।",
|
|
4
6
|
"composer.contextUrlLabel": "यूआरएल",
|
|
5
7
|
"composer.contextInvalidUrl": "मान्य HTTP या HTTPS यूआरएल दर्ज करें।",
|
|
6
8
|
"composer.contextAttach": "अटैच करें",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "このブロックは一時的に利用できません。",
|
|
5
|
+
"block.loading": "ブロックを読み込み中です。",
|
|
4
6
|
"composer.contextUrlLabel": "URL",
|
|
5
7
|
"composer.contextInvalidUrl":
|
|
6
8
|
"有効な HTTP または HTTPS の URL を入力してください。",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "이 블록은 일시적으로 사용할 수 없습니다.",
|
|
5
|
+
"block.loading": "블록을 불러오는 중입니다.",
|
|
4
6
|
"composer.contextUrlLabel": "URL",
|
|
5
7
|
"composer.contextInvalidUrl": "유효한 HTTP 또는 HTTPS URL을 입력하세요.",
|
|
6
8
|
"composer.contextAttach": "첨부",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "Este bloco está temporariamente indisponível.",
|
|
5
|
+
"block.loading": "Carregando bloco.",
|
|
4
6
|
"composer.contextUrlLabel": "Endereço URL",
|
|
5
7
|
"composer.contextInvalidUrl": "Insira uma URL HTTP ou HTTPS válida.",
|
|
6
8
|
"composer.contextAttach": "Anexar",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "此区块暂时不可用。",
|
|
5
|
+
"block.loading": "正在加载区块。",
|
|
4
6
|
"composer.contextUrlLabel": "网址",
|
|
5
7
|
"composer.contextInvalidUrl": "请输入有效的 HTTP 或 HTTPS 网址。",
|
|
6
8
|
"composer.contextAttach": "附加",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolkitAgentChatTranslation } from "../types.js";
|
|
2
2
|
|
|
3
3
|
const messages: ToolkitAgentChatTranslation = {
|
|
4
|
+
"block.renderUnavailable": "此區塊暫時無法使用。",
|
|
5
|
+
"block.loading": "正在載入區塊。",
|
|
4
6
|
"composer.contextUrlLabel": "網址",
|
|
5
7
|
"composer.contextInvalidUrl": "請輸入有效的 HTTP 或 HTTPS 網址。",
|
|
6
8
|
"composer.contextAttach": "附加",
|
|
@@ -11,8 +11,13 @@ const mocks = vi.hoisted(() => {
|
|
|
11
11
|
variables: undefined as unknown,
|
|
12
12
|
mutate: vi.fn(),
|
|
13
13
|
reset: vi.fn(),
|
|
14
|
+
onPersisted: undefined as ((orgId: string | null) => void) | undefined,
|
|
14
15
|
});
|
|
15
|
-
type MutateOptions = {
|
|
16
|
+
type MutateOptions = {
|
|
17
|
+
onError?: () => void;
|
|
18
|
+
onSuccess?: () => void;
|
|
19
|
+
onSettled?: () => void;
|
|
20
|
+
};
|
|
16
21
|
const succeed = (_value: unknown, options?: MutateOptions) => {
|
|
17
22
|
options?.onSuccess?.();
|
|
18
23
|
options?.onSettled?.();
|
|
@@ -47,7 +52,12 @@ vi.mock("@agent-native/core/client/org", () => ({
|
|
|
47
52
|
useCreateOrg: () => mocks.createOrg,
|
|
48
53
|
useJoinByDomain: () => mocks.joinByDomain,
|
|
49
54
|
useOrg: mocks.useOrg,
|
|
50
|
-
useSwitchOrg: (
|
|
55
|
+
useSwitchOrg: (options?: {
|
|
56
|
+
onPersisted?: (orgId: string | null) => void;
|
|
57
|
+
}) => {
|
|
58
|
+
mocks.switchOrg.onPersisted = options?.onPersisted;
|
|
59
|
+
return mocks.switchOrg;
|
|
60
|
+
},
|
|
51
61
|
}));
|
|
52
62
|
|
|
53
63
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
@@ -118,6 +128,7 @@ describe("OrgSwitcher (account menu)", () => {
|
|
|
118
128
|
mutation.mutate.mockReset();
|
|
119
129
|
mutation.mutate.mockImplementation(mocks.succeed);
|
|
120
130
|
mutation.reset.mockReset();
|
|
131
|
+
mutation.onPersisted = undefined;
|
|
121
132
|
}
|
|
122
133
|
mocks.useSession.mockReturnValue({
|
|
123
134
|
session: { email: "owner@example.com", name: "Session Name" },
|
|
@@ -769,6 +780,96 @@ describe("OrgSwitcher (account menu)", () => {
|
|
|
769
780
|
expect(trigger().getAttribute("aria-expanded")).toBe("false");
|
|
770
781
|
});
|
|
771
782
|
|
|
783
|
+
it("closes explicit selections on persistence and leaves reopened menus open", async () => {
|
|
784
|
+
mocks.useOrg.mockReturnValue({ data: ownerOrg, isLoading: false });
|
|
785
|
+
let currentOptions: Parameters<typeof mocks.succeed>[1] = undefined;
|
|
786
|
+
mocks.switchOrg.mutate.mockImplementationOnce((_orgId, options) => {
|
|
787
|
+
currentOptions = options;
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
render(<OrgSwitcher />);
|
|
791
|
+
openMenu();
|
|
792
|
+
await select("Acme");
|
|
793
|
+
|
|
794
|
+
expect(trigger().getAttribute("aria-expanded")).toBe("true");
|
|
795
|
+
act(() => mocks.switchOrg.onPersisted?.("org-1"));
|
|
796
|
+
expect(trigger().getAttribute("aria-expanded")).toBe("false");
|
|
797
|
+
expect(currentOptions?.onSuccess).toBeDefined();
|
|
798
|
+
|
|
799
|
+
openMenu();
|
|
800
|
+
act(() => currentOptions?.onSuccess?.());
|
|
801
|
+
expect(trigger().getAttribute("aria-expanded")).toBe("true");
|
|
802
|
+
|
|
803
|
+
let otherOptions: Parameters<typeof mocks.succeed>[1] = undefined;
|
|
804
|
+
mocks.switchOrg.mutate.mockImplementationOnce((_orgId, options) => {
|
|
805
|
+
otherOptions = options;
|
|
806
|
+
});
|
|
807
|
+
await select("Globex");
|
|
808
|
+
|
|
809
|
+
expect(trigger().getAttribute("aria-expanded")).toBe("true");
|
|
810
|
+
act(() => mocks.switchOrg.onPersisted?.("org-2"));
|
|
811
|
+
expect(trigger().getAttribute("aria-expanded")).toBe("false");
|
|
812
|
+
openMenu();
|
|
813
|
+
act(() => otherOptions?.onSuccess?.());
|
|
814
|
+
expect(trigger().getAttribute("aria-expanded")).toBe("true");
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("retries recovery after a failed Personal selection on the same mount", async () => {
|
|
818
|
+
mocks.useOrg.mockReturnValue({
|
|
819
|
+
data: { ...ownerOrg, orgId: null, orgName: null, role: null },
|
|
820
|
+
isLoading: false,
|
|
821
|
+
});
|
|
822
|
+
const attempts: Array<Parameters<typeof mocks.succeed>[1]> = [];
|
|
823
|
+
mocks.switchOrg.mutate.mockImplementation((_orgId, options) => {
|
|
824
|
+
attempts.push(options);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
render(<OrgSwitcher />);
|
|
828
|
+
expect(attempts).toHaveLength(1);
|
|
829
|
+
openMenu();
|
|
830
|
+
|
|
831
|
+
act(() => attempts[0]?.onError?.());
|
|
832
|
+
act(() => attempts[0]?.onSettled?.());
|
|
833
|
+
expect(attempts).toHaveLength(1);
|
|
834
|
+
|
|
835
|
+
await select("Acme");
|
|
836
|
+
expect(attempts).toHaveLength(2);
|
|
837
|
+
act(() => attempts[1]?.onError?.());
|
|
838
|
+
|
|
839
|
+
expect(attempts).toHaveLength(3);
|
|
840
|
+
expect(attempts[2]?.onSuccess).toBeUndefined();
|
|
841
|
+
expect(attempts[2]?.onSettled).toBeDefined();
|
|
842
|
+
|
|
843
|
+
act(() => attempts[2]?.onError?.());
|
|
844
|
+
act(() => attempts[2]?.onSettled?.());
|
|
845
|
+
expect(attempts).toHaveLength(3);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it("keeps same-target personal recovery separate from explicit selection", async () => {
|
|
849
|
+
mocks.useOrg.mockReturnValue({
|
|
850
|
+
data: {
|
|
851
|
+
...ownerOrg,
|
|
852
|
+
orgId: null,
|
|
853
|
+
orgName: null,
|
|
854
|
+
role: null,
|
|
855
|
+
},
|
|
856
|
+
isLoading: false,
|
|
857
|
+
});
|
|
858
|
+
mocks.switchOrg.mutate.mockImplementationOnce(() => {});
|
|
859
|
+
|
|
860
|
+
render(<OrgSwitcher />);
|
|
861
|
+
expect(mocks.switchOrg.mutate).toHaveBeenCalledWith(
|
|
862
|
+
"org-1",
|
|
863
|
+
expect.objectContaining({ onError: expect.any(Function) }),
|
|
864
|
+
);
|
|
865
|
+
openMenu();
|
|
866
|
+
await select("Acme");
|
|
867
|
+
|
|
868
|
+
expect(mocks.switchOrg.mutate).toHaveBeenCalledTimes(1);
|
|
869
|
+
act(() => mocks.switchOrg.onPersisted?.("org-1"));
|
|
870
|
+
expect(trigger().getAttribute("aria-expanded")).toBe("true");
|
|
871
|
+
});
|
|
872
|
+
|
|
772
873
|
it("accepts invitations and joins domain matches from the menu", async () => {
|
|
773
874
|
mocks.useOrg.mockReturnValue({
|
|
774
875
|
data: {
|
|
@@ -475,7 +475,27 @@ export function OrgSwitcher({
|
|
|
475
475
|
const { session } = useSession();
|
|
476
476
|
const { enabled: demoModeEnabled } = useDemoModeStatus();
|
|
477
477
|
const t = useT();
|
|
478
|
-
const
|
|
478
|
+
const [open, setOpen] = useState(false);
|
|
479
|
+
const pendingOrgSwitchRef = useRef<
|
|
480
|
+
| { kind: "explicit"; orgId: string; closedEarly: boolean }
|
|
481
|
+
| { kind: "personal-recovery"; orgId: string }
|
|
482
|
+
| null
|
|
483
|
+
>(null);
|
|
484
|
+
const switchOrg = useSwitchOrg({
|
|
485
|
+
onPersisted: (orgId) => {
|
|
486
|
+
const pendingSwitch = pendingOrgSwitchRef.current;
|
|
487
|
+
if (
|
|
488
|
+
orgId === null ||
|
|
489
|
+
pendingSwitch?.kind !== "explicit" ||
|
|
490
|
+
pendingSwitch.orgId !== orgId ||
|
|
491
|
+
pendingSwitch.closedEarly
|
|
492
|
+
) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
pendingSwitch.closedEarly = true;
|
|
496
|
+
setOpen(false);
|
|
497
|
+
},
|
|
498
|
+
});
|
|
479
499
|
const createOrg = useCreateOrg();
|
|
480
500
|
const acceptInvitation = useAcceptInvitation();
|
|
481
501
|
const joinByDomain = useJoinByDomain();
|
|
@@ -487,7 +507,6 @@ export function OrgSwitcher({
|
|
|
487
507
|
{ enabled: !!email },
|
|
488
508
|
);
|
|
489
509
|
const avatarUrl = useAvatarUrl(email);
|
|
490
|
-
const [open, setOpen] = useState(false);
|
|
491
510
|
const [view, setView] = useState<MenuView>("main");
|
|
492
511
|
const [createOpen, setCreateOpen] = useState(false);
|
|
493
512
|
const [newName, setNewName] = useState("");
|
|
@@ -513,22 +532,49 @@ export function OrgSwitcher({
|
|
|
513
532
|
// Accounts that picked the retired "Personal" choice have `orgId: null`
|
|
514
533
|
// stored while still holding memberships, which strands them outside the
|
|
515
534
|
// org-scoped Builder.io connection and vault credentials. Move them back.
|
|
516
|
-
//
|
|
517
|
-
//
|
|
535
|
+
// Automatic recovery failures wait for the next org fetch to avoid a loop
|
|
536
|
+
// on mutation state changes. A failed explicit switch from Personal gets one
|
|
537
|
+
// recovery pass because this effect may have skipped the pending selection.
|
|
518
538
|
const personalRecoveryRef = useRef(false);
|
|
539
|
+
const [personalRecoveryRetryEpoch, setPersonalRecoveryRetryEpoch] =
|
|
540
|
+
useState(0);
|
|
519
541
|
const firstMembershipId = org?.orgs?.[0]?.orgId ?? null;
|
|
520
542
|
const switchOrgMutate = switchOrg.mutate;
|
|
521
543
|
useEffect(() => {
|
|
522
|
-
if (
|
|
544
|
+
if (
|
|
545
|
+
org?.orgId ||
|
|
546
|
+
!firstMembershipId ||
|
|
547
|
+
personalRecoveryRef.current ||
|
|
548
|
+
pendingOrgSwitchRef.current
|
|
549
|
+
) {
|
|
523
550
|
return;
|
|
524
551
|
}
|
|
525
552
|
personalRecoveryRef.current = true;
|
|
553
|
+
const recovery = {
|
|
554
|
+
kind: "personal-recovery" as const,
|
|
555
|
+
orgId: firstMembershipId,
|
|
556
|
+
};
|
|
557
|
+
pendingOrgSwitchRef.current = recovery;
|
|
526
558
|
switchOrgMutate(firstMembershipId, {
|
|
527
559
|
onError: () => {
|
|
528
560
|
personalRecoveryRef.current = false;
|
|
561
|
+
if (pendingOrgSwitchRef.current === recovery) {
|
|
562
|
+
pendingOrgSwitchRef.current = null;
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
onSettled: () => {
|
|
566
|
+
if (pendingOrgSwitchRef.current === recovery) {
|
|
567
|
+
pendingOrgSwitchRef.current = null;
|
|
568
|
+
}
|
|
529
569
|
},
|
|
530
570
|
});
|
|
531
|
-
}, [
|
|
571
|
+
}, [
|
|
572
|
+
org?.orgId,
|
|
573
|
+
firstMembershipId,
|
|
574
|
+
switchOrgMutate,
|
|
575
|
+
dataUpdatedAt,
|
|
576
|
+
personalRecoveryRetryEpoch,
|
|
577
|
+
]);
|
|
532
578
|
|
|
533
579
|
const showView = (next: MenuView) => {
|
|
534
580
|
viewChangedRef.current = true;
|
|
@@ -670,8 +716,27 @@ export function OrgSwitcher({
|
|
|
670
716
|
);
|
|
671
717
|
|
|
672
718
|
const selectOrg = (orgId: string) => {
|
|
673
|
-
if (switchOrg.isPending) return;
|
|
674
|
-
|
|
719
|
+
if (switchOrg.isPending || pendingOrgSwitchRef.current) return;
|
|
720
|
+
const selectedFromPersonal = org?.orgId == null;
|
|
721
|
+
const selection = { kind: "explicit" as const, orgId, closedEarly: false };
|
|
722
|
+
pendingOrgSwitchRef.current = selection;
|
|
723
|
+
switchOrg.mutate(orgId, {
|
|
724
|
+
onError: () => {
|
|
725
|
+
if (pendingOrgSwitchRef.current === selection) {
|
|
726
|
+
pendingOrgSwitchRef.current = null;
|
|
727
|
+
if (selectedFromPersonal && !selection.closedEarly) {
|
|
728
|
+
personalRecoveryRef.current = false;
|
|
729
|
+
setPersonalRecoveryRetryEpoch((epoch) => epoch + 1);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
onSuccess: () => {
|
|
734
|
+
if (pendingOrgSwitchRef.current === selection) {
|
|
735
|
+
pendingOrgSwitchRef.current = null;
|
|
736
|
+
}
|
|
737
|
+
if (!selection.closedEarly) setOpen(false);
|
|
738
|
+
},
|
|
739
|
+
});
|
|
675
740
|
};
|
|
676
741
|
const isSwitchingTo = (orgId: string) =>
|
|
677
742
|
switchOrg.isPending && switchOrg.variables === orgId;
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
type MouseEvent as ReactMouseEvent,
|
|
21
21
|
} from "react";
|
|
22
22
|
|
|
23
|
+
import {
|
|
24
|
+
BlockRenderErrorFallback,
|
|
25
|
+
BlockRenderErrorBoundary,
|
|
26
|
+
} from "../app/blocks/BlockRenderErrorBoundary.js";
|
|
23
27
|
import {
|
|
24
28
|
useRegistryBlockData,
|
|
25
29
|
type RegistryBlockDataChangeMeta,
|
|
@@ -178,9 +182,12 @@ export function RegistryBlockNodeView(props: NodeViewProps) {
|
|
|
178
182
|
selectNode(event, true)
|
|
179
183
|
}
|
|
180
184
|
>
|
|
181
|
-
<
|
|
185
|
+
<BlockRenderErrorFallback
|
|
186
|
+
blockId={block.id}
|
|
187
|
+
blockType={blockType}
|
|
182
188
|
message={block.loadError.message}
|
|
183
189
|
rawSource={block.loadError.rawSource}
|
|
190
|
+
interactive
|
|
184
191
|
/>
|
|
185
192
|
</NodeViewWrapper>
|
|
186
193
|
);
|
|
@@ -206,8 +213,26 @@ export function RegistryBlockNodeView(props: NodeViewProps) {
|
|
|
206
213
|
onChange: commitBlockData,
|
|
207
214
|
});
|
|
208
215
|
if (registered) {
|
|
209
|
-
body =
|
|
210
|
-
|
|
216
|
+
body = (
|
|
217
|
+
<BlockRenderErrorBoundary
|
|
218
|
+
blockId={block.id}
|
|
219
|
+
blockType={blockType}
|
|
220
|
+
resetKey={block.data}
|
|
221
|
+
interactive
|
|
222
|
+
>
|
|
223
|
+
{registered.body}
|
|
224
|
+
</BlockRenderErrorBoundary>
|
|
225
|
+
);
|
|
226
|
+
editSurface = registered.editSurface ? (
|
|
227
|
+
<BlockRenderErrorBoundary
|
|
228
|
+
blockId={block.id}
|
|
229
|
+
blockType={blockType}
|
|
230
|
+
resetKey={block.data}
|
|
231
|
+
interactive
|
|
232
|
+
>
|
|
233
|
+
{registered.editSurface}
|
|
234
|
+
</BlockRenderErrorBoundary>
|
|
235
|
+
) : null;
|
|
211
236
|
} else if (sideMap?.renderLegacyBlock) {
|
|
212
237
|
const selfEdits =
|
|
213
238
|
editable && Boolean(sideMap.legacyBlockSelfEdits?.(blockType));
|
|
@@ -281,20 +306,11 @@ export function RegistryBlockLoadErrorView({
|
|
|
281
306
|
rawSource?: string;
|
|
282
307
|
}) {
|
|
283
308
|
return (
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
{rawSource ? (
|
|
290
|
-
<pre className="overflow-auto whitespace-pre-wrap font-mono text-xs text-foreground">
|
|
291
|
-
{rawSource}
|
|
292
|
-
</pre>
|
|
293
|
-
) : null}
|
|
294
|
-
<p role="alert" className="text-muted-foreground">
|
|
295
|
-
{message}
|
|
296
|
-
</p>
|
|
297
|
-
</div>
|
|
309
|
+
<BlockRenderErrorFallback
|
|
310
|
+
message={message}
|
|
311
|
+
rawSource={rawSource}
|
|
312
|
+
interactive
|
|
313
|
+
/>
|
|
298
314
|
);
|
|
299
315
|
}
|
|
300
316
|
|
package/src/ui/slider.tsx
CHANGED
|
@@ -5,8 +5,10 @@ import { cn } from "../utils.js";
|
|
|
5
5
|
|
|
6
6
|
const Slider = React.forwardRef<
|
|
7
7
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
8
|
-
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
9
|
-
|
|
8
|
+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
|
|
9
|
+
thumbAriaLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
>(({ className, thumbAriaLabel, ...props }, ref) => (
|
|
10
12
|
<SliderPrimitive.Root
|
|
11
13
|
ref={ref}
|
|
12
14
|
className={cn(
|
|
@@ -18,7 +20,10 @@ const Slider = React.forwardRef<
|
|
|
18
20
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
|
19
21
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
|
20
22
|
</SliderPrimitive.Track>
|
|
21
|
-
<SliderPrimitive.Thumb
|
|
23
|
+
<SliderPrimitive.Thumb
|
|
24
|
+
aria-label={thumbAriaLabel}
|
|
25
|
+
className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
26
|
+
/>
|
|
22
27
|
</SliderPrimitive.Root>
|
|
23
28
|
));
|
|
24
29
|
Slider.displayName = SliderPrimitive.Root.displayName;
|