@backmatter/studio 0.0.16 → 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-_EDqWID3.js → UiKitApp-D7vllRQE.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-B2PrFgpz.js → WorkspaceRoutes-DH1LD-2Q.js} +26374 -25887
- package/dist/chunks/{dashboard-BnlnMnnr.js → dashboard-Dsaeud2x.js} +2 -2
- package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
- package/dist/chunks/{index-D_BHF_8q.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 +45 -1
- package/dist/site-widget.js +2799 -2791
- package/dist/studio.css +93 -1
- package/package.json +2 -2
- package/src/api.ts +19 -0
- package/src/chat/ChatProvider.tsx +6 -0
- package/src/chat/Composer.tsx +3 -1
- package/src/chrome/Toast.tsx +115 -0
- package/src/chrome/WorkspaceStatusBar.tsx +12 -3
- package/src/chrome/commit-error.ts +13 -0
- package/src/chrome/pull-failure.ts +61 -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/pages/BlockSettingsDialog.tsx +19 -115
- package/src/pages/BlocksList.tsx +43 -35
- package/src/pages/BodyProse.tsx +16 -1
- package/src/pages/CodeSessionPage.tsx +180 -7
- package/src/pages/DocEditorPage.tsx +87 -92
- package/src/pages/RenderedBody.tsx +17 -2
- package/src/pages/branch-notice.ts +16 -0
- package/src/pages/code-session-context.tsx +2 -0
- package/src/shell-resets.css +15 -0
- package/src/ui/Input.tsx +178 -20
- package/src/ui/popup.ts +11 -2
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
IconGlobe,
|
|
15
15
|
} from "@central-icons-react/round-outlined-radius-2-stroke-1.5";
|
|
16
16
|
import { ChatPanel } from "../chat/ChatPanel";
|
|
17
|
-
import { ChatProvider, ChatSessionScope } from "../chat/ChatProvider";
|
|
17
|
+
import { ChatProvider, ChatSessionScope, useChatBusy } from "../chat/ChatProvider";
|
|
18
18
|
import { useReplicaState } from "../replica/useReplica";
|
|
19
19
|
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
20
20
|
import { apiCredentials, apiHeaders, apiUrl } from "../studio-config";
|
|
@@ -35,7 +35,8 @@ import { CodeSessionFiles, type FilesView } from "./CodeSessionFiles";
|
|
|
35
35
|
import { PreviewSizeSwitch, PreviewViewport } from "./PreviewSize";
|
|
36
36
|
import { persistPreviewSize, readPreviewSize, type PreviewSize } from "./preview-size";
|
|
37
37
|
import type { WorkspaceFile } from "./code-session-files";
|
|
38
|
-
import { fetchCodeSessionEdits } from "../api";
|
|
38
|
+
import { clearCodeSessionEdits, fetchCodeSessionEdits, updateCodeSessionBranch } from "../api";
|
|
39
|
+
import { branchNoticeKind, type BranchNoticeKind } from "./branch-notice";
|
|
39
40
|
import { usePublishCodeSession } from "./code-session-context";
|
|
40
41
|
import { sessionDocument } from "./session-preview";
|
|
41
42
|
import { useSessionPreview } from "./use-session-preview";
|
|
@@ -80,6 +81,10 @@ export function CodeSessionScreen({
|
|
|
80
81
|
models = [],
|
|
81
82
|
files,
|
|
82
83
|
chat,
|
|
84
|
+
notice = null,
|
|
85
|
+
noticeBusy = false,
|
|
86
|
+
onUpdateBranch = () => {},
|
|
87
|
+
onClearEdits = () => {},
|
|
83
88
|
}: {
|
|
84
89
|
state: OpenState;
|
|
85
90
|
host: HostView;
|
|
@@ -108,9 +113,16 @@ export function CodeSessionScreen({
|
|
|
108
113
|
/** Models whose routes name the document under a pointed block. */
|
|
109
114
|
models?: readonly RouteModel[];
|
|
110
115
|
chat: React.ReactNode;
|
|
116
|
+
notice?: BranchNoticeKind | null;
|
|
117
|
+
/** Chat is writing, or the checkout is already being replaced. */
|
|
118
|
+
noticeBusy?: boolean;
|
|
119
|
+
onUpdateBranch?: () => void;
|
|
120
|
+
onClearEdits?: () => void;
|
|
111
121
|
}) {
|
|
112
122
|
// The entry's ticket sets the host's cookie on first load; later loads ride that cookie.
|
|
113
123
|
// A reload uses the route the frame is on, which can differ from the last address typed.
|
|
124
|
+
const chatBusy = useChatBusy();
|
|
125
|
+
const actionBusy = noticeBusy || chatBusy;
|
|
114
126
|
const frameSrc = preview ? frameAddress(preview, reloads > 0 ? location : path, reloads) : null;
|
|
115
127
|
const frameRef = useRef<HTMLIFrameElement>(null);
|
|
116
128
|
const heldFrameRef = useRef<HTMLIFrameElement | null>(null);
|
|
@@ -201,6 +213,38 @@ export function CodeSessionScreen({
|
|
|
201
213
|
</div>
|
|
202
214
|
</div>
|
|
203
215
|
|
|
216
|
+
{notice ? (
|
|
217
|
+
<div {...stylex.props(styles.notice)} role="status">
|
|
218
|
+
<span {...stylex.props(styles.noticeText)}>
|
|
219
|
+
{notice === "edits"
|
|
220
|
+
? "This branch has new commits. Commit them, or clear the edit set, before updating."
|
|
221
|
+
: notice === "fold"
|
|
222
|
+
? "This branch has new commits. The last commit is still folding."
|
|
223
|
+
: "This branch has new commits."}
|
|
224
|
+
</span>
|
|
225
|
+
{notice === "update" ? (
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
{...stylex.props(styles.noticeButton)}
|
|
229
|
+
disabled={actionBusy}
|
|
230
|
+
onClick={onUpdateBranch}
|
|
231
|
+
>
|
|
232
|
+
Update branch
|
|
233
|
+
</button>
|
|
234
|
+
) : null}
|
|
235
|
+
{notice === "edits" ? (
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
{...stylex.props(styles.noticeButton)}
|
|
239
|
+
disabled={actionBusy}
|
|
240
|
+
onClick={onClearEdits}
|
|
241
|
+
>
|
|
242
|
+
Clear edit set
|
|
243
|
+
</button>
|
|
244
|
+
) : null}
|
|
245
|
+
</div>
|
|
246
|
+
) : null}
|
|
247
|
+
|
|
204
248
|
<div {...stylex.props(styles.canvas)}>
|
|
205
249
|
{tab === "files" ? (
|
|
206
250
|
<CodeSessionFiles {...files} />
|
|
@@ -556,16 +600,26 @@ export default function CodeSessionPage() {
|
|
|
556
600
|
|
|
557
601
|
const sessionId = state.kind === "ready" ? state.sessionId : null;
|
|
558
602
|
const [editCount, setEditCount] = useState(0);
|
|
603
|
+
const [foldPending, setFoldPending] = useState(false);
|
|
604
|
+
const [baseSha, setBaseSha] = useState<string | null>(null);
|
|
605
|
+
const [updating, setUpdating] = useState(false);
|
|
559
606
|
const publishCodeSession = usePublishCodeSession();
|
|
560
607
|
|
|
561
608
|
const refreshEdits = useCallback(() => {
|
|
562
609
|
if (!sessionId) {
|
|
563
610
|
setEditCount(0);
|
|
611
|
+
setFoldPending(false);
|
|
564
612
|
return;
|
|
565
613
|
}
|
|
566
614
|
void fetchCodeSessionEdits(workspace.id, sessionId)
|
|
567
|
-
.then((list) =>
|
|
568
|
-
|
|
615
|
+
.then((list) => {
|
|
616
|
+
setEditCount(list.files.length);
|
|
617
|
+
setFoldPending(list.foldPending);
|
|
618
|
+
})
|
|
619
|
+
.catch(() => {
|
|
620
|
+
setEditCount(0);
|
|
621
|
+
setFoldPending(false);
|
|
622
|
+
});
|
|
569
623
|
}, [sessionId, workspace.id]);
|
|
570
624
|
|
|
571
625
|
// The host's dev server does not push a reload through the preview proxy.
|
|
@@ -591,9 +645,9 @@ export default function CodeSessionPage() {
|
|
|
591
645
|
publishCodeSession(null);
|
|
592
646
|
return;
|
|
593
647
|
}
|
|
594
|
-
publishCodeSession({ sessionId, editCount, refresh: refreshEdits });
|
|
648
|
+
publishCodeSession({ sessionId, editCount, refresh: refreshEdits, updating });
|
|
595
649
|
return () => publishCodeSession(null);
|
|
596
|
-
}, [sessionId, editCount, refreshEdits, publishCodeSession]);
|
|
650
|
+
}, [sessionId, editCount, refreshEdits, publishCodeSession, updating]);
|
|
597
651
|
|
|
598
652
|
// The agent and the dev server change the checkout, so read it fresh when the list is opened.
|
|
599
653
|
const loadCheckout = useCallback(async () => {
|
|
@@ -666,6 +720,7 @@ export default function CodeSessionPage() {
|
|
|
666
720
|
useEffect(() => {
|
|
667
721
|
let cancelled = false;
|
|
668
722
|
setState({ kind: "loading" });
|
|
723
|
+
setBaseSha(null);
|
|
669
724
|
setHost({ phase: null, furthest: null, log: "", timedOut: false });
|
|
670
725
|
setPreview(null);
|
|
671
726
|
const stored = storedPreviewPath(workspace.id);
|
|
@@ -707,8 +762,9 @@ export default function CodeSessionPage() {
|
|
|
707
762
|
setState({ kind: "fail", error: text || `request failed: ${res.status}` });
|
|
708
763
|
return;
|
|
709
764
|
}
|
|
710
|
-
const body = (await res.json()) as { session?: { id?: string } };
|
|
765
|
+
const body = (await res.json()) as { session?: { id?: string; baseSha?: string } };
|
|
711
766
|
const sessionId = body.session?.id;
|
|
767
|
+
setBaseSha(typeof body.session?.baseSha === "string" ? body.session.baseSha : null);
|
|
712
768
|
if (!sessionId) {
|
|
713
769
|
setState({ kind: "fail", error: "invalid response" });
|
|
714
770
|
return;
|
|
@@ -752,6 +808,78 @@ export default function CodeSessionPage() {
|
|
|
752
808
|
};
|
|
753
809
|
}, [workspace.id, branch]);
|
|
754
810
|
|
|
811
|
+
async function bootAgain(id: string, openPath: string) {
|
|
812
|
+
setPath(openPath);
|
|
813
|
+
setLocation(openPath);
|
|
814
|
+
setReloads(0);
|
|
815
|
+
setPreview(null);
|
|
816
|
+
setHost({ phase: null, furthest: null, log: "", timedOut: false });
|
|
817
|
+
const statusUrl = apiUrl(`/workspaces/${workspace.id}/code-sessions/${id}/status`);
|
|
818
|
+
const deadline = Date.now() + HOST_WAIT_MS;
|
|
819
|
+
while (Date.now() < deadline) {
|
|
820
|
+
const polled = await fetch(statusUrl, {
|
|
821
|
+
credentials: apiCredentials(),
|
|
822
|
+
headers: apiHeaders(),
|
|
823
|
+
}).catch(() => null);
|
|
824
|
+
if (polled?.ok) {
|
|
825
|
+
const status = (await polled.json()) as { phase: HostPhase; log: string };
|
|
826
|
+
setHost((current) => ({
|
|
827
|
+
phase: status.phase,
|
|
828
|
+
furthest:
|
|
829
|
+
status.phase === "installing" || status.phase === "starting"
|
|
830
|
+
? status.phase
|
|
831
|
+
: current.furthest,
|
|
832
|
+
log: status.log,
|
|
833
|
+
timedOut: false,
|
|
834
|
+
}));
|
|
835
|
+
if (status.phase === "ready") {
|
|
836
|
+
const entry = await requestPreview(workspace.id, id);
|
|
837
|
+
if (entry) setPreview(entry);
|
|
838
|
+
else setState({ kind: "fail", error: "The preview could not be opened." });
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (status.phase === "failed" || status.phase === "stopped") return;
|
|
842
|
+
}
|
|
843
|
+
await new Promise((resolve) => setTimeout(resolve, STATUS_POLL_MS));
|
|
844
|
+
}
|
|
845
|
+
setHost((current) => ({ ...current, timedOut: true }));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function onUpdateBranch() {
|
|
849
|
+
if (!sessionId || updating) return;
|
|
850
|
+
setUpdating(true);
|
|
851
|
+
try {
|
|
852
|
+
const result = await updateCodeSessionBranch(workspace.id, sessionId);
|
|
853
|
+
setBaseSha(result.baseSha);
|
|
854
|
+
await bootAgain(sessionId, location);
|
|
855
|
+
} catch {
|
|
856
|
+
// The checkout stayed on its commit, and the preview that is up stays up.
|
|
857
|
+
} finally {
|
|
858
|
+
setUpdating(false);
|
|
859
|
+
refreshEdits();
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function onClearEdits() {
|
|
864
|
+
if (!sessionId || updating) return;
|
|
865
|
+
const confirmed = window.confirm(
|
|
866
|
+
"Clear the edit set? The files go back to this session's commit. The chat stays.",
|
|
867
|
+
);
|
|
868
|
+
if (!confirmed) return;
|
|
869
|
+
setUpdating(true);
|
|
870
|
+
void (async () => {
|
|
871
|
+
try {
|
|
872
|
+
await clearCodeSessionEdits(workspace.id, sessionId);
|
|
873
|
+
await bootAgain(sessionId, location);
|
|
874
|
+
} catch {
|
|
875
|
+
// The edit set is still there.
|
|
876
|
+
} finally {
|
|
877
|
+
setUpdating(false);
|
|
878
|
+
refreshEdits();
|
|
879
|
+
}
|
|
880
|
+
})();
|
|
881
|
+
}
|
|
882
|
+
|
|
755
883
|
async function openInNewTab() {
|
|
756
884
|
if (state.kind !== "ready") return;
|
|
757
885
|
// The iframe's cookie is partitioned under Studio, so a new tab needs its own ticket.
|
|
@@ -803,6 +931,19 @@ export default function CodeSessionPage() {
|
|
|
803
931
|
onSelect: (path) => void selectFile(path),
|
|
804
932
|
}}
|
|
805
933
|
chat={chatColumn}
|
|
934
|
+
notice={
|
|
935
|
+
updating
|
|
936
|
+
? null
|
|
937
|
+
: branchNoticeKind({
|
|
938
|
+
baseSha,
|
|
939
|
+
headSha: git?.head?.sha ?? null,
|
|
940
|
+
editCount,
|
|
941
|
+
foldPending,
|
|
942
|
+
})
|
|
943
|
+
}
|
|
944
|
+
noticeBusy={updating}
|
|
945
|
+
onUpdateBranch={() => void onUpdateBranch()}
|
|
946
|
+
onClearEdits={onClearEdits}
|
|
806
947
|
previewSize={previewSize}
|
|
807
948
|
onPreviewSize={(size) => {
|
|
808
949
|
setPreviewSize(size);
|
|
@@ -940,6 +1081,38 @@ const styles = stylex.create({
|
|
|
940
1081
|
backgroundColor: colors.text,
|
|
941
1082
|
opacity: { default: 1, ":disabled": 0.3 },
|
|
942
1083
|
},
|
|
1084
|
+
notice: {
|
|
1085
|
+
display: "flex",
|
|
1086
|
+
alignItems: "center",
|
|
1087
|
+
justifyContent: "space-between",
|
|
1088
|
+
gap: space.md,
|
|
1089
|
+
paddingInline: space.md,
|
|
1090
|
+
paddingBlock: space.sm,
|
|
1091
|
+
borderBottomWidth: 1,
|
|
1092
|
+
borderBottomStyle: "solid",
|
|
1093
|
+
borderBottomColor: colors.border,
|
|
1094
|
+
backgroundColor: colors.bgSurface,
|
|
1095
|
+
},
|
|
1096
|
+
noticeText: {
|
|
1097
|
+
fontSize: type.sm,
|
|
1098
|
+
color: colors.text,
|
|
1099
|
+
},
|
|
1100
|
+
noticeButton: {
|
|
1101
|
+
flexShrink: 0,
|
|
1102
|
+
font: "inherit",
|
|
1103
|
+
fontSize: type.xs,
|
|
1104
|
+
fontWeight: 500,
|
|
1105
|
+
cursor: { default: "pointer", ":disabled": "default" },
|
|
1106
|
+
paddingBlock: "4px",
|
|
1107
|
+
paddingInline: space.sm,
|
|
1108
|
+
borderWidth: 1,
|
|
1109
|
+
borderStyle: "solid",
|
|
1110
|
+
borderColor: colors.border,
|
|
1111
|
+
borderRadius: radius.sm,
|
|
1112
|
+
color: colors.text,
|
|
1113
|
+
backgroundColor: colors.bgPanelInset,
|
|
1114
|
+
opacity: { default: 1, ":disabled": 0.4 },
|
|
1115
|
+
},
|
|
943
1116
|
stage: {
|
|
944
1117
|
display: "flex",
|
|
945
1118
|
flex: 1,
|
|
@@ -5,6 +5,7 @@ import { useOutletContext, useParams, useSearchParams, Link, useNavigate } from
|
|
|
5
5
|
import * as stylex from "@stylexjs/stylex";
|
|
6
6
|
import {
|
|
7
7
|
fetchDoc,
|
|
8
|
+
fetchDocs,
|
|
8
9
|
notifyTreeChanged,
|
|
9
10
|
type TreeChangeDetail,
|
|
10
11
|
createPreviewTicket,
|
|
@@ -17,8 +18,11 @@ import {
|
|
|
17
18
|
type SchemaPayload,
|
|
18
19
|
type SchemaField,
|
|
19
20
|
} from "../api";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
21
|
+
import { FieldControl } from "../fields/FieldControl";
|
|
22
|
+
import { loadRelationDocuments } from "../fields/load-relation-documents";
|
|
23
|
+
import { omitField } from "../fields/omit-field";
|
|
24
|
+
import { relationTargets } from "../fields/relation-targets";
|
|
25
|
+
import { useReplicaState } from "../replica/useReplica";
|
|
22
26
|
import { useSetEditorActions } from "../chrome/EditorActionsContext";
|
|
23
27
|
import {
|
|
24
28
|
DOC_CHANGED_EVENT,
|
|
@@ -130,48 +134,6 @@ function emptyFrontmatter(model: SchemaPayload["models"][number] | undefined) {
|
|
|
130
134
|
return out;
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
function JsonField({
|
|
134
|
-
value,
|
|
135
|
-
kind,
|
|
136
|
-
readOnly,
|
|
137
|
-
onValidChange,
|
|
138
|
-
}: {
|
|
139
|
-
value: unknown;
|
|
140
|
-
kind: "object" | "array";
|
|
141
|
-
readOnly?: boolean;
|
|
142
|
-
onValidChange: (next: unknown) => void;
|
|
143
|
-
}) {
|
|
144
|
-
const serialized = JSON.stringify(value ?? (kind === "array" ? [] : {}), null, 2);
|
|
145
|
-
const [draft, setDraft] = useState(serialized);
|
|
146
|
-
const [focused, setFocused] = useState(false);
|
|
147
|
-
const [invalid, setInvalid] = useState(false);
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
if (!focused) {
|
|
150
|
-
setDraft(serialized);
|
|
151
|
-
setInvalid(false);
|
|
152
|
-
}
|
|
153
|
-
}, [serialized, focused]);
|
|
154
|
-
return (
|
|
155
|
-
<Textarea
|
|
156
|
-
value={draft}
|
|
157
|
-
readOnly={readOnly}
|
|
158
|
-
aria-invalid={invalid || undefined}
|
|
159
|
-
onFocus={() => setFocused(true)}
|
|
160
|
-
onBlur={() => setFocused(false)}
|
|
161
|
-
onChange={(e) => {
|
|
162
|
-
const next = e.target.value;
|
|
163
|
-
setDraft(next);
|
|
164
|
-
try {
|
|
165
|
-
onValidChange(JSON.parse(next || (kind === "array" ? "[]" : "{}")));
|
|
166
|
-
setInvalid(false);
|
|
167
|
-
} catch {
|
|
168
|
-
setInvalid(true);
|
|
169
|
-
}
|
|
170
|
-
}}
|
|
171
|
-
/>
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
137
|
/** Status-bar label for the doc's state against the branch HEAD. */
|
|
176
138
|
export function docStatusLabel(status: DocStatus | null): string | null {
|
|
177
139
|
if (status === null) return null;
|
|
@@ -202,7 +164,31 @@ export function DocEditorPage() {
|
|
|
202
164
|
const [docFormat, setDocFormat] = useState<"mdx" | "page">("mdx");
|
|
203
165
|
const [fieldKinds, setFieldKinds] = useState<Record<string, string>>({});
|
|
204
166
|
const [fieldDefs, setFieldDefs] = useState<Record<string, SchemaField>>({});
|
|
205
|
-
const
|
|
167
|
+
const replica = useReplicaState();
|
|
168
|
+
const objects = useMemo(
|
|
169
|
+
() => replica.checkout(workspace.id)?.schema?.payload.objects ?? [],
|
|
170
|
+
[replica, workspace.id],
|
|
171
|
+
);
|
|
172
|
+
const relationTargetTypes = useMemo(() => {
|
|
173
|
+
const targets = new Set<string>();
|
|
174
|
+
const groups: Record<string, SchemaField>[] = [];
|
|
175
|
+
if (model?.fields) groups.push(model.fields);
|
|
176
|
+
for (const block of blocks) {
|
|
177
|
+
if (block.fields) groups.push(block.fields);
|
|
178
|
+
}
|
|
179
|
+
for (const block of richtext) {
|
|
180
|
+
if (block.fields) groups.push(block.fields);
|
|
181
|
+
}
|
|
182
|
+
for (const fields of groups) {
|
|
183
|
+
for (const type of relationTargets(fields, models, objects)) {
|
|
184
|
+
targets.add(type);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [...targets].sort();
|
|
188
|
+
}, [model, blocks, richtext, models, objects]);
|
|
189
|
+
const [documentLists, setDocumentLists] = useState<
|
|
190
|
+
Record<string, Array<{ slug: string; frontmatter: Record<string, unknown> }>> | undefined
|
|
191
|
+
>(undefined);
|
|
206
192
|
const [parseFailed, setParseFailed] = useState(false);
|
|
207
193
|
const [error, setError] = useState<string | null>(null);
|
|
208
194
|
const [status, setStatus] = useState<string | null>(null);
|
|
@@ -305,6 +291,31 @@ export function DocEditorPage() {
|
|
|
305
291
|
};
|
|
306
292
|
}, [frameIdentity, previewPath, workspaceId, siteUrl, branch, tree]);
|
|
307
293
|
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (relationTargetTypes.length === 0) {
|
|
296
|
+
setDocumentLists({});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
let cancelled = false;
|
|
300
|
+
setDocumentLists(undefined);
|
|
301
|
+
void loadRelationDocuments(
|
|
302
|
+
relationTargetTypes,
|
|
303
|
+
async () => (await fetchDocs(workspaceId)).docs,
|
|
304
|
+
async (relationType, relationSlug) => ({
|
|
305
|
+
frontmatter: (await fetchDoc(workspaceId, relationType, relationSlug)).doc.frontmatter,
|
|
306
|
+
}),
|
|
307
|
+
)
|
|
308
|
+
.then((loaded) => {
|
|
309
|
+
if (!cancelled) setDocumentLists(loaded);
|
|
310
|
+
})
|
|
311
|
+
.catch(() => {
|
|
312
|
+
if (!cancelled) setDocumentLists({});
|
|
313
|
+
});
|
|
314
|
+
return () => {
|
|
315
|
+
cancelled = true;
|
|
316
|
+
};
|
|
317
|
+
}, [workspaceId, type, slug, relationTargetTypes.join("\0")]);
|
|
318
|
+
|
|
308
319
|
useEffect(() => {
|
|
309
320
|
setPreviewLoading(true);
|
|
310
321
|
}, [previewUrl]);
|
|
@@ -1103,55 +1114,33 @@ export function DocEditorPage() {
|
|
|
1103
1114
|
</Text>
|
|
1104
1115
|
) : (
|
|
1105
1116
|
detailNames.map((name) => {
|
|
1106
|
-
const
|
|
1107
|
-
const composite = kind === "object" || kind === "array";
|
|
1108
|
-
const CustomInput = resolveFieldInput(studio, fieldDefs[name]);
|
|
1117
|
+
const field = fieldDefs[name]!;
|
|
1109
1118
|
return (
|
|
1110
1119
|
<Field key={name} label={fieldLabel(name)}>
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
value={frontmatter[name]}
|
|
1136
|
-
readOnly={aiEditing || parseFailed}
|
|
1137
|
-
onValidChange={(next) => {
|
|
1138
|
-
markEdited();
|
|
1139
|
-
setFrontmatter((prev) => ({ ...prev, [name]: next }));
|
|
1140
|
-
}}
|
|
1141
|
-
/>
|
|
1142
|
-
) : (
|
|
1143
|
-
<Input
|
|
1144
|
-
value={String(frontmatter[name] ?? "")}
|
|
1145
|
-
readOnly={aiEditing || parseFailed}
|
|
1146
|
-
onChange={(e) => {
|
|
1147
|
-
markEdited();
|
|
1148
|
-
setFrontmatter((prev) => ({
|
|
1149
|
-
...prev,
|
|
1150
|
-
[name]: e.target.value,
|
|
1151
|
-
}));
|
|
1152
|
-
}}
|
|
1153
|
-
/>
|
|
1154
|
-
)}
|
|
1120
|
+
<FieldControl
|
|
1121
|
+
name={name}
|
|
1122
|
+
field={field}
|
|
1123
|
+
value={frontmatter[name]}
|
|
1124
|
+
models={models}
|
|
1125
|
+
objects={objects}
|
|
1126
|
+
documentLists={documentLists}
|
|
1127
|
+
readOnly={aiEditing || parseFailed}
|
|
1128
|
+
blocksEditor={({ value, readOnly: blocksReadOnly, onChange: onBlocksChange }) => (
|
|
1129
|
+
<BlocksList
|
|
1130
|
+
blocks={blocksFromFrontmatter(value)}
|
|
1131
|
+
catalog={blocks}
|
|
1132
|
+
models={models}
|
|
1133
|
+
objects={objects}
|
|
1134
|
+
documentLists={documentLists}
|
|
1135
|
+
readOnly={blocksReadOnly}
|
|
1136
|
+
onChange={onBlocksChange}
|
|
1137
|
+
/>
|
|
1138
|
+
)}
|
|
1139
|
+
onChange={(next) => {
|
|
1140
|
+
markEdited();
|
|
1141
|
+
setFrontmatter((prev) => omitField(prev, name, next));
|
|
1142
|
+
}}
|
|
1143
|
+
/>
|
|
1155
1144
|
</Field>
|
|
1156
1145
|
);
|
|
1157
1146
|
})
|
|
@@ -1160,6 +1149,9 @@ export function DocEditorPage() {
|
|
|
1160
1149
|
<BlocksList
|
|
1161
1150
|
blocks={blocksFromFrontmatter(frontmatter[blocksField])}
|
|
1162
1151
|
catalog={blocks}
|
|
1152
|
+
models={models}
|
|
1153
|
+
objects={objects}
|
|
1154
|
+
documentLists={documentLists}
|
|
1163
1155
|
readOnly={aiEditing || parseFailed}
|
|
1164
1156
|
focusBlock={blockFocus}
|
|
1165
1157
|
onChange={(next) => {
|
|
@@ -1171,6 +1163,9 @@ export function DocEditorPage() {
|
|
|
1171
1163
|
<BodyField
|
|
1172
1164
|
value={body}
|
|
1173
1165
|
catalog={richtext}
|
|
1166
|
+
models={models}
|
|
1167
|
+
objects={objects}
|
|
1168
|
+
documentLists={documentLists}
|
|
1174
1169
|
readOnly={aiEditing || parseFailed}
|
|
1175
1170
|
onChange={(value) => {
|
|
1176
1171
|
markEdited();
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type ReactNode,
|
|
10
10
|
} from "react";
|
|
11
11
|
import * as stylex from "@stylexjs/stylex";
|
|
12
|
-
import type { BlockSnapshot } from "../api";
|
|
12
|
+
import type { BlockSnapshot, SchemaField } from "../api";
|
|
13
13
|
import { contentBlockFields, contentBlockLabel, contentBlockProps } from "../content-blocks";
|
|
14
14
|
import { colors, radius, space } from "../styles/tokens.stylex";
|
|
15
15
|
import {
|
|
@@ -40,16 +40,28 @@ export type RenderedBodyHandle = {
|
|
|
40
40
|
|
|
41
41
|
const BODY_HOST = "body";
|
|
42
42
|
|
|
43
|
+
type ModelRef = { type: string; external?: boolean; fields?: Record<string, SchemaField> };
|
|
44
|
+
type RelationDocumentLists = Record<
|
|
45
|
+
string,
|
|
46
|
+
Array<{ slug: string; frontmatter: Record<string, unknown> }>
|
|
47
|
+
>;
|
|
48
|
+
|
|
43
49
|
export const RenderedBody = forwardRef<
|
|
44
50
|
RenderedBodyHandle,
|
|
45
51
|
{
|
|
46
52
|
value: string;
|
|
47
53
|
catalog?: Array<BlockSnapshot & { label?: string }>;
|
|
54
|
+
models: ModelRef[];
|
|
55
|
+
objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
|
|
56
|
+
documentLists?: RelationDocumentLists;
|
|
48
57
|
readOnly?: boolean;
|
|
49
58
|
labelledBy: string;
|
|
50
59
|
onChange: (value: string) => void;
|
|
51
60
|
}
|
|
52
|
-
>(function RenderedBody(
|
|
61
|
+
>(function RenderedBody(
|
|
62
|
+
{ value, catalog = [], models, objects = [], documentLists, readOnly, labelledBy, onChange },
|
|
63
|
+
ref,
|
|
64
|
+
) {
|
|
53
65
|
const local = useRef(value);
|
|
54
66
|
const hosts = useRef(new Map<string, HTMLElement>());
|
|
55
67
|
const linking = useRef(false);
|
|
@@ -218,6 +230,9 @@ export const RenderedBody = forwardRef<
|
|
|
218
230
|
title={contentBlockLabel(settings.def.name)}
|
|
219
231
|
fields={contentBlockFields(settings.def)}
|
|
220
232
|
value={contentBlockProps(settings.block.markdown, settings.def.name)}
|
|
233
|
+
models={models}
|
|
234
|
+
objects={objects}
|
|
235
|
+
documentLists={documentLists}
|
|
221
236
|
readOnly={readOnly}
|
|
222
237
|
onOpenChange={(open) => {
|
|
223
238
|
if (!open) setSettingsId(null);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// packages/studio/src/pages/branch-notice.ts
|
|
2
|
+
|
|
3
|
+
export type BranchNoticeKind = "update" | "edits" | "fold";
|
|
4
|
+
|
|
5
|
+
/** What to show when this session's commit is behind the branch tip. */
|
|
6
|
+
export function branchNoticeKind(input: {
|
|
7
|
+
baseSha: string | null;
|
|
8
|
+
headSha: string | null;
|
|
9
|
+
editCount: number;
|
|
10
|
+
foldPending: boolean;
|
|
11
|
+
}): BranchNoticeKind | null {
|
|
12
|
+
if (!input.baseSha || !input.headSha || input.baseSha === input.headSha) return null;
|
|
13
|
+
if (input.editCount > 0) return "edits";
|
|
14
|
+
if (input.foldPending) return "fold";
|
|
15
|
+
return "update";
|
|
16
|
+
}
|
|
@@ -13,6 +13,8 @@ export type CodeSessionContextValue = {
|
|
|
13
13
|
sessionId: string;
|
|
14
14
|
editCount: number;
|
|
15
15
|
refresh: () => void;
|
|
16
|
+
/** The checkout is being replaced. Chat waits until the host boots. */
|
|
17
|
+
updating?: boolean;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
const CodeSessionContext = createContext<CodeSessionContextValue | null>(null);
|
package/src/shell-resets.css
CHANGED
|
@@ -150,10 +150,20 @@ html[data-theme="dark"]:has(.backmatter-studio)
|
|
|
150
150
|
outline-color: #fafafa;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
[data-no-ring]:focus,
|
|
153
154
|
[data-no-ring]:focus-visible {
|
|
154
155
|
outline: none;
|
|
156
|
+
outline-offset: 0;
|
|
157
|
+
box-shadow: none;
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
:is(
|
|
161
|
+
.backmatter-studio,
|
|
162
|
+
[data-backmatter-panel],
|
|
163
|
+
[data-base-ui-portal],
|
|
164
|
+
[data-selection-bar]
|
|
165
|
+
)
|
|
166
|
+
[data-no-ring]:focus,
|
|
157
167
|
:is(
|
|
158
168
|
.backmatter-studio,
|
|
159
169
|
[data-backmatter-panel],
|
|
@@ -161,8 +171,13 @@ html[data-theme="dark"]:has(.backmatter-studio)
|
|
|
161
171
|
[data-selection-bar]
|
|
162
172
|
)
|
|
163
173
|
[data-no-ring]:focus-visible,
|
|
174
|
+
html[data-theme="dark"]:has(.backmatter-studio)
|
|
175
|
+
:is(.backmatter-studio, [data-base-ui-portal], [data-selection-bar])
|
|
176
|
+
[data-no-ring]:focus,
|
|
164
177
|
html[data-theme="dark"]:has(.backmatter-studio)
|
|
165
178
|
:is(.backmatter-studio, [data-base-ui-portal], [data-selection-bar])
|
|
166
179
|
[data-no-ring]:focus-visible {
|
|
167
180
|
outline: none;
|
|
181
|
+
outline-offset: 0;
|
|
182
|
+
box-shadow: none;
|
|
168
183
|
}
|