@cueloop/client 0.1.0-alpha.77 → 0.1.0-alpha.79
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 +14 -13
- package/src/App.autoclose.test.tsx +3 -3
- package/src/App.diff.test.tsx +22 -5
- package/src/App.export.test.tsx +1 -2
- package/src/App.focus.test.ts +1 -59
- package/src/App.identity.test.tsx +3 -3
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +91 -26
- package/src/App.readonly.test.tsx +6 -5
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +39 -8
- package/src/App.test.tsx +33 -30
- package/src/App.toast.test.tsx +11 -4
- package/src/App.tsx +263 -274
- package/src/App.walk.test.tsx +11 -10
- package/src/App.workbench.test.tsx +86 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +1 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-screens.tsx +19 -9
- package/src/app-view-model.ts +3 -1
- package/src/components/AnnotatableFileView.tsx +1 -1
- package/src/components/AnnotationCards.tsx +18 -2
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/CompletionOverlay.stories.tsx +15 -1
- package/src/components/CompletionOverlay.tsx +22 -5
- package/src/components/ConfirmCard.stories.tsx +2 -0
- package/src/components/ConfirmCard.tsx +45 -18
- package/src/components/ConfirmDialog.stories.tsx +3 -3
- package/src/components/ConfirmDialog.tsx +11 -12
- package/src/components/DiffContentView.tsx +18 -11
- package/src/components/DiscussionMarkerRail.tsx +9 -4
- package/src/components/FileContentsView.tsx +1 -1
- package/src/components/GridTabContent.tsx +8 -2
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/PromptDialog.test.tsx +2 -2
- package/src/components/PromptDialog.tsx +9 -10
- package/src/components/SettingsDialog.tsx +1 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.tsx +4 -1
- package/src/components/ThreadTree.tsx +12 -7
- package/src/components/ThreadView.selection.test.tsx +88 -1
- package/src/components/ThreadView.slash.test.tsx +0 -36
- package/src/components/ThreadView.tsx +200 -67
- package/src/components/Toast.stories.tsx +7 -0
- package/src/components/Toast.tsx +15 -8
- package/src/components/WalkWizard.stories.tsx +2 -2
- package/src/components/WalkWizard.tsx +10 -12
- package/src/components/__snapshots__/stories.test.tsx.snap +436 -182
- package/src/components/primitives/Button.tsx +6 -2
- package/src/components/primitives/Card.tsx +5 -2
- package/src/components/primitives/Dialog.tsx +2 -2
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tree.tsx +5 -1
- package/src/components/primitives/frame.ts +5 -0
- package/src/components/session-tree.ts +7 -6
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +13 -4
- package/src/components/stories.test.tsx +12 -4
- package/src/components/story-tree.test.ts +32 -24
- package/src/components/story.ts +32 -15
- package/src/config.test.ts +18 -0
- package/src/config.ts +8 -1
- package/src/diff-fold.ts +1 -1
- package/src/diff-intraline.test.ts +75 -2
- package/src/diff-intraline.ts +95 -26
- package/src/intent-dispatch.test.ts +12 -3
- package/src/intent-dispatch.ts +10 -25
- package/src/key-bindings.ts +0 -6
- package/src/keymap.test.ts +2 -2
- package/src/keymap.ts +0 -2
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/prototype-browser.test.ts +9 -1
- package/src/prototype-browser.ts +22 -0
- package/src/run.ts +39 -25
- package/src/serve.ts +1 -1
- package/src/share.ts +16 -2
- package/src/slash-palette.test.ts +17 -0
- package/src/slash-palette.ts +11 -0
- package/src/test-support.ts +14 -0
- package/src/thread-chords.test.ts +55 -178
- package/src/thread-chords.ts +78 -169
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +3 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +60 -5
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +6 -1
- package/src/thread-controller.ts +386 -78
- package/src/use-annotation-surface.tsx +88 -63
- package/src/use-settings-dialog.tsx +14 -3
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/view-plan.test.ts +56 -6
- package/src/view-plan.ts +154 -5
- package/src/App.share-choice.test.tsx +0 -105
- package/src/components/ManageAccessDialog.stories.tsx +0 -36
- package/src/components/ManageAccessDialog.test.tsx +0 -94
- package/src/components/ManageAccessDialog.tsx +0 -120
- package/src/components/ShareChoiceDialog.stories.tsx +0 -34
- package/src/components/ShareChoiceDialog.test.tsx +0 -103
- package/src/components/ShareChoiceDialog.tsx +0 -82
- package/src/components/ThreadsSidebar.stories.tsx +0 -36
- package/src/components/ThreadsSidebar.tsx +0 -71
- package/src/components/agent-launcher.stories.tsx +0 -26
- package/src/components/agent-launcher.test.tsx +0 -50
- package/src/components/agent-launcher.tsx +0 -149
package/src/share.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Publish a
|
|
2
|
+
* Publish a thread as a share: pack it, stream it to the gateway's
|
|
3
3
|
* `share` user over SSH, and copy the returned `ssh p_…@host` line to the
|
|
4
4
|
* clipboard. The planner holds no key, so all this side does is upload; the
|
|
5
5
|
* gateway seals and stores. Used by both `cueloop share` and the in-TUI
|
|
@@ -41,6 +41,11 @@ export function shareIdFromLine(line: string): string | undefined {
|
|
|
41
41
|
return line.match(/^ssh (\S+)@/)?.[1];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** The paste line for a share id: `ssh p_…@host`. Used to copy an existing link. */
|
|
45
|
+
export function formatShareLine(shareId: string, target: ShareTarget = {}): string {
|
|
46
|
+
return `ssh ${shareId}@${target.host ?? DEFAULT_SHARE_HOST}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
/** Upload the session to the gateway and copy the resulting ssh line. */
|
|
45
50
|
export async function publishShare(
|
|
46
51
|
session: Thread,
|
|
@@ -108,7 +113,8 @@ export function mergeFromShare(remote: Thread): SharedMerge {
|
|
|
108
113
|
export async function pushShare(
|
|
109
114
|
shareId: string,
|
|
110
115
|
annotations: Array<Omit<Annotation, "createdAt">>,
|
|
111
|
-
|
|
116
|
+
// ShareAccess sets a private allowlist; "public" clears it (makes the link public); undefined leaves it
|
|
117
|
+
access?: ShareAccess | "public",
|
|
112
118
|
target: ShareTarget = {},
|
|
113
119
|
): Promise<void> {
|
|
114
120
|
const payload = access ? { shareId, annotations, access } : { shareId, annotations };
|
|
@@ -121,6 +127,14 @@ export async function pushShare(
|
|
|
121
127
|
if (code !== 0) throw new Error(`gateway push failed: ${stderr.trim() || `ssh exited ${code}`}`);
|
|
122
128
|
}
|
|
123
129
|
|
|
130
|
+
/** Revoke a share: the gateway deletes the blob so its link stops resolving. Owner-gated; idempotent. */
|
|
131
|
+
export async function revokeShare(shareId: string, target: ShareTarget = {}): Promise<void> {
|
|
132
|
+
const { stderr, code } = await runShareSsh("cueloop-revoke", Buffer.from(shareId), target);
|
|
133
|
+
|
|
134
|
+
if (code !== 0)
|
|
135
|
+
throw new Error(`gateway revoke failed: ${stderr.trim() || `ssh exited ${code}`}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
124
138
|
export interface ShareWatchHandlers {
|
|
125
139
|
/** The whole session record, each time the share changes. */
|
|
126
140
|
onSession: (session: Thread) => void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
activeSlashToken,
|
|
4
|
+
insertSlashItem,
|
|
4
5
|
isStandaloneSlashQuery,
|
|
5
6
|
scoreMatch,
|
|
6
7
|
slashFilter,
|
|
@@ -84,3 +85,19 @@ describe("isStandaloneSlashQuery", () => {
|
|
|
84
85
|
expect(isStandaloneSlashQuery("just prose")).toBe(false);
|
|
85
86
|
});
|
|
86
87
|
});
|
|
88
|
+
|
|
89
|
+
describe("insertSlashItem", () => {
|
|
90
|
+
test("replaces the active slash token with the reference and lands the caret after it", () => {
|
|
91
|
+
const result = insertSlashItem("look at /rev", 12, "review");
|
|
92
|
+
|
|
93
|
+
expect(result.text).toBe("look at /review ");
|
|
94
|
+
expect(result.caret).toBe("look at /review ".length);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("keeps text after the caret intact", () => {
|
|
98
|
+
const result = insertSlashItem("/pl tail", 3, "plan");
|
|
99
|
+
|
|
100
|
+
expect(result.text).toBe("/plan tail");
|
|
101
|
+
expect(result.caret).toBe("/plan ".length);
|
|
102
|
+
});
|
|
103
|
+
});
|
package/src/slash-palette.ts
CHANGED
|
@@ -86,6 +86,17 @@ export function isStandaloneSlashQuery(text: string): boolean {
|
|
|
86
86
|
return /^\/\S*$/.test(text.trim());
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
export function insertSlashItem(text: string, caret: number, itemName: string) {
|
|
90
|
+
const token = activeSlashToken(text, caret) ?? "";
|
|
91
|
+
const cut = caret - token.length;
|
|
92
|
+
const insertion = `/${itemName} `;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
text: text.slice(0, cut) + insertion + text.slice(caret),
|
|
96
|
+
caret: cut + insertion.length,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
/**
|
|
90
101
|
* The character ranges of every completed "/name" in the draft that names a real
|
|
91
102
|
* action or skill, so a surface can paint the references. A half-typed "/na" is
|
package/src/test-support.ts
CHANGED
|
@@ -157,6 +157,20 @@ export async function pressKey(
|
|
|
157
157
|
await settle(setup);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Run a thread nav-mode command: esc drops the surface into nav mode, then a bare
|
|
162
|
+
* key acts (a letter, or a named key like "enter"/"backspace"). The caret and any
|
|
163
|
+
* held selection carry into nav, so the command lands where the caret is.
|
|
164
|
+
*/
|
|
165
|
+
export async function navCommand(setup: TestRendererSetup, key: string): Promise<void> {
|
|
166
|
+
await press(setup, "escape");
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
168
|
+
await settle(setup);
|
|
169
|
+
// a single letter is a discrete keypress (nav command); named keys route through press
|
|
170
|
+
if (key.length === 1) await pressKey(setup, key);
|
|
171
|
+
else await press(setup, key);
|
|
172
|
+
}
|
|
173
|
+
|
|
160
174
|
/** A 0-based cell position within a captured char frame. */
|
|
161
175
|
export interface FrameLocation {
|
|
162
176
|
row: number;
|
|
@@ -1,202 +1,79 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
expect(matchesLeader({ ctrl: true, name: "g" }, [DEFAULT_LEADER])).toBe(true);
|
|
15
|
-
expect(matchesLeader({ ctrl: true, name: "G" }, [DEFAULT_LEADER])).toBe(true);
|
|
16
|
-
expect(matchesLeader({ name: "g" }, [DEFAULT_LEADER])).toBe(false);
|
|
17
|
-
expect(matchesLeader({ ctrl: true, name: "x" }, [DEFAULT_LEADER])).toBe(false);
|
|
18
|
-
expect(matchesLeader({ ctrl: true, meta: true, name: "g" }, [DEFAULT_LEADER])).toBe(false);
|
|
19
|
-
expect(matchesLeader({ name: "," }, [","])).toBe(true);
|
|
2
|
+
import { resolveNavKey } from "./thread-chords";
|
|
3
|
+
|
|
4
|
+
const owner = { isOwner: true, resolved: false, treeActive: false };
|
|
5
|
+
const readOnly = { type: "status" as const, message: "observer - read-only" };
|
|
6
|
+
const submitted = { type: "status" as const, message: "review submitted - read-only" };
|
|
7
|
+
|
|
8
|
+
describe("resolveNavKey", () => {
|
|
9
|
+
test("the session commands: submit, edit, share", () => {
|
|
10
|
+
expect(resolveNavKey({ name: "return" }, owner)).toEqual({ type: "openSubmit" });
|
|
11
|
+
expect(resolveNavKey({ name: "enter" }, owner)).toEqual({ type: "openSubmit" });
|
|
12
|
+
expect(resolveNavKey({ name: "e" }, owner)).toEqual({ type: "edit" });
|
|
13
|
+
expect(resolveNavKey({ name: "s" }, owner)).toEqual({ type: "share" });
|
|
20
14
|
});
|
|
21
15
|
|
|
22
|
-
test("
|
|
23
|
-
expect(leaderHint(["ctrl+g"])).toBe("⌃g ");
|
|
24
|
-
expect(leaderHint([","])).toBe(", ");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("a key after the leader runs the same intent as its Option chord", () => {
|
|
16
|
+
test("a diff row's keys win over the shared letters, then fall through to curation", () => {
|
|
28
17
|
const diff = { ...owner, isDiff: true };
|
|
29
18
|
|
|
30
|
-
expect(
|
|
31
|
-
expect(
|
|
32
|
-
expect(
|
|
33
|
-
expect(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("resolveThreadChord", () => {
|
|
39
|
-
test("ctrl+enter opens the submit overlay for an owner with an open review", () => {
|
|
40
|
-
// Assert: the terminal-safe chord and the cmd forms all submit
|
|
41
|
-
expect(resolveThreadChord({ name: "return", ctrl: true }, owner)).toEqual({
|
|
42
|
-
type: "openSubmit",
|
|
43
|
-
});
|
|
44
|
-
expect(resolveThreadChord({ name: "return", super: true }, owner)).toEqual({
|
|
45
|
-
type: "openSubmit",
|
|
46
|
-
});
|
|
47
|
-
expect(resolveThreadChord({ name: "enter", meta: true }, owner)).toEqual({
|
|
48
|
-
type: "openSubmit",
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("session chords map to their intents", () => {
|
|
53
|
-
// Assert
|
|
54
|
-
expect(resolveThreadChord({ name: "e", ctrl: true }, owner)).toEqual({ type: "edit" });
|
|
55
|
-
expect(resolveThreadChord({ name: "s", ctrl: true }, owner)).toEqual({ type: "share" });
|
|
19
|
+
expect(resolveNavKey({ name: "x" }, diff)).toEqual({ type: "rejectChange" });
|
|
20
|
+
expect(resolveNavKey({ name: "c" }, diff)).toEqual({ type: "foldFile" });
|
|
21
|
+
expect(resolveNavKey({ name: "d" }, diff)).toEqual({ type: "toggleDiffView" });
|
|
22
|
+
expect(resolveNavKey({ name: "k" }, diff)).toEqual({ type: "walkStart" });
|
|
23
|
+
// a letter with no diff meaning still reaches the curation commands
|
|
24
|
+
expect(resolveNavKey({ name: "n" }, diff)).toEqual({ type: "nextAnnotation" });
|
|
56
25
|
});
|
|
57
26
|
|
|
58
|
-
test("
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
expect(
|
|
64
|
-
expect(
|
|
65
|
-
expect(resolveThreadChord({ name: "u", meta: true }, diff)).toEqual({
|
|
66
|
-
type: "restoreCuration",
|
|
67
|
-
});
|
|
68
|
-
expect(resolveThreadChord({ name: "c", meta: true }, diff)).toEqual({ type: "foldFile" });
|
|
69
|
-
expect(resolveThreadChord({ name: "d", meta: true }, diff)).toEqual({ type: "toggleDiffView" });
|
|
70
|
-
expect(resolveThreadChord({ name: "k", meta: true }, diff)).toEqual({ type: "walkStart" });
|
|
71
|
-
// the rail letters are untouched
|
|
72
|
-
expect(resolveThreadChord({ name: "n", meta: true }, diff)).toEqual({ type: "nextAnnotation" });
|
|
73
|
-
// rejecting is the owner's, and closed once resolved
|
|
74
|
-
expect(resolveThreadChord({ name: "x", meta: true }, { ...diff, isOwner: false })).toEqual({
|
|
75
|
-
type: "status",
|
|
76
|
-
message: "observer - read-only",
|
|
77
|
-
});
|
|
78
|
-
expect(resolveThreadChord({ name: "k", meta: true }, { ...diff, resolved: true })).toEqual({
|
|
79
|
-
type: "status",
|
|
80
|
-
message: "review submitted - read-only",
|
|
81
|
-
});
|
|
27
|
+
test("the discussion and curation commands", () => {
|
|
28
|
+
expect(resolveNavKey({ name: "n" }, owner)).toEqual({ type: "nextAnnotation" });
|
|
29
|
+
expect(resolveNavKey({ name: "p" }, owner)).toEqual({ type: "prevAnnotation" });
|
|
30
|
+
expect(resolveNavKey({ name: "backspace" }, owner)).toEqual({ type: "removeAnnotation" });
|
|
31
|
+
expect(resolveNavKey({ name: "r" }, owner)).toEqual({ type: "openRename" });
|
|
32
|
+
expect(resolveNavKey({ name: "x" }, owner)).toEqual({ type: "cut" });
|
|
33
|
+
expect(resolveNavKey({ name: "u" }, owner)).toEqual({ type: "restoreCuration" });
|
|
82
34
|
});
|
|
83
35
|
|
|
84
|
-
test("
|
|
85
|
-
|
|
86
|
-
expect(resolveThreadChord({ name: "n", meta: true }, owner)).toEqual({
|
|
87
|
-
type: "nextAnnotation",
|
|
88
|
-
});
|
|
89
|
-
expect(resolveThreadChord({ name: "p", meta: true }, owner)).toEqual({
|
|
90
|
-
type: "prevAnnotation",
|
|
91
|
-
});
|
|
92
|
-
expect(resolveThreadChord({ name: "e", meta: true }, owner)).toEqual({ type: "editCard" });
|
|
93
|
-
expect(resolveThreadChord({ name: "backspace", meta: true }, owner)).toEqual({
|
|
94
|
-
type: "removeAnnotation",
|
|
95
|
-
});
|
|
96
|
-
expect(resolveThreadChord({ name: "r", meta: true }, owner)).toEqual({ type: "openRename" });
|
|
97
|
-
expect(resolveThreadChord({ name: "x", meta: true }, owner)).toEqual({ type: "cut" });
|
|
98
|
-
expect(resolveThreadChord({ name: "u", meta: true }, owner)).toEqual({
|
|
99
|
-
type: "restoreCuration",
|
|
100
|
-
});
|
|
101
|
-
expect(resolveThreadChord({ name: "x", meta: true }, { ...owner, isOwner: false })).toEqual({
|
|
102
|
-
type: "status",
|
|
103
|
-
message: "observer - read-only",
|
|
104
|
-
});
|
|
105
|
-
});
|
|
36
|
+
test("the tree commands: move on the Tree tab, go, branch, label, fork, hand off", () => {
|
|
37
|
+
const onTree = { ...owner, treeActive: true };
|
|
106
38
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(
|
|
110
|
-
|
|
111
|
-
).
|
|
112
|
-
expect(
|
|
39
|
+
expect(resolveNavKey({ name: "n" }, onTree)).toEqual({ type: "treeMove", direction: 1 });
|
|
40
|
+
expect(resolveNavKey({ name: "p" }, onTree)).toEqual({ type: "treeMove", direction: -1 });
|
|
41
|
+
expect(resolveNavKey({ name: "g" }, owner)).toEqual({ type: "treeGo" });
|
|
42
|
+
expect(resolveNavKey({ name: "b" }, owner)).toEqual({ type: "treeBranch" });
|
|
43
|
+
expect(resolveNavKey({ name: "l" }, owner)).toEqual({ type: "treeLabel" });
|
|
44
|
+
expect(resolveNavKey({ name: "f" }, owner)).toEqual({ type: "treeFork" });
|
|
45
|
+
expect(resolveNavKey({ name: "h" }, owner)).toEqual({ type: "treeForkShare" });
|
|
113
46
|
});
|
|
114
47
|
|
|
115
|
-
test("
|
|
116
|
-
|
|
117
|
-
expect(resolveThreadChord({ name: "return" }, owner)).toBeNull();
|
|
118
|
-
expect(resolveThreadChord({ name: "s" }, owner)).toBeNull();
|
|
119
|
-
expect(resolveThreadChord({ name: "x", ctrl: true }, owner)).toBeNull();
|
|
48
|
+
test("a shift letter resolves as its uppercase, which no command claims, so it returns to typing", () => {
|
|
49
|
+
expect(resolveNavKey({ name: "x", shift: true }, { ...owner, isDiff: true })).toBeNull();
|
|
120
50
|
});
|
|
121
51
|
|
|
122
|
-
test("a collaborator is
|
|
123
|
-
// Assert
|
|
52
|
+
test("a collaborator is refused the owner's commands; renaming stays open", () => {
|
|
124
53
|
const collaborator = { ...owner, isOwner: false };
|
|
125
|
-
const readOnly = { type: "status" as const, message: "observer - read-only" };
|
|
126
54
|
|
|
127
|
-
expect(
|
|
128
|
-
expect(
|
|
129
|
-
expect(
|
|
130
|
-
expect(
|
|
131
|
-
|
|
132
|
-
).
|
|
55
|
+
expect(resolveNavKey({ name: "return" }, collaborator)).toEqual(readOnly);
|
|
56
|
+
expect(resolveNavKey({ name: "e" }, collaborator)).toEqual(readOnly);
|
|
57
|
+
expect(resolveNavKey({ name: "s" }, collaborator)).toEqual(readOnly);
|
|
58
|
+
expect(resolveNavKey({ name: "x" }, collaborator)).toEqual(readOnly);
|
|
59
|
+
expect(resolveNavKey({ name: "g" }, collaborator)).toEqual(readOnly);
|
|
60
|
+
expect(resolveNavKey({ name: "f" }, collaborator)).toEqual(readOnly);
|
|
61
|
+
expect(resolveNavKey({ name: "r" }, collaborator)).toEqual({ type: "openRename" });
|
|
133
62
|
});
|
|
134
63
|
|
|
135
|
-
test("a resolved review refuses every change and says so;
|
|
136
|
-
// Arrange
|
|
64
|
+
test("a resolved review refuses every change and says so; a fork stays open", () => {
|
|
137
65
|
const resolved = { ...owner, resolved: true };
|
|
138
|
-
const submitted = { type: "status" as const, message: "review submitted - read-only" };
|
|
139
66
|
|
|
140
|
-
|
|
141
|
-
expect(
|
|
142
|
-
expect(
|
|
143
|
-
expect(
|
|
144
|
-
expect(
|
|
145
|
-
expect(
|
|
146
|
-
expect(resolveThreadChord({ name: "r", meta: true }, resolved)).toEqual({ type: "openRename" });
|
|
147
|
-
expect(resolveThreadChord({ name: "s", ctrl: true }, resolved)).toEqual({ type: "share" });
|
|
67
|
+
expect(resolveNavKey({ name: "return" }, resolved)).toBeNull();
|
|
68
|
+
expect(resolveNavKey({ name: "e" }, resolved)).toEqual(submitted);
|
|
69
|
+
expect(resolveNavKey({ name: "x" }, resolved)).toEqual(submitted);
|
|
70
|
+
expect(resolveNavKey({ name: "b" }, resolved)).toEqual(submitted);
|
|
71
|
+
expect(resolveNavKey({ name: "f" }, resolved)).toEqual({ type: "treeFork" });
|
|
72
|
+
expect(resolveNavKey({ name: "s" }, resolved)).toEqual({ type: "share" });
|
|
148
73
|
});
|
|
149
74
|
|
|
150
|
-
test("
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const readOnly = { type: "status" as const, message: "observer - read-only" };
|
|
154
|
-
const submitted = { type: "status" as const, message: "review submitted - read-only" };
|
|
155
|
-
|
|
156
|
-
// Assert: next / previous follow the tab that shows
|
|
157
|
-
expect(resolveThreadChord({ name: "t", meta: true }, owner)).toEqual({ type: "toggleTree" });
|
|
158
|
-
expect(resolveThreadChord({ name: "n", meta: true }, onTree)).toEqual({
|
|
159
|
-
type: "treeMove",
|
|
160
|
-
direction: 1,
|
|
161
|
-
});
|
|
162
|
-
expect(resolveThreadChord({ name: "p", meta: true }, onTree)).toEqual({
|
|
163
|
-
type: "treeMove",
|
|
164
|
-
direction: -1,
|
|
165
|
-
});
|
|
166
|
-
expect(resolveThreadChord({ name: "n", meta: true }, owner)).toEqual({
|
|
167
|
-
type: "nextAnnotation",
|
|
168
|
-
});
|
|
169
|
-
expect(resolveThreadChord({ name: "g", meta: true }, owner)).toEqual({ type: "treeGo" });
|
|
170
|
-
expect(resolveThreadChord({ name: "b", meta: true }, owner)).toEqual({ type: "treeBranch" });
|
|
171
|
-
expect(resolveThreadChord({ name: "l", meta: true }, owner)).toEqual({ type: "treeLabel" });
|
|
172
|
-
expect(resolveThreadChord({ name: "f", meta: true }, owner)).toEqual({ type: "treeFork" });
|
|
173
|
-
expect(resolveThreadChord({ name: "h", meta: true }, owner)).toEqual({ type: "treeForkShare" });
|
|
174
|
-
// moving the tree is the owner's; a fork is open after a verdict, a move is not
|
|
175
|
-
expect(resolveThreadChord({ name: "g", meta: true }, { ...owner, isOwner: false })).toEqual(
|
|
176
|
-
readOnly,
|
|
177
|
-
);
|
|
178
|
-
expect(resolveThreadChord({ name: "f", meta: true }, { ...owner, isOwner: false })).toEqual(
|
|
179
|
-
readOnly,
|
|
180
|
-
);
|
|
181
|
-
expect(resolveThreadChord({ name: "b", meta: true }, { ...owner, resolved: true })).toEqual(
|
|
182
|
-
submitted,
|
|
183
|
-
);
|
|
184
|
-
expect(resolveThreadChord({ name: "f", meta: true }, { ...owner, resolved: true })).toEqual({
|
|
185
|
-
type: "treeFork",
|
|
186
|
-
});
|
|
187
|
-
expect(resolveThreadChord({ name: "t", meta: true }, { ...owner, isOwner: false })).toEqual({
|
|
188
|
-
type: "toggleTree",
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("an observer cannot edit or delete cards either", () => {
|
|
193
|
-
// Arrange
|
|
194
|
-
const observer = { ...owner, isOwner: false };
|
|
195
|
-
const readOnly = { type: "status" as const, message: "observer - read-only" };
|
|
196
|
-
|
|
197
|
-
// Assert
|
|
198
|
-
expect(resolveThreadChord({ name: "e", meta: true }, observer)).toEqual(readOnly);
|
|
199
|
-
expect(resolveThreadChord({ name: "backspace", meta: true }, observer)).toEqual(readOnly);
|
|
200
|
-
expect(resolveThreadChord({ name: "r", meta: true }, observer)).toEqual({ type: "openRename" });
|
|
75
|
+
test("an unmapped key returns null, so the surface returns to typing", () => {
|
|
76
|
+
expect(resolveNavKey({ name: "z" }, owner)).toBeNull();
|
|
77
|
+
expect(resolveNavKey({ name: "j" }, owner)).toBeNull();
|
|
201
78
|
});
|
|
202
79
|
});
|