@backmatter/studio 0.0.12 → 0.0.14
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-fYeVEf4-.js} +323 -321
- package/dist/chunks/{WorkspaceRoutes-CCDPtLla.js → WorkspaceRoutes-Dt7JvEWT.js} +33082 -33099
- package/dist/chunks/{dashboard-Dw0zfTXg.js → dashboard-BuY2aGkO.js} +2 -2
- package/dist/chunks/{index-DhZGCAru.js → index-NWDqEZYW.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/index.js +2 -2
- package/dist/site-widget.js +8491 -9824
- package/package.json +2 -2
- package/src/api.ts +8 -23
- package/src/chat/ChangeCard.tsx +6 -2
- package/src/chat/ChatMessage.tsx +23 -18
- package/src/chat/ChatProvider.tsx +13 -11
- package/src/chat/chat-helpers.ts +56 -8
- 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/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/LegacyWorkspaceRedirect.tsx +2 -9
- package/src/pages/WorkspaceLayout.tsx +0 -5
- package/src/pages/preview-url.ts +6 -20
- package/src/preview-frame.tsx +10 -4
- package/src/replica/store.ts +1 -1
- package/src/site-bar.tsx +3 -6
- package/src/site-dashboard-href.ts +1 -3
- package/src/workspace-path.ts +3 -23
|
@@ -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
|
|
@@ -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
|
}),
|
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
// packages/studio/src/chrome/content-repo-path.ts
|
|
2
2
|
import { IMPLICIT_SINGLETON_SLUG, repoPathFromModel } from "@backmatter/core";
|
|
3
3
|
import type { DocListItem } from "../api";
|
|
4
|
-
import {
|
|
4
|
+
import type { ContentTypeNav } from "./content-type-groups";
|
|
5
5
|
|
|
6
6
|
export function docRepoPath(
|
|
7
7
|
model: ContentTypeNav,
|
|
8
|
-
doc: Pick<DocListItem, "
|
|
9
|
-
defaultLocale: string,
|
|
8
|
+
doc: Pick<DocListItem, "slug">,
|
|
10
9
|
): string {
|
|
11
|
-
|
|
12
|
-
? doc.locale || defaultLocale
|
|
13
|
-
: undefined;
|
|
14
|
-
return repoPathFromModel(model, locale, doc.slug);
|
|
10
|
+
return repoPathFromModel(model, doc.slug);
|
|
15
11
|
}
|
|
16
12
|
|
|
17
|
-
export function singletonRepoPath(
|
|
18
|
-
model:
|
|
19
|
-
defaultLocale: string,
|
|
20
|
-
): string {
|
|
21
|
-
return docRepoPath(
|
|
22
|
-
model,
|
|
23
|
-
{ slug: IMPLICIT_SINGLETON_SLUG, locale: defaultLocale },
|
|
24
|
-
defaultLocale,
|
|
25
|
-
);
|
|
13
|
+
export function singletonRepoPath(model: ContentTypeNav): string {
|
|
14
|
+
return docRepoPath(model, { slug: IMPLICIT_SINGLETON_SLUG });
|
|
26
15
|
}
|
|
27
16
|
|
|
28
17
|
export function repoPathBasename(repoPath: string): string {
|
|
@@ -39,14 +28,8 @@ export function fileExtFromRepoPath(repoPath: string): string {
|
|
|
39
28
|
/** Label for a row in the content tree (repo-relative filename). */
|
|
40
29
|
export function docTreeLabel(
|
|
41
30
|
model: ContentTypeNav,
|
|
42
|
-
doc: Pick<DocListItem, "
|
|
43
|
-
defaultLocale: string,
|
|
31
|
+
doc: Pick<DocListItem, "slug">,
|
|
44
32
|
): string {
|
|
45
|
-
const repoPath = docRepoPath(model, doc
|
|
46
|
-
|
|
47
|
-
const locale = doc.locale || defaultLocale;
|
|
48
|
-
if (modelHasLocale(model.path) && locale !== defaultLocale) {
|
|
49
|
-
return `${locale}/${name}`;
|
|
50
|
-
}
|
|
51
|
-
return name;
|
|
33
|
+
const repoPath = docRepoPath(model, doc);
|
|
34
|
+
return repoPathBasename(repoPath);
|
|
52
35
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// packages/studio/src/chrome/folder-create-context.ts
|
|
2
2
|
import { contentExtFromPath } from "@backmatter/core";
|
|
3
3
|
import type { ContentTypeNav } from "./content-type-groups";
|
|
4
|
-
import { modelHasLocale } from "./content-type-groups";
|
|
5
4
|
import { treePathFromRepo } from "./content-path-tree";
|
|
6
5
|
import type { PathTreeFolder, PathTreeNode } from "./content-path-tree";
|
|
7
6
|
import { docRepoPath, repoPathBasename } from "./content-repo-path";
|
|
@@ -10,7 +9,6 @@ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
10
9
|
|
|
11
10
|
export type FolderCreateContext = {
|
|
12
11
|
type: string;
|
|
13
|
-
locale: string;
|
|
14
12
|
ext: string;
|
|
15
13
|
/** JSON landing pages need `blocks: []` in frontmatter on create. */
|
|
16
14
|
pageFormat: boolean;
|
|
@@ -35,19 +33,8 @@ export function slugFromNewFileName(name: string, expectedExt: string): string {
|
|
|
35
33
|
return base;
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
export function folderPathKeyForModel(
|
|
39
|
-
model:
|
|
40
|
-
locale: string | undefined,
|
|
41
|
-
defaultLocale: string,
|
|
42
|
-
): string {
|
|
43
|
-
const loc = modelHasLocale(model.path)
|
|
44
|
-
? locale || defaultLocale
|
|
45
|
-
: undefined;
|
|
46
|
-
const repo = docRepoPath(
|
|
47
|
-
model,
|
|
48
|
-
{ slug: "new", locale: loc ?? "" },
|
|
49
|
-
defaultLocale,
|
|
50
|
-
);
|
|
36
|
+
export function folderPathKeyForModel(model: ContentTypeNav): string {
|
|
37
|
+
const repo = docRepoPath(model, { slug: "new" });
|
|
51
38
|
const tree = treePathFromRepo(repo);
|
|
52
39
|
const parts = tree.split("/").filter(Boolean);
|
|
53
40
|
parts.pop();
|
|
@@ -57,26 +44,15 @@ export function folderPathKeyForModel(
|
|
|
57
44
|
export function resolveFolderCreateContext(
|
|
58
45
|
pathKey: string,
|
|
59
46
|
models: ContentTypeNav[],
|
|
60
|
-
locales: string[],
|
|
61
|
-
defaultLocale: string,
|
|
62
47
|
): FolderCreateContext | null {
|
|
63
48
|
const matches: FolderCreateContext[] = [];
|
|
64
49
|
for (const model of models) {
|
|
65
50
|
if (model.singleton) continue;
|
|
66
51
|
const ext = contentExtFromPath(model.path);
|
|
67
52
|
const pageFormat = ext === "json";
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (key === pathKey) {
|
|
72
|
-
matches.push({ type: model.type, locale, ext, pageFormat });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
const key = folderPathKeyForModel(model, undefined, defaultLocale);
|
|
77
|
-
if (key === pathKey) {
|
|
78
|
-
matches.push({ type: model.type, locale: "", ext, pageFormat });
|
|
79
|
-
}
|
|
53
|
+
const key = folderPathKeyForModel(model);
|
|
54
|
+
if (key === pathKey) {
|
|
55
|
+
matches.push({ type: model.type, ext, pageFormat });
|
|
80
56
|
}
|
|
81
57
|
}
|
|
82
58
|
if (matches.length !== 1) return null;
|
|
@@ -119,23 +95,23 @@ function insertFolderPath(nodes: PathTreeNode[], segments: string[], pathKey: st
|
|
|
119
95
|
return next;
|
|
120
96
|
}
|
|
121
97
|
|
|
122
|
-
|
|
98
|
+
function treeHasFile(nodes: PathTreeNode[]): boolean {
|
|
99
|
+
for (const node of nodes) {
|
|
100
|
+
if (node.kind === "file") return true;
|
|
101
|
+
if (treeHasFile(node.children)) return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Empty type folders so the first file in a model still has a folder row. */
|
|
123
107
|
export function withModelFolderPlaceholders(
|
|
124
108
|
tree: PathTreeNode[],
|
|
125
109
|
models: ContentTypeNav[],
|
|
126
|
-
locales: string[],
|
|
127
|
-
defaultLocale: string,
|
|
128
110
|
): PathTreeNode[] {
|
|
129
111
|
const keys = new Set<string>();
|
|
130
112
|
for (const model of models) {
|
|
131
113
|
if (model.singleton) continue;
|
|
132
|
-
|
|
133
|
-
for (const locale of locales) {
|
|
134
|
-
keys.add(folderPathKeyForModel(model, locale, defaultLocale));
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
keys.add(folderPathKeyForModel(model, undefined, defaultLocale));
|
|
138
|
-
}
|
|
114
|
+
keys.add(folderPathKeyForModel(model));
|
|
139
115
|
}
|
|
140
116
|
let out = tree;
|
|
141
117
|
for (const key of [...keys].sort(compareName)) {
|