@brass-build/client 0.1.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/AGENTS.md +255 -0
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +836 -0
- package/dist/api-fetch.d.ts +16 -0
- package/dist/api-fetch.d.ts.map +1 -0
- package/dist/api-fetch.js +97 -0
- package/dist/api-fetch.js.map +1 -0
- package/dist/api-headers.d.ts +3 -0
- package/dist/api-headers.d.ts.map +1 -0
- package/dist/api-headers.js +25 -0
- package/dist/api-headers.js.map +1 -0
- package/dist/auth-flow.d.ts +23 -0
- package/dist/auth-flow.d.ts.map +1 -0
- package/dist/auth-flow.js +76 -0
- package/dist/auth-flow.js.map +1 -0
- package/dist/client.d.ts +106 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1278 -0
- package/dist/client.js.map +1 -0
- package/dist/doc-url-sync.d.ts +19 -0
- package/dist/doc-url-sync.d.ts.map +1 -0
- package/dist/doc-url-sync.js +181 -0
- package/dist/doc-url-sync.js.map +1 -0
- package/dist/document-branches.d.ts +32 -0
- package/dist/document-branches.d.ts.map +1 -0
- package/dist/document-branches.js +89 -0
- package/dist/document-branches.js.map +1 -0
- package/dist/document-checkpoints.d.ts +24 -0
- package/dist/document-checkpoints.d.ts.map +1 -0
- package/dist/document-checkpoints.js +115 -0
- package/dist/document-checkpoints.js.map +1 -0
- package/dist/document-commands.d.ts +93 -0
- package/dist/document-commands.d.ts.map +1 -0
- package/dist/document-commands.js +423 -0
- package/dist/document-commands.js.map +1 -0
- package/dist/document-signals.d.ts +53 -0
- package/dist/document-signals.d.ts.map +1 -0
- package/dist/document-signals.js +114 -0
- package/dist/document-signals.js.map +1 -0
- package/dist/document-streams.d.ts +22 -0
- package/dist/document-streams.d.ts.map +1 -0
- package/dist/document-streams.js +98 -0
- package/dist/document-streams.js.map +1 -0
- package/dist/document-thumbnail-capture.d.ts +27 -0
- package/dist/document-thumbnail-capture.d.ts.map +1 -0
- package/dist/document-thumbnail-capture.js +160 -0
- package/dist/document-thumbnail-capture.js.map +1 -0
- package/dist/documents-namespace.d.ts +134 -0
- package/dist/documents-namespace.d.ts.map +1 -0
- package/dist/documents-namespace.js +190 -0
- package/dist/documents-namespace.js.map +1 -0
- package/dist/documents.d.ts +146 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +2045 -0
- package/dist/documents.js.map +1 -0
- package/dist/emitter.d.ts +15 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +90 -0
- package/dist/emitter.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +22 -0
- package/dist/errors.js.map +1 -0
- package/dist/export-guest.d.ts +9 -0
- package/dist/export-guest.d.ts.map +1 -0
- package/dist/export-guest.js +225 -0
- package/dist/export-guest.js.map +1 -0
- package/dist/fetch-retry.d.ts +16 -0
- package/dist/fetch-retry.d.ts.map +1 -0
- package/dist/fetch-retry.js +90 -0
- package/dist/fetch-retry.js.map +1 -0
- package/dist/guest-link.d.ts +3 -0
- package/dist/guest-link.d.ts.map +1 -0
- package/dist/guest-link.js +13 -0
- package/dist/guest-link.js.map +1 -0
- package/dist/handles.d.ts +57 -0
- package/dist/handles.d.ts.map +1 -0
- package/dist/handles.js +1375 -0
- package/dist/handles.js.map +1 -0
- package/dist/hosted-runtime.d.ts +33 -0
- package/dist/hosted-runtime.d.ts.map +1 -0
- package/dist/hosted-runtime.js +99 -0
- package/dist/hosted-runtime.js.map +1 -0
- package/dist/hosted-ui.d.ts +84 -0
- package/dist/hosted-ui.d.ts.map +1 -0
- package/dist/hosted-ui.js +673 -0
- package/dist/hosted-ui.js.map +1 -0
- package/dist/importer.d.ts +22 -0
- package/dist/importer.d.ts.map +1 -0
- package/dist/importer.js +54 -0
- package/dist/importer.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -0
- package/dist/init-keys.d.ts +3 -0
- package/dist/init-keys.d.ts.map +1 -0
- package/dist/init-keys.js +13 -0
- package/dist/init-keys.js.map +1 -0
- package/dist/internal.d.ts +6 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +17 -0
- package/dist/internal.js.map +1 -0
- package/dist/jwt.d.ts +3 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +22 -0
- package/dist/jwt.js.map +1 -0
- package/dist/launch-context.d.ts +4 -0
- package/dist/launch-context.d.ts.map +1 -0
- package/dist/launch-context.js +44 -0
- package/dist/launch-context.js.map +1 -0
- package/dist/login-hint.d.ts +4 -0
- package/dist/login-hint.d.ts.map +1 -0
- package/dist/login-hint.js +63 -0
- package/dist/login-hint.js.map +1 -0
- package/dist/mutate-batching.d.ts +14 -0
- package/dist/mutate-batching.d.ts.map +1 -0
- package/dist/mutate-batching.js +65 -0
- package/dist/mutate-batching.js.map +1 -0
- package/dist/open-document.d.ts +127 -0
- package/dist/open-document.d.ts.map +1 -0
- package/dist/open-document.js +2121 -0
- package/dist/open-document.js.map +1 -0
- package/dist/page-title.d.ts +3 -0
- package/dist/page-title.d.ts.map +1 -0
- package/dist/page-title.js +32 -0
- package/dist/page-title.js.map +1 -0
- package/dist/perf.d.ts +9 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +39 -0
- package/dist/perf.js.map +1 -0
- package/dist/presence.d.ts +62 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +331 -0
- package/dist/presence.js.map +1 -0
- package/dist/record-paging.d.ts +99 -0
- package/dist/record-paging.d.ts.map +1 -0
- package/dist/record-paging.js +220 -0
- package/dist/record-paging.js.map +1 -0
- package/dist/schema-conflict-dialog.d.ts +10 -0
- package/dist/schema-conflict-dialog.d.ts.map +1 -0
- package/dist/schema-conflict-dialog.js +164 -0
- package/dist/schema-conflict-dialog.js.map +1 -0
- package/dist/selection-persistence.d.ts +5 -0
- package/dist/selection-persistence.d.ts.map +1 -0
- package/dist/selection-persistence.js +90 -0
- package/dist/selection-persistence.js.map +1 -0
- package/dist/serializer.d.ts +3 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +2 -0
- package/dist/serializer.js.map +1 -0
- package/dist/session-persistence.d.ts +4 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +68 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/session-toolbar.d.ts +62 -0
- package/dist/session-toolbar.d.ts.map +1 -0
- package/dist/session-toolbar.js +1205 -0
- package/dist/session-toolbar.js.map +1 -0
- package/dist/session.d.ts +69 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +926 -0
- package/dist/session.js.map +1 -0
- package/dist/tail-ops.d.ts +28 -0
- package/dist/tail-ops.d.ts.map +1 -0
- package/dist/tail-ops.js +46 -0
- package/dist/tail-ops.js.map +1 -0
- package/dist/topic-lifecycle.d.ts +41 -0
- package/dist/topic-lifecycle.d.ts.map +1 -0
- package/dist/topic-lifecycle.js +169 -0
- package/dist/topic-lifecycle.js.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/undo.d.ts +18 -0
- package/dist/undo.d.ts.map +1 -0
- package/dist/undo.js +110 -0
- package/dist/undo.js.map +1 -0
- package/dist/version-bar.d.ts +22 -0
- package/dist/version-bar.d.ts.map +1 -0
- package/dist/version-bar.js +169 -0
- package/dist/version-bar.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/dist/vite.d.ts +15 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +36 -0
- package/dist/vite.js.map +1 -0
- package/dist/ws-client.d.ts +53 -0
- package/dist/ws-client.d.ts.map +1 -0
- package/dist/ws-client.js +604 -0
- package/dist/ws-client.js.map +1 -0
- package/dist/ws-frame-limit.d.ts +21 -0
- package/dist/ws-frame-limit.d.ts.map +1 -0
- package/dist/ws-frame-limit.js +40 -0
- package/dist/ws-frame-limit.js.map +1 -0
- package/docs/app-manifest.md +65 -0
- package/docs/custom-chrome.md +147 -0
- package/docs/hosted-ui-advanced.md +77 -0
- package/docs/imported-documents.md +261 -0
- package/docs/presence.md +129 -0
- package/docs/schemas.md +135 -0
- package/docs/streams.md +248 -0
- package/package.json +60 -0
- package/scripts/prepare.mjs +50 -0
- package/src/api-fetch.ts +127 -0
- package/src/api-headers.ts +28 -0
- package/src/auth-flow.ts +125 -0
- package/src/client.ts +1457 -0
- package/src/doc-url-sync.ts +183 -0
- package/src/document-branches.ts +124 -0
- package/src/document-checkpoints.ts +135 -0
- package/src/document-commands.ts +651 -0
- package/src/document-signals.ts +234 -0
- package/src/document-streams.ts +132 -0
- package/src/document-thumbnail-capture.ts +191 -0
- package/src/documents-namespace.ts +516 -0
- package/src/documents.ts +2426 -0
- package/src/emitter.ts +129 -0
- package/src/errors.ts +45 -0
- package/src/export-guest.ts +282 -0
- package/src/fetch-retry.ts +119 -0
- package/src/guest-link.ts +14 -0
- package/src/handles.ts +1731 -0
- package/src/hosted-runtime.ts +109 -0
- package/src/hosted-ui.ts +1020 -0
- package/src/importer.ts +89 -0
- package/src/index.ts +267 -0
- package/src/init-keys.ts +15 -0
- package/src/internal.ts +23 -0
- package/src/jwt.ts +18 -0
- package/src/launch-context.ts +43 -0
- package/src/login-hint.ts +60 -0
- package/src/mutate-batching.ts +63 -0
- package/src/open-document.ts +2869 -0
- package/src/page-title.ts +33 -0
- package/src/perf.ts +46 -0
- package/src/presence.ts +471 -0
- package/src/record-paging.ts +270 -0
- package/src/schema-conflict-dialog.ts +176 -0
- package/src/selection-persistence.ts +98 -0
- package/src/serializer.ts +15 -0
- package/src/session-persistence.ts +69 -0
- package/src/session-toolbar.ts +1457 -0
- package/src/session.ts +1262 -0
- package/src/tail-ops.ts +63 -0
- package/src/topic-lifecycle.ts +217 -0
- package/src/types.ts +75 -0
- package/src/undo.ts +125 -0
- package/src/version-bar.ts +221 -0
- package/src/version.ts +8 -0
- package/src/vite.ts +49 -0
- package/src/ws-client.ts +678 -0
- package/src/ws-frame-limit.ts +52 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
// Document file-menu commands: the per-doc list of built-in actions a
|
|
2
|
+
// multi-doc app reads as data and places into its own chrome (Version
|
|
3
|
+
// history / Branches / Details / Share / Delete suggested for the menu,
|
|
4
|
+
// Undo / Redo suggested for the toolbar), plus the branches submenu.
|
|
5
|
+
//
|
|
6
|
+
// The SDK owns the action set, each command's live `enabled` / `running`
|
|
7
|
+
// state, and its `run()`; the app owns placement and styling. Each
|
|
8
|
+
// command carries a `surface` hint ('menu' | 'toolbar') the app is free
|
|
9
|
+
// to honor or ignore. The SDK does not render the menu itself: an app
|
|
10
|
+
// surfaces its own actions with its own buttons and calls SDK methods
|
|
11
|
+
// directly.
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
DocumentHistoryResult,
|
|
15
|
+
DocumentSettingsFocus,
|
|
16
|
+
DocumentSettingsResult,
|
|
17
|
+
DriveResolveResult,
|
|
18
|
+
} from './hosted-ui.js';
|
|
19
|
+
import type { BranchSummary } from './documents-namespace.js';
|
|
20
|
+
|
|
21
|
+
// Hosted-UI capabilities the built-in commands need (history modal,
|
|
22
|
+
// manage / share actions, branch picker). Injected by the factory; same
|
|
23
|
+
// DI shape as the Document's other providers.
|
|
24
|
+
export interface HostedUiProvider {
|
|
25
|
+
documentHistory(
|
|
26
|
+
docId: string,
|
|
27
|
+
options?: { branch?: string },
|
|
28
|
+
): Promise<DocumentHistoryResult | null>;
|
|
29
|
+
// Resolves with the copy's id when the user duplicated the document
|
|
30
|
+
// there and asked to open it; `null` otherwise.
|
|
31
|
+
documentSettings(
|
|
32
|
+
docId: string,
|
|
33
|
+
options?: { focus?: DocumentSettingsFocus },
|
|
34
|
+
): Promise<DocumentSettingsResult | null>;
|
|
35
|
+
// Optional callback invoked after a settings-driven rename so the
|
|
36
|
+
// Document can refresh its cached name. The factory wires this to
|
|
37
|
+
// a `documents.get` call; passing the new name keeps the round
|
|
38
|
+
// trip on the SDK side rather than forcing the app to re-fetch.
|
|
39
|
+
refreshName?: (docId: string) => Promise<string | null>;
|
|
40
|
+
// Called by the `history` built-in for a fork outcome or cross-branch
|
|
41
|
+
// version: the doc checks out only its own branch in place, so
|
|
42
|
+
// switching branches needs a higher-level coordinator. The session
|
|
43
|
+
// wires this to `session.open` (single-doc apps get instance-swap
|
|
44
|
+
// routing for free); multi-doc apps wire their own.
|
|
45
|
+
openSiblingDoc?: (
|
|
46
|
+
docId: string,
|
|
47
|
+
options: { branch?: string; version?: number },
|
|
48
|
+
) => Promise<void>;
|
|
49
|
+
selectBranch?: (
|
|
50
|
+
docId: string,
|
|
51
|
+
options?: { branch?: string },
|
|
52
|
+
) => Promise<{ branch_id: string } | null>;
|
|
53
|
+
// Open the hosted drive-divergence resolution surface
|
|
54
|
+
// (`Document.resolveDrive()` routes through this). Resolves with the
|
|
55
|
+
// outcome (a fresh mirror doc id, or `null` newDocId for an in-place
|
|
56
|
+
// publish-over), or `null` when dismissed.
|
|
57
|
+
driveResolve?: (docId: string) => Promise<DriveResolveResult | null>;
|
|
58
|
+
// Open the hosted acquire-organization surface for a caller with zero
|
|
59
|
+
// organizations; resolves with the organization they created or
|
|
60
|
+
// joined, or `null` if dismissed. `createDocument`'s implicit-org
|
|
61
|
+
// resolution falls back to this instead of throwing.
|
|
62
|
+
acquireOrganization?: () => Promise<{ organization_id: string } | null>;
|
|
63
|
+
// Whether someone can produce a file from this document, the same
|
|
64
|
+
// question the hosted share dialog gates its download row on. The
|
|
65
|
+
// `share` built-in's label names the download only when this resolves
|
|
66
|
+
// true, so the menu entry offers exactly what the dialog holds. The
|
|
67
|
+
// factory wires it to a `documents.get` read. `null` means the read
|
|
68
|
+
// could not tell (it was refused or it failed), which the label treats
|
|
69
|
+
// as a no and retries; only `true` / `false` settle it.
|
|
70
|
+
documentDownloadable?: (docId: string) => Promise<boolean | null>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// One entry in the file menu. The Document owns these (each doc
|
|
74
|
+
// carries its own command list with its own `running` state); the
|
|
75
|
+
// session concatenates its orphan commands ('new' / 'open') with the
|
|
76
|
+
// active doc's commands so single-doc apps see one unified list.
|
|
77
|
+
export interface Command {
|
|
78
|
+
// Stable identifier: 'new' | 'open' | 'history' | 'branches' |
|
|
79
|
+
// 'manage' | 'share' | 'undo' | 'redo'.
|
|
80
|
+
readonly id: string;
|
|
81
|
+
// Display label, sentence case, ready to render verbatim. A command may
|
|
82
|
+
// relabel as its state settles (`share` names the download once it knows
|
|
83
|
+
// the document has one), so read it on every notification the way
|
|
84
|
+
// `enabled` is read.
|
|
85
|
+
readonly label: string;
|
|
86
|
+
// 'session' (New / Open, which put a different document on screen),
|
|
87
|
+
// 'document' (this document: version history, branches, share, manage),
|
|
88
|
+
// or 'edit' (Undo / Redo). Render a separator where the group changes
|
|
89
|
+
// when laying commands out in one surface.
|
|
90
|
+
readonly group: CommandGroup;
|
|
91
|
+
// Suggested chrome for this command: 'menu' (a hamburger / file menu)
|
|
92
|
+
// or 'toolbar' (a prominent button). A hint, not a requirement; the
|
|
93
|
+
// app filters `commands` by `surface` to place each one, or ignores
|
|
94
|
+
// it and lays them out however it likes.
|
|
95
|
+
readonly surface: CommandSurface;
|
|
96
|
+
// Whether invoking the command would do anything right now. The SDK
|
|
97
|
+
// recomputes this as state changes; re-read on every notification.
|
|
98
|
+
readonly enabled: boolean;
|
|
99
|
+
// True while `run()` is in flight. Drives the pending affordance
|
|
100
|
+
// (disable + relabel) without the app tracking it.
|
|
101
|
+
readonly running: boolean;
|
|
102
|
+
// Optional submenu items. When present, render the command as a
|
|
103
|
+
// submenu parent (label + disclosure chevron) and the children as
|
|
104
|
+
// nested items. The parent's own `run()` is a no-op; each child
|
|
105
|
+
// has its own `run()`. Used by the built-in `branches` command.
|
|
106
|
+
readonly children?: readonly Command[];
|
|
107
|
+
// True when this command represents the currently active choice in
|
|
108
|
+
// a submenu (e.g. the active branch). Render a checkmark or bold.
|
|
109
|
+
readonly selected?: boolean;
|
|
110
|
+
// Invoke the command. Always resolves; a failure is routed to
|
|
111
|
+
// `Brass.onError`, never thrown, so a render-loop `onclick` handler
|
|
112
|
+
// does not need a `.catch`.
|
|
113
|
+
run(): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type CommandGroup = 'session' | 'document' | 'edit';
|
|
117
|
+
|
|
118
|
+
// Suggested chrome for a command. The app is free to honor or ignore it.
|
|
119
|
+
export type CommandSurface = 'menu' | 'toolbar';
|
|
120
|
+
|
|
121
|
+
// Built-in doc-scoped command labels. The session adds 'new' / 'open'
|
|
122
|
+
// (which don't need an active doc) at its own level.
|
|
123
|
+
const DOC_COMMAND_LABELS = {
|
|
124
|
+
history: 'Version history...',
|
|
125
|
+
branches: 'Branches...',
|
|
126
|
+
// Rename / move / delete are one surface ("the document's place in the
|
|
127
|
+
// filesystem"), so they are one menu entry, not separate Details + Delete.
|
|
128
|
+
manage: 'Manage...',
|
|
129
|
+
share: 'Share...',
|
|
130
|
+
undo: 'Undo',
|
|
131
|
+
redo: 'Redo',
|
|
132
|
+
} as const;
|
|
133
|
+
|
|
134
|
+
// The `share` label on a document someone can take a file away from.
|
|
135
|
+
// Taking a copy out is one row of the share dialog, beside the access
|
|
136
|
+
// roster, the way a phone's share sheet holds "save a file" next to "send
|
|
137
|
+
// to a person", so it earns a name in the entry that opens that dialog
|
|
138
|
+
// rather than a menu entry of its own. A document with no file to hand
|
|
139
|
+
// over keeps the plain label, so the entry never names a download the
|
|
140
|
+
// dialog does not offer.
|
|
141
|
+
const SHARE_WITH_DOWNLOAD_LABEL = 'Share / Download...';
|
|
142
|
+
|
|
143
|
+
// How many times a document's downloadable read may be attempted before the
|
|
144
|
+
// label stays as it is for the rest of the document's life, and how long a
|
|
145
|
+
// read that could not tell holds off the next attempt. The delay is what
|
|
146
|
+
// makes the retry cover the case it exists for: a session emits several
|
|
147
|
+
// times in its first second, so retrying per render would spend every
|
|
148
|
+
// attempt before the install that refused the read has landed.
|
|
149
|
+
const DOWNLOADABLE_MAX_ATTEMPTS = 3;
|
|
150
|
+
const DOWNLOADABLE_RETRY_DELAY_MS = 5000;
|
|
151
|
+
|
|
152
|
+
// Suggested chrome per built-in: the per-doc actions live in the file
|
|
153
|
+
// menu; Undo / Redo are toolbar buttons (the shape canvas / text apps
|
|
154
|
+
// reach for). 'branches' is built separately but shares the 'menu' hint.
|
|
155
|
+
const DOC_COMMAND_SURFACES: Record<
|
|
156
|
+
keyof typeof DOC_COMMAND_LABELS,
|
|
157
|
+
CommandSurface
|
|
158
|
+
> = {
|
|
159
|
+
history: 'menu',
|
|
160
|
+
branches: 'menu',
|
|
161
|
+
manage: 'menu',
|
|
162
|
+
share: 'menu',
|
|
163
|
+
undo: 'toolbar',
|
|
164
|
+
redo: 'toolbar',
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function commandToError(err: unknown): Error {
|
|
168
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Assemble a live `Command`. The returned object exposes `label` /
|
|
172
|
+
// `enabled` / `running` as getters over the injected state, so one stable
|
|
173
|
+
// instance reflects state changes and reconcilers match it by reference;
|
|
174
|
+
// never spread it (`{ ...cmd }` snapshots the getters into frozen values).
|
|
175
|
+
export function makeLiveCommand(
|
|
176
|
+
spec: Pick<Command, 'id' | 'group' | 'surface' | 'selected'> & {
|
|
177
|
+
// A function for a label that tracks state (`share` names the download
|
|
178
|
+
// once the document is known to have one).
|
|
179
|
+
label: string | (() => string);
|
|
180
|
+
// Defaults to "enabled unless running".
|
|
181
|
+
isEnabled?: () => boolean;
|
|
182
|
+
impl: () => Promise<void> | void;
|
|
183
|
+
},
|
|
184
|
+
wiring: {
|
|
185
|
+
// Shared per-id running map; defaults to a flag local to this command.
|
|
186
|
+
running?: Map<string, boolean>;
|
|
187
|
+
onError: (err: unknown) => void;
|
|
188
|
+
// Fires after each running flip so subscribers re-read.
|
|
189
|
+
notify?: () => void;
|
|
190
|
+
},
|
|
191
|
+
): Command {
|
|
192
|
+
const { running: map, onError, notify } = wiring;
|
|
193
|
+
const label = spec.label;
|
|
194
|
+
const readLabel = typeof label === 'function' ? label : (): string => label;
|
|
195
|
+
let localFlag = false;
|
|
196
|
+
const readRunning = map
|
|
197
|
+
? (): boolean => map.get(spec.id) === true
|
|
198
|
+
: (): boolean => localFlag;
|
|
199
|
+
const setRunning = (value: boolean): void => {
|
|
200
|
+
if (map) map.set(spec.id, value);
|
|
201
|
+
else localFlag = value;
|
|
202
|
+
notify?.();
|
|
203
|
+
};
|
|
204
|
+
const isEnabled = spec.isEnabled ?? ((): boolean => !readRunning());
|
|
205
|
+
return {
|
|
206
|
+
id: spec.id,
|
|
207
|
+
group: spec.group,
|
|
208
|
+
surface: spec.surface,
|
|
209
|
+
...(spec.selected !== undefined ? { selected: spec.selected } : {}),
|
|
210
|
+
get label(): string {
|
|
211
|
+
return readLabel();
|
|
212
|
+
},
|
|
213
|
+
get enabled(): boolean {
|
|
214
|
+
return isEnabled();
|
|
215
|
+
},
|
|
216
|
+
get running(): boolean {
|
|
217
|
+
return readRunning();
|
|
218
|
+
},
|
|
219
|
+
async run(): Promise<void> {
|
|
220
|
+
if (readRunning() || !isEnabled()) return;
|
|
221
|
+
setRunning(true);
|
|
222
|
+
try {
|
|
223
|
+
await spec.impl();
|
|
224
|
+
} catch (err) {
|
|
225
|
+
onError(err);
|
|
226
|
+
} finally {
|
|
227
|
+
setRunning(false);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// The contract the built-in command implementations need back from the
|
|
234
|
+
// owning Document. Wide because the file menu is, by design, the
|
|
235
|
+
// surface that reaches across every other doc capability (history /
|
|
236
|
+
// checkout, branches, undo, rename).
|
|
237
|
+
export interface CommandMenuHost {
|
|
238
|
+
docId(): string | null;
|
|
239
|
+
branchId(): string | null;
|
|
240
|
+
// True when the doc is showing a historical version (built-in
|
|
241
|
+
// `enabled` rules disable edit affordances then).
|
|
242
|
+
readOnly(): boolean;
|
|
243
|
+
// True when the doc is live (not closed / suspended).
|
|
244
|
+
isLive(): boolean;
|
|
245
|
+
canUndo(): boolean;
|
|
246
|
+
canRedo(): boolean;
|
|
247
|
+
undo(): void;
|
|
248
|
+
redo(): void;
|
|
249
|
+
// Same-branch version preview folds onto the doc in place.
|
|
250
|
+
checkoutVersion(version: number): Promise<void>;
|
|
251
|
+
// Show the version-browsing surface over the live document, without
|
|
252
|
+
// checking anything out. The hosted history dialog returns this when the
|
|
253
|
+
// reader wants to browse in the application rather than pick in a modal.
|
|
254
|
+
browseVersions(): void;
|
|
255
|
+
// Branches submenu reads the list + provider presence and creates.
|
|
256
|
+
branches(): readonly BranchSummary[];
|
|
257
|
+
hasBranchProvider(): boolean;
|
|
258
|
+
createBranch(input: { name?: string; from_version?: number }): Promise<BranchSummary>;
|
|
259
|
+
// Re-pull the branch list from the server. The `branches` built-in
|
|
260
|
+
// calls this after the hosted dialog closes without a switch, so a
|
|
261
|
+
// rename made in the dialog reaches the cached list the branch chip
|
|
262
|
+
// and page title read the active branch name from.
|
|
263
|
+
refreshBranches(): Promise<void>;
|
|
264
|
+
// The `details` built-in refreshes the cached doc name after a rename.
|
|
265
|
+
setName(name: string): void;
|
|
266
|
+
// Fire the Document's `commands` subscribe event so the menu
|
|
267
|
+
// re-renders (a label / running / registration change).
|
|
268
|
+
onCommandsChanged(): void;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export class CommandMenu {
|
|
272
|
+
// Hosted-UI provider for built-in commands. `null` until the factory
|
|
273
|
+
// wires it; the built-ins' `enabled` rule reads `_hostedUi !== null`
|
|
274
|
+
// so they disable in tests / offline flows.
|
|
275
|
+
private _hostedUi: HostedUiProvider | null = null;
|
|
276
|
+
// Per-built-in `running` flag, set during a command's `run()` and
|
|
277
|
+
// cleared after. Drives the pending-affordance UI.
|
|
278
|
+
private readonly _builtinRunning = new Map<string, boolean>();
|
|
279
|
+
// Lazily-built built-in command instances. The `Command` shape
|
|
280
|
+
// exposes getters that read live state (`enabled` / `running`), so
|
|
281
|
+
// the same object reflects state changes without recreating.
|
|
282
|
+
private _builtinCommands: readonly Command[] | null = null;
|
|
283
|
+
// Optional error reporter for built-in command failures. Wired by
|
|
284
|
+
// the factory to `Brass.onError`'s emit path; `null` falls back
|
|
285
|
+
// to `console.error`.
|
|
286
|
+
private _onError: ((err: Error) => void) | null = null;
|
|
287
|
+
// Whether this document has a file someone can take away, which is what
|
|
288
|
+
// the `share` label names. `false` until the read lands, so the entry
|
|
289
|
+
// only ever promises what is confirmed.
|
|
290
|
+
private _downloadable = false;
|
|
291
|
+
private _downloadableSettled = false;
|
|
292
|
+
private _downloadableInFlight = false;
|
|
293
|
+
private _downloadableAttempts = 0;
|
|
294
|
+
private _downloadableRetryAt = 0;
|
|
295
|
+
|
|
296
|
+
constructor(private readonly host: CommandMenuHost) {}
|
|
297
|
+
|
|
298
|
+
setHostedUiProvider(provider: HostedUiProvider | null): void {
|
|
299
|
+
this._hostedUi = provider;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Optional error sink for the doc's built-in commands. Wired by
|
|
303
|
+
// the factory to `Brass.onError`'s emit path so async failures
|
|
304
|
+
// surface through one stream. `null` falls back to `console.error`.
|
|
305
|
+
setErrorReporter(reporter: ((err: Error) => void) | null): void {
|
|
306
|
+
this._onError = reporter;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// The per-doc file menu, as data. Each Document carries its own list
|
|
310
|
+
// with its own `running` state; multi-doc apps render one menu per
|
|
311
|
+
// pane. The app filters by `surface` to place each command.
|
|
312
|
+
get commands(): readonly Command[] {
|
|
313
|
+
this._resolveDownloadable();
|
|
314
|
+
return this._builtins();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Ask, on a read of the list, whether this document has a file to hand
|
|
318
|
+
// over. Reading the list is what every menu renderer does as it mounts,
|
|
319
|
+
// so the answer lands long before a reader opens the menu, while an app
|
|
320
|
+
// that renders no menu never spends the request.
|
|
321
|
+
//
|
|
322
|
+
// A read that could not tell (`null`: refused, or failed) leaves the
|
|
323
|
+
// plain `Share...` label and lets a later render try again, because the
|
|
324
|
+
// moment it is most likely to be refused is the first create in an
|
|
325
|
+
// organization, where the install lands seconds afterwards and the
|
|
326
|
+
// document becomes readable. The delay and the attempt cap are what keep
|
|
327
|
+
// that to a few spaced requests rather than one per render.
|
|
328
|
+
private _resolveDownloadable(): void {
|
|
329
|
+
if (this._downloadableSettled || this._downloadableInFlight) return;
|
|
330
|
+
if (this._downloadableAttempts >= DOWNLOADABLE_MAX_ATTEMPTS) return;
|
|
331
|
+
if (Date.now() < this._downloadableRetryAt) return;
|
|
332
|
+
const resolve = this._hostedUi?.documentDownloadable;
|
|
333
|
+
const docId = this.host.docId();
|
|
334
|
+
if (!resolve || docId === null) return;
|
|
335
|
+
this._downloadableAttempts++;
|
|
336
|
+
this._downloadableInFlight = true;
|
|
337
|
+
void resolve(docId).then(
|
|
338
|
+
(downloadable) => {
|
|
339
|
+
this._downloadableInFlight = false;
|
|
340
|
+
if (downloadable === null) {
|
|
341
|
+
this._downloadableRetryAt = Date.now() + DOWNLOADABLE_RETRY_DELAY_MS;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this._downloadableSettled = true;
|
|
345
|
+
if (downloadable === this._downloadable) return;
|
|
346
|
+
this._downloadable = downloadable;
|
|
347
|
+
this.host.onCommandsChanged();
|
|
348
|
+
},
|
|
349
|
+
() => {
|
|
350
|
+
this._downloadableInFlight = false;
|
|
351
|
+
this._downloadableRetryAt = Date.now() + DOWNLOADABLE_RETRY_DELAY_MS;
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Invoke a command by id. A convenience for keyboard shortcuts
|
|
357
|
+
// (`Ctrl-Z` -> `doc.run('undo')`). No-op for an unknown id.
|
|
358
|
+
async run(commandId: string): Promise<void> {
|
|
359
|
+
const command = this.commands.find((c) => c.id === commandId);
|
|
360
|
+
if (command) await command.run();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Fork a new branch from a historical version and switch to it. Drives
|
|
364
|
+
// the hosted version bar's "Branch from here": create the branch at
|
|
365
|
+
// `fromVersion` (with the bar-supplied initial `name`, e.g. the version's
|
|
366
|
+
// timestamp), then open it via the same instance-swap the branches
|
|
367
|
+
// submenu's create uses. No-op without a hosted provider (offline /
|
|
368
|
+
// tests) or an open doc.
|
|
369
|
+
async forkFromVersion(fromVersion: number, name?: string): Promise<void> {
|
|
370
|
+
const hosted = this._hostedUi;
|
|
371
|
+
const docId = this.host.docId();
|
|
372
|
+
if (!hosted || docId === null) return;
|
|
373
|
+
const created = await this.host.createBranch({
|
|
374
|
+
from_version: fromVersion,
|
|
375
|
+
...(name ? { name } : {}),
|
|
376
|
+
});
|
|
377
|
+
if (hosted.openSiblingDoc) {
|
|
378
|
+
await hosted.openSiblingDoc(docId, { branch: created.branch_id });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Internal: lazy-build the doc-scoped built-in command list. The
|
|
383
|
+
// returned `Command` objects have getters that read live state, so
|
|
384
|
+
// an `enabled` / `running` change is reflected without recreating.
|
|
385
|
+
private _builtins(): readonly Command[] {
|
|
386
|
+
if (this._builtinCommands !== null) return this._builtinCommands;
|
|
387
|
+
const hasHostedUi = (): boolean => this._hostedUi !== null;
|
|
388
|
+
const editable = (): boolean => !this.host.readOnly() && this.host.isLive();
|
|
389
|
+
const list: Command[] = [
|
|
390
|
+
this._buildCommand(
|
|
391
|
+
'history',
|
|
392
|
+
DOC_COMMAND_LABELS.history,
|
|
393
|
+
'document',
|
|
394
|
+
DOC_COMMAND_SURFACES.history,
|
|
395
|
+
hasHostedUi,
|
|
396
|
+
async () => {
|
|
397
|
+
const hosted = this._hostedUi;
|
|
398
|
+
const docId = this.host.docId();
|
|
399
|
+
if (!hosted || docId === null) return;
|
|
400
|
+
const branchId = this.host.branchId();
|
|
401
|
+
const result = await hosted.documentHistory(
|
|
402
|
+
docId,
|
|
403
|
+
branchId ? { branch: branchId } : undefined,
|
|
404
|
+
);
|
|
405
|
+
if (!result) return;
|
|
406
|
+
// Same-branch version preview folds onto this doc in place.
|
|
407
|
+
// Cross-branch / fork outcomes can't (they switch state
|
|
408
|
+
// entirely), so delegate to `openSiblingDoc`. Discriminate on
|
|
409
|
+
// `typeof === 'number'`, not `!== undefined`: the hosted embed
|
|
410
|
+
// evolves independently, and a fork serialized as `version: null`
|
|
411
|
+
// (a natural JSON shape) must not be misread as a version open,
|
|
412
|
+
// which would drop the user into a read-only checkout at a bogus
|
|
413
|
+
// version instead of their new editable branch.
|
|
414
|
+
const version =
|
|
415
|
+
typeof result.version === 'number' ? result.version : null;
|
|
416
|
+
// Browse-in-the-app carries no version: it opens the host's
|
|
417
|
+
// version surface where the reader already is, so it is checked
|
|
418
|
+
// before the version / fork split rather than falling into the
|
|
419
|
+
// fork branch on its absent version.
|
|
420
|
+
if (version === null && result.browse === true) {
|
|
421
|
+
this.host.browseVersions();
|
|
422
|
+
} else if (version !== null && result.branch_id === branchId) {
|
|
423
|
+
await this.host.checkoutVersion(version);
|
|
424
|
+
} else if (hosted.openSiblingDoc) {
|
|
425
|
+
await hosted.openSiblingDoc(docId, {
|
|
426
|
+
branch: result.branch_id,
|
|
427
|
+
...(version !== null ? { version } : {}),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
),
|
|
432
|
+
this._buildBranchesCommand(),
|
|
433
|
+
this._buildCommand(
|
|
434
|
+
'share',
|
|
435
|
+
() =>
|
|
436
|
+
this._downloadable
|
|
437
|
+
? SHARE_WITH_DOWNLOAD_LABEL
|
|
438
|
+
: DOC_COMMAND_LABELS.share,
|
|
439
|
+
'document',
|
|
440
|
+
DOC_COMMAND_SURFACES.share,
|
|
441
|
+
hasHostedUi,
|
|
442
|
+
async () => {
|
|
443
|
+
const hosted = this._hostedUi;
|
|
444
|
+
const docId = this.host.docId();
|
|
445
|
+
if (!hosted || docId === null) return;
|
|
446
|
+
// The two surfaces are one dialog with a cross-link, so a
|
|
447
|
+
// duplicate started from Share reports its copy here too.
|
|
448
|
+
const outcome = await hosted.documentSettings(docId, {
|
|
449
|
+
focus: 'share',
|
|
450
|
+
});
|
|
451
|
+
await this._openDuplicate(outcome);
|
|
452
|
+
},
|
|
453
|
+
),
|
|
454
|
+
// Last of the document's entries, because it is the one holding a
|
|
455
|
+
// delete.
|
|
456
|
+
this._buildCommand(
|
|
457
|
+
'manage',
|
|
458
|
+
DOC_COMMAND_LABELS.manage,
|
|
459
|
+
'document',
|
|
460
|
+
DOC_COMMAND_SURFACES.manage,
|
|
461
|
+
hasHostedUi,
|
|
462
|
+
async () => {
|
|
463
|
+
const hosted = this._hostedUi;
|
|
464
|
+
const docId = this.host.docId();
|
|
465
|
+
if (!hosted || docId === null) return;
|
|
466
|
+
// The Manage surface is rename + move + duplicate + delete in
|
|
467
|
+
// one. A delete there ends via the server's `doc_gone` frame
|
|
468
|
+
// (routed into `close()`); a rename is reflected by the
|
|
469
|
+
// refreshName pull below.
|
|
470
|
+
const outcome = await hosted.documentSettings(docId, {
|
|
471
|
+
focus: 'details',
|
|
472
|
+
});
|
|
473
|
+
if (await this._openDuplicate(outcome)) return;
|
|
474
|
+
if (hosted.refreshName) {
|
|
475
|
+
const newName = await hosted.refreshName(docId);
|
|
476
|
+
if (newName !== null) this.host.setName(newName);
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
),
|
|
480
|
+
this._buildCommand(
|
|
481
|
+
'undo',
|
|
482
|
+
DOC_COMMAND_LABELS.undo,
|
|
483
|
+
'edit',
|
|
484
|
+
DOC_COMMAND_SURFACES.undo,
|
|
485
|
+
() => editable() && this.host.canUndo(),
|
|
486
|
+
() => {
|
|
487
|
+
this.host.undo();
|
|
488
|
+
},
|
|
489
|
+
),
|
|
490
|
+
this._buildCommand(
|
|
491
|
+
'redo',
|
|
492
|
+
DOC_COMMAND_LABELS.redo,
|
|
493
|
+
'edit',
|
|
494
|
+
DOC_COMMAND_SURFACES.redo,
|
|
495
|
+
() => editable() && this.host.canRedo(),
|
|
496
|
+
() => {
|
|
497
|
+
this.host.redo();
|
|
498
|
+
},
|
|
499
|
+
),
|
|
500
|
+
];
|
|
501
|
+
this._builtinCommands = list;
|
|
502
|
+
return list;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private _reportError = (err: unknown): void => {
|
|
506
|
+
if (this._onError) this._onError(commandToError(err));
|
|
507
|
+
else console.error('[Brass] doc command failed:', err);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Build a single command over the shared per-id running map, notifying
|
|
511
|
+
// the host on each flip so the menu re-renders.
|
|
512
|
+
private _buildCommand(
|
|
513
|
+
id: string,
|
|
514
|
+
label: string | (() => string),
|
|
515
|
+
group: CommandGroup,
|
|
516
|
+
surface: CommandSurface,
|
|
517
|
+
isEnabled: () => boolean,
|
|
518
|
+
impl: () => Promise<void> | void,
|
|
519
|
+
): Command {
|
|
520
|
+
return makeLiveCommand(
|
|
521
|
+
{ id, label, group, surface, isEnabled, impl },
|
|
522
|
+
{
|
|
523
|
+
running: this._builtinRunning,
|
|
524
|
+
onError: this._reportError,
|
|
525
|
+
notify: () => this.host.onCommandsChanged(),
|
|
526
|
+
},
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Open the copy a settings-surface duplicate produced. The copy is a
|
|
531
|
+
// different document, not another state of this one, so it goes through
|
|
532
|
+
// the same coordinator a branch switch does. Reports whether it opened,
|
|
533
|
+
// so a caller can skip the follow-up reads that describe the doc it just
|
|
534
|
+
// navigated away from.
|
|
535
|
+
private async _openDuplicate(
|
|
536
|
+
outcome: DocumentSettingsResult | null,
|
|
537
|
+
): Promise<boolean> {
|
|
538
|
+
const hosted = this._hostedUi;
|
|
539
|
+
if (!outcome || !hosted?.openSiblingDoc) return false;
|
|
540
|
+
await hosted.openSiblingDoc(outcome.newDocId, {});
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Build the branches submenu command. The parent is always present
|
|
545
|
+
// (enabled when the doc has a hostedUi provider for openSiblingDoc);
|
|
546
|
+
// its children are rebuilt dynamically from `doc.branches` on every
|
|
547
|
+
// access so the menu tracks branch creation / refresh reactively.
|
|
548
|
+
private _buildBranchesCommand(): Command {
|
|
549
|
+
const isEnabled = (): boolean => this._hostedUi !== null;
|
|
550
|
+
const readRunning = (): boolean =>
|
|
551
|
+
this._builtinRunning.get('branches') === true;
|
|
552
|
+
|
|
553
|
+
const buildChild = (
|
|
554
|
+
id: string,
|
|
555
|
+
label: string,
|
|
556
|
+
selected: boolean,
|
|
557
|
+
impl: () => Promise<void> | void,
|
|
558
|
+
): Command =>
|
|
559
|
+
makeLiveCommand(
|
|
560
|
+
{
|
|
561
|
+
id,
|
|
562
|
+
label,
|
|
563
|
+
group: 'document',
|
|
564
|
+
surface: DOC_COMMAND_SURFACES.branches,
|
|
565
|
+
selected,
|
|
566
|
+
impl,
|
|
567
|
+
},
|
|
568
|
+
{ onError: this._reportError },
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const buildChildren = (): readonly Command[] => {
|
|
572
|
+
const items: Command[] = [];
|
|
573
|
+
const docId = this.host.docId();
|
|
574
|
+
const branchId = this.host.branchId();
|
|
575
|
+
for (const b of this.host.branches()) {
|
|
576
|
+
const isCurrent = b.branch_id === branchId;
|
|
577
|
+
items.push(
|
|
578
|
+
buildChild(`branch:${b.branch_id}`, b.name, isCurrent, async () => {
|
|
579
|
+
if (isCurrent) return;
|
|
580
|
+
const hosted = this._hostedUi;
|
|
581
|
+
if (hosted?.openSiblingDoc && docId) {
|
|
582
|
+
await hosted.openSiblingDoc(docId, {
|
|
583
|
+
branch: b.branch_id,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (this.host.hasBranchProvider() && docId !== null) {
|
|
590
|
+
items.push(
|
|
591
|
+
buildChild('branch:new', 'New branch...', false, async () => {
|
|
592
|
+
await this.host.createBranch({});
|
|
593
|
+
const branches = this.host.branches();
|
|
594
|
+
const newest = branches[branches.length - 1];
|
|
595
|
+
if (newest) {
|
|
596
|
+
const hosted = this._hostedUi;
|
|
597
|
+
if (hosted?.openSiblingDoc && docId) {
|
|
598
|
+
await hosted.openSiblingDoc(docId, {
|
|
599
|
+
branch: newest.branch_id,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}),
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return items;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
id: 'branches',
|
|
611
|
+
label: DOC_COMMAND_LABELS.branches,
|
|
612
|
+
group: 'document',
|
|
613
|
+
surface: DOC_COMMAND_SURFACES.branches,
|
|
614
|
+
get enabled(): boolean {
|
|
615
|
+
return isEnabled();
|
|
616
|
+
},
|
|
617
|
+
get running(): boolean {
|
|
618
|
+
return readRunning();
|
|
619
|
+
},
|
|
620
|
+
get children(): readonly Command[] {
|
|
621
|
+
return buildChildren();
|
|
622
|
+
},
|
|
623
|
+
run: async (): Promise<void> => {
|
|
624
|
+
const hosted = this._hostedUi;
|
|
625
|
+
const docId = this.host.docId();
|
|
626
|
+
if (!hosted || !docId) return;
|
|
627
|
+
if (hosted.selectBranch) {
|
|
628
|
+
const branchId = this.host.branchId();
|
|
629
|
+
const result = await hosted.selectBranch(
|
|
630
|
+
docId,
|
|
631
|
+
branchId ? { branch: branchId } : undefined,
|
|
632
|
+
);
|
|
633
|
+
if (result && result.branch_id !== branchId && hosted.openSiblingDoc) {
|
|
634
|
+
// A different branch was chosen: reopening on it reloads the
|
|
635
|
+
// branch list as part of the open.
|
|
636
|
+
await hosted.openSiblingDoc(docId, {
|
|
637
|
+
branch: result.branch_id,
|
|
638
|
+
});
|
|
639
|
+
} else {
|
|
640
|
+
// No switch (dialog dismissed, or the active branch re-picked).
|
|
641
|
+
// The dialog can rename / set-default in place, so re-pull the
|
|
642
|
+
// list; a rename of the current branch otherwise leaves the
|
|
643
|
+
// branch chip + page title stale (they read the name from the
|
|
644
|
+
// cached list, which the dialog never touches).
|
|
645
|
+
await this.host.refreshBranches();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|