@cueloop/client 0.1.0-alpha.76 → 0.1.0-alpha.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/App.identity.test.tsx +78 -0
- package/src/App.share-choice.test.tsx +105 -0
- package/src/App.toast.test.tsx +3 -1
- package/src/App.tsx +176 -5
- package/src/app-screens.tsx +13 -0
- package/src/app-view-model.ts +1 -0
- package/src/components/AnnotatableFileView.tsx +9 -1
- package/src/components/AnnotationCards.tsx +22 -4
- package/src/components/DiffContentView.tsx +5 -1
- package/src/components/GridTabContent.tsx +2 -0
- package/src/components/ManageAccessDialog.stories.tsx +36 -0
- package/src/components/ManageAccessDialog.test.tsx +94 -0
- package/src/components/ManageAccessDialog.tsx +120 -0
- package/src/components/ShareChoiceDialog.stories.tsx +34 -0
- package/src/components/ShareChoiceDialog.test.tsx +103 -0
- package/src/components/ShareChoiceDialog.tsx +82 -0
- package/src/components/ThreadView.tsx +5 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +93 -7
- package/src/config-identity.test.ts +78 -0
- package/src/config.ts +44 -1
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/intent-dispatch.test.ts +17 -2
- package/src/intent-dispatch.ts +7 -1
- package/src/keymap.ts +1 -0
- package/src/share.ts +10 -2
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.live-diff.test.ts +1 -0
- package/src/thread-controller.share.test.ts +36 -2
- package/src/thread-controller.tree.test.ts +1 -0
- package/src/thread-controller.ts +14 -0
- package/src/use-annotation-surface.tsx +10 -2
- package/src/use-settings-dialog.tsx +24 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
12
12
|
import { createTextAttributes, type KeyEvent, type ScrollBoxRenderable } from "@opentui/core";
|
|
13
|
-
import type { Thread } from "@cueloop/schema";
|
|
13
|
+
import type { Annotation, Thread } from "@cueloop/schema";
|
|
14
14
|
import { diffRowText, fileChangeCounts, type DiffRow, type Mark } from "../view-diff";
|
|
15
15
|
import type { TextSpan } from "../thread-selection";
|
|
16
16
|
import type { QuickAction } from "../config";
|
|
@@ -95,6 +95,8 @@ export interface DiffContentViewProps {
|
|
|
95
95
|
onAnnotate: (span: TextSpan, body: string) => void;
|
|
96
96
|
onReply: (rootAnnotationId: string, body: string) => void;
|
|
97
97
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
98
|
+
/** The author's display name for a comment's hover tooltip. */
|
|
99
|
+
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
98
100
|
leaderCombos?: readonly string[];
|
|
99
101
|
onLeaderCommand?: (key: KeyEvent) => void;
|
|
100
102
|
onExit: () => void;
|
|
@@ -489,6 +491,7 @@ export function DiffContentView({
|
|
|
489
491
|
onAnnotate,
|
|
490
492
|
onReply,
|
|
491
493
|
onUpdateAnnotation,
|
|
494
|
+
resolveAuthorLabel,
|
|
492
495
|
leaderCombos,
|
|
493
496
|
onLeaderCommand,
|
|
494
497
|
onExit,
|
|
@@ -524,6 +527,7 @@ export function DiffContentView({
|
|
|
524
527
|
onAnnotate,
|
|
525
528
|
onReply,
|
|
526
529
|
onUpdateAnnotation,
|
|
530
|
+
resolveAuthorLabel,
|
|
527
531
|
leaderCombos,
|
|
528
532
|
onLeaderCommand,
|
|
529
533
|
onExit,
|
|
@@ -34,6 +34,7 @@ export type DiffSurfaceProps = Pick<
|
|
|
34
34
|
| "onAnnotate"
|
|
35
35
|
| "onReply"
|
|
36
36
|
| "onUpdateAnnotation"
|
|
37
|
+
| "resolveAuthorLabel"
|
|
37
38
|
| "leaderCombos"
|
|
38
39
|
| "onLeaderCommand"
|
|
39
40
|
| "onExit"
|
|
@@ -121,6 +122,7 @@ export function GridTabContent(props: {
|
|
|
121
122
|
onAddComment={(anchor, body) => props.onAddFileComment(filePath, anchor, body)}
|
|
122
123
|
onReply={props.surface.onReply}
|
|
123
124
|
onUpdateAnnotation={props.surface.onUpdateAnnotation}
|
|
125
|
+
resolveAuthorLabel={props.surface.resolveAuthorLabel}
|
|
124
126
|
onExit={props.surface.onExit}
|
|
125
127
|
theme={props.theme}
|
|
126
128
|
/>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ManageAccessDialog } from "./ManageAccessDialog";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Dialogs/ManageAccess" };
|
|
7
|
+
|
|
8
|
+
export const WithHandles: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<ManageAccessDialog
|
|
11
|
+
isOpen
|
|
12
|
+
logins={["octocat", "hubot"]}
|
|
13
|
+
onAdd={() => {}}
|
|
14
|
+
onRemove={() => {}}
|
|
15
|
+
onCreateLink={() => {}}
|
|
16
|
+
onClose={() => {}}
|
|
17
|
+
theme={DARK}
|
|
18
|
+
/>
|
|
19
|
+
),
|
|
20
|
+
size: { width: 60, height: 18 },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Empty: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<ManageAccessDialog
|
|
26
|
+
isOpen
|
|
27
|
+
logins={[]}
|
|
28
|
+
onAdd={() => {}}
|
|
29
|
+
onRemove={() => {}}
|
|
30
|
+
onCreateLink={() => {}}
|
|
31
|
+
onClose={() => {}}
|
|
32
|
+
theme={DARK}
|
|
33
|
+
/>
|
|
34
|
+
),
|
|
35
|
+
size: { width: 60, height: 18 },
|
|
36
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { DARK } from "../theme";
|
|
5
|
+
import { clickText, locateTextInFrame, settle, typeText } from "../test-support";
|
|
6
|
+
import { ManageAccessDialog } from "./ManageAccessDialog";
|
|
7
|
+
|
|
8
|
+
function AccessHarness({ initial }: { initial: string[] }): React.ReactNode {
|
|
9
|
+
const [logins, setLogins] = useState<string[]>(initial);
|
|
10
|
+
const [open, setOpen] = useState(true);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<box style={{ width: 60, height: 18 }}>
|
|
14
|
+
<ManageAccessDialog
|
|
15
|
+
isOpen={open}
|
|
16
|
+
logins={logins}
|
|
17
|
+
onAdd={(login) => setLogins((prev) => [...prev, login])}
|
|
18
|
+
onRemove={(login) => setLogins((prev) => prev.filter((entry) => entry !== login))}
|
|
19
|
+
onCreateLink={() => {}}
|
|
20
|
+
onClose={() => setOpen(false)}
|
|
21
|
+
theme={DARK}
|
|
22
|
+
/>
|
|
23
|
+
</box>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function renderAccess(initial: string[]): Promise<Awaited<ReturnType<typeof testRender>>> {
|
|
28
|
+
const setup = await testRender(<AccessHarness initial={initial} />, { width: 60, height: 18 });
|
|
29
|
+
|
|
30
|
+
await settle(setup);
|
|
31
|
+
|
|
32
|
+
return setup;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("ManageAccessDialog", () => {
|
|
36
|
+
test("renders the allowlist of GitHub handles", async () => {
|
|
37
|
+
const setup = await renderAccess(["octocat", "hubot"]);
|
|
38
|
+
const frame = setup.captureCharFrame();
|
|
39
|
+
|
|
40
|
+
expect(locateTextInFrame(frame, "@octocat")).not.toBeNull();
|
|
41
|
+
expect(locateTextInFrame(frame, "@hubot")).not.toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("clicking a row removes that handle from the allowlist", async () => {
|
|
45
|
+
const setup = await renderAccess(["octocat", "hubot"]);
|
|
46
|
+
|
|
47
|
+
await clickText(setup, "@octocat");
|
|
48
|
+
|
|
49
|
+
expect(locateTextInFrame(setup.captureCharFrame(), "@octocat")).toBeNull();
|
|
50
|
+
expect(locateTextInFrame(setup.captureCharFrame(), "@hubot")).not.toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("typing a handle and pressing add appends it to the allowlist", async () => {
|
|
54
|
+
const setup = await renderAccess([]);
|
|
55
|
+
|
|
56
|
+
await typeText(setup, "octocat");
|
|
57
|
+
await clickText(setup, "add");
|
|
58
|
+
|
|
59
|
+
expect(locateTextInFrame(setup.captureCharFrame(), "@octocat")).not.toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("clicking create link publishes the private link", async () => {
|
|
63
|
+
let created = false;
|
|
64
|
+
const setup = await testRender(
|
|
65
|
+
<box style={{ width: 60, height: 18 }}>
|
|
66
|
+
<ManageAccessDialog
|
|
67
|
+
isOpen
|
|
68
|
+
logins={["octocat"]}
|
|
69
|
+
onAdd={() => {}}
|
|
70
|
+
onRemove={() => {}}
|
|
71
|
+
onCreateLink={() => {
|
|
72
|
+
created = true;
|
|
73
|
+
}}
|
|
74
|
+
onClose={() => {}}
|
|
75
|
+
theme={DARK}
|
|
76
|
+
/>
|
|
77
|
+
</box>,
|
|
78
|
+
{ width: 60, height: 18 },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await settle(setup);
|
|
82
|
+
await clickText(setup, "create link");
|
|
83
|
+
|
|
84
|
+
expect(created).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("clicking close dismisses the dialog", async () => {
|
|
88
|
+
const setup = await renderAccess([]);
|
|
89
|
+
|
|
90
|
+
await clickText(setup, "close");
|
|
91
|
+
|
|
92
|
+
expect(locateTextInFrame(setup.captureCharFrame(), "Manage access")).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner surface for a private share's allowlist: the GitHub logins allowed to
|
|
3
|
+
* open the private link. Type a handle and add it; click a row to remove one.
|
|
4
|
+
* Editing persists through the controller, so the list survives a reload.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
8
|
+
import { useKeyboard } from "@opentui/react";
|
|
9
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
10
|
+
import type { KeyBinding, TextareaRenderable } from "@opentui/core";
|
|
11
|
+
import type { Theme } from "../theme";
|
|
12
|
+
import { useComponentTheme } from "./theme-context";
|
|
13
|
+
import { Dialog } from "./primitives/Dialog";
|
|
14
|
+
|
|
15
|
+
// enter adds the handle rather than inserting a newline into the one-line field
|
|
16
|
+
const ADD_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
|
|
17
|
+
|
|
18
|
+
export interface ManageAccessDialogProps {
|
|
19
|
+
isOpen: boolean;
|
|
20
|
+
logins: string[];
|
|
21
|
+
onAdd: (login: string) => void;
|
|
22
|
+
onRemove: (login: string) => void;
|
|
23
|
+
/** Publish the private link with the current allowlist and copy its connection line. */
|
|
24
|
+
onCreateLink: () => void;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
theme?: Theme;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ManageAccessDialog({
|
|
30
|
+
isOpen,
|
|
31
|
+
logins,
|
|
32
|
+
onAdd,
|
|
33
|
+
onRemove,
|
|
34
|
+
onCreateLink,
|
|
35
|
+
onClose,
|
|
36
|
+
theme,
|
|
37
|
+
}: ManageAccessDialogProps): React.ReactNode {
|
|
38
|
+
const tokens = useComponentTheme(theme);
|
|
39
|
+
const { width: terminalWidth } = useTerminalDimensions();
|
|
40
|
+
const inputRef = useRef<TextareaRenderable | null>(null);
|
|
41
|
+
const [draft, setDraft] = useState("");
|
|
42
|
+
const [inputGeneration, setInputGeneration] = useState(0);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
inputRef.current?.focus();
|
|
46
|
+
}, [inputGeneration]);
|
|
47
|
+
|
|
48
|
+
const addDraft = (): void => {
|
|
49
|
+
const handle = draft.trim().replace(/^@/, "").toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (handle && !logins.includes(handle)) onAdd(handle);
|
|
52
|
+
setDraft("");
|
|
53
|
+
setInputGeneration((generation) => generation + 1);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
useKeyboard((key) => {
|
|
57
|
+
if (isOpen && key.name === "escape") onClose();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!isOpen) return null;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Dialog
|
|
64
|
+
isOpen
|
|
65
|
+
title=" Manage access "
|
|
66
|
+
width={Math.min(54, terminalWidth - 6)}
|
|
67
|
+
height={Math.min(18, 9 + logins.length)}
|
|
68
|
+
background={tokens.elevated}
|
|
69
|
+
onDismiss={onClose}
|
|
70
|
+
theme={theme}
|
|
71
|
+
>
|
|
72
|
+
<box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 1, paddingRight: 1 }}>
|
|
73
|
+
<text fg={tokens.textDim}>Only these GitHub users can open the private link.</text>
|
|
74
|
+
<box style={{ height: 1 }} />
|
|
75
|
+
{logins.length === 0 ? (
|
|
76
|
+
<text fg={tokens.textDim}>no one yet - add a handle below</text>
|
|
77
|
+
) : (
|
|
78
|
+
logins.map((login) => (
|
|
79
|
+
<box key={login} onMouseUp={() => onRemove(login)} style={{ flexDirection: "row" }}>
|
|
80
|
+
<text fg={tokens.text}>{`@${login}`}</text>
|
|
81
|
+
<box style={{ flexGrow: 1 }} />
|
|
82
|
+
<text fg={tokens.textDim}>remove</text>
|
|
83
|
+
</box>
|
|
84
|
+
))
|
|
85
|
+
)}
|
|
86
|
+
<box style={{ flexGrow: 1 }} />
|
|
87
|
+
<box style={{ flexDirection: "row" }}>
|
|
88
|
+
<text fg={tokens.textDim}>{"@"}</text>
|
|
89
|
+
<textarea
|
|
90
|
+
key={inputGeneration}
|
|
91
|
+
ref={inputRef}
|
|
92
|
+
focused
|
|
93
|
+
placeholder="handle"
|
|
94
|
+
keyBindings={ADD_KEY_BINDINGS}
|
|
95
|
+
onContentChange={() => setDraft(inputRef.current?.plainText ?? "")}
|
|
96
|
+
style={{
|
|
97
|
+
height: 1,
|
|
98
|
+
flexGrow: 1,
|
|
99
|
+
backgroundColor: tokens.elevated,
|
|
100
|
+
focusedBackgroundColor: tokens.elevated,
|
|
101
|
+
textColor: tokens.text,
|
|
102
|
+
focusedTextColor: tokens.text,
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
</box>
|
|
106
|
+
<box style={{ flexDirection: "row" }}>
|
|
107
|
+
<box onMouseUp={addDraft} style={{ marginRight: 3 }}>
|
|
108
|
+
<text fg={tokens.accent}>add</text>
|
|
109
|
+
</box>
|
|
110
|
+
<box onMouseUp={onCreateLink} style={{ marginRight: 3 }}>
|
|
111
|
+
<text fg={tokens.accent}>create link</text>
|
|
112
|
+
</box>
|
|
113
|
+
<box onMouseUp={onClose}>
|
|
114
|
+
<text fg={tokens.textDim}>close</text>
|
|
115
|
+
</box>
|
|
116
|
+
</box>
|
|
117
|
+
</box>
|
|
118
|
+
</Dialog>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ShareChoiceDialog } from "./ShareChoiceDialog";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Dialogs/ShareChoice" };
|
|
7
|
+
|
|
8
|
+
export const PublicSelected: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<ShareChoiceDialog
|
|
11
|
+
isOpen
|
|
12
|
+
selectedIndex={0}
|
|
13
|
+
onPublicShare={() => {}}
|
|
14
|
+
onPrivateShare={() => {}}
|
|
15
|
+
onClose={() => {}}
|
|
16
|
+
theme={DARK}
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
size: { width: 60, height: 12 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const PrivateSelected: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<ShareChoiceDialog
|
|
25
|
+
isOpen
|
|
26
|
+
selectedIndex={1}
|
|
27
|
+
onPublicShare={() => {}}
|
|
28
|
+
onPrivateShare={() => {}}
|
|
29
|
+
onClose={() => {}}
|
|
30
|
+
theme={DARK}
|
|
31
|
+
/>
|
|
32
|
+
),
|
|
33
|
+
size: { width: 60, height: 12 },
|
|
34
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { DARK } from "../theme";
|
|
5
|
+
import { clickText, settle } from "../test-support";
|
|
6
|
+
import { ShareChoiceDialog } from "./ShareChoiceDialog";
|
|
7
|
+
|
|
8
|
+
interface ShareCounts {
|
|
9
|
+
publicShares: number;
|
|
10
|
+
privateShares: number;
|
|
11
|
+
closes: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function renderDialog(selectedIndex = 0): Promise<{
|
|
15
|
+
setup: Awaited<ReturnType<typeof testRender>>;
|
|
16
|
+
counts: ShareCounts;
|
|
17
|
+
}> {
|
|
18
|
+
const counts: ShareCounts = { publicShares: 0, privateShares: 0, closes: 0 };
|
|
19
|
+
const setup = await testRender(
|
|
20
|
+
<box style={{ width: 60, height: 12 }}>
|
|
21
|
+
<ShareChoiceDialog
|
|
22
|
+
isOpen
|
|
23
|
+
selectedIndex={selectedIndex}
|
|
24
|
+
onPublicShare={() => {
|
|
25
|
+
counts.publicShares += 1;
|
|
26
|
+
}}
|
|
27
|
+
onPrivateShare={() => {
|
|
28
|
+
counts.privateShares += 1;
|
|
29
|
+
}}
|
|
30
|
+
onClose={() => {
|
|
31
|
+
counts.closes += 1;
|
|
32
|
+
}}
|
|
33
|
+
theme={DARK}
|
|
34
|
+
/>
|
|
35
|
+
</box>,
|
|
36
|
+
{ width: 60, height: 12 },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await settle(setup);
|
|
40
|
+
|
|
41
|
+
return { setup, counts };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("ShareChoiceDialog", () => {
|
|
45
|
+
test("offers both the public and private choice", async () => {
|
|
46
|
+
// Arrange + Act
|
|
47
|
+
const { setup } = await renderDialog();
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
const frame = setup.captureCharFrame();
|
|
51
|
+
|
|
52
|
+
expect(frame).toContain("public link");
|
|
53
|
+
expect(frame).toContain("private link");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("clicking the public choice publishes and closes", async () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
const { setup, counts } = await renderDialog();
|
|
59
|
+
|
|
60
|
+
// Act
|
|
61
|
+
await clickText(setup, "public link");
|
|
62
|
+
|
|
63
|
+
// Assert
|
|
64
|
+
expect(counts.publicShares).toBe(1);
|
|
65
|
+
expect(counts.privateShares).toBe(0);
|
|
66
|
+
expect(counts.closes).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("clicking the private choice opens manage-access and closes", async () => {
|
|
70
|
+
// Arrange
|
|
71
|
+
const { setup, counts } = await renderDialog();
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
await clickText(setup, "private link");
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(counts.privateShares).toBe(1);
|
|
78
|
+
expect(counts.publicShares).toBe(0);
|
|
79
|
+
expect(counts.closes).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("renders nothing while closed", async () => {
|
|
83
|
+
// Arrange + Act
|
|
84
|
+
const setup = await testRender(
|
|
85
|
+
<box style={{ width: 60, height: 12 }}>
|
|
86
|
+
<ShareChoiceDialog
|
|
87
|
+
isOpen={false}
|
|
88
|
+
selectedIndex={0}
|
|
89
|
+
onPublicShare={() => {}}
|
|
90
|
+
onPrivateShare={() => {}}
|
|
91
|
+
onClose={() => {}}
|
|
92
|
+
theme={DARK}
|
|
93
|
+
/>
|
|
94
|
+
</box>,
|
|
95
|
+
{ width: 60, height: 12 },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
await settle(setup);
|
|
99
|
+
|
|
100
|
+
// Assert
|
|
101
|
+
expect(setup.captureCharFrame()).not.toContain("public link");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The share public/private choice, as an app-level overlay the share shortcut
|
|
3
|
+
* opens from any view. Public publishes a link for anyone; private opens the
|
|
4
|
+
* manage-access allowlist surface. The header share button and the shortcut both
|
|
5
|
+
* open this one surface. Selection moves with the keyboard (owned by the App) or
|
|
6
|
+
* a pointer click on a row.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
11
|
+
import type { Theme } from "../theme";
|
|
12
|
+
import { useComponentTheme } from "./theme-context";
|
|
13
|
+
import { Dialog } from "./primitives/Dialog";
|
|
14
|
+
|
|
15
|
+
export type ShareChoice = "public" | "private";
|
|
16
|
+
|
|
17
|
+
interface ShareChoiceRow {
|
|
18
|
+
choice: ShareChoice;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const SHARE_CHOICES: ShareChoiceRow[] = [
|
|
23
|
+
{ choice: "public", label: "public link" },
|
|
24
|
+
{ choice: "private", label: "private link" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export interface ShareChoiceDialogProps {
|
|
28
|
+
isOpen: boolean;
|
|
29
|
+
/** The highlighted row; the App drives it from the shareChoice mode. */
|
|
30
|
+
selectedIndex: number;
|
|
31
|
+
/** Publish a public link and copy its connection line. */
|
|
32
|
+
onPublicShare: () => void;
|
|
33
|
+
/** Open the private-share manage-access allowlist surface. */
|
|
34
|
+
onPrivateShare: () => void;
|
|
35
|
+
onClose: () => void;
|
|
36
|
+
theme?: Theme;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ShareChoiceDialog({
|
|
40
|
+
isOpen,
|
|
41
|
+
selectedIndex,
|
|
42
|
+
onPublicShare,
|
|
43
|
+
onPrivateShare,
|
|
44
|
+
onClose,
|
|
45
|
+
theme,
|
|
46
|
+
}: ShareChoiceDialogProps): React.ReactNode {
|
|
47
|
+
const tokens = useComponentTheme(theme);
|
|
48
|
+
const { width: terminalWidth } = useTerminalDimensions();
|
|
49
|
+
|
|
50
|
+
const pick = (choice: ShareChoice): void => {
|
|
51
|
+
onClose();
|
|
52
|
+
if (choice === "public") onPublicShare();
|
|
53
|
+
else onPrivateShare();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (!isOpen) return null;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Dialog
|
|
60
|
+
isOpen
|
|
61
|
+
title=" Share "
|
|
62
|
+
width={Math.min(46, terminalWidth - 6)}
|
|
63
|
+
height={7}
|
|
64
|
+
background={tokens.elevated}
|
|
65
|
+
onDismiss={onClose}
|
|
66
|
+
theme={theme}
|
|
67
|
+
>
|
|
68
|
+
<box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 1, paddingRight: 1 }}>
|
|
69
|
+
{SHARE_CHOICES.map((row, index) => (
|
|
70
|
+
<box key={row.choice} onMouseUp={() => pick(row.choice)} style={{ flexDirection: "row" }}>
|
|
71
|
+
<text fg={index === selectedIndex ? tokens.accent : tokens.text}>
|
|
72
|
+
{index === selectedIndex ? "› " : " "}
|
|
73
|
+
{row.label}
|
|
74
|
+
</text>
|
|
75
|
+
</box>
|
|
76
|
+
))}
|
|
77
|
+
<box style={{ flexGrow: 1 }} />
|
|
78
|
+
<text fg={tokens.textDim}>↑↓ choose · enter select · esc cancel</text>
|
|
79
|
+
</box>
|
|
80
|
+
</Dialog>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import React, { useEffect, useRef } from "react";
|
|
16
16
|
import type { KeyEvent, ScrollBoxRenderable } from "@opentui/core";
|
|
17
|
-
import type { Thread } from "@cueloop/schema";
|
|
17
|
+
import type { Annotation, Thread } from "@cueloop/schema";
|
|
18
18
|
import { displayText, type DisplayBlock, type Mark } from "../view-plan";
|
|
19
19
|
import type { TextSpan } from "../thread-selection";
|
|
20
20
|
import type { QuickAction } from "../config";
|
|
@@ -127,6 +127,8 @@ export interface ThreadViewProps {
|
|
|
127
127
|
onAnnotate: (span: TextSpan, body: string) => void;
|
|
128
128
|
onReply: (rootAnnotationId: string, body: string) => void;
|
|
129
129
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
130
|
+
/** The author's display name for a comment's hover tooltip. */
|
|
131
|
+
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
130
132
|
leaderCombos?: readonly string[];
|
|
131
133
|
onLeaderCommand?: (key: KeyEvent) => void;
|
|
132
134
|
onExit: () => void;
|
|
@@ -150,6 +152,7 @@ export function ThreadView({
|
|
|
150
152
|
onAnnotate,
|
|
151
153
|
onReply,
|
|
152
154
|
onUpdateAnnotation,
|
|
155
|
+
resolveAuthorLabel,
|
|
153
156
|
leaderCombos,
|
|
154
157
|
onLeaderCommand,
|
|
155
158
|
onExit,
|
|
@@ -178,6 +181,7 @@ export function ThreadView({
|
|
|
178
181
|
onAnnotate,
|
|
179
182
|
onReply,
|
|
180
183
|
onUpdateAnnotation,
|
|
184
|
+
resolveAuthorLabel,
|
|
181
185
|
leaderCombos,
|
|
182
186
|
onLeaderCommand,
|
|
183
187
|
onExit,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Bun Snapshot v1, https://
|
|
1
|
+
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
2
|
|
|
3
3
|
exports[`stories catalog Surfaces/AnnotatableFileView/Loaded renders, snapshots, and carries its colors: Surfaces/AnnotatableFileView/Loaded 1`] = `
|
|
4
4
|
" ▎1 export function add(a: number, b: number): number {
|
|
@@ -667,6 +667,50 @@ exports[`stories catalog Overlays/KeybindsDialog/Cheatsheet renders, snapshots,
|
|
|
667
667
|
|
|
668
668
|
|
|
669
669
|
|
|
670
|
+
"
|
|
671
|
+
`;
|
|
672
|
+
|
|
673
|
+
exports[`stories catalog Dialogs/ManageAccess/Empty renders, snapshots, and carries its colors: Dialogs/ManageAccess/Empty 1`] = `
|
|
674
|
+
"
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
┌─ Manage access ────────────────────────────────────┐
|
|
680
|
+
│ Only these GitHub users can open the private link. │
|
|
681
|
+
│ │
|
|
682
|
+
│ no one yet - add a handle below │
|
|
683
|
+
│ │
|
|
684
|
+
│ │
|
|
685
|
+
│ @handle │
|
|
686
|
+
│ add create link close │
|
|
687
|
+
└────────────────────────────────────────────────────┘
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
"
|
|
693
|
+
`;
|
|
694
|
+
|
|
695
|
+
exports[`stories catalog Dialogs/ManageAccess/WithHandles renders, snapshots, and carries its colors: Dialogs/ManageAccess/WithHandles 1`] = `
|
|
696
|
+
"
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
┌─ Manage access ────────────────────────────────────┐
|
|
701
|
+
│ Only these GitHub users can open the private link. │
|
|
702
|
+
│ │
|
|
703
|
+
│ @octocat remove │
|
|
704
|
+
│ @hubot remove │
|
|
705
|
+
│ │
|
|
706
|
+
│ │
|
|
707
|
+
│ │
|
|
708
|
+
│ @handle │
|
|
709
|
+
│ add create link close │
|
|
710
|
+
└────────────────────────────────────────────────────┘
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
|
|
670
714
|
"
|
|
671
715
|
`;
|
|
672
716
|
|
|
@@ -999,6 +1043,16 @@ exports[`stories catalog Chrome/SettingsRows/AllKinds renders, snapshots, and ca
|
|
|
999
1043
|
|
|
1000
1044
|
|
|
1001
1045
|
|
|
1046
|
+
"
|
|
1047
|
+
`;
|
|
1048
|
+
|
|
1049
|
+
exports[`stories catalog Chrome/ShareMenu/Closed renders, snapshots, and carries its colors: Chrome/ShareMenu/Closed 1`] = `
|
|
1050
|
+
" share
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
|
|
1002
1056
|
"
|
|
1003
1057
|
`;
|
|
1004
1058
|
|
|
@@ -1367,6 +1421,15 @@ exports[`stories catalog Chrome/AgentLauncher/Launcher renders, snapshots, and c
|
|
|
1367
1421
|
|
|
1368
1422
|
|
|
1369
1423
|
|
|
1424
|
+
"
|
|
1425
|
+
`;
|
|
1426
|
+
|
|
1427
|
+
exports[`stories catalog Layout/MenuControl/OneOpenAtATime renders, snapshots, and carries its colors: Layout/MenuControl/OneOpenAtATime 1`] = `
|
|
1428
|
+
"
|
|
1429
|
+
alpha: open
|
|
1430
|
+
beta: closed
|
|
1431
|
+
|
|
1432
|
+
|
|
1370
1433
|
"
|
|
1371
1434
|
`;
|
|
1372
1435
|
|
|
@@ -1888,11 +1951,34 @@ exports[`stories catalog Foundations/Theme/ProviderSwap renders, snapshots, and
|
|
|
1888
1951
|
"
|
|
1889
1952
|
`;
|
|
1890
1953
|
|
|
1891
|
-
exports[`stories catalog
|
|
1892
|
-
"
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1954
|
+
exports[`stories catalog Dialogs/ShareChoice/PrivateSelected renders, snapshots, and carries its colors: Dialogs/ShareChoice/PrivateSelected 1`] = `
|
|
1955
|
+
"
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
┌─ Share ────────────────────────────────────┐
|
|
1959
|
+
│ public link │
|
|
1960
|
+
│ › private link │
|
|
1961
|
+
│ │
|
|
1962
|
+
│ │
|
|
1963
|
+
│ ↑↓ choose · enter select · esc cancel │
|
|
1964
|
+
└────────────────────────────────────────────┘
|
|
1965
|
+
|
|
1966
|
+
|
|
1967
|
+
"
|
|
1968
|
+
`;
|
|
1969
|
+
|
|
1970
|
+
exports[`stories catalog Dialogs/ShareChoice/PublicSelected renders, snapshots, and carries its colors: Dialogs/ShareChoice/PublicSelected 1`] = `
|
|
1971
|
+
"
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
┌─ Share ────────────────────────────────────┐
|
|
1975
|
+
│ › public link │
|
|
1976
|
+
│ private link │
|
|
1977
|
+
│ │
|
|
1978
|
+
│ │
|
|
1979
|
+
│ ↑↓ choose · enter select · esc cancel │
|
|
1980
|
+
└────────────────────────────────────────────┘
|
|
1981
|
+
|
|
1982
|
+
|
|
1897
1983
|
"
|
|
1898
1984
|
`;
|