@backmatter/studio 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{UiKitApp-BDsrXN12.js → UiKitApp-D7vllRQE.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-BTZ1Ah4F.js → WorkspaceRoutes-DH1LD-2Q.js} +30013 -27965
- package/dist/chunks/{dashboard-FT7dm5wi.js → dashboard-Dsaeud2x.js} +55 -54
- package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
- package/dist/chunks/{index-DHJvh5Uy.js → index-CGMqw2NH.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/define.js +1 -1
- package/dist/index.js +4 -4
- package/dist/site-widget.css +49 -1
- package/dist/site-widget.js +8341 -8058
- package/dist/studio.css +279 -13
- package/package.json +2 -2
- package/src/WorkspaceRoutes.tsx +5 -2
- package/src/api.ts +82 -7
- package/src/chat/ChangeCard.tsx +8 -2
- package/src/chat/ChatMessage.tsx +171 -102
- package/src/chat/ChatPanel.tsx +54 -34
- package/src/chat/ChatProvider.tsx +122 -20
- package/src/chat/Composer.tsx +51 -8
- package/src/chat/PointerChip.tsx +9 -5
- package/src/chat/ThreadHistory.tsx +3 -3
- package/src/chat/chat-context.ts +67 -3
- package/src/chat/chat-helpers.ts +12 -0
- package/src/chat/chat-state.ts +11 -0
- package/src/chrome/AppShell.tsx +16 -5
- package/src/chrome/CommandPalette.tsx +12 -0
- package/src/chrome/ContentExplorerSidebar.tsx +23 -3
- package/src/chrome/Toast.tsx +115 -0
- package/src/chrome/UserMenu.tsx +34 -3
- package/src/chrome/WorkspaceStatusBar.tsx +221 -30
- package/src/chrome/commit-error.ts +13 -0
- package/src/chrome/content-api-url.ts +0 -78
- package/src/chrome/content-tree-visibility.ts +13 -3
- package/src/chrome/explorer-icons.tsx +5 -0
- package/src/chrome/pull-failure.ts +61 -0
- package/src/code-session-entry.ts +9 -0
- package/src/components/WorkspaceSwitcher.tsx +2 -2
- package/src/element-origin.ts +149 -0
- package/src/fields/FieldControl.tsx +748 -0
- package/src/fields/choose-field-input.ts +64 -0
- package/src/fields/load-relation-documents.ts +35 -0
- package/src/fields/omit-field.ts +12 -0
- package/src/fields/relation-targets.ts +44 -0
- package/src/fields/resolve-object-fields.ts +21 -0
- package/src/host-pointer-bootstrap.ts +11 -0
- package/src/host-pointer.ts +147 -0
- package/src/install-preview-pointer.ts +19 -3
- package/src/pages/BlockSettingsDialog.tsx +19 -115
- package/src/pages/BlocksList.tsx +43 -35
- package/src/pages/BodyProse.tsx +16 -1
- package/src/pages/CodeSessionFiles.tsx +221 -0
- package/src/pages/CodeSessionPage.tsx +1361 -0
- package/src/pages/DocEditorPage.tsx +120 -107
- package/src/pages/PreviewSize.tsx +181 -0
- package/src/pages/RenderedBody.tsx +17 -2
- package/src/pages/SettingsGeneralPage.tsx +67 -2
- package/src/pages/WorkspaceHomePage.tsx +313 -0
- package/src/pages/WorkspaceLayout.tsx +10 -1
- package/src/pages/branch-notice.ts +16 -0
- package/src/pages/code-session-boot.ts +43 -0
- package/src/pages/code-session-context.tsx +53 -0
- package/src/pages/code-session-files.ts +42 -0
- package/src/pages/code-session-frame.ts +52 -0
- package/src/pages/code-session-routes.ts +42 -0
- package/src/pages/home-rows.ts +140 -0
- package/src/pages/preview-size.ts +33 -0
- package/src/pages/session-preview.ts +109 -0
- package/src/pages/use-session-preview.ts +70 -0
- package/src/replica/store.ts +11 -0
- package/src/replica/types.ts +6 -0
- package/src/shell-resets.css +15 -0
- package/src/site-pointer.ts +24 -5
- package/src/studio-config.ts +1 -1
- package/src/ui/Input.tsx +178 -20
- package/src/ui/ListRow.tsx +12 -3
- package/src/ui/Page.tsx +12 -1
- package/src/ui/popup.ts +11 -2
- package/src/workspace-path.ts +35 -3
- package/src/chrome/ContentApiPopover.tsx +0 -215
- package/src/pages/WorkspaceHomeRedirect.tsx +0 -17
|
@@ -7,9 +7,11 @@ import { Popover } from "@base-ui/react/popover";
|
|
|
7
7
|
import * as stylex from "@stylexjs/stylex";
|
|
8
8
|
import {
|
|
9
9
|
commitWorkspace,
|
|
10
|
+
commitCodeSessionEdits,
|
|
10
11
|
countConflicts,
|
|
11
12
|
createGitBranch,
|
|
12
13
|
defaultCommitMessage,
|
|
14
|
+
fetchCodeSessionEdits,
|
|
13
15
|
fetchGit,
|
|
14
16
|
notifyHeadChanged,
|
|
15
17
|
notifyTreeChanged,
|
|
@@ -17,12 +19,13 @@ import {
|
|
|
17
19
|
notifyWorkspaceChanged,
|
|
18
20
|
pullFromGithub,
|
|
19
21
|
resolveDoc,
|
|
22
|
+
type ApiError,
|
|
20
23
|
type ChangeStatus,
|
|
21
24
|
type GitChange,
|
|
22
25
|
} from "../api";
|
|
23
|
-
import { contentDocPath } from "../workspace-path";
|
|
26
|
+
import { contentDocPath, isWorkspaceCodePath } from "../workspace-path";
|
|
27
|
+
import { useCodeSessionContext } from "../pages/code-session-context";
|
|
24
28
|
import {
|
|
25
|
-
commitErrorSummary,
|
|
26
29
|
explainCommitError,
|
|
27
30
|
problemsForRef,
|
|
28
31
|
type CommitErrorView,
|
|
@@ -33,10 +36,12 @@ import { WorkspaceSwitcher } from "../components/WorkspaceSwitcher";
|
|
|
33
36
|
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
34
37
|
import { cx, popup, popupClassName } from "../ui/popup";
|
|
35
38
|
import { useEditorStatus } from "./EditorStatusContext";
|
|
36
|
-
import {
|
|
37
|
-
import { OpenSiteButton } from "./OpenSiteButton";
|
|
39
|
+
import { openableSites } from "./open-site";
|
|
40
|
+
import { OpenSiteButton, showsOpenSite } from "./OpenSiteButton";
|
|
38
41
|
import { GitBranchIcon } from "./icons";
|
|
42
|
+
import { pullFailureProblems, type PullProblem } from "./pull-failure";
|
|
39
43
|
import { StatusBar, StatusItem, StatusSpacer } from "./StatusBar";
|
|
44
|
+
import { Toast } from "./Toast";
|
|
40
45
|
import { replicaStore } from "../replica/instance";
|
|
41
46
|
import { useReplicaSession } from "../replica/ReplicaProvider";
|
|
42
47
|
import { useReplicaState } from "../replica/useReplica";
|
|
@@ -168,7 +173,17 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
168
173
|
const [resolvingKey, setResolvingKey] = useState<string | null>(null);
|
|
169
174
|
const [message, setMessage] = useState("");
|
|
170
175
|
const [error, setError] = useState<string | null>(null);
|
|
176
|
+
const [pullProblems, setPullProblems] = useState<PullProblem[] | null>(null);
|
|
171
177
|
const [commitFailure, setCommitFailure] = useState<CommitErrorView | null>(null);
|
|
178
|
+
const onCodePage = isWorkspaceCodePath(location.pathname);
|
|
179
|
+
const codeSession = useCodeSessionContext();
|
|
180
|
+
const codeWorkspaceId = workspaceNav?.workspaceId ?? workspaceId;
|
|
181
|
+
const [codeFiles, setCodeFiles] = useState<Array<{ path: string; deleted: boolean }>>([]);
|
|
182
|
+
const [foldPending, setFoldPending] = useState(false);
|
|
183
|
+
const [codeFoldErrors, setCodeFoldErrors] = useState<string[]>([]);
|
|
184
|
+
const [codeCommitOpen, setCodeCommitOpen] = useState(false);
|
|
185
|
+
const [codeMessage, setCodeMessage] = useState("");
|
|
186
|
+
const [codeCommitting, setCodeCommitting] = useState(false);
|
|
172
187
|
// Commit sha from the last poll. A new sha means HEAD moved. The preview
|
|
173
188
|
// iframe does not reload for that; its page follows the SSE head event.
|
|
174
189
|
const lastHead = useRef<string | null | undefined>(undefined);
|
|
@@ -192,6 +207,35 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
192
207
|
}
|
|
193
208
|
}
|
|
194
209
|
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!onCodePage || !codeSession) {
|
|
212
|
+
setCodeFiles([]);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
let cancelled = false;
|
|
216
|
+
const load = () => {
|
|
217
|
+
void fetchCodeSessionEdits(codeWorkspaceId, codeSession.sessionId)
|
|
218
|
+
.then((list) => {
|
|
219
|
+
if (!cancelled) {
|
|
220
|
+
setCodeFiles(list.files);
|
|
221
|
+
setFoldPending(list.foldPending);
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
.catch(() => {
|
|
225
|
+
if (!cancelled) {
|
|
226
|
+
setCodeFiles([]);
|
|
227
|
+
setFoldPending(false);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
load();
|
|
232
|
+
const timer = window.setInterval(load, POLL_MS);
|
|
233
|
+
return () => {
|
|
234
|
+
cancelled = true;
|
|
235
|
+
window.clearInterval(timer);
|
|
236
|
+
};
|
|
237
|
+
}, [onCodePage, codeSession, codeWorkspaceId]);
|
|
238
|
+
|
|
195
239
|
useEffect(() => {
|
|
196
240
|
lastHead.current = undefined;
|
|
197
241
|
void reloadGit();
|
|
@@ -264,13 +308,15 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
264
308
|
async function onPull() {
|
|
265
309
|
setPulling(true);
|
|
266
310
|
setError(null);
|
|
311
|
+
setPullProblems(null);
|
|
267
312
|
try {
|
|
268
313
|
await pullFromGithub(workspaceId);
|
|
269
314
|
notifyHeadChanged();
|
|
270
315
|
await reloadGit();
|
|
271
316
|
setOpen(false);
|
|
272
317
|
} catch (err) {
|
|
273
|
-
|
|
318
|
+
setOpen(false);
|
|
319
|
+
setPullProblems(pullFailureProblems(err));
|
|
274
320
|
} finally {
|
|
275
321
|
setPulling(false);
|
|
276
322
|
}
|
|
@@ -304,6 +350,46 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
304
350
|
}
|
|
305
351
|
}
|
|
306
352
|
|
|
353
|
+
function defaultCodeSessionMessage(files: Array<{ path: string }>) {
|
|
354
|
+
if (files.length === 1) return `Update ${files[0]!.path}`;
|
|
355
|
+
return `Commit ${files.length} files from a code session`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function onCodeSessionCommit(e?: React.FormEvent) {
|
|
359
|
+
e?.preventDefault();
|
|
360
|
+
if (!codeSession || codeCommitting) return;
|
|
361
|
+
setCodeCommitting(true);
|
|
362
|
+
setError(null);
|
|
363
|
+
try {
|
|
364
|
+
const result = await commitCodeSessionEdits(
|
|
365
|
+
codeWorkspaceId,
|
|
366
|
+
codeSession.sessionId,
|
|
367
|
+
codeMessage,
|
|
368
|
+
);
|
|
369
|
+
setCodeMessage("");
|
|
370
|
+
setCodeFoldErrors(result.foldErrors);
|
|
371
|
+
if (result.foldErrors.length === 0) setCodeCommitOpen(false);
|
|
372
|
+
codeSession.refresh();
|
|
373
|
+
const list = await fetchCodeSessionEdits(codeWorkspaceId, codeSession.sessionId);
|
|
374
|
+
setCodeFiles(list.files);
|
|
375
|
+
setFoldPending(list.foldPending);
|
|
376
|
+
notifyHeadChanged();
|
|
377
|
+
await reloadGit();
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const apiErr = err as ApiError;
|
|
380
|
+
if (apiErr.code === "branch_moved") {
|
|
381
|
+
setError("The branch changed while committing. Try again.");
|
|
382
|
+
} else if (apiErr.code === "working_tree") {
|
|
383
|
+
const paths = apiErr.details?.length ? apiErr.details.join(", ") : "";
|
|
384
|
+
setError(paths ? `Uncommitted Studio changes: ${paths}` : "Uncommitted Studio changes block this commit.");
|
|
385
|
+
} else {
|
|
386
|
+
setError(err instanceof Error ? err.message : "Commit failed");
|
|
387
|
+
}
|
|
388
|
+
} finally {
|
|
389
|
+
setCodeCommitting(false);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
307
393
|
async function onCommit(e?: React.FormEvent) {
|
|
308
394
|
e?.preventDefault();
|
|
309
395
|
if (committing || conflictCount > 0 || changeCount === 0) return;
|
|
@@ -338,9 +424,26 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
338
424
|
? "Committing…"
|
|
339
425
|
: conflictCount > 0
|
|
340
426
|
? `Resolve ${conflictCount} ${conflictCount === 1 ? "conflict" : "conflicts"}`
|
|
341
|
-
:
|
|
427
|
+
: changeCount === 0
|
|
428
|
+
? "Deploy"
|
|
429
|
+
: `Deploy ${changeCount} changes`;
|
|
430
|
+
|
|
431
|
+
const codeEditCount =
|
|
432
|
+
codeFiles.length > 0 ? codeFiles.length : (codeSession?.editCount ?? 0);
|
|
433
|
+
const codeCommitReady =
|
|
434
|
+
Boolean(codeSession) &&
|
|
435
|
+
(codeEditCount > 0 || codeFoldErrors.length > 0 || foldPending);
|
|
436
|
+
const codeCommitLabel = codeCommitting
|
|
437
|
+
? "Committing…"
|
|
438
|
+
: codeEditCount > 0
|
|
439
|
+
? `Commit ${codeEditCount} ${codeEditCount === 1 ? "file" : "files"}`
|
|
440
|
+
: "Commit";
|
|
342
441
|
|
|
343
442
|
return (
|
|
443
|
+
<>
|
|
444
|
+
{pullProblems ? (
|
|
445
|
+
<Toast title="Pull failed" problems={pullProblems} onDismiss={() => setPullProblems(null)} />
|
|
446
|
+
) : null}
|
|
344
447
|
<StatusBar>
|
|
345
448
|
<div {...stylex.props(styles.identity)}>
|
|
346
449
|
{workspaceNav ? (
|
|
@@ -478,27 +581,21 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
478
581
|
</Combobox.Positioner>
|
|
479
582
|
</Combobox.Portal>
|
|
480
583
|
</Combobox.Root>
|
|
481
|
-
{workspaceNav?.workspaceId
|
|
584
|
+
{workspaceNav?.workspaceId &&
|
|
585
|
+
replica.sessionUserId &&
|
|
586
|
+
showsOpenSite() &&
|
|
587
|
+
openableSites(workspaceNav.siteUrls).length > 0 ? (
|
|
482
588
|
<>
|
|
483
589
|
<span {...stylex.props(styles.divider)} aria-hidden />
|
|
484
|
-
<
|
|
590
|
+
<OpenSiteButton
|
|
485
591
|
workspaceId={workspaceNav.workspaceId}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
pathname={location.pathname}
|
|
489
|
-
search={location.search}
|
|
592
|
+
userId={replica.sessionUserId}
|
|
593
|
+
siteUrls={workspaceNav.siteUrls}
|
|
490
594
|
/>
|
|
491
|
-
{replica.sessionUserId ? (
|
|
492
|
-
<OpenSiteButton
|
|
493
|
-
workspaceId={workspaceNav.workspaceId}
|
|
494
|
-
userId={replica.sessionUserId}
|
|
495
|
-
siteUrls={workspaceNav.siteUrls}
|
|
496
|
-
/>
|
|
497
|
-
) : null}
|
|
498
595
|
</>
|
|
499
596
|
) : null}
|
|
500
597
|
</div>
|
|
501
|
-
{changeCount > 0 ? (
|
|
598
|
+
{!onCodePage && changeCount > 0 ? (
|
|
502
599
|
<StatusItem tone={conflictCount > 0 ? "danger" : "muted"}>
|
|
503
600
|
{changeCount} uncommitted
|
|
504
601
|
</StatusItem>
|
|
@@ -506,7 +603,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
506
603
|
{error ? (
|
|
507
604
|
<StatusItem tone="danger">{error}</StatusItem>
|
|
508
605
|
) : commitFailure ? (
|
|
509
|
-
<StatusItem tone="danger">
|
|
606
|
+
<StatusItem tone="danger">Can't commit</StatusItem>
|
|
510
607
|
) : refreshError ? (
|
|
511
608
|
<StatusItem tone="danger">{refreshError}</StatusItem>
|
|
512
609
|
) : null}
|
|
@@ -514,24 +611,117 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
514
611
|
{inEditor && editor.saveLabel ? (
|
|
515
612
|
<StatusItem tone={editor.saveTone}>{editor.saveLabel}</StatusItem>
|
|
516
613
|
) : null}
|
|
517
|
-
{
|
|
614
|
+
{onCodePage ? (
|
|
615
|
+
<Popover.Root
|
|
616
|
+
open={codeCommitOpen}
|
|
617
|
+
onOpenChange={(next) => {
|
|
618
|
+
if (!codeCommitReady) return;
|
|
619
|
+
setCodeCommitOpen(next);
|
|
620
|
+
}}
|
|
621
|
+
>
|
|
622
|
+
<Popover.Trigger
|
|
623
|
+
disabled={!codeCommitReady || codeCommitting}
|
|
624
|
+
aria-label={codeCommitLabel}
|
|
625
|
+
className={cx(styles.commit)}
|
|
626
|
+
>
|
|
627
|
+
{codeCommitLabel}
|
|
628
|
+
</Popover.Trigger>
|
|
629
|
+
<Popover.Portal>
|
|
630
|
+
<Popover.Positioner
|
|
631
|
+
side="bottom"
|
|
632
|
+
align="end"
|
|
633
|
+
sideOffset={6}
|
|
634
|
+
className={cx(popup.positioner)}
|
|
635
|
+
>
|
|
636
|
+
<Popover.Popup
|
|
637
|
+
aria-label="Commit code session"
|
|
638
|
+
className={(state) => popupClassName(state, styles.commitPopup)}
|
|
639
|
+
>
|
|
640
|
+
<form
|
|
641
|
+
onSubmit={(e) => void onCodeSessionCommit(e)}
|
|
642
|
+
{...stylex.props(styles.commitForm)}
|
|
643
|
+
>
|
|
644
|
+
<p {...stylex.props(popup.groupLabel)}>
|
|
645
|
+
{codeEditCount} {codeEditCount === 1 ? "file" : "files"} →{" "}
|
|
646
|
+
{git?.currentBranch}
|
|
647
|
+
</p>
|
|
648
|
+
<ul {...stylex.props(styles.commitFiles)}>
|
|
649
|
+
{codeFiles.map((file) => (
|
|
650
|
+
<li key={file.path}>
|
|
651
|
+
<div {...stylex.props(styles.changeItem)}>
|
|
652
|
+
<span {...stylex.props(styles.changeName)}>{file.path}</span>
|
|
653
|
+
<span {...stylex.props(popup.itemMeta)}>
|
|
654
|
+
{file.deleted ? "deleted" : "modified"}
|
|
655
|
+
</span>
|
|
656
|
+
</div>
|
|
657
|
+
</li>
|
|
658
|
+
))}
|
|
659
|
+
</ul>
|
|
660
|
+
{codeFoldErrors.length > 0 ? (
|
|
661
|
+
<ul {...stylex.props(styles.commitFiles)}>
|
|
662
|
+
{codeFoldErrors.map((foldError) => (
|
|
663
|
+
<li key={foldError}>
|
|
664
|
+
<p {...stylex.props(styles.conflictError)}>{foldError}</p>
|
|
665
|
+
</li>
|
|
666
|
+
))}
|
|
667
|
+
</ul>
|
|
668
|
+
) : null}
|
|
669
|
+
<div {...stylex.props(popup.separator)} />
|
|
670
|
+
<div {...stylex.props(styles.commitFooter)}>
|
|
671
|
+
<input
|
|
672
|
+
value={codeMessage}
|
|
673
|
+
onChange={(e) => setCodeMessage(e.target.value)}
|
|
674
|
+
placeholder={defaultCodeSessionMessage(codeFiles)}
|
|
675
|
+
aria-label="Commit message"
|
|
676
|
+
autoComplete="off"
|
|
677
|
+
spellCheck={false}
|
|
678
|
+
data-no-ring
|
|
679
|
+
data-1p-ignore
|
|
680
|
+
data-lpignore
|
|
681
|
+
autoFocus
|
|
682
|
+
disabled={codeCommitting}
|
|
683
|
+
{...stylex.props(styles.commitField)}
|
|
684
|
+
/>
|
|
685
|
+
<div {...stylex.props(styles.commitSubmit)}>
|
|
686
|
+
<button
|
|
687
|
+
type="submit"
|
|
688
|
+
disabled={codeCommitting}
|
|
689
|
+
{...stylex.props(styles.commitSubmitBtn)}
|
|
690
|
+
>
|
|
691
|
+
{codeCommitting ? "Committing…" : "Commit"}
|
|
692
|
+
</button>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
</form>
|
|
696
|
+
</Popover.Popup>
|
|
697
|
+
</Popover.Positioner>
|
|
698
|
+
</Popover.Portal>
|
|
699
|
+
</Popover.Root>
|
|
700
|
+
) : null}
|
|
701
|
+
{!onCodePage ? (
|
|
518
702
|
<Popover.Root
|
|
519
703
|
open={commitOpen}
|
|
520
704
|
onOpenChange={(next) => {
|
|
521
|
-
if (action === "blocked") return;
|
|
705
|
+
if (changeCount === 0 || action === "blocked") return;
|
|
522
706
|
setCommitOpen(next);
|
|
523
707
|
}}
|
|
524
708
|
>
|
|
525
709
|
<Popover.Trigger
|
|
526
|
-
disabled={
|
|
710
|
+
disabled={
|
|
711
|
+
changeCount === 0 || commitTriggerDisabled(action, { committing, syncing })
|
|
712
|
+
}
|
|
527
713
|
aria-label={
|
|
528
|
-
|
|
529
|
-
? "
|
|
530
|
-
: action === "
|
|
531
|
-
?
|
|
532
|
-
:
|
|
714
|
+
changeCount === 0
|
|
715
|
+
? "Deploy"
|
|
716
|
+
: action === "blocked"
|
|
717
|
+
? "Writers cannot publish"
|
|
718
|
+
: action === "resolve"
|
|
719
|
+
? commitLabel
|
|
720
|
+
: `Commit ${changeCount} ${changeCount === 1 ? "file" : "files"}`
|
|
721
|
+
}
|
|
722
|
+
title={
|
|
723
|
+
changeCount > 0 && action === "blocked" ? "Writers cannot publish" : undefined
|
|
533
724
|
}
|
|
534
|
-
title={action === "blocked" ? "Writers cannot publish" : undefined}
|
|
535
725
|
className={cx(styles.commit, action === "resolve" && styles.commitBlocked)}
|
|
536
726
|
>
|
|
537
727
|
{commitLabel}
|
|
@@ -628,6 +818,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
628
818
|
</Popover.Root>
|
|
629
819
|
) : null}
|
|
630
820
|
</StatusBar>
|
|
821
|
+
</>
|
|
631
822
|
);
|
|
632
823
|
}
|
|
633
824
|
|
|
@@ -27,6 +27,9 @@ export function explainCommitError(err: unknown): CommitErrorView {
|
|
|
27
27
|
if (apiErr.code === "github_not_bound") {
|
|
28
28
|
return { kind: "message", text: "Connect GitHub first" };
|
|
29
29
|
}
|
|
30
|
+
if (apiErr.code === "unknown_type") {
|
|
31
|
+
return { kind: "message", text: unknownTypeMessage(apiErr.details ?? []) };
|
|
32
|
+
}
|
|
30
33
|
if (apiErr.code === "validation") {
|
|
31
34
|
return { kind: "validation", files: groupValidation(validationLines(apiErr)) };
|
|
32
35
|
}
|
|
@@ -52,6 +55,16 @@ export function problemsForRef(
|
|
|
52
55
|
return view.files.find((file) => file.ref === ref)?.problems ?? [];
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
function unknownTypeMessage(types: string[]): string {
|
|
59
|
+
if (types.length === 0) {
|
|
60
|
+
return "A file uses a model this workspace doesn't have, so it can't be committed.";
|
|
61
|
+
}
|
|
62
|
+
if (types.length === 1) {
|
|
63
|
+
return `${types[0]} isn't a model in this workspace. Pull couldn't load that model file, so this draft can't be committed.`;
|
|
64
|
+
}
|
|
65
|
+
return `${types.join(", ")} aren't models in this workspace. Pull couldn't load those model files, so these drafts can't be committed.`;
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
function validationLines(err: ApiError): string[] {
|
|
56
69
|
if (err.details && err.details.length > 0) return err.details;
|
|
57
70
|
const body = err.message.replace(/^validation:\s*/, "");
|
|
@@ -1,48 +1,6 @@
|
|
|
1
1
|
// packages/studio/src/chrome/content-api-url.ts
|
|
2
2
|
// Public content GET the host reads — same URLs as @backmatter/core api-loader.
|
|
3
3
|
|
|
4
|
-
export type ContentApiDoc = { type: string; slug: string };
|
|
5
|
-
|
|
6
|
-
export type ContentApiContext = {
|
|
7
|
-
doc: ContentApiDoc | null;
|
|
8
|
-
type: string | null;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function contentApiContextFromPath(
|
|
12
|
-
pathname: string,
|
|
13
|
-
_search = "",
|
|
14
|
-
): ContentApiContext {
|
|
15
|
-
const doc = pathname.match(/\/content\/([^/]+)\/([^/]+)\/?$/);
|
|
16
|
-
if (doc) {
|
|
17
|
-
return {
|
|
18
|
-
doc: {
|
|
19
|
-
type: decodeURIComponent(doc[1]!),
|
|
20
|
-
slug: decodeURIComponent(doc[2]!),
|
|
21
|
-
},
|
|
22
|
-
type: null,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const type = pathname.match(/\/content\/([^/]+)\/?$/);
|
|
26
|
-
if (type) {
|
|
27
|
-
return { doc: null, type: decodeURIComponent(type[1]!) };
|
|
28
|
-
}
|
|
29
|
-
const legacyDoc = pathname.match(/\/docs\/([^/]+)\/([^/]+)\/([^/]+)\/?$/);
|
|
30
|
-
if (legacyDoc) {
|
|
31
|
-
return {
|
|
32
|
-
doc: {
|
|
33
|
-
type: decodeURIComponent(legacyDoc[2]!),
|
|
34
|
-
slug: decodeURIComponent(legacyDoc[3]!),
|
|
35
|
-
},
|
|
36
|
-
type: null,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const legacyType = pathname.match(/\/types\/([^/]+)\/?$/);
|
|
40
|
-
if (legacyType) {
|
|
41
|
-
return { doc: null, type: decodeURIComponent(legacyType[1]!) };
|
|
42
|
-
}
|
|
43
|
-
return { doc: null, type: null };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
4
|
export function publicContentApiUrl(input: {
|
|
47
5
|
apiUrl: string;
|
|
48
6
|
workspaceId: string;
|
|
@@ -64,39 +22,3 @@ export function publicContentApiUrl(input: {
|
|
|
64
22
|
if (!input.slug && input.type) url.searchParams.set("type", input.type);
|
|
65
23
|
return url.toString();
|
|
66
24
|
}
|
|
67
|
-
|
|
68
|
-
export function contentApiLinks(input: {
|
|
69
|
-
apiUrl: string;
|
|
70
|
-
workspaceId: string;
|
|
71
|
-
branch?: string | null;
|
|
72
|
-
defaultBranch?: string | null;
|
|
73
|
-
pathname: string;
|
|
74
|
-
search?: string;
|
|
75
|
-
}): Array<{ label: string; href: string }> {
|
|
76
|
-
const ctx = contentApiContextFromPath(input.pathname, input.search);
|
|
77
|
-
const base = {
|
|
78
|
-
apiUrl: input.apiUrl,
|
|
79
|
-
workspaceId: input.workspaceId,
|
|
80
|
-
branch: input.branch,
|
|
81
|
-
defaultBranch: input.defaultBranch,
|
|
82
|
-
};
|
|
83
|
-
const links: Array<{ label: string; href: string }> = [];
|
|
84
|
-
if (ctx.doc) {
|
|
85
|
-
links.push({
|
|
86
|
-
label: "This document",
|
|
87
|
-
href: publicContentApiUrl({
|
|
88
|
-
...base,
|
|
89
|
-
type: ctx.doc.type,
|
|
90
|
-
slug: ctx.doc.slug,
|
|
91
|
-
}),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
links.push({
|
|
95
|
-
label: ctx.type ? `${ctx.type} docs` : "All docs",
|
|
96
|
-
href: publicContentApiUrl({
|
|
97
|
-
...base,
|
|
98
|
-
type: ctx.type ?? undefined,
|
|
99
|
-
}),
|
|
100
|
-
});
|
|
101
|
-
return links;
|
|
102
|
-
}
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
// packages/studio/src/chrome/content-tree-visibility.ts
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import type { KeyValueStore } from "../last-workspace";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isWorkspaceContentPath,
|
|
6
|
+
isWorkspaceEditorPath,
|
|
7
|
+
isWorkspaceHomePath,
|
|
8
|
+
} from "../workspace-path";
|
|
5
9
|
|
|
6
10
|
export const CONTENT_TREE_OPEN_KEY = "backmatter-content-tree-open";
|
|
7
11
|
const CONTENT_TREE_EVENT = "backmatter-content-tree";
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
|
-
* The type index
|
|
11
|
-
* A document follows the editor toggle.
|
|
14
|
+
* The content index and a type index keep the file tree open.
|
|
15
|
+
* A document follows the editor toggle. Home, settings, and media have no tree.
|
|
12
16
|
*/
|
|
13
17
|
export function contentTreeVisible(pathname: string, open: boolean) {
|
|
18
|
+
if (isWorkspaceHomePath(pathname)) return false;
|
|
14
19
|
if (!isWorkspaceContentPath(pathname)) return false;
|
|
15
20
|
if (isWorkspaceEditorPath(pathname)) return open;
|
|
16
21
|
return true;
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
/** Content rail is current wherever the file tree is open. */
|
|
25
|
+
export function contentRailCurrent(pathname: string, treeOpen: boolean) {
|
|
26
|
+
return contentTreeVisible(pathname, treeOpen);
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export function readContentTreeOpen(storage: KeyValueStore = localStorage) {
|
|
20
30
|
try {
|
|
21
31
|
return storage.getItem(CONTENT_TREE_OPEN_KEY) !== "0";
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
IconChevronRightSmall,
|
|
8
8
|
IconBrackets2,
|
|
9
9
|
IconFileText,
|
|
10
|
+
IconCode,
|
|
10
11
|
IconFiles,
|
|
11
12
|
IconFolder1,
|
|
12
13
|
IconGit,
|
|
@@ -70,6 +71,10 @@ export function ExplorerRailMediaIcon() {
|
|
|
70
71
|
return <Central icon={IconImages1} size={RAIL} />;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
export function ExplorerRailCodeIcon() {
|
|
75
|
+
return <Central icon={IconCode} size={RAIL} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
export function ExplorerRailSettingsIcon() {
|
|
74
79
|
return <Central icon={IconSettingsGear2} size={RAIL} />;
|
|
75
80
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// packages/studio/src/chrome/pull-failure.ts
|
|
2
|
+
import type { ApiError } from "../api";
|
|
3
|
+
|
|
4
|
+
export type PullProblem = {
|
|
5
|
+
/** Repo path, when the failure names a file. */
|
|
6
|
+
file: string;
|
|
7
|
+
text: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** What the pull-failed notice says. One problem per file, in words. */
|
|
11
|
+
export function pullFailureProblems(err: unknown): PullProblem[] {
|
|
12
|
+
const lines = rawLines(err);
|
|
13
|
+
if (lines.length === 0) return [{ file: "", text: "Pull failed." }];
|
|
14
|
+
return lines.map(explainPullLine);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function rawLines(err: unknown): string[] {
|
|
18
|
+
const details = (err as ApiError).details;
|
|
19
|
+
if (Array.isArray(details) && details.length > 0 && details.every((line) => typeof line === "string")) {
|
|
20
|
+
return details;
|
|
21
|
+
}
|
|
22
|
+
if (err instanceof Error) {
|
|
23
|
+
const message = err.message.replace(/^github_import_failed:\s*/, "");
|
|
24
|
+
if (message && message !== "github_import_failed") return [message];
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function explainPullLine(line: string): PullProblem {
|
|
30
|
+
const split = line.indexOf(": ");
|
|
31
|
+
const file = split === -1 ? "" : line.slice(0, split);
|
|
32
|
+
const rest = split === -1 ? line : line.slice(split + 2);
|
|
33
|
+
|
|
34
|
+
const renamed = rest.match(/defines name "([^"]+)" but the file is named "([^"]+)"/);
|
|
35
|
+
if (renamed) {
|
|
36
|
+
return {
|
|
37
|
+
file,
|
|
38
|
+
text: `The name is ${renamed[1]}, but the file is ${renamed[2]}. Rename one so they match.`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (/field\.locale/.test(rest)) {
|
|
42
|
+
return {
|
|
43
|
+
file,
|
|
44
|
+
text: "This file calls field.locale(), which was removed, so it didn't load.",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (/import is not allowed/.test(rest)) {
|
|
48
|
+
return {
|
|
49
|
+
file,
|
|
50
|
+
text: "This file imports something other than defineModel, field, or an object, so it didn't load.",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const cleaned = rest
|
|
55
|
+
.replace(/^model "[^"]+" failed to evaluate:\s*/i, "")
|
|
56
|
+
.replace(/^object "[^"]+" failed to evaluate:\s*/i, "")
|
|
57
|
+
.replace(/TypeError:\s*/g, "")
|
|
58
|
+
.replace(/\s*\([^)]*\)/g, "")
|
|
59
|
+
.trim();
|
|
60
|
+
return { file, text: cleaned || "This file didn't load." };
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// packages/studio/src/code-session-entry.ts
|
|
2
|
+
import { canDevelop } from "./workspace/permissions";
|
|
3
|
+
import { codeSessionPath } from "./workspace-path";
|
|
4
|
+
|
|
5
|
+
/** Where a developer opens a code session. Writers and editors get nothing. */
|
|
6
|
+
export function codeSessionEntry(role: string, slug: string): string | null {
|
|
7
|
+
if (!canDevelop(role)) return null;
|
|
8
|
+
return codeSessionPath(slug);
|
|
9
|
+
}
|
|
@@ -47,9 +47,9 @@ export function WorkspaceSwitcher({
|
|
|
47
47
|
(id: string) => {
|
|
48
48
|
setOpen(false);
|
|
49
49
|
const next = workspaces.find((workspace) => workspace.id === id);
|
|
50
|
-
if (next
|
|
50
|
+
if (next) navigate(workspaceHomePath(next.slug));
|
|
51
51
|
},
|
|
52
|
-
[navigate,
|
|
52
|
+
[navigate, workspaces],
|
|
53
53
|
);
|
|
54
54
|
|
|
55
55
|
// Number keys 1–9 jump straight to a workspace while the menu is open.
|