@backmatter/studio 0.0.13 → 0.0.15
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-ZZXQt3zy.js → UiKitApp-BDsrXN12.js} +323 -321
- package/dist/chunks/{WorkspaceRoutes-CCDPtLla.js → WorkspaceRoutes-BTZ1Ah4F.js} +33286 -33321
- package/dist/chunks/{dashboard-Dw0zfTXg.js → dashboard-FT7dm5wi.js} +2 -2
- package/dist/chunks/{index-DhZGCAru.js → index-DHJvh5Uy.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/index.js +51 -52
- package/dist/site-widget.js +9547 -10892
- package/package.json +2 -2
- package/src/api.ts +8 -26
- package/src/chat/ChangeCard.tsx +2 -10
- package/src/chat/ChatMessage.tsx +24 -19
- package/src/chat/ChatProvider.tsx +17 -15
- package/src/chat/DocListCard.tsx +2 -16
- package/src/chat/MentionPopover.tsx +8 -9
- package/src/chat/PointerChip.tsx +1 -1
- package/src/chat/chat-context.ts +7 -7
- package/src/chat/chat-helpers.ts +57 -14
- package/src/chrome/CommandPalette.tsx +2 -10
- package/src/chrome/ContentExplorerSidebar.tsx +51 -88
- package/src/chrome/EditorFocusContext.tsx +3 -3
- package/src/chrome/EditorStatusContext.tsx +0 -3
- package/src/chrome/WorkspaceNavContext.tsx +0 -2
- package/src/chrome/WorkspaceStatusBar.tsx +6 -23
- package/src/chrome/content-api-url.ts +3 -12
- package/src/chrome/content-repo-path.ts +8 -25
- package/src/chrome/content-type-groups.ts +0 -3
- package/src/chrome/folder-create-context.ts +15 -39
- package/src/pages/DocEditorPage.tsx +17 -33
- package/src/pages/EmbedChatPage.tsx +3 -3
- package/src/pages/EmbedPublishPage.tsx +2 -3
- package/src/pages/LegacyWorkspaceRedirect.tsx +2 -9
- package/src/pages/WorkspaceLayout.tsx +0 -5
- package/src/pages/preview-url.ts +6 -20
- package/src/pages/type-docs.ts +3 -6
- package/src/preview-frame.tsx +10 -4
- package/src/replica/store.ts +2 -10
- package/src/replica/types.ts +1 -2
- package/src/site-bar.tsx +3 -6
- package/src/site-dashboard-href.ts +1 -3
- package/src/site-widget.tsx +2 -3
- package/src/workspace-path.ts +3 -23
package/src/chat/chat-helpers.ts
CHANGED
|
@@ -7,11 +7,28 @@ import type { ChatChangeSummary, ChatThread, DocStatus, ChatChangeStatus } from
|
|
|
7
7
|
import type { DocRef } from "../chrome/EditorFocusContext";
|
|
8
8
|
|
|
9
9
|
export type WriteToolOutput =
|
|
10
|
-
| { ok: true; change
|
|
10
|
+
| { ok: true; change?: ChatChangeSummary; changes?: ChatChangeSummary[] }
|
|
11
11
|
| { ok: false; error: string; message: string };
|
|
12
12
|
|
|
13
13
|
export function isWriteTool(name: string) {
|
|
14
|
-
return
|
|
14
|
+
return (
|
|
15
|
+
name === "writeDoc" ||
|
|
16
|
+
name === "createDoc" ||
|
|
17
|
+
name === "renameDoc" ||
|
|
18
|
+
name === "deleteDoc" ||
|
|
19
|
+
name === "deleteFolder" ||
|
|
20
|
+
name === "moveFolder"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One write tool may record several files, as a folder move or delete does. */
|
|
25
|
+
export function changesInToolOutput(output: unknown): ChatChangeSummary[] {
|
|
26
|
+
if (!output || typeof output !== "object") return [];
|
|
27
|
+
const body = output as { ok?: boolean; change?: ChatChangeSummary; changes?: ChatChangeSummary[] };
|
|
28
|
+
if (body.ok === false) return [];
|
|
29
|
+
const many = Array.isArray(body.changes) ? body.changes.filter((change) => change?.id) : [];
|
|
30
|
+
if (many.length > 0) return many;
|
|
31
|
+
return body.change?.id ? [body.change] : [];
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
export function isCommitTool(name: string) {
|
|
@@ -22,7 +39,6 @@ const TREE_STATUSES = new Set<DocStatus>(["clean", "modified", "added", "conflic
|
|
|
22
39
|
|
|
23
40
|
/** Statuses a tool result wants applied to the local doc list immediately. */
|
|
24
41
|
export type TreeStatusPatch = {
|
|
25
|
-
locale: string;
|
|
26
42
|
type: string;
|
|
27
43
|
slug: string;
|
|
28
44
|
status: DocStatus;
|
|
@@ -38,8 +54,6 @@ export function treeStatusPatches(output: unknown): TreeStatusPatch[] {
|
|
|
38
54
|
if (!row || typeof row !== "object") continue;
|
|
39
55
|
const item = row as Partial<TreeStatusPatch>;
|
|
40
56
|
if (
|
|
41
|
-
typeof item.locale !== "string" ||
|
|
42
|
-
!item.locale ||
|
|
43
57
|
typeof item.type !== "string" ||
|
|
44
58
|
!item.type ||
|
|
45
59
|
typeof item.slug !== "string" ||
|
|
@@ -50,7 +64,6 @@ export function treeStatusPatches(output: unknown): TreeStatusPatch[] {
|
|
|
50
64
|
continue;
|
|
51
65
|
}
|
|
52
66
|
patches.push({
|
|
53
|
-
locale: item.locale,
|
|
54
67
|
type: item.type,
|
|
55
68
|
slug: item.slug,
|
|
56
69
|
status: item.status as DocStatus,
|
|
@@ -60,6 +73,26 @@ export function treeStatusPatches(output: unknown): TreeStatusPatch[] {
|
|
|
60
73
|
return patches;
|
|
61
74
|
}
|
|
62
75
|
|
|
76
|
+
/** A deleted or absent row leaves the content tree. Other rows update status. */
|
|
77
|
+
export function detailForTreePatch(
|
|
78
|
+
workspaceId: string,
|
|
79
|
+
patch: TreeStatusPatch,
|
|
80
|
+
): {
|
|
81
|
+
workspaceId: string;
|
|
82
|
+
type: string;
|
|
83
|
+
slug: string;
|
|
84
|
+
status?: TreeStatusPatch["status"];
|
|
85
|
+
remove?: boolean;
|
|
86
|
+
} {
|
|
87
|
+
const gone = patch.absent === true || patch.status === "deleted";
|
|
88
|
+
return {
|
|
89
|
+
workspaceId,
|
|
90
|
+
type: patch.type,
|
|
91
|
+
slug: patch.slug,
|
|
92
|
+
...(gone ? { remove: true } : { status: patch.status }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
63
96
|
export function toolLabel(name: string, input: unknown, output: unknown, state: string): string {
|
|
64
97
|
const ref = (input as { docRef?: { type?: string; slug?: string } } | undefined)?.docRef;
|
|
65
98
|
const doc = ref?.type && ref.slug ? `${ref.type}/${ref.slug}` : "";
|
|
@@ -84,6 +117,17 @@ export function toolLabel(name: string, input: unknown, output: unknown, state:
|
|
|
84
117
|
return running ? `Creating ${doc}…` : `Created ${doc}`;
|
|
85
118
|
case "renameDoc":
|
|
86
119
|
return running ? `Renaming ${doc}…` : `Renamed ${doc}`;
|
|
120
|
+
case "deleteDoc":
|
|
121
|
+
return running ? `Deleting ${doc}…` : `Deleted ${doc}`;
|
|
122
|
+
case "deleteFolder": {
|
|
123
|
+
const path = (input as { path?: string } | undefined)?.path ?? "folder";
|
|
124
|
+
return running ? `Deleting ${path}…` : `Deleted ${path}`;
|
|
125
|
+
}
|
|
126
|
+
case "moveFolder": {
|
|
127
|
+
const from = (input as { from?: string } | undefined)?.from ?? "folder";
|
|
128
|
+
const to = (input as { to?: string } | undefined)?.to ?? "";
|
|
129
|
+
return running ? `Moving ${from}…` : `Moved ${from} to ${to}`;
|
|
130
|
+
}
|
|
87
131
|
case "commit": {
|
|
88
132
|
if (running) return "Publishing…";
|
|
89
133
|
const published = output as { ok?: boolean; files?: unknown[] } | undefined;
|
|
@@ -221,15 +265,14 @@ export function changeIdsInMessage(message: UIMessage): string[] {
|
|
|
221
265
|
if (!isToolUIPart(part)) continue;
|
|
222
266
|
if (!isWriteTool(getToolName(part))) continue;
|
|
223
267
|
if (part.state !== "output-available") continue;
|
|
224
|
-
const
|
|
225
|
-
if (out?.ok) ids.push(out.change.id);
|
|
268
|
+
for (const change of changesInToolOutput(part.output)) ids.push(change.id);
|
|
226
269
|
}
|
|
227
270
|
return ids;
|
|
228
271
|
}
|
|
229
272
|
|
|
230
273
|
export type ChangeStackItem = Pick<
|
|
231
274
|
ChatChangeSummary,
|
|
232
|
-
"id" | "
|
|
275
|
+
"id" | "type" | "slug" | "status" | "createdAt"
|
|
233
276
|
>;
|
|
234
277
|
|
|
235
278
|
function isLaterChange(a: ChangeStackItem, b: ChangeStackItem) {
|
|
@@ -247,10 +290,10 @@ export function presentedChangeStatus(
|
|
|
247
290
|
all: ChangeStackItem[],
|
|
248
291
|
): ChatChangeStatus {
|
|
249
292
|
if (change.status !== "applied") return change.status;
|
|
250
|
-
const key = `${change.
|
|
293
|
+
const key = `${change.type}/${change.slug}`;
|
|
251
294
|
const covered = all.some((other) => {
|
|
252
295
|
if (other.id === change.id) return false;
|
|
253
|
-
if (`${other.
|
|
296
|
+
if (`${other.type}/${other.slug}` !== key) return false;
|
|
254
297
|
if (other.status !== "applied" && other.status !== "kept") return false;
|
|
255
298
|
return isLaterChange(other, change);
|
|
256
299
|
});
|
|
@@ -267,9 +310,9 @@ export function stackChanges(
|
|
|
267
310
|
for (const part of message.parts) {
|
|
268
311
|
if (!isToolUIPart(part) || part.state !== "output-available") continue;
|
|
269
312
|
if (!isWriteTool(getToolName(part))) continue;
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
313
|
+
for (const change of changesInToolOutput(part.output)) {
|
|
314
|
+
byId.set(change.id, change);
|
|
315
|
+
}
|
|
273
316
|
}
|
|
274
317
|
}
|
|
275
318
|
for (const change of Object.values(overrides)) {
|
|
@@ -357,7 +357,7 @@ export function CommandPalette() {
|
|
|
357
357
|
}
|
|
358
358
|
for (const doc of docs) {
|
|
359
359
|
list.push({
|
|
360
|
-
id: `doc-${doc.
|
|
360
|
+
id: `doc-${doc.type}-${doc.slug}`,
|
|
361
361
|
group: "Documents",
|
|
362
362
|
icon: "document",
|
|
363
363
|
label: `${doc.type}/${doc.slug}`,
|
|
@@ -370,15 +370,7 @@ export function CommandPalette() {
|
|
|
370
370
|
: doc.status === "modified"
|
|
371
371
|
? "Modified"
|
|
372
372
|
: doc.status,
|
|
373
|
-
run: go(
|
|
374
|
-
contentDocPath(
|
|
375
|
-
workspaceNav.workspaceSlug,
|
|
376
|
-
doc.type,
|
|
377
|
-
doc.slug,
|
|
378
|
-
doc.locale,
|
|
379
|
-
workspaceNav.defaultLocale,
|
|
380
|
-
),
|
|
381
|
-
),
|
|
373
|
+
run: go(contentDocPath(workspaceNav.workspaceSlug, doc.type, doc.slug)),
|
|
382
374
|
});
|
|
383
375
|
}
|
|
384
376
|
}
|
|
@@ -3,7 +3,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
|
|
5
5
|
import * as stylex from "@stylexjs/stylex";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
notifyTreeChanged,
|
|
8
|
+
saveDoc,
|
|
9
|
+
type DocListItem,
|
|
10
|
+
type DocStatus,
|
|
11
|
+
type TreeChangeDetail,
|
|
12
|
+
} from "../api";
|
|
7
13
|
import { useReplicaRevision, useReplicaState } from "../replica/useReplica";
|
|
8
14
|
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
9
15
|
import { Tooltip } from "../ui/Tooltip";
|
|
@@ -121,10 +127,7 @@ function ContentExplorerSidebar({
|
|
|
121
127
|
return map;
|
|
122
128
|
}, [replica, replicaRevision, workspace.workspaceId]);
|
|
123
129
|
|
|
124
|
-
const route = useMemo(
|
|
125
|
-
() => parseContentRoute(location.pathname, searchParams.get("locale") ?? ""),
|
|
126
|
-
[location.pathname, searchParams],
|
|
127
|
-
);
|
|
130
|
+
const route = useMemo(() => parseContentRoute(location.pathname), [location.pathname]);
|
|
128
131
|
const git = replica.git(workspace.workspaceId);
|
|
129
132
|
const pendingCount =
|
|
130
133
|
git?.changes.filter((c) => c.status !== "deleted").length ?? 0;
|
|
@@ -162,6 +165,28 @@ function ContentExplorerSidebar({
|
|
|
162
165
|
});
|
|
163
166
|
}, [workspace, route, models, docsByType]);
|
|
164
167
|
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
function onTree(event: Event) {
|
|
170
|
+
const detail = (event as CustomEvent<TreeChangeDetail>).detail;
|
|
171
|
+
if (!detail?.type || !detail.slug || detail.remove) return;
|
|
172
|
+
if (
|
|
173
|
+
detail.workspaceId &&
|
|
174
|
+
detail.workspaceId !== workspace.workspaceId &&
|
|
175
|
+
detail.workspaceId !== workspace.workspaceSlug
|
|
176
|
+
) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const model = models.find((item) => item.type === detail.type);
|
|
180
|
+
if (!model) return;
|
|
181
|
+
const repoPath = docRepoPath(model, { slug: detail.slug });
|
|
182
|
+
const parents = folderKeysOnPathTo(treePathFromRepo(repoPath));
|
|
183
|
+
const folder = parents[parents.length - 1];
|
|
184
|
+
if (folder) expandFolderPath(folder);
|
|
185
|
+
}
|
|
186
|
+
window.addEventListener("backmatter:tree-changed", onTree);
|
|
187
|
+
return () => window.removeEventListener("backmatter:tree-changed", onTree);
|
|
188
|
+
}, [models, workspace, expandFolderPath]);
|
|
189
|
+
|
|
165
190
|
const empty = workspace.contentTypes.length === 0;
|
|
166
191
|
const settingsActive = isWorkspaceSettingsPath(location.pathname);
|
|
167
192
|
const treeOpen = useContentTreeOpen();
|
|
@@ -374,31 +399,21 @@ function ExplorerSection({
|
|
|
374
399
|
const leaves: PathTreeFile[] = [];
|
|
375
400
|
for (const model of models) {
|
|
376
401
|
for (const doc of docsByType.get(model.type) ?? []) {
|
|
377
|
-
|
|
402
|
+
if (doc.status === "deleted") continue;
|
|
403
|
+
const repoPath = docRepoPath(model, doc);
|
|
378
404
|
const treePath = treePathFromRepo(repoPath);
|
|
379
405
|
leaves.push({
|
|
380
406
|
kind: "file",
|
|
381
407
|
name: treePath.split("/").pop() ?? treePath,
|
|
382
408
|
repoPath: treePath,
|
|
383
|
-
href: contentDocPath(
|
|
384
|
-
|
|
385
|
-
doc.type,
|
|
386
|
-
doc.slug,
|
|
387
|
-
doc.locale ?? "",
|
|
388
|
-
workspace.defaultLocale,
|
|
389
|
-
),
|
|
390
|
-
active: docMatchesRoute(route, doc, workspace.defaultLocale),
|
|
409
|
+
href: contentDocPath(workspace.workspaceSlug, doc.type, doc.slug),
|
|
410
|
+
active: docMatchesRoute(route, doc),
|
|
391
411
|
dirty: isDirtyStatus(doc.status),
|
|
392
412
|
});
|
|
393
413
|
}
|
|
394
414
|
}
|
|
395
415
|
const built = buildPathTree(leaves);
|
|
396
|
-
return withModelFolderPlaceholders(
|
|
397
|
-
built,
|
|
398
|
-
models,
|
|
399
|
-
workspace.locales,
|
|
400
|
-
workspace.defaultLocale,
|
|
401
|
-
);
|
|
416
|
+
return withModelFolderPlaceholders(built, models);
|
|
402
417
|
}, [models, docsByType, workspace, route]);
|
|
403
418
|
|
|
404
419
|
const startNewFile = useCallback(
|
|
@@ -422,12 +437,7 @@ function ExplorerSection({
|
|
|
422
437
|
return;
|
|
423
438
|
}
|
|
424
439
|
const list = docsByType.get(ctx.type) ?? [];
|
|
425
|
-
const
|
|
426
|
-
const exists = list.some(
|
|
427
|
-
(d) =>
|
|
428
|
-
d.slug === slug &&
|
|
429
|
-
(d.locale || workspace.defaultLocale) === localeForDoc,
|
|
430
|
-
);
|
|
440
|
+
const exists = list.some((d) => d.slug === slug);
|
|
431
441
|
if (exists) {
|
|
432
442
|
setNewFileError("A file with that name already exists.");
|
|
433
443
|
return;
|
|
@@ -436,30 +446,18 @@ function ExplorerSection({
|
|
|
436
446
|
setNewFileError(null);
|
|
437
447
|
try {
|
|
438
448
|
const frontmatter = ctx.pageFormat ? { blocks: [] } : {};
|
|
439
|
-
const { doc } = await saveDoc(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
slug,
|
|
444
|
-
{ frontmatter, body: "" },
|
|
445
|
-
);
|
|
449
|
+
const { doc } = await saveDoc(workspace.workspaceId, ctx.type, slug, {
|
|
450
|
+
frontmatter,
|
|
451
|
+
body: "",
|
|
452
|
+
});
|
|
446
453
|
notifyTreeChanged({
|
|
447
454
|
workspaceId: workspace.workspaceId,
|
|
448
|
-
locale: doc.locale,
|
|
449
455
|
type: doc.type,
|
|
450
456
|
slug: doc.slug,
|
|
451
457
|
status: doc.status,
|
|
452
458
|
});
|
|
453
459
|
setNewFileFolder(null);
|
|
454
|
-
navigate(
|
|
455
|
-
contentDocPath(
|
|
456
|
-
workspace.workspaceSlug,
|
|
457
|
-
doc.type,
|
|
458
|
-
doc.slug,
|
|
459
|
-
doc.locale ?? "",
|
|
460
|
-
workspace.defaultLocale,
|
|
461
|
-
),
|
|
462
|
-
);
|
|
460
|
+
navigate(contentDocPath(workspace.workspaceSlug, doc.type, doc.slug));
|
|
463
461
|
} catch (err) {
|
|
464
462
|
setNewFileError(err instanceof Error ? err.message : "Could not create file.");
|
|
465
463
|
} finally {
|
|
@@ -483,8 +481,6 @@ function ExplorerSection({
|
|
|
483
481
|
expanded={expanded}
|
|
484
482
|
onToggleFolder={onToggleFolder}
|
|
485
483
|
models={models}
|
|
486
|
-
locales={workspace.locales}
|
|
487
|
-
defaultLocale={workspace.defaultLocale}
|
|
488
484
|
canWrite={canWrite}
|
|
489
485
|
newFileFolder={newFileFolder}
|
|
490
486
|
newFileError={newFileError}
|
|
@@ -507,8 +503,6 @@ function PathTree({
|
|
|
507
503
|
expanded,
|
|
508
504
|
onToggleFolder,
|
|
509
505
|
models,
|
|
510
|
-
locales,
|
|
511
|
-
defaultLocale,
|
|
512
506
|
canWrite,
|
|
513
507
|
newFileFolder,
|
|
514
508
|
newFileError,
|
|
@@ -522,8 +516,6 @@ function PathTree({
|
|
|
522
516
|
expanded: Set<string>;
|
|
523
517
|
onToggleFolder: (pathKey: string) => void;
|
|
524
518
|
models: ContentTypeNav[];
|
|
525
|
-
locales: string[];
|
|
526
|
-
defaultLocale: string;
|
|
527
519
|
canWrite: boolean;
|
|
528
520
|
newFileFolder: string | null;
|
|
529
521
|
newFileError: string | null;
|
|
@@ -548,12 +540,7 @@ function PathTree({
|
|
|
548
540
|
onToggle={() => onToggleFolder(node.pathKey)}
|
|
549
541
|
createContext={
|
|
550
542
|
canWrite
|
|
551
|
-
? resolveFolderCreateContext(
|
|
552
|
-
node.pathKey,
|
|
553
|
-
models,
|
|
554
|
-
locales,
|
|
555
|
-
defaultLocale,
|
|
556
|
-
)
|
|
543
|
+
? resolveFolderCreateContext(node.pathKey, models)
|
|
557
544
|
: null
|
|
558
545
|
}
|
|
559
546
|
onNewFile={
|
|
@@ -566,23 +553,13 @@ function PathTree({
|
|
|
566
553
|
<NewFileTreeRow
|
|
567
554
|
depth={depth + 1}
|
|
568
555
|
ext={
|
|
569
|
-
resolveFolderCreateContext(
|
|
570
|
-
node.pathKey,
|
|
571
|
-
models,
|
|
572
|
-
locales,
|
|
573
|
-
defaultLocale,
|
|
574
|
-
)?.ext ?? "mdx"
|
|
556
|
+
resolveFolderCreateContext(node.pathKey, models)?.ext ?? "mdx"
|
|
575
557
|
}
|
|
576
558
|
error={newFileError}
|
|
577
559
|
disabled={creating}
|
|
578
560
|
onCancel={onCancelNewFile}
|
|
579
561
|
onSubmit={(name) => {
|
|
580
|
-
const ctx = resolveFolderCreateContext(
|
|
581
|
-
node.pathKey,
|
|
582
|
-
models,
|
|
583
|
-
locales,
|
|
584
|
-
defaultLocale,
|
|
585
|
-
);
|
|
562
|
+
const ctx = resolveFolderCreateContext(node.pathKey, models);
|
|
586
563
|
if (ctx) onSubmitNewFile(node.pathKey, ctx, name);
|
|
587
564
|
}}
|
|
588
565
|
/>
|
|
@@ -594,8 +571,6 @@ function PathTree({
|
|
|
594
571
|
expanded={expanded}
|
|
595
572
|
onToggleFolder={onToggleFolder}
|
|
596
573
|
models={models}
|
|
597
|
-
locales={locales}
|
|
598
|
-
defaultLocale={defaultLocale}
|
|
599
574
|
canWrite={canWrite}
|
|
600
575
|
newFileFolder={newFileFolder}
|
|
601
576
|
newFileError={newFileError}
|
|
@@ -807,11 +782,9 @@ function findActiveRepoPath(
|
|
|
807
782
|
if (!route.type || !route.docSlug) return null;
|
|
808
783
|
const model = models.find((m) => m.type === route.type);
|
|
809
784
|
if (!model) return null;
|
|
810
|
-
const doc = (docsByType.get(route.type) ?? []).find((d) =>
|
|
811
|
-
docMatchesRoute(route, d, workspace.defaultLocale),
|
|
812
|
-
);
|
|
785
|
+
const doc = (docsByType.get(route.type) ?? []).find((d) => docMatchesRoute(route, d));
|
|
813
786
|
if (!doc) return null;
|
|
814
|
-
return docRepoPath(model, doc
|
|
787
|
+
return docRepoPath(model, doc);
|
|
815
788
|
}
|
|
816
789
|
|
|
817
790
|
function IconButton({
|
|
@@ -840,28 +813,18 @@ function IconButton({
|
|
|
840
813
|
type ContentRoute = {
|
|
841
814
|
type: string | null;
|
|
842
815
|
docSlug: string | null;
|
|
843
|
-
locale: string;
|
|
844
816
|
};
|
|
845
817
|
|
|
846
|
-
function parseContentRoute(pathname: string
|
|
818
|
+
function parseContentRoute(pathname: string): ContentRoute {
|
|
847
819
|
const match = pathname.match(/\/content\/([^/]+)(?:\/([^/?]+))?/);
|
|
848
|
-
if (!match) return { type: null, docSlug: null
|
|
820
|
+
if (!match) return { type: null, docSlug: null };
|
|
849
821
|
const type = decodeURIComponent(match[1]!);
|
|
850
822
|
const docSlug = match[2] ? decodeURIComponent(match[2]) : null;
|
|
851
|
-
return { type, docSlug
|
|
823
|
+
return { type, docSlug };
|
|
852
824
|
}
|
|
853
825
|
|
|
854
|
-
function docMatchesRoute(
|
|
855
|
-
route
|
|
856
|
-
doc: DocListItem,
|
|
857
|
-
defaultLocale: string,
|
|
858
|
-
) {
|
|
859
|
-
if (!route.type || route.docSlug !== doc.slug || route.type !== doc.type) {
|
|
860
|
-
return false;
|
|
861
|
-
}
|
|
862
|
-
const docLocale = doc.locale || defaultLocale;
|
|
863
|
-
const urlLocale = route.locale || defaultLocale;
|
|
864
|
-
return docLocale === urlLocale;
|
|
826
|
+
function docMatchesRoute(route: ContentRoute, doc: DocListItem) {
|
|
827
|
+
return Boolean(route.type && route.docSlug === doc.slug && route.type === doc.type);
|
|
865
828
|
}
|
|
866
829
|
|
|
867
830
|
function isDirtyStatus(status: DocStatus) {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
useState,
|
|
12
12
|
} from "react";
|
|
13
13
|
|
|
14
|
-
export type DocRef = {
|
|
14
|
+
export type DocRef = { type: string; slug: string };
|
|
15
15
|
|
|
16
16
|
export type EditorSelection = { start: number; end: number };
|
|
17
17
|
|
|
@@ -23,12 +23,12 @@ export type EditorFocus = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
export function docKey(ref: DocRef): string {
|
|
26
|
-
return `${ref.
|
|
26
|
+
return `${ref.type}/${ref.slug}`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function sameDocRef(a: DocRef | null | undefined, b: DocRef | null | undefined) {
|
|
30
30
|
if (!a || !b) return false;
|
|
31
|
-
return a.
|
|
31
|
+
return a.type === b.type && a.slug === b.slug;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
type EditorFocusContextValue = {
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
} from "react";
|
|
9
9
|
|
|
10
10
|
export type EditorStatus = {
|
|
11
|
-
locale: string | null;
|
|
12
11
|
/** committed | uncommitted | conflict */
|
|
13
12
|
status: string | null;
|
|
14
13
|
saveLabel: string | null;
|
|
@@ -16,7 +15,6 @@ export type EditorStatus = {
|
|
|
16
15
|
};
|
|
17
16
|
|
|
18
17
|
const EMPTY: EditorStatus = {
|
|
19
|
-
locale: null,
|
|
20
18
|
status: null,
|
|
21
19
|
saveLabel: null,
|
|
22
20
|
saveTone: "muted",
|
|
@@ -38,7 +36,6 @@ const EditorStatusContext = createContext<EditorStatusContextValue | null>(
|
|
|
38
36
|
|
|
39
37
|
function sameStatus(a: EditorStatus, b: EditorStatus) {
|
|
40
38
|
return (
|
|
41
|
-
a.locale === b.locale &&
|
|
42
39
|
a.status === b.status &&
|
|
43
40
|
a.saveLabel === b.saveLabel &&
|
|
44
41
|
a.saveTone === b.saveTone
|
|
@@ -56,12 +56,11 @@ export const STATUS_LABEL: Record<ChangeStatus, string> = {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
function changeKey(doc: GitChange) {
|
|
59
|
-
return `${doc.
|
|
59
|
+
return `${doc.type}/${doc.slug}`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function ConflictResolvePanel({
|
|
63
63
|
workspaceSlug,
|
|
64
|
-
defaultLocale,
|
|
65
64
|
conflicts,
|
|
66
65
|
resolvingKey,
|
|
67
66
|
error,
|
|
@@ -69,7 +68,6 @@ function ConflictResolvePanel({
|
|
|
69
68
|
onResolve,
|
|
70
69
|
}: {
|
|
71
70
|
workspaceSlug: string | null;
|
|
72
|
-
defaultLocale: string;
|
|
73
71
|
conflicts: GitChange[];
|
|
74
72
|
resolvingKey: string | null;
|
|
75
73
|
error: string | null;
|
|
@@ -88,13 +86,7 @@ function ConflictResolvePanel({
|
|
|
88
86
|
{conflicts.map((doc) => {
|
|
89
87
|
const key = changeKey(doc);
|
|
90
88
|
const href = workspaceSlug
|
|
91
|
-
? contentDocPath(
|
|
92
|
-
workspaceSlug,
|
|
93
|
-
doc.type,
|
|
94
|
-
doc.slug,
|
|
95
|
-
doc.locale,
|
|
96
|
-
defaultLocale,
|
|
97
|
-
)
|
|
89
|
+
? contentDocPath(workspaceSlug, doc.type, doc.slug)
|
|
98
90
|
: null;
|
|
99
91
|
return (
|
|
100
92
|
<li key={key} {...stylex.props(styles.conflictRow)}>
|
|
@@ -107,7 +99,6 @@ function ConflictResolvePanel({
|
|
|
107
99
|
<span {...stylex.props(styles.changeName)}>
|
|
108
100
|
{doc.type}/{doc.slug}
|
|
109
101
|
</span>
|
|
110
|
-
<span {...stylex.props(popup.itemMeta)}>{doc.locale}</span>
|
|
111
102
|
</Link>
|
|
112
103
|
) : (
|
|
113
104
|
<span {...stylex.props(styles.changeName)}>
|
|
@@ -293,10 +284,9 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
293
284
|
setResolvingKey(key);
|
|
294
285
|
setError(null);
|
|
295
286
|
try {
|
|
296
|
-
await resolveDoc(workspaceId, doc.
|
|
287
|
+
await resolveDoc(workspaceId, doc.type, doc.slug, strategy);
|
|
297
288
|
notifyTreeChanged({
|
|
298
289
|
workspaceId,
|
|
299
|
-
locale: doc.locale,
|
|
300
290
|
type: doc.type,
|
|
301
291
|
slug: doc.slug,
|
|
302
292
|
status: strategy === "theirs" ? "clean" : "modified",
|
|
@@ -341,13 +331,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
341
331
|
const slug = ref.slice(slash + 1);
|
|
342
332
|
const doc = changes.find((change) => change.type === type && change.slug === slug);
|
|
343
333
|
if (!doc) return null;
|
|
344
|
-
return contentDocPath(
|
|
345
|
-
workspaceNav.workspaceSlug,
|
|
346
|
-
doc.type,
|
|
347
|
-
doc.slug,
|
|
348
|
-
doc.locale,
|
|
349
|
-
workspaceNav.defaultLocale,
|
|
350
|
-
);
|
|
334
|
+
return contentDocPath(workspaceNav.workspaceSlug, doc.type, doc.slug);
|
|
351
335
|
}
|
|
352
336
|
|
|
353
337
|
const commitLabel = committing
|
|
@@ -444,7 +428,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
444
428
|
<div {...stylex.props(styles.changes)}>
|
|
445
429
|
<p {...stylex.props(popup.groupLabel)}>Uncommitted</p>
|
|
446
430
|
{changes.map((doc) => (
|
|
447
|
-
<ChangeRow key={
|
|
431
|
+
<ChangeRow key={changeKey(doc)} doc={doc} />
|
|
448
432
|
))}
|
|
449
433
|
</div>
|
|
450
434
|
</>
|
|
@@ -566,7 +550,6 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
566
550
|
{action === "resolve" ? (
|
|
567
551
|
<ConflictResolvePanel
|
|
568
552
|
workspaceSlug={workspaceNav?.workspaceSlug ?? null}
|
|
569
|
-
defaultLocale={workspaceNav?.defaultLocale ?? "en"}
|
|
570
553
|
conflicts={conflicts}
|
|
571
554
|
resolvingKey={resolvingKey}
|
|
572
555
|
error={error}
|
|
@@ -598,7 +581,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
598
581
|
) : null}
|
|
599
582
|
<ul {...stylex.props(styles.commitFiles)}>
|
|
600
583
|
{changes.map((doc) => (
|
|
601
|
-
<li key={
|
|
584
|
+
<li key={changeKey(doc)}>
|
|
602
585
|
<ChangeRow
|
|
603
586
|
doc={doc}
|
|
604
587
|
invalid={
|
|
@@ -1,7 +1,7 @@
|
|
|
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 = {
|
|
4
|
+
export type ContentApiDoc = { type: string; slug: string };
|
|
5
5
|
|
|
6
6
|
export type ContentApiContext = {
|
|
7
7
|
doc: ContentApiDoc | null;
|
|
@@ -10,16 +10,12 @@ export type ContentApiContext = {
|
|
|
10
10
|
|
|
11
11
|
export function contentApiContextFromPath(
|
|
12
12
|
pathname: string,
|
|
13
|
-
|
|
13
|
+
_search = "",
|
|
14
14
|
): ContentApiContext {
|
|
15
|
-
const localeFromQuery = new URLSearchParams(
|
|
16
|
-
search.startsWith("?") ? search.slice(1) : search,
|
|
17
|
-
).get("locale");
|
|
18
15
|
const doc = pathname.match(/\/content\/([^/]+)\/([^/]+)\/?$/);
|
|
19
16
|
if (doc) {
|
|
20
17
|
return {
|
|
21
18
|
doc: {
|
|
22
|
-
locale: localeFromQuery || "en",
|
|
23
19
|
type: decodeURIComponent(doc[1]!),
|
|
24
20
|
slug: decodeURIComponent(doc[2]!),
|
|
25
21
|
},
|
|
@@ -34,7 +30,6 @@ export function contentApiContextFromPath(
|
|
|
34
30
|
if (legacyDoc) {
|
|
35
31
|
return {
|
|
36
32
|
doc: {
|
|
37
|
-
locale: decodeURIComponent(legacyDoc[1]!),
|
|
38
33
|
type: decodeURIComponent(legacyDoc[2]!),
|
|
39
34
|
slug: decodeURIComponent(legacyDoc[3]!),
|
|
40
35
|
},
|
|
@@ -53,15 +48,12 @@ export function publicContentApiUrl(input: {
|
|
|
53
48
|
workspaceId: string;
|
|
54
49
|
branch?: string | null;
|
|
55
50
|
defaultBranch?: string | null;
|
|
56
|
-
locale?: string;
|
|
57
51
|
type?: string;
|
|
58
52
|
slug?: string;
|
|
59
53
|
}): string {
|
|
60
54
|
const origin = input.apiUrl.replace(/\/$/, "");
|
|
61
55
|
const path =
|
|
62
|
-
input.
|
|
63
|
-
? `/${input.locale}/${input.type}/${input.slug}`
|
|
64
|
-
: "";
|
|
56
|
+
input.type && input.slug ? `/${input.type}/${input.slug}` : "";
|
|
65
57
|
if (!input.workspaceId.startsWith("ws_")) {
|
|
66
58
|
throw new Error("public content URLs must use the workspace id (ws_…), not the slug");
|
|
67
59
|
}
|
|
@@ -94,7 +86,6 @@ export function contentApiLinks(input: {
|
|
|
94
86
|
label: "This document",
|
|
95
87
|
href: publicContentApiUrl({
|
|
96
88
|
...base,
|
|
97
|
-
locale: ctx.doc.locale,
|
|
98
89
|
type: ctx.doc.type,
|
|
99
90
|
slug: ctx.doc.slug,
|
|
100
91
|
}),
|