@tangle-network/ui 11.4.0 → 11.6.0
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/CHANGELOG.md +22 -0
- package/README.md +27 -2
- package/dist/chat.js +3 -3
- package/dist/{chunk-4ORVXSMU.js → chunk-J2G7KCYH.js} +13 -14
- package/dist/{chunk-7O22XQ4D.js → chunk-PLWRAS5Z.js} +1 -1
- package/dist/{chunk-NZLLDHEG.js → chunk-QGTIBEPX.js} +608 -420
- package/dist/{chunk-SRAKCVPO.js → chunk-R5GVQDL3.js} +1 -1
- package/dist/{chunk-IH2VRPGZ.js → chunk-RKC3GXHE.js} +2 -2
- package/dist/{chunk-J3YZIQNI.js → chunk-XPYPZMYT.js} +1 -1
- package/dist/{chunk-HW4ROISY.js → chunk-XROQW5J7.js} +1 -1
- package/dist/{document-editor-pane-MX3FIZSE.js → document-editor-pane-A3JURPW5.js} +1 -1
- package/dist/{document-editor-pane-C3cYEDWZ.d.ts → document-editor-pane-IAEQn_Dl.d.ts} +8 -4
- package/dist/editor.d.ts +5 -4
- package/dist/editor.js +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/files.js +1 -1
- package/dist/hooks.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -8
- package/dist/run.d.ts +48 -1
- package/dist/run.js +9 -5
- package/dist/sdk-hooks.js +3 -3
- package/package.json +1 -1
- package/src/editor/document-editor-pane.test.tsx +58 -0
- package/src/editor/editor-lazy.test.tsx +102 -0
- package/src/editor/editor-lazy.ts +40 -0
- package/src/editor/editor-loading.tsx +25 -0
- package/src/editor/editor-peers.test.ts +250 -0
- package/src/editor/editor-peers.ts +272 -0
- package/src/editor/editor-provider.test.tsx +226 -0
- package/src/editor/editor-provider.tsx +301 -265
- package/src/editor/editor-toolbar.tsx +1 -1
- package/src/editor/markdown-document-editor.test.tsx +121 -0
- package/src/editor/markdown-document-editor.tsx +114 -82
- package/src/editor/tiptap-editor.test.tsx +228 -0
- package/src/editor/tiptap-editor.tsx +162 -130
- package/src/run/index.ts +6 -0
- /package/dist/{chunk-47XH56SV.js → chunk-LQS34IGP.js} +0 -0
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
InlineThinkingItem,
|
|
8
8
|
RunGroup
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-R5GVQDL3.js";
|
|
10
10
|
import {
|
|
11
11
|
ToolCallGroup,
|
|
12
12
|
ToolCallStep
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-J2G7KCYH.js";
|
|
14
14
|
import {
|
|
15
15
|
getToolDisplayMetadata
|
|
16
16
|
} from "./chunk-ULDNFLIM.js";
|
|
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { a as ArtifactPaneProps } from './artifact-pane-DpuWcAiF.js';
|
|
4
4
|
import { HocuspocusProvider } from '@hocuspocus/provider';
|
|
5
|
-
import * as
|
|
5
|
+
import * as Yjs from 'yjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Connection state for the Hocuspocus provider.
|
|
@@ -24,7 +24,7 @@ interface Collaborator {
|
|
|
24
24
|
*/
|
|
25
25
|
interface EditorContextValue {
|
|
26
26
|
/** The Y.Doc instance for collaboration */
|
|
27
|
-
doc:
|
|
27
|
+
doc: Yjs.Doc;
|
|
28
28
|
/** The Hocuspocus provider instance */
|
|
29
29
|
provider: HocuspocusProvider | null;
|
|
30
30
|
/** Current connection state */
|
|
@@ -83,9 +83,13 @@ interface EditorProviderProps {
|
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* EditorProvider wraps children with Hocuspocus collaboration context.
|
|
86
|
-
* Manages WebSocket connection, Y.Doc, and awareness state.
|
|
86
|
+
* Manages WebSocket connection, Y.Doc, and awareness state. Loads `yjs` and
|
|
87
|
+
* `@hocuspocus/provider` — and no tiptap package — before it renders children,
|
|
88
|
+
* because every child hook reads a context that only the loaded provider can
|
|
89
|
+
* supply. A consumer that drives its own editor from that context therefore
|
|
90
|
+
* needs those two peers alone.
|
|
87
91
|
*/
|
|
88
|
-
declare function EditorProvider(
|
|
92
|
+
declare function EditorProvider(props: EditorProviderProps): react_jsx_runtime.JSX.Element;
|
|
89
93
|
/**
|
|
90
94
|
* Hook to access the editor context.
|
|
91
95
|
* Must be used within an EditorProvider.
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as Collaborator, a as ConnectionState } from './document-editor-pane-
|
|
2
|
-
export { D as DocumentEditorBackend, b as DocumentEditorMode, c as DocumentEditorPane, d as DocumentEditorPaneCollaborationConfig, e as DocumentEditorPaneProps, E as EditorContextValue, f as EditorProvider, g as EditorProviderProps, h as EditorTokenRefreshResult, i as EditorUser, u as useEditorContext } from './document-editor-pane-
|
|
1
|
+
import { C as Collaborator, a as ConnectionState } from './document-editor-pane-IAEQn_Dl.js';
|
|
2
|
+
export { D as DocumentEditorBackend, b as DocumentEditorMode, c as DocumentEditorPane, d as DocumentEditorPaneCollaborationConfig, e as DocumentEditorPaneProps, E as EditorContextValue, f as EditorProvider, g as EditorProviderProps, h as EditorTokenRefreshResult, i as EditorUser, u as useEditorContext } from './document-editor-pane-IAEQn_Dl.js';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
import { Editor } from '@tiptap/react';
|
|
5
5
|
import { HocuspocusProvider } from '@hocuspocus/provider';
|
|
@@ -41,9 +41,10 @@ interface TiptapEditorProps {
|
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* TiptapEditor - Collaborative markdown editor with Y.js sync.
|
|
44
|
-
* Must be used within an EditorProvider.
|
|
44
|
+
* Must be used within an EditorProvider. Loads its tiptap, Hocuspocus and
|
|
45
|
+
* yjs peers on first render.
|
|
45
46
|
*/
|
|
46
|
-
declare function TiptapEditor(
|
|
47
|
+
declare function TiptapEditor(props: TiptapEditorProps): react_jsx_runtime.JSX.Element;
|
|
47
48
|
/**
|
|
48
49
|
* Collaborators list component.
|
|
49
50
|
* Shows active users in the document.
|
package/dist/editor.js
CHANGED
package/dist/files.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { GitStatusEntry, ContextMenuItem, ContextMenuOpenContext, GitStatus } from '@pierre/trees';
|
|
3
3
|
import { ReactNode, CSSProperties } from 'react';
|
|
4
|
-
import { b as DocumentEditorMode, D as DocumentEditorBackend, d as DocumentEditorPaneCollaborationConfig } from './document-editor-pane-
|
|
4
|
+
import { b as DocumentEditorMode, D as DocumentEditorBackend, d as DocumentEditorPaneCollaborationConfig } from './document-editor-pane-IAEQn_Dl.js';
|
|
5
5
|
import { a as ArtifactPaneProps } from './artifact-pane-DpuWcAiF.js';
|
|
6
6
|
import '@hocuspocus/provider';
|
|
7
7
|
import 'yjs';
|
package/dist/files.js
CHANGED
package/dist/hooks.js
CHANGED
|
@@ -11,15 +11,15 @@ import {
|
|
|
11
11
|
useSSEStream,
|
|
12
12
|
useSdkSession,
|
|
13
13
|
useToolCallStream
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-XROQW5J7.js";
|
|
15
15
|
import "./chunk-OEX7NZE3.js";
|
|
16
16
|
import {
|
|
17
17
|
useAutoScroll,
|
|
18
18
|
useRunCollapseState,
|
|
19
19
|
useRunGroups
|
|
20
20
|
} from "./chunk-AZWDI2JG.js";
|
|
21
|
-
import "./chunk-
|
|
22
|
-
import "./chunk-
|
|
21
|
+
import "./chunk-XPYPZMYT.js";
|
|
22
|
+
import "./chunk-J2G7KCYH.js";
|
|
23
23
|
import "./chunk-ULDNFLIM.js";
|
|
24
24
|
import "./chunk-AAUNOHVL.js";
|
|
25
25
|
import "./chunk-ZRVH3WCA.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { Avatar, AvatarFallback, AvatarImage, Badge, BadgeProps, Card, CardConte
|
|
|
3
3
|
export { Logo, LogoProps, TangleKnot } from '@tangle-network/brand';
|
|
4
4
|
export { A as ArtifactPane, a as ArtifactPaneProps } from './artifact-pane-DpuWcAiF.js';
|
|
5
5
|
export { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineCustomItem, AgentTimelineItem, AgentTimelineMessageItem, AgentTimelineProps, AgentTimelineStatusItem, AgentTimelineTone, AgentTimelineToolGroupItem, AgentTimelineToolItem, ChatContainer, ChatContainerProps, ChatMessage, ChatMessageProps, MessageList, MessageListProps, MessageRole, ThinkingIndicator, ThinkingIndicatorProps, UserMessage, UserMessageProps } from './chat.js';
|
|
6
|
-
export { ExpandedToolDetail, ExpandedToolDetailProps, InlineThinkingItem, InlineThinkingItemProps, InlineToolItem, InlineToolItemProps, LiveDuration, RunGroup, RunGroupProps } from './run.js';
|
|
6
|
+
export { ExpandedToolDetail, ExpandedToolDetailProps, InlineThinkingItem, InlineThinkingItemProps, InlineToolItem, InlineToolItemProps, LiveDuration, RunGroup, RunGroupProps, RunRowShell, RunRowShellProps, RunRowStatus, RunRowStatusDot } from './run.js';
|
|
7
7
|
export { F as FeedSegment, T as ToolCallData, a as ToolCallFeed, b as ToolCallFeedProps, c as ToolCallStatus, d as ToolCallType, p as parseToolEvent } from './tool-call-feed-D9iofJgW.js';
|
|
8
8
|
export { OpenUIAction, OpenUIActionsNode, OpenUIArtifactRenderer, OpenUIArtifactRendererProps, OpenUIBadgeNode, OpenUICardNode, OpenUICodeNode, OpenUIComponentNode, OpenUIGridNode, OpenUIHeadingNode, OpenUIKeyValueNode, OpenUIMarkdownNode, OpenUIPrimitive, OpenUISeparatorNode, OpenUIStackNode, OpenUIStatNode, OpenUITableNode, OpenUITextNode } from './openui.js';
|
|
9
9
|
export { FileArtifactPane, FileArtifactPaneProps, FileFormat, FileNode, FilePreview, FilePreviewProps, FileTabData, FileTabs, FileTabsProps, FileTree, FileTreeProps, FileTreeVisibilityOptions, RichFileTree, RichFileTreeGitEntry, RichFileTreeGitStatus, RichFileTreeProps, RichFileTreeThemeVars, detectFileFormat, fileExtension, filterFileTree, getCodeLanguage, getFormatLabel, getSyntaxLanguage } from './files.js';
|
|
@@ -35,7 +35,7 @@ import '@radix-ui/react-progress';
|
|
|
35
35
|
import '@radix-ui/react-switch';
|
|
36
36
|
import '@radix-ui/react-label';
|
|
37
37
|
import '@pierre/trees';
|
|
38
|
-
import './document-editor-pane-
|
|
38
|
+
import './document-editor-pane-IAEQn_Dl.js';
|
|
39
39
|
import '@hocuspocus/provider';
|
|
40
40
|
import 'yjs';
|
|
41
41
|
import 'nanostores';
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
useSSEStream,
|
|
21
21
|
useSdkSession,
|
|
22
22
|
useToolCallStream
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-XROQW5J7.js";
|
|
24
24
|
import {
|
|
25
25
|
addMessage,
|
|
26
26
|
addParts,
|
|
@@ -146,26 +146,28 @@ import {
|
|
|
146
146
|
MessageList,
|
|
147
147
|
ThinkingIndicator,
|
|
148
148
|
UserMessage
|
|
149
|
-
} from "./chunk-
|
|
149
|
+
} from "./chunk-RKC3GXHE.js";
|
|
150
150
|
import {
|
|
151
151
|
useAutoScroll,
|
|
152
152
|
useRunCollapseState,
|
|
153
153
|
useRunGroups
|
|
154
154
|
} from "./chunk-AZWDI2JG.js";
|
|
155
|
-
import "./chunk-
|
|
155
|
+
import "./chunk-LQS34IGP.js";
|
|
156
156
|
import {
|
|
157
157
|
ToolCallFeed,
|
|
158
158
|
parseToolEvent
|
|
159
|
-
} from "./chunk-
|
|
159
|
+
} from "./chunk-XPYPZMYT.js";
|
|
160
160
|
import {
|
|
161
161
|
InlineThinkingItem,
|
|
162
162
|
RunGroup
|
|
163
|
-
} from "./chunk-
|
|
163
|
+
} from "./chunk-R5GVQDL3.js";
|
|
164
164
|
import {
|
|
165
165
|
ExpandedToolDetail,
|
|
166
166
|
InlineToolItem,
|
|
167
|
-
LiveDuration
|
|
168
|
-
|
|
167
|
+
LiveDuration,
|
|
168
|
+
RunRowShell,
|
|
169
|
+
RunRowStatusDot
|
|
170
|
+
} from "./chunk-J2G7KCYH.js";
|
|
169
171
|
import {
|
|
170
172
|
TOOL_CATEGORY_ICONS,
|
|
171
173
|
formatBytes,
|
|
@@ -219,7 +221,7 @@ import {
|
|
|
219
221
|
FileTree,
|
|
220
222
|
RichFileTree,
|
|
221
223
|
filterFileTree
|
|
222
|
-
} from "./chunk-
|
|
224
|
+
} from "./chunk-PLWRAS5Z.js";
|
|
223
225
|
import {
|
|
224
226
|
detectFileFormat,
|
|
225
227
|
fileExtension,
|
|
@@ -324,6 +326,8 @@ export {
|
|
|
324
326
|
RedactedDocument,
|
|
325
327
|
RichFileTree,
|
|
326
328
|
RunGroup,
|
|
329
|
+
RunRowShell,
|
|
330
|
+
RunRowStatusDot,
|
|
327
331
|
SegmentedControl,
|
|
328
332
|
Select,
|
|
329
333
|
SelectContent,
|
package/dist/run.d.ts
CHANGED
|
@@ -28,6 +28,53 @@ interface RunGroupProps {
|
|
|
28
28
|
*/
|
|
29
29
|
declare const RunGroup: React.MemoExoticComponent<({ run, partMap, collapsed, onToggle, branding, renderToolDetail, headerActions, renderToolActions, }: RunGroupProps) => react_jsx_runtime.JSX.Element | null>;
|
|
30
30
|
|
|
31
|
+
type RunRowStatus = "running" | "success" | "error" | "idle";
|
|
32
|
+
/**
|
|
33
|
+
* Trailing status indicator shared by every run row: a spinner while running,
|
|
34
|
+
* a green/red dot on terminal states, nothing when idle. Status is shown here
|
|
35
|
+
* — it never replaces the row's semantic lead icon.
|
|
36
|
+
*/
|
|
37
|
+
declare function RunRowStatusDot({ status }: {
|
|
38
|
+
status: RunRowStatus;
|
|
39
|
+
}): react_jsx_runtime.JSX.Element | null;
|
|
40
|
+
interface RunRowShellProps {
|
|
41
|
+
/** Semantic lead glyph — always visible; never replaced by status. */
|
|
42
|
+
icon: ReactNode;
|
|
43
|
+
/** Row title. A string in the common case; a node when the title carries its
|
|
44
|
+
* own treatment (e.g. an active-state shimmer sweep) — it renders inline in
|
|
45
|
+
* the shell's title slot either way. */
|
|
46
|
+
title: ReactNode;
|
|
47
|
+
/** Secondary inline text (tool path/command, or a reasoning preview). */
|
|
48
|
+
description?: string;
|
|
49
|
+
/** Render the description in mono (tools) vs prose (reasoning). */
|
|
50
|
+
descriptionMono?: boolean;
|
|
51
|
+
status?: RunRowStatus;
|
|
52
|
+
/** Live-ticks a duration while running. */
|
|
53
|
+
startTime?: number;
|
|
54
|
+
/** Static duration shown once the row is no longer running. */
|
|
55
|
+
durationMs?: number;
|
|
56
|
+
/** Radius shaping when rows are stacked into a group. */
|
|
57
|
+
groupPosition?: "single" | "first" | "middle" | "last";
|
|
58
|
+
/** Message shown below the header while collapsed (tool errors). */
|
|
59
|
+
collapsedError?: string;
|
|
60
|
+
actions?: ReactNode;
|
|
61
|
+
/** Controlled open state. Omit for internal (uncontrolled) state. */
|
|
62
|
+
open?: boolean;
|
|
63
|
+
onOpenChange?: (open: boolean) => void;
|
|
64
|
+
defaultOpen?: boolean;
|
|
65
|
+
className?: string;
|
|
66
|
+
contentClassName?: string;
|
|
67
|
+
/** Expanded body. When absent the row is not expandable. */
|
|
68
|
+
children?: ReactNode;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Shared shell for the agent activity rows (reasoning + tool). Owns the
|
|
72
|
+
* bordered container, the lead icon badge, title/description, the trailing
|
|
73
|
+
* meta cluster (duration + status + chevron) and the collapsible body, so
|
|
74
|
+
* both row kinds read as one family instead of two bespoke layouts.
|
|
75
|
+
*/
|
|
76
|
+
declare function RunRowShell({ icon, title, description, descriptionMono, status, startTime, durationMs, groupPosition, collapsedError, actions, open: openProp, onOpenChange, defaultOpen, className, contentClassName, children, }: RunRowShellProps): react_jsx_runtime.JSX.Element;
|
|
77
|
+
|
|
31
78
|
interface InlineToolItemProps {
|
|
32
79
|
part: ToolPart;
|
|
33
80
|
renderToolDetail?: CustomToolRenderer;
|
|
@@ -71,4 +118,4 @@ declare function LiveDuration({ startTime }: {
|
|
|
71
118
|
startTime: number;
|
|
72
119
|
}): react_jsx_runtime.JSX.Element;
|
|
73
120
|
|
|
74
|
-
export { ExpandedToolDetail, type ExpandedToolDetailProps, InlineThinkingItem, type InlineThinkingItemProps, InlineToolItem, type InlineToolItemProps, LiveDuration, RunGroup, type RunGroupProps };
|
|
121
|
+
export { ExpandedToolDetail, type ExpandedToolDetailProps, InlineThinkingItem, type InlineThinkingItemProps, InlineToolItem, type InlineToolItemProps, LiveDuration, RunGroup, type RunGroupProps, RunRowShell, type RunRowShellProps, type RunRowStatus, RunRowStatusDot };
|
package/dist/run.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-LQS34IGP.js";
|
|
2
2
|
import {
|
|
3
3
|
ToolCallFeed,
|
|
4
4
|
parseToolEvent
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-XPYPZMYT.js";
|
|
6
6
|
import {
|
|
7
7
|
InlineThinkingItem,
|
|
8
8
|
RunGroup
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-R5GVQDL3.js";
|
|
10
10
|
import {
|
|
11
11
|
ExpandedToolDetail,
|
|
12
12
|
InlineToolItem,
|
|
13
|
-
LiveDuration
|
|
14
|
-
|
|
13
|
+
LiveDuration,
|
|
14
|
+
RunRowShell,
|
|
15
|
+
RunRowStatusDot
|
|
16
|
+
} from "./chunk-J2G7KCYH.js";
|
|
15
17
|
import "./chunk-ULDNFLIM.js";
|
|
16
18
|
import "./chunk-AAUNOHVL.js";
|
|
17
19
|
import "./chunk-3M7BPSSW.js";
|
|
@@ -27,6 +29,8 @@ export {
|
|
|
27
29
|
InlineToolItem,
|
|
28
30
|
LiveDuration,
|
|
29
31
|
RunGroup,
|
|
32
|
+
RunRowShell,
|
|
33
|
+
RunRowStatusDot,
|
|
30
34
|
ToolCallFeed,
|
|
31
35
|
parseToolEvent
|
|
32
36
|
};
|
package/dist/sdk-hooks.js
CHANGED
|
@@ -5,15 +5,15 @@ import {
|
|
|
5
5
|
useSSEStream,
|
|
6
6
|
useSdkSession,
|
|
7
7
|
useToolCallStream
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-XROQW5J7.js";
|
|
9
9
|
import "./chunk-OEX7NZE3.js";
|
|
10
10
|
import {
|
|
11
11
|
useAutoScroll,
|
|
12
12
|
useRunCollapseState,
|
|
13
13
|
useRunGroups
|
|
14
14
|
} from "./chunk-AZWDI2JG.js";
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-XPYPZMYT.js";
|
|
16
|
+
import "./chunk-J2G7KCYH.js";
|
|
17
17
|
import "./chunk-ULDNFLIM.js";
|
|
18
18
|
import "./chunk-AAUNOHVL.js";
|
|
19
19
|
import "./chunk-ZRVH3WCA.js";
|
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type * as EditorPeers from "./editor-peers";
|
|
4
|
+
|
|
5
|
+
const markdown = "# Release notes\n\nThe editor loads on demand.";
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.resetModules();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/** Loads the pane with every peer loader rejecting, as for a consumer that
|
|
12
|
+
* installed neither tiptap nor the collaboration stack. The spies report
|
|
13
|
+
* whether a render reached a loader at all. */
|
|
14
|
+
async function paneWithoutPeers(message: string) {
|
|
15
|
+
const loaders = {
|
|
16
|
+
loadDocumentEditorPeers: vi.fn(async () => {
|
|
17
|
+
throw new Error(message);
|
|
18
|
+
}),
|
|
19
|
+
loadCollaborationPeers: vi.fn(async () => {
|
|
20
|
+
throw new Error(message);
|
|
21
|
+
}),
|
|
22
|
+
loadEditorProviderPeers: vi.fn(async () => {
|
|
23
|
+
throw new Error(message);
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
vi.resetModules();
|
|
27
|
+
vi.doMock("./editor-peers", async () => ({
|
|
28
|
+
...(await vi.importActual<typeof EditorPeers>("./editor-peers")),
|
|
29
|
+
...loaders,
|
|
30
|
+
}));
|
|
31
|
+
const { DocumentEditorPane } = await import("./document-editor-pane");
|
|
32
|
+
return { DocumentEditorPane, loaders };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("DocumentEditorPane without the editor peers", () => {
|
|
36
|
+
it("renders the preview, because only the edit tab needs the peers", async () => {
|
|
37
|
+
const { DocumentEditorPane, loaders } = await paneWithoutPeers(
|
|
38
|
+
"peers are not installed",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
render(<DocumentEditorPane title="Notes" markdown={markdown} />);
|
|
42
|
+
|
|
43
|
+
// The promise this pane's editors carry is exactly this: a consumer that
|
|
44
|
+
// never opens the edit tab pays nothing for tiptap.
|
|
45
|
+
expect(screen.getByText("Release notes")).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByText("The editor loads on demand.")).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByRole("tab", { name: "Edit" })).toBeInTheDocument();
|
|
48
|
+
|
|
49
|
+
// Reaching no loader is the claim. Asserting only that the placeholder is
|
|
50
|
+
// absent would also pass for a pane that rendered nothing at all.
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(screen.queryByText("Loading editor…")).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
expect(loaders.loadDocumentEditorPeers).not.toHaveBeenCalled();
|
|
55
|
+
expect(loaders.loadCollaborationPeers).not.toHaveBeenCalled();
|
|
56
|
+
expect(loaders.loadEditorProviderPeers).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { Component, type ReactNode, Suspense } from "react";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { retryableLazyEditor } from "./editor-lazy";
|
|
5
|
+
import { MissingEditorPeersError } from "./editor-peers";
|
|
6
|
+
|
|
7
|
+
/** Catches the rejection the lazy component throws, and can mount it again. */
|
|
8
|
+
class ResettableBoundary extends Component<
|
|
9
|
+
{ children: ReactNode },
|
|
10
|
+
{ failure: Error | null }
|
|
11
|
+
> {
|
|
12
|
+
state = { failure: null as Error | null };
|
|
13
|
+
|
|
14
|
+
static getDerivedStateFromError(error: Error) {
|
|
15
|
+
return { failure: error };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
render() {
|
|
19
|
+
if (this.state.failure) {
|
|
20
|
+
return (
|
|
21
|
+
<button type="button" onClick={() => this.setState({ failure: null })}>
|
|
22
|
+
failed: {this.state.failure.message}
|
|
23
|
+
</button>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return this.props.children;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function Loaded() {
|
|
31
|
+
return <span>editor</span>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** React reports a caught render error on the console; the assertions read it. */
|
|
35
|
+
let consoleError: ReturnType<typeof vi.spyOn>;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
consoleError.mockRestore();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function renderRetryable(load: () => Promise<typeof Loaded>) {
|
|
46
|
+
const lazyEditor = retryableLazyEditor(load);
|
|
47
|
+
function Host() {
|
|
48
|
+
const LazyEditor = lazyEditor();
|
|
49
|
+
return (
|
|
50
|
+
<Suspense fallback={<span>loading</span>}>
|
|
51
|
+
<LazyEditor />
|
|
52
|
+
</Suspense>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return render(
|
|
56
|
+
<ResettableBoundary>
|
|
57
|
+
<Host />
|
|
58
|
+
</ResettableBoundary>,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("retryableLazyEditor", () => {
|
|
63
|
+
it("re-attempts the load after a transient failure", async () => {
|
|
64
|
+
// `lazy` caches its first rejection for the life of the component, so one
|
|
65
|
+
// failed chunk fetch would otherwise hold the editor broken until a full
|
|
66
|
+
// page reload.
|
|
67
|
+
const load = vi
|
|
68
|
+
.fn<() => Promise<typeof Loaded>>()
|
|
69
|
+
.mockRejectedValueOnce(new Error("Failed to fetch dynamically imported module"))
|
|
70
|
+
.mockResolvedValue(Loaded);
|
|
71
|
+
|
|
72
|
+
renderRetryable(load);
|
|
73
|
+
|
|
74
|
+
const retry = await screen.findByRole("button");
|
|
75
|
+
expect(retry).toHaveTextContent("Failed to fetch dynamically imported module");
|
|
76
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
77
|
+
|
|
78
|
+
fireEvent.click(retry);
|
|
79
|
+
|
|
80
|
+
expect(await screen.findByText("editor")).toBeInTheDocument();
|
|
81
|
+
expect(load).toHaveBeenCalledTimes(2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("keeps a missing peer permanent, so a remount does not import it again", async () => {
|
|
85
|
+
// A package does not install itself mid-session. Retrying would only
|
|
86
|
+
// repeat the same failure and hide the install list behind a loop.
|
|
87
|
+
const load = vi
|
|
88
|
+
.fn<() => Promise<typeof Loaded>>()
|
|
89
|
+
.mockRejectedValue(new MissingEditorPeersError("Install @tiptap/react."));
|
|
90
|
+
|
|
91
|
+
renderRetryable(load);
|
|
92
|
+
|
|
93
|
+
const retry = await screen.findByRole("button");
|
|
94
|
+
expect(retry).toHaveTextContent("Install @tiptap/react.");
|
|
95
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
96
|
+
|
|
97
|
+
fireEvent.click(retry);
|
|
98
|
+
|
|
99
|
+
expect(await screen.findByRole("button")).toHaveTextContent("Install @tiptap/react.");
|
|
100
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ComponentType, lazy, type LazyExoticComponent } from "react";
|
|
2
|
+
import { isMissingEditorPeersError } from "./editor-peers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds a `lazy` editor component that a remount can re-attempt.
|
|
6
|
+
*
|
|
7
|
+
* `lazy` caches its first rejection for the life of the component. That is
|
|
8
|
+
* right for a missing peer, which stays missing for the session, and wrong for
|
|
9
|
+
* a transient chunk fetch: one failed network request would otherwise hold the
|
|
10
|
+
* editor broken until a full page reload.
|
|
11
|
+
*
|
|
12
|
+
* The returned function gives the component to render, and a caller must call
|
|
13
|
+
* it on every render: a transient failure replaces the component, and a caller
|
|
14
|
+
* that holds the one it got earlier keeps replaying the cached rejection.
|
|
15
|
+
*
|
|
16
|
+
* A `MissingEditorPeersError` keeps the cached rejection, so the install list
|
|
17
|
+
* reaches the consumer's error boundary once and the loader does not run
|
|
18
|
+
* again. Any other rejection replaces the component, so the next mount — an
|
|
19
|
+
* error boundary that resets, or a remount by the parent — starts a fresh
|
|
20
|
+
* attempt.
|
|
21
|
+
*/
|
|
22
|
+
export function retryableLazyEditor<P extends object>(
|
|
23
|
+
loadComponent: () => Promise<ComponentType<P>>,
|
|
24
|
+
): () => LazyExoticComponent<ComponentType<P>> {
|
|
25
|
+
function build(): LazyExoticComponent<ComponentType<P>> {
|
|
26
|
+
return lazy(async () => {
|
|
27
|
+
try {
|
|
28
|
+
return { default: await loadComponent() };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!isMissingEditorPeersError(error)) {
|
|
31
|
+
current = build();
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let current = build();
|
|
39
|
+
return () => current;
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stands in for an editor while its optional peers load. The editors reach
|
|
7
|
+
* tiptap through a dynamic import, so an editor's first frame waits for that
|
|
8
|
+
* chunk. The live region announces the wait, which a screen reader has no
|
|
9
|
+
* other way to notice. Callers put their own minimum height first, so a
|
|
10
|
+
* caller-supplied className still wins.
|
|
11
|
+
*/
|
|
12
|
+
export function EditorLoadingPlaceholder({ className }: { className?: string }) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
role="status"
|
|
16
|
+
aria-live="polite"
|
|
17
|
+
className={cn(
|
|
18
|
+
"flex w-full items-center justify-center rounded-lg border border-border border-dashed bg-muted text-muted-foreground text-sm",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
Loading editor…
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|