@backmatter/studio 0.0.15 → 0.0.16
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-_EDqWID3.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-BTZ1Ah4F.js → WorkspaceRoutes-B2PrFgpz.js} +28979 -27418
- package/dist/chunks/{dashboard-FT7dm5wi.js → dashboard-BnlnMnnr.js} +55 -54
- package/dist/chunks/{index-DHJvh5Uy.js → index-D_BHF_8q.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/index.js +2 -2
- package/dist/site-widget.css +4 -0
- package/dist/site-widget.js +8304 -8029
- package/dist/studio.css +186 -12
- package/package.json +2 -2
- package/src/WorkspaceRoutes.tsx +5 -2
- package/src/api.ts +63 -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 +116 -20
- package/src/chat/Composer.tsx +48 -7
- 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/UserMenu.tsx +34 -3
- package/src/chrome/WorkspaceStatusBar.tsx +209 -27
- 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/code-session-entry.ts +9 -0
- package/src/components/WorkspaceSwitcher.tsx +2 -2
- package/src/element-origin.ts +149 -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/CodeSessionFiles.tsx +221 -0
- package/src/pages/CodeSessionPage.tsx +1188 -0
- package/src/pages/DocEditorPage.tsx +33 -15
- package/src/pages/PreviewSize.tsx +181 -0
- package/src/pages/SettingsGeneralPage.tsx +67 -2
- package/src/pages/WorkspaceHomePage.tsx +313 -0
- package/src/pages/WorkspaceLayout.tsx +10 -1
- package/src/pages/code-session-boot.ts +43 -0
- package/src/pages/code-session-context.tsx +51 -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/site-pointer.ts +24 -5
- package/src/studio-config.ts +1 -1
- package/src/ui/ListRow.tsx +12 -3
- package/src/ui/Page.tsx +12 -1
- package/src/workspace-path.ts +35 -3
- package/src/chrome/ContentApiPopover.tsx +0 -215
- package/src/pages/WorkspaceHomeRedirect.tsx +0 -17
package/src/chrome/AppShell.tsx
CHANGED
|
@@ -7,17 +7,23 @@ import { ChatPanel } from "../chat/ChatPanel";
|
|
|
7
7
|
import { ChatProvider, ChatSessionScope } from "../chat/ChatProvider";
|
|
8
8
|
import { colors } from "../styles/tokens.stylex";
|
|
9
9
|
import { TooltipProvider } from "../ui/Tooltip";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cmsWorkspaceSlug,
|
|
12
|
+
isOrgPickerPath,
|
|
13
|
+
isWorkspaceCodePath,
|
|
14
|
+
isWorkspaceListPath,
|
|
15
|
+
} from "../workspace-path";
|
|
11
16
|
import { AppSidebar } from "./AppSidebar";
|
|
12
17
|
import { WorkspaceRail } from "./ContentExplorerSidebar";
|
|
13
18
|
import { CommandPalette } from "./CommandPalette";
|
|
14
|
-
import { FloatingUserMenu } from "./UserMenu";
|
|
19
|
+
import { CornerUserMenu, FloatingUserMenu } from "./UserMenu";
|
|
15
20
|
import { EditorActionsProvider } from "./EditorActionsContext";
|
|
16
21
|
import { EditorFocusProvider } from "./EditorFocusContext";
|
|
17
22
|
import { EditorStatusProvider } from "./EditorStatusContext";
|
|
18
23
|
import { WorkspaceNavProvider, useWorkspaceNav } from "./WorkspaceNavContext";
|
|
19
24
|
import { installParentChatHotkey } from "./hotkeys";
|
|
20
25
|
import { WorkspaceStatusBar } from "./WorkspaceStatusBar";
|
|
26
|
+
import { CodeSessionProvider } from "../pages/code-session-context";
|
|
21
27
|
|
|
22
28
|
export function AppShell({ children }: { children: React.ReactNode }) {
|
|
23
29
|
return (
|
|
@@ -26,7 +32,9 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
|
|
26
32
|
<EditorStatusProvider>
|
|
27
33
|
<EditorActionsProvider>
|
|
28
34
|
<EditorFocusProvider>
|
|
29
|
-
<
|
|
35
|
+
<CodeSessionProvider>
|
|
36
|
+
<AppShellInner>{children}</AppShellInner>
|
|
37
|
+
</CodeSessionProvider>
|
|
30
38
|
</EditorFocusProvider>
|
|
31
39
|
</EditorActionsProvider>
|
|
32
40
|
</EditorStatusProvider>
|
|
@@ -43,6 +51,7 @@ function AppShellInner({ children }: { children: React.ReactNode }) {
|
|
|
43
51
|
const workspaceKey = cmsWorkspaceSlug(location.pathname);
|
|
44
52
|
const workspaceNav = useWorkspaceNav();
|
|
45
53
|
const orgPicker = isOrgPickerPath(location.pathname);
|
|
54
|
+
const workspaceList = isWorkspaceListPath(location.pathname);
|
|
46
55
|
const embed = searchParams.get("embed") === "1";
|
|
47
56
|
const [treeHost, setTreeHost] = useState<HTMLDivElement | null>(null);
|
|
48
57
|
const inWorkspace = Boolean(workspaceKey && !embed && !orgPicker);
|
|
@@ -52,8 +61,9 @@ function AppShellInner({ children }: { children: React.ReactNode }) {
|
|
|
52
61
|
return installParentChatHotkey();
|
|
53
62
|
}, [embed]);
|
|
54
63
|
|
|
64
|
+
const codeSession = Boolean(workspaceKey && isWorkspaceCodePath(location.pathname));
|
|
55
65
|
const chat =
|
|
56
|
-
workspaceKey && !embed ? (
|
|
66
|
+
workspaceKey && !embed && !codeSession ? (
|
|
57
67
|
<ChatSessionScope>
|
|
58
68
|
<ChatBar />
|
|
59
69
|
<ChatPanel />
|
|
@@ -64,7 +74,7 @@ function AppShellInner({ children }: { children: React.ReactNode }) {
|
|
|
64
74
|
<div {...stylex.props(styles.shell)}>
|
|
65
75
|
{workspaceKey && !embed ? <WorkspaceStatusBar workspaceId={workspaceKey} /> : null}
|
|
66
76
|
<div {...stylex.props(styles.body)}>
|
|
67
|
-
{orgPicker || embed ? null : inWorkspace ? (
|
|
77
|
+
{orgPicker || embed || workspaceList ? null : inWorkspace ? (
|
|
68
78
|
<WorkspaceRail workspace={workspaceNav} treeHost={treeHost} />
|
|
69
79
|
) : (
|
|
70
80
|
<AppSidebar />
|
|
@@ -78,6 +88,7 @@ function AppShellInner({ children }: { children: React.ReactNode }) {
|
|
|
78
88
|
</div>
|
|
79
89
|
</div>
|
|
80
90
|
{orgPicker ? <FloatingUserMenu /> : null}
|
|
91
|
+
{workspaceList ? <CornerUserMenu /> : null}
|
|
81
92
|
{embed ? null : <CommandPalette />}
|
|
82
93
|
</div>
|
|
83
94
|
);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./hotkeys";
|
|
28
28
|
import { parsePreviewMessage } from "@backmatter/core/preview";
|
|
29
29
|
import { useWorkspaceNav } from "./WorkspaceNavContext";
|
|
30
|
+
import { codeSessionEntry } from "../code-session-entry";
|
|
30
31
|
import {
|
|
31
32
|
contentDocPath,
|
|
32
33
|
contentTypePath,
|
|
@@ -310,6 +311,17 @@ export function CommandPalette() {
|
|
|
310
311
|
});
|
|
311
312
|
}
|
|
312
313
|
if (workspaceNav) {
|
|
314
|
+
const codeHref = codeSessionEntry(workspaceNav.role, workspaceNav.workspaceSlug);
|
|
315
|
+
if (codeHref) {
|
|
316
|
+
list.push({
|
|
317
|
+
id: "code-session",
|
|
318
|
+
group: "Go to",
|
|
319
|
+
icon: "document",
|
|
320
|
+
label: "Code session",
|
|
321
|
+
hint: "Host and chat",
|
|
322
|
+
run: go(codeHref),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
313
325
|
list.push(
|
|
314
326
|
{
|
|
315
327
|
id: "settings-general",
|
|
@@ -16,13 +16,14 @@ import { Tooltip } from "../ui/Tooltip";
|
|
|
16
16
|
import {
|
|
17
17
|
contentDocPath,
|
|
18
18
|
contentRailHref,
|
|
19
|
-
|
|
19
|
+
isWorkspaceCodePath,
|
|
20
20
|
isWorkspaceEditorPath,
|
|
21
21
|
isWorkspaceMediaPath,
|
|
22
22
|
isWorkspaceSettingsPath,
|
|
23
23
|
mediaPath,
|
|
24
24
|
settingsPath,
|
|
25
25
|
} from "../workspace-path";
|
|
26
|
+
import { codeSessionEntry } from "../code-session-entry";
|
|
26
27
|
import { UserMenu } from "./UserMenu";
|
|
27
28
|
import {
|
|
28
29
|
groupContentTypes,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
ExplorerHeaderListIcon,
|
|
39
40
|
ExplorerHeaderNewFileIcon,
|
|
40
41
|
ExplorerTreeNewFileIcon,
|
|
42
|
+
ExplorerRailCodeIcon,
|
|
41
43
|
ExplorerRailFilesIcon,
|
|
42
44
|
ExplorerRailHistoryIcon,
|
|
43
45
|
ExplorerRailMediaIcon,
|
|
@@ -62,6 +64,7 @@ import {
|
|
|
62
64
|
} from "./folder-create-context";
|
|
63
65
|
import { canWriteContent } from "../workspace/permissions";
|
|
64
66
|
import {
|
|
67
|
+
contentRailCurrent,
|
|
65
68
|
contentTreeVisible,
|
|
66
69
|
toggleContentTree,
|
|
67
70
|
useContentTreeOpen,
|
|
@@ -264,6 +267,7 @@ function ContentExplorerSidebar({
|
|
|
264
267
|
pendingCount={pendingCount}
|
|
265
268
|
settingsActive={settingsActive}
|
|
266
269
|
workspaceSlug={workspace.workspaceSlug}
|
|
270
|
+
role={workspace.role}
|
|
267
271
|
treeOpen={treeOpen}
|
|
268
272
|
/>
|
|
269
273
|
{tree && treeHost ? createPortal(tree, treeHost) : null}
|
|
@@ -271,22 +275,26 @@ function ContentExplorerSidebar({
|
|
|
271
275
|
);
|
|
272
276
|
}
|
|
273
277
|
|
|
274
|
-
function ActivityRail({
|
|
278
|
+
export function ActivityRail({
|
|
275
279
|
pendingCount,
|
|
276
280
|
settingsActive,
|
|
277
281
|
workspaceSlug,
|
|
282
|
+
role,
|
|
278
283
|
treeOpen,
|
|
279
284
|
}: {
|
|
280
285
|
pendingCount: number;
|
|
281
286
|
settingsActive: boolean;
|
|
282
287
|
workspaceSlug: string;
|
|
288
|
+
role: string;
|
|
283
289
|
treeOpen: boolean;
|
|
284
290
|
}) {
|
|
285
291
|
const navigate = useNavigate();
|
|
286
292
|
const location = useLocation();
|
|
287
293
|
const onDocument = isWorkspaceEditorPath(location.pathname);
|
|
288
294
|
const filesLabel = onDocument ? (treeOpen ? "Hide files" : "Show files") : "Content";
|
|
289
|
-
const filesActive =
|
|
295
|
+
const filesActive = contentRailCurrent(location.pathname, treeOpen);
|
|
296
|
+
const codeHref = codeSessionEntry(role, workspaceSlug);
|
|
297
|
+
const codeActive = isWorkspaceCodePath(location.pathname);
|
|
290
298
|
|
|
291
299
|
return (
|
|
292
300
|
<nav
|
|
@@ -351,6 +359,18 @@ function ActivityRail({
|
|
|
351
359
|
<ExplorerRailMediaIcon />
|
|
352
360
|
</Link>
|
|
353
361
|
</Tooltip>
|
|
362
|
+
{codeHref ? (
|
|
363
|
+
<Tooltip label="Code session" side="right">
|
|
364
|
+
<Link
|
|
365
|
+
to={codeHref}
|
|
366
|
+
aria-label="Code session"
|
|
367
|
+
aria-current={codeActive ? "page" : undefined}
|
|
368
|
+
{...stylex.props(styles.railBtn, codeActive && styles.railBtnActive)}
|
|
369
|
+
>
|
|
370
|
+
<ExplorerRailCodeIcon />
|
|
371
|
+
</Link>
|
|
372
|
+
</Tooltip>
|
|
373
|
+
) : null}
|
|
354
374
|
</div>
|
|
355
375
|
<div {...stylex.props(styles.railBottom)}>
|
|
356
376
|
<Tooltip label="Settings" side="right">
|
package/src/chrome/UserMenu.tsx
CHANGED
|
@@ -16,10 +16,28 @@ export function FloatingUserMenu() {
|
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** Small account control for full-screen pages that have no sidebar. */
|
|
20
|
+
export function CornerUserMenu() {
|
|
21
|
+
return (
|
|
22
|
+
<div {...stylex.props(styles.corner)}>
|
|
23
|
+
<UserMenu quiet placement="below-end" />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function UserMenu({
|
|
29
|
+
quiet = false,
|
|
30
|
+
compact = false,
|
|
31
|
+
placement = "above-start",
|
|
32
|
+
}: {
|
|
33
|
+
quiet?: boolean;
|
|
34
|
+
compact?: boolean;
|
|
35
|
+
placement?: "above-start" | "below-end";
|
|
36
|
+
}) {
|
|
20
37
|
const { email, onLogout } = useAppChrome();
|
|
21
38
|
const location = useLocation();
|
|
22
39
|
const onAccount = location.pathname.replace(/\/$/, "") === "/account";
|
|
40
|
+
const belowEnd = placement === "below-end";
|
|
23
41
|
|
|
24
42
|
return (
|
|
25
43
|
<Menu.Root>
|
|
@@ -29,6 +47,7 @@ export function UserMenu({ quiet = false, compact = false }: { quiet?: boolean;
|
|
|
29
47
|
styles.trigger,
|
|
30
48
|
quiet && styles.triggerQuiet,
|
|
31
49
|
compact && styles.triggerCompact,
|
|
50
|
+
belowEnd && styles.triggerCorner,
|
|
32
51
|
onAccount && styles.triggerActive,
|
|
33
52
|
)}
|
|
34
53
|
>
|
|
@@ -40,8 +59,8 @@ export function UserMenu({ quiet = false, compact = false }: { quiet?: boolean;
|
|
|
40
59
|
</Menu.Trigger>
|
|
41
60
|
<Menu.Portal>
|
|
42
61
|
<Menu.Positioner
|
|
43
|
-
side="top"
|
|
44
|
-
align={compact ? "center" : "start"}
|
|
62
|
+
side={belowEnd ? "bottom" : "top"}
|
|
63
|
+
align={belowEnd ? "end" : compact ? "center" : "start"}
|
|
45
64
|
sideOffset={6}
|
|
46
65
|
className={cx(popup.positioner)}
|
|
47
66
|
>
|
|
@@ -81,6 +100,13 @@ const styles = stylex.create({
|
|
|
81
100
|
zIndex: z.dropdown,
|
|
82
101
|
maxWidth: "220px",
|
|
83
102
|
},
|
|
103
|
+
corner: {
|
|
104
|
+
position: "fixed",
|
|
105
|
+
top: space.lg,
|
|
106
|
+
right: space.lg,
|
|
107
|
+
zIndex: z.dropdown,
|
|
108
|
+
maxWidth: "240px",
|
|
109
|
+
},
|
|
84
110
|
trigger: {
|
|
85
111
|
display: "flex",
|
|
86
112
|
width: "100%",
|
|
@@ -128,6 +154,11 @@ const styles = stylex.create({
|
|
|
128
154
|
fontSize: type.sm,
|
|
129
155
|
fontWeight: 500,
|
|
130
156
|
},
|
|
157
|
+
triggerCorner: {
|
|
158
|
+
minHeight: "44px",
|
|
159
|
+
justifyContent: "flex-end",
|
|
160
|
+
textAlign: "right",
|
|
161
|
+
},
|
|
131
162
|
triggerActive: {
|
|
132
163
|
backgroundColor: colors.bgActive,
|
|
133
164
|
},
|
|
@@ -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,10 +19,12 @@ 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
29
|
commitErrorSummary,
|
|
26
30
|
explainCommitError,
|
|
@@ -33,8 +37,8 @@ import { WorkspaceSwitcher } from "../components/WorkspaceSwitcher";
|
|
|
33
37
|
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
34
38
|
import { cx, popup, popupClassName } from "../ui/popup";
|
|
35
39
|
import { useEditorStatus } from "./EditorStatusContext";
|
|
36
|
-
import {
|
|
37
|
-
import { OpenSiteButton } from "./OpenSiteButton";
|
|
40
|
+
import { openableSites } from "./open-site";
|
|
41
|
+
import { OpenSiteButton, showsOpenSite } from "./OpenSiteButton";
|
|
38
42
|
import { GitBranchIcon } from "./icons";
|
|
39
43
|
import { StatusBar, StatusItem, StatusSpacer } from "./StatusBar";
|
|
40
44
|
import { replicaStore } from "../replica/instance";
|
|
@@ -169,6 +173,15 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
169
173
|
const [message, setMessage] = useState("");
|
|
170
174
|
const [error, setError] = useState<string | null>(null);
|
|
171
175
|
const [commitFailure, setCommitFailure] = useState<CommitErrorView | null>(null);
|
|
176
|
+
const onCodePage = isWorkspaceCodePath(location.pathname);
|
|
177
|
+
const codeSession = useCodeSessionContext();
|
|
178
|
+
const codeWorkspaceId = workspaceNav?.workspaceId ?? workspaceId;
|
|
179
|
+
const [codeFiles, setCodeFiles] = useState<Array<{ path: string; deleted: boolean }>>([]);
|
|
180
|
+
const [foldPending, setFoldPending] = useState(false);
|
|
181
|
+
const [codeFoldErrors, setCodeFoldErrors] = useState<string[]>([]);
|
|
182
|
+
const [codeCommitOpen, setCodeCommitOpen] = useState(false);
|
|
183
|
+
const [codeMessage, setCodeMessage] = useState("");
|
|
184
|
+
const [codeCommitting, setCodeCommitting] = useState(false);
|
|
172
185
|
// Commit sha from the last poll. A new sha means HEAD moved. The preview
|
|
173
186
|
// iframe does not reload for that; its page follows the SSE head event.
|
|
174
187
|
const lastHead = useRef<string | null | undefined>(undefined);
|
|
@@ -192,6 +205,35 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
192
205
|
}
|
|
193
206
|
}
|
|
194
207
|
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!onCodePage || !codeSession) {
|
|
210
|
+
setCodeFiles([]);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let cancelled = false;
|
|
214
|
+
const load = () => {
|
|
215
|
+
void fetchCodeSessionEdits(codeWorkspaceId, codeSession.sessionId)
|
|
216
|
+
.then((list) => {
|
|
217
|
+
if (!cancelled) {
|
|
218
|
+
setCodeFiles(list.files);
|
|
219
|
+
setFoldPending(list.foldPending);
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
.catch(() => {
|
|
223
|
+
if (!cancelled) {
|
|
224
|
+
setCodeFiles([]);
|
|
225
|
+
setFoldPending(false);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
load();
|
|
230
|
+
const timer = window.setInterval(load, POLL_MS);
|
|
231
|
+
return () => {
|
|
232
|
+
cancelled = true;
|
|
233
|
+
window.clearInterval(timer);
|
|
234
|
+
};
|
|
235
|
+
}, [onCodePage, codeSession, codeWorkspaceId]);
|
|
236
|
+
|
|
195
237
|
useEffect(() => {
|
|
196
238
|
lastHead.current = undefined;
|
|
197
239
|
void reloadGit();
|
|
@@ -304,6 +346,46 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
304
346
|
}
|
|
305
347
|
}
|
|
306
348
|
|
|
349
|
+
function defaultCodeSessionMessage(files: Array<{ path: string }>) {
|
|
350
|
+
if (files.length === 1) return `Update ${files[0]!.path}`;
|
|
351
|
+
return `Commit ${files.length} files from a code session`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function onCodeSessionCommit(e?: React.FormEvent) {
|
|
355
|
+
e?.preventDefault();
|
|
356
|
+
if (!codeSession || codeCommitting) return;
|
|
357
|
+
setCodeCommitting(true);
|
|
358
|
+
setError(null);
|
|
359
|
+
try {
|
|
360
|
+
const result = await commitCodeSessionEdits(
|
|
361
|
+
codeWorkspaceId,
|
|
362
|
+
codeSession.sessionId,
|
|
363
|
+
codeMessage,
|
|
364
|
+
);
|
|
365
|
+
setCodeMessage("");
|
|
366
|
+
setCodeFoldErrors(result.foldErrors);
|
|
367
|
+
if (result.foldErrors.length === 0) setCodeCommitOpen(false);
|
|
368
|
+
codeSession.refresh();
|
|
369
|
+
const list = await fetchCodeSessionEdits(codeWorkspaceId, codeSession.sessionId);
|
|
370
|
+
setCodeFiles(list.files);
|
|
371
|
+
setFoldPending(list.foldPending);
|
|
372
|
+
notifyHeadChanged();
|
|
373
|
+
await reloadGit();
|
|
374
|
+
} catch (err) {
|
|
375
|
+
const apiErr = err as ApiError;
|
|
376
|
+
if (apiErr.code === "branch_moved") {
|
|
377
|
+
setError("The branch changed while committing. Try again.");
|
|
378
|
+
} else if (apiErr.code === "working_tree") {
|
|
379
|
+
const paths = apiErr.details?.length ? apiErr.details.join(", ") : "";
|
|
380
|
+
setError(paths ? `Uncommitted Studio changes: ${paths}` : "Uncommitted Studio changes block this commit.");
|
|
381
|
+
} else {
|
|
382
|
+
setError(err instanceof Error ? err.message : "Commit failed");
|
|
383
|
+
}
|
|
384
|
+
} finally {
|
|
385
|
+
setCodeCommitting(false);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
307
389
|
async function onCommit(e?: React.FormEvent) {
|
|
308
390
|
e?.preventDefault();
|
|
309
391
|
if (committing || conflictCount > 0 || changeCount === 0) return;
|
|
@@ -338,7 +420,20 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
338
420
|
? "Committing…"
|
|
339
421
|
: conflictCount > 0
|
|
340
422
|
? `Resolve ${conflictCount} ${conflictCount === 1 ? "conflict" : "conflicts"}`
|
|
341
|
-
:
|
|
423
|
+
: changeCount === 0
|
|
424
|
+
? "Deploy"
|
|
425
|
+
: `Deploy ${changeCount} changes`;
|
|
426
|
+
|
|
427
|
+
const codeEditCount =
|
|
428
|
+
codeFiles.length > 0 ? codeFiles.length : (codeSession?.editCount ?? 0);
|
|
429
|
+
const codeCommitReady =
|
|
430
|
+
Boolean(codeSession) &&
|
|
431
|
+
(codeEditCount > 0 || codeFoldErrors.length > 0 || foldPending);
|
|
432
|
+
const codeCommitLabel = codeCommitting
|
|
433
|
+
? "Committing…"
|
|
434
|
+
: codeEditCount > 0
|
|
435
|
+
? `Commit ${codeEditCount} ${codeEditCount === 1 ? "file" : "files"}`
|
|
436
|
+
: "Commit";
|
|
342
437
|
|
|
343
438
|
return (
|
|
344
439
|
<StatusBar>
|
|
@@ -478,27 +573,21 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
478
573
|
</Combobox.Positioner>
|
|
479
574
|
</Combobox.Portal>
|
|
480
575
|
</Combobox.Root>
|
|
481
|
-
{workspaceNav?.workspaceId
|
|
576
|
+
{workspaceNav?.workspaceId &&
|
|
577
|
+
replica.sessionUserId &&
|
|
578
|
+
showsOpenSite() &&
|
|
579
|
+
openableSites(workspaceNav.siteUrls).length > 0 ? (
|
|
482
580
|
<>
|
|
483
581
|
<span {...stylex.props(styles.divider)} aria-hidden />
|
|
484
|
-
<
|
|
582
|
+
<OpenSiteButton
|
|
485
583
|
workspaceId={workspaceNav.workspaceId}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
pathname={location.pathname}
|
|
489
|
-
search={location.search}
|
|
584
|
+
userId={replica.sessionUserId}
|
|
585
|
+
siteUrls={workspaceNav.siteUrls}
|
|
490
586
|
/>
|
|
491
|
-
{replica.sessionUserId ? (
|
|
492
|
-
<OpenSiteButton
|
|
493
|
-
workspaceId={workspaceNav.workspaceId}
|
|
494
|
-
userId={replica.sessionUserId}
|
|
495
|
-
siteUrls={workspaceNav.siteUrls}
|
|
496
|
-
/>
|
|
497
|
-
) : null}
|
|
498
587
|
</>
|
|
499
588
|
) : null}
|
|
500
589
|
</div>
|
|
501
|
-
{changeCount > 0 ? (
|
|
590
|
+
{!onCodePage && changeCount > 0 ? (
|
|
502
591
|
<StatusItem tone={conflictCount > 0 ? "danger" : "muted"}>
|
|
503
592
|
{changeCount} uncommitted
|
|
504
593
|
</StatusItem>
|
|
@@ -514,24 +603,117 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
514
603
|
{inEditor && editor.saveLabel ? (
|
|
515
604
|
<StatusItem tone={editor.saveTone}>{editor.saveLabel}</StatusItem>
|
|
516
605
|
) : null}
|
|
517
|
-
{
|
|
606
|
+
{onCodePage ? (
|
|
607
|
+
<Popover.Root
|
|
608
|
+
open={codeCommitOpen}
|
|
609
|
+
onOpenChange={(next) => {
|
|
610
|
+
if (!codeCommitReady) return;
|
|
611
|
+
setCodeCommitOpen(next);
|
|
612
|
+
}}
|
|
613
|
+
>
|
|
614
|
+
<Popover.Trigger
|
|
615
|
+
disabled={!codeCommitReady || codeCommitting}
|
|
616
|
+
aria-label={codeCommitLabel}
|
|
617
|
+
className={cx(styles.commit)}
|
|
618
|
+
>
|
|
619
|
+
{codeCommitLabel}
|
|
620
|
+
</Popover.Trigger>
|
|
621
|
+
<Popover.Portal>
|
|
622
|
+
<Popover.Positioner
|
|
623
|
+
side="bottom"
|
|
624
|
+
align="end"
|
|
625
|
+
sideOffset={6}
|
|
626
|
+
className={cx(popup.positioner)}
|
|
627
|
+
>
|
|
628
|
+
<Popover.Popup
|
|
629
|
+
aria-label="Commit code session"
|
|
630
|
+
className={(state) => popupClassName(state, styles.commitPopup)}
|
|
631
|
+
>
|
|
632
|
+
<form
|
|
633
|
+
onSubmit={(e) => void onCodeSessionCommit(e)}
|
|
634
|
+
{...stylex.props(styles.commitForm)}
|
|
635
|
+
>
|
|
636
|
+
<p {...stylex.props(popup.groupLabel)}>
|
|
637
|
+
{codeEditCount} {codeEditCount === 1 ? "file" : "files"} →{" "}
|
|
638
|
+
{git?.currentBranch}
|
|
639
|
+
</p>
|
|
640
|
+
<ul {...stylex.props(styles.commitFiles)}>
|
|
641
|
+
{codeFiles.map((file) => (
|
|
642
|
+
<li key={file.path}>
|
|
643
|
+
<div {...stylex.props(styles.changeItem)}>
|
|
644
|
+
<span {...stylex.props(styles.changeName)}>{file.path}</span>
|
|
645
|
+
<span {...stylex.props(popup.itemMeta)}>
|
|
646
|
+
{file.deleted ? "deleted" : "modified"}
|
|
647
|
+
</span>
|
|
648
|
+
</div>
|
|
649
|
+
</li>
|
|
650
|
+
))}
|
|
651
|
+
</ul>
|
|
652
|
+
{codeFoldErrors.length > 0 ? (
|
|
653
|
+
<ul {...stylex.props(styles.commitFiles)}>
|
|
654
|
+
{codeFoldErrors.map((foldError) => (
|
|
655
|
+
<li key={foldError}>
|
|
656
|
+
<p {...stylex.props(styles.conflictError)}>{foldError}</p>
|
|
657
|
+
</li>
|
|
658
|
+
))}
|
|
659
|
+
</ul>
|
|
660
|
+
) : null}
|
|
661
|
+
<div {...stylex.props(popup.separator)} />
|
|
662
|
+
<div {...stylex.props(styles.commitFooter)}>
|
|
663
|
+
<input
|
|
664
|
+
value={codeMessage}
|
|
665
|
+
onChange={(e) => setCodeMessage(e.target.value)}
|
|
666
|
+
placeholder={defaultCodeSessionMessage(codeFiles)}
|
|
667
|
+
aria-label="Commit message"
|
|
668
|
+
autoComplete="off"
|
|
669
|
+
spellCheck={false}
|
|
670
|
+
data-no-ring
|
|
671
|
+
data-1p-ignore
|
|
672
|
+
data-lpignore
|
|
673
|
+
autoFocus
|
|
674
|
+
disabled={codeCommitting}
|
|
675
|
+
{...stylex.props(styles.commitField)}
|
|
676
|
+
/>
|
|
677
|
+
<div {...stylex.props(styles.commitSubmit)}>
|
|
678
|
+
<button
|
|
679
|
+
type="submit"
|
|
680
|
+
disabled={codeCommitting}
|
|
681
|
+
{...stylex.props(styles.commitSubmitBtn)}
|
|
682
|
+
>
|
|
683
|
+
{codeCommitting ? "Committing…" : "Commit"}
|
|
684
|
+
</button>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
</form>
|
|
688
|
+
</Popover.Popup>
|
|
689
|
+
</Popover.Positioner>
|
|
690
|
+
</Popover.Portal>
|
|
691
|
+
</Popover.Root>
|
|
692
|
+
) : null}
|
|
693
|
+
{!onCodePage ? (
|
|
518
694
|
<Popover.Root
|
|
519
695
|
open={commitOpen}
|
|
520
696
|
onOpenChange={(next) => {
|
|
521
|
-
if (action === "blocked") return;
|
|
697
|
+
if (changeCount === 0 || action === "blocked") return;
|
|
522
698
|
setCommitOpen(next);
|
|
523
699
|
}}
|
|
524
700
|
>
|
|
525
701
|
<Popover.Trigger
|
|
526
|
-
disabled={
|
|
702
|
+
disabled={
|
|
703
|
+
changeCount === 0 || commitTriggerDisabled(action, { committing, syncing })
|
|
704
|
+
}
|
|
527
705
|
aria-label={
|
|
528
|
-
|
|
529
|
-
? "
|
|
530
|
-
: action === "
|
|
531
|
-
?
|
|
532
|
-
:
|
|
706
|
+
changeCount === 0
|
|
707
|
+
? "Deploy"
|
|
708
|
+
: action === "blocked"
|
|
709
|
+
? "Writers cannot publish"
|
|
710
|
+
: action === "resolve"
|
|
711
|
+
? commitLabel
|
|
712
|
+
: `Commit ${changeCount} ${changeCount === 1 ? "file" : "files"}`
|
|
713
|
+
}
|
|
714
|
+
title={
|
|
715
|
+
changeCount > 0 && action === "blocked" ? "Writers cannot publish" : undefined
|
|
533
716
|
}
|
|
534
|
-
title={action === "blocked" ? "Writers cannot publish" : undefined}
|
|
535
717
|
className={cx(styles.commit, action === "resolve" && styles.commitBlocked)}
|
|
536
718
|
>
|
|
537
719
|
{commitLabel}
|
|
@@ -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
|
-
}
|