@backmatter/studio 0.0.2 → 0.0.4
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/EmbedPublishPage-hx1xoAhI.js +149 -0
- package/dist/chunks/{UiKitPage-CTZtLwJ0.js → UiKitPage-BOunq6mr.js} +12 -11
- package/dist/chunks/WorkspaceRoutes-CTFTWu3L.js +57731 -0
- package/dist/chunks/{dashboard-nvFBzpZv.js → dashboard-upmiawx5.js} +216 -215
- package/dist/chunks/{index-Cr-IOgdC.js → index-CBvevwfT.js} +9 -8
- package/dist/chunks/status-bar-action-DiXhfcj3.js +30254 -0
- package/dist/dashboard.js +2 -2
- package/dist/index.js +139 -228
- package/dist/site-widget.js +292 -0
- package/dist/studio.css +46 -6
- package/package.json +7 -2
- package/src/api.ts +4 -0
- package/src/chat/ChangeCard.tsx +11 -6
- package/src/chat/ChatMessage.tsx +27 -2
- package/src/chat/ChatPanel.tsx +12 -2
- package/src/chat/ChatProvider.tsx +40 -3
- package/src/chat/Composer.tsx +92 -6
- package/src/chat/DocLink.tsx +31 -0
- package/src/chat/DocListCard.tsx +3 -3
- package/src/chat/PointerChip.tsx +139 -0
- package/src/chat/chat-context.ts +80 -1
- package/src/chat/icons.tsx +20 -0
- package/src/chat/session-expiry.ts +13 -5
- package/src/chrome/OpenSiteButton.tsx +33 -25
- package/src/index.css +9 -1
- package/src/pages/EmbedChatPage.tsx +25 -1
- package/src/pages/EmbedPublishPage.tsx +279 -84
- package/src/pages/GithubConnectPage.tsx +44 -19
- package/src/pages/SelectRepositoriesLink.tsx +30 -0
- package/src/pages/SettingsGeneralPage.tsx +2 -0
- package/src/pages/SettingsGithubPage.tsx +8 -3
- package/src/pages/host-env.ts +7 -2
- package/src/site-pointer.ts +271 -0
- package/src/site-widget.tsx +391 -0
- package/src/site.tsx +11 -7
- package/dist/chunks/WorkspaceRoutes-DyRVES9t.js +0 -87308
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/studio/src/pages/EmbedPublishPage.tsx
|
|
2
2
|
// The CMS commit flow, alone, so the on-site bar can frame it.
|
|
3
|
-
import { useEffect, useMemo, useState, type FormEvent } from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState, type FormEvent } from "react";
|
|
4
4
|
import { installParentChatHotkey } from "../chrome/hotkeys";
|
|
5
5
|
import * as stylex from "@stylexjs/stylex";
|
|
6
6
|
import {
|
|
@@ -13,14 +13,13 @@ import {
|
|
|
13
13
|
publishCommittedFiles,
|
|
14
14
|
resolveDoc,
|
|
15
15
|
type ApiError,
|
|
16
|
+
type ChangeStatus,
|
|
16
17
|
type GitChange,
|
|
17
18
|
type GitState,
|
|
18
19
|
} from "../api";
|
|
19
20
|
import { statusBarAction } from "../chrome/status-bar-action";
|
|
20
|
-
import { colors, space, type } from "../styles/tokens.stylex";
|
|
21
|
-
import {
|
|
22
|
-
import { Input } from "../ui/Input";
|
|
23
|
-
import { Text } from "../ui/Text";
|
|
21
|
+
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
22
|
+
import { popup } from "../ui/popup";
|
|
24
23
|
import { canPublish } from "../workspace/permissions";
|
|
25
24
|
|
|
26
25
|
const POLL_MS = 8000;
|
|
@@ -50,7 +49,24 @@ function changeKey(doc: GitChange) {
|
|
|
50
49
|
return `${doc.locale}/${doc.type}/${doc.slug}`;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
|
|
52
|
+
const STATUS_LABEL: Record<ChangeStatus, string> = {
|
|
53
|
+
modified: "modified",
|
|
54
|
+
added: "new",
|
|
55
|
+
conflict: "conflict",
|
|
56
|
+
deleted: "deleted",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const hover = "@media (hover: hover)";
|
|
60
|
+
|
|
61
|
+
export function EmbedPublishPage({
|
|
62
|
+
workspaceId,
|
|
63
|
+
onStatus,
|
|
64
|
+
onPublished,
|
|
65
|
+
}: {
|
|
66
|
+
workspaceId: string;
|
|
67
|
+
onStatus?: (status: { changeCount: number; conflictCount: number }) => void;
|
|
68
|
+
onPublished?: () => void;
|
|
69
|
+
}) {
|
|
54
70
|
useEffect(() => installParentChatHotkey(), []);
|
|
55
71
|
const [git, setGit] = useState<GitState | null>(null);
|
|
56
72
|
const [role, setRole] = useState("viewer");
|
|
@@ -73,6 +89,9 @@ export function EmbedPublishPage({ workspaceId }: { workspaceId: string }) {
|
|
|
73
89
|
};
|
|
74
90
|
}, [workspaceId]);
|
|
75
91
|
|
|
92
|
+
const onStatusRef = useRef(onStatus);
|
|
93
|
+
onStatusRef.current = onStatus;
|
|
94
|
+
|
|
76
95
|
useEffect(() => {
|
|
77
96
|
let stop = false;
|
|
78
97
|
const load = () => {
|
|
@@ -80,14 +99,13 @@ export function EmbedPublishPage({ workspaceId }: { workspaceId: string }) {
|
|
|
80
99
|
.then((next) => {
|
|
81
100
|
if (stop) return;
|
|
82
101
|
setGit(next);
|
|
102
|
+
const changeCount = next.changes.length;
|
|
103
|
+
const conflictCount = countConflicts(next);
|
|
104
|
+
onStatusRef.current?.({ changeCount, conflictCount });
|
|
83
105
|
const origin = parentOrigin();
|
|
84
|
-
if (!origin) return;
|
|
106
|
+
if (!origin || window.parent === window) return;
|
|
85
107
|
window.parent.postMessage(
|
|
86
|
-
{
|
|
87
|
-
type: "backmatter:git",
|
|
88
|
-
changeCount: next.changes.length,
|
|
89
|
-
conflictCount: countConflicts(next),
|
|
90
|
-
},
|
|
108
|
+
{ type: "backmatter:git", changeCount, conflictCount },
|
|
91
109
|
origin,
|
|
92
110
|
);
|
|
93
111
|
})
|
|
@@ -143,8 +161,11 @@ export function EmbedPublishPage({ workspaceId }: { workspaceId: string }) {
|
|
|
143
161
|
const result = await commitWorkspace(workspaceId, message);
|
|
144
162
|
setMessage("");
|
|
145
163
|
publishCommittedFiles(workspaceId, result.files);
|
|
164
|
+
onPublished?.();
|
|
146
165
|
const origin = parentOrigin();
|
|
147
|
-
if (origin
|
|
166
|
+
if (origin && window.parent !== window) {
|
|
167
|
+
window.parent.postMessage({ type: "backmatter:published" }, origin);
|
|
168
|
+
}
|
|
148
169
|
setGit(await fetchGit(workspaceId));
|
|
149
170
|
} catch (err) {
|
|
150
171
|
setError(formatCommitError(err));
|
|
@@ -153,107 +174,281 @@ export function EmbedPublishPage({ workspaceId }: { workspaceId: string }) {
|
|
|
153
174
|
}
|
|
154
175
|
}
|
|
155
176
|
|
|
156
|
-
const
|
|
157
|
-
? "Committing…"
|
|
158
|
-
: conflictCount > 0
|
|
159
|
-
? `Resolve ${conflictCount} ${conflictCount === 1 ? "conflict" : "conflicts"}`
|
|
160
|
-
: action === "blocked"
|
|
161
|
-
? "Writers cannot publish"
|
|
162
|
-
: changes.length === 0
|
|
163
|
-
? "Nothing to publish"
|
|
164
|
-
: `Deploy ${changes.length} ${changes.length === 1 ? "change" : "changes"}`;
|
|
177
|
+
const branch = git?.currentBranch ?? "main";
|
|
165
178
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
size="sm"
|
|
186
|
-
variant="secondary"
|
|
179
|
+
if (conflicts.length > 0) {
|
|
180
|
+
return (
|
|
181
|
+
<div {...stylex.props(styles.form)}>
|
|
182
|
+
<p {...stylex.props(popup.groupLabel)}>Resolve conflicts</p>
|
|
183
|
+
<p {...stylex.props(styles.hint)}>
|
|
184
|
+
Pick a side for each file. Commit stays blocked until every conflict is resolved.
|
|
185
|
+
</p>
|
|
186
|
+
{error ? <p {...stylex.props(styles.error)}>{error}</p> : null}
|
|
187
|
+
<ul {...stylex.props(styles.files, styles.conflictList)}>
|
|
188
|
+
{conflicts.map((doc) => {
|
|
189
|
+
const key = changeKey(doc);
|
|
190
|
+
return (
|
|
191
|
+
<li key={key} {...stylex.props(styles.conflictRow)}>
|
|
192
|
+
<span {...stylex.props(styles.fileName)}>
|
|
193
|
+
{doc.type}/{doc.slug}
|
|
194
|
+
</span>
|
|
195
|
+
<div {...stylex.props(styles.conflictActions)}>
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
187
198
|
disabled={resolvingKey !== null}
|
|
188
|
-
onClick={() => void onResolve(
|
|
199
|
+
onClick={() => void onResolve(doc, "mine")}
|
|
200
|
+
{...stylex.props(styles.conflictBtn)}
|
|
189
201
|
>
|
|
190
|
-
Keep mine
|
|
191
|
-
</
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
variant="secondary"
|
|
202
|
+
{resolvingKey === `${key}:mine` ? "Keeping…" : "Keep mine"}
|
|
203
|
+
</button>
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
195
206
|
disabled={resolvingKey !== null}
|
|
196
|
-
onClick={() => void onResolve(
|
|
207
|
+
onClick={() => void onResolve(doc, "theirs")}
|
|
208
|
+
{...stylex.props(styles.conflictBtn, styles.conflictBtnDanger)}
|
|
197
209
|
>
|
|
198
|
-
Take theirs
|
|
199
|
-
</
|
|
210
|
+
{resolvingKey === `${key}:theirs` ? "Taking…" : "Take theirs"}
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
</li>
|
|
214
|
+
);
|
|
215
|
+
})}
|
|
216
|
+
</ul>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<form {...stylex.props(styles.form)} onSubmit={(event) => void onCommit(event)}>
|
|
223
|
+
<p {...stylex.props(popup.groupLabel)}>
|
|
224
|
+
{changes.length === 0
|
|
225
|
+
? "Nothing to publish"
|
|
226
|
+
: `${changes.length} ${changes.length === 1 ? "file" : "files"} → ${branch}`}
|
|
227
|
+
</p>
|
|
228
|
+
{changes.length > 0 ? (
|
|
229
|
+
<ul {...stylex.props(styles.files)}>
|
|
230
|
+
{changes.map((doc) => (
|
|
231
|
+
<li key={changeKey(doc)}>
|
|
232
|
+
<div {...stylex.props(styles.file)}>
|
|
233
|
+
<span {...stylex.props(styles.fileName)}>
|
|
234
|
+
{doc.type}/{doc.slug}
|
|
200
235
|
</span>
|
|
201
|
-
|
|
236
|
+
<span {...stylex.props(popup.itemMeta)}>{STATUS_LABEL[doc.status]}</span>
|
|
237
|
+
</div>
|
|
202
238
|
</li>
|
|
203
239
|
))}
|
|
204
240
|
</ul>
|
|
205
|
-
)}
|
|
206
|
-
{conflicts.length > 0 ? null : (
|
|
207
|
-
<Input
|
|
208
|
-
value={message}
|
|
209
|
-
placeholder={placeholder}
|
|
210
|
-
aria-label="Commit message"
|
|
211
|
-
onChange={(event) => setMessage(event.target.value)}
|
|
212
|
-
/>
|
|
213
|
-
)}
|
|
214
|
-
{error ? (
|
|
215
|
-
<Text as="p" size="sm" tone="danger">
|
|
216
|
-
{error}
|
|
217
|
-
</Text>
|
|
218
241
|
) : null}
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
242
|
+
{changes.length > 0 ? <div {...stylex.props(popup.separator)} /> : null}
|
|
243
|
+
{changes.length > 0 ? (
|
|
244
|
+
<div {...stylex.props(styles.footer)}>
|
|
245
|
+
<input
|
|
246
|
+
value={message}
|
|
247
|
+
onChange={(event) => setMessage(event.target.value)}
|
|
248
|
+
placeholder={placeholder}
|
|
249
|
+
aria-label="Commit message"
|
|
250
|
+
autoComplete="off"
|
|
251
|
+
spellCheck={false}
|
|
252
|
+
data-no-ring
|
|
253
|
+
data-1p-ignore
|
|
254
|
+
data-lpignore
|
|
255
|
+
disabled={committing || action !== "commit"}
|
|
256
|
+
{...stylex.props(styles.field)}
|
|
257
|
+
/>
|
|
258
|
+
{error ? <p {...stylex.props(styles.error, styles.errorInFooter)}>{error}</p> : null}
|
|
259
|
+
{action === "blocked" ? (
|
|
260
|
+
<p {...stylex.props(styles.hint)}>Writers cannot publish</p>
|
|
261
|
+
) : (
|
|
262
|
+
<button
|
|
263
|
+
type="submit"
|
|
264
|
+
disabled={committing || action !== "commit"}
|
|
265
|
+
{...stylex.props(styles.submit)}
|
|
266
|
+
>
|
|
267
|
+
{committing ? "Committing…" : "Commit & push"}
|
|
268
|
+
</button>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
) : error ? (
|
|
272
|
+
<p {...stylex.props(styles.error)}>{error}</p>
|
|
273
|
+
) : null}
|
|
226
274
|
</form>
|
|
227
275
|
);
|
|
228
276
|
}
|
|
229
277
|
|
|
230
278
|
const styles = stylex.create({
|
|
231
|
-
|
|
279
|
+
form: {
|
|
232
280
|
display: "flex",
|
|
233
281
|
flexDirection: "column",
|
|
234
|
-
|
|
235
|
-
minHeight: "100%",
|
|
236
|
-
boxSizing: "border-box",
|
|
237
|
-
padding: space.xl,
|
|
282
|
+
paddingTop: space.xs,
|
|
238
283
|
backgroundColor: colors.bgSurface,
|
|
239
284
|
color: colors.text,
|
|
240
|
-
fontSize: type.sm,
|
|
241
285
|
},
|
|
242
|
-
|
|
286
|
+
files: {
|
|
243
287
|
margin: 0,
|
|
244
288
|
padding: 0,
|
|
245
289
|
listStyle: "none",
|
|
290
|
+
maxHeight: "160px",
|
|
291
|
+
overflowY: "auto",
|
|
292
|
+
paddingBottom: space.xs,
|
|
293
|
+
},
|
|
294
|
+
file: {
|
|
295
|
+
display: "flex",
|
|
296
|
+
alignItems: "center",
|
|
297
|
+
gap: space.sm,
|
|
298
|
+
minHeight: 30,
|
|
299
|
+
margin: 0,
|
|
300
|
+
marginInline: space.xs,
|
|
301
|
+
paddingInline: space.md,
|
|
302
|
+
paddingBlock: space.sm,
|
|
303
|
+
fontFamily: type.mono,
|
|
304
|
+
fontSize: type.xs,
|
|
305
|
+
color: colors.textMuted,
|
|
306
|
+
},
|
|
307
|
+
fileName: {
|
|
308
|
+
minWidth: 0,
|
|
309
|
+
flex: 1,
|
|
310
|
+
overflow: "hidden",
|
|
311
|
+
textOverflow: "ellipsis",
|
|
312
|
+
whiteSpace: "nowrap",
|
|
313
|
+
},
|
|
314
|
+
footer: {
|
|
246
315
|
display: "flex",
|
|
247
316
|
flexDirection: "column",
|
|
317
|
+
alignItems: "stretch",
|
|
248
318
|
gap: space.md,
|
|
319
|
+
paddingInline: space.lg,
|
|
320
|
+
paddingTop: space.md,
|
|
321
|
+
paddingBottom: space.lg,
|
|
322
|
+
},
|
|
323
|
+
field: {
|
|
324
|
+
width: "100%",
|
|
325
|
+
boxSizing: "border-box",
|
|
326
|
+
minHeight: "28px",
|
|
327
|
+
paddingInline: space.md,
|
|
328
|
+
paddingBlock: space.sm,
|
|
329
|
+
borderWidth: 1,
|
|
330
|
+
borderStyle: "solid",
|
|
331
|
+
borderColor: colors.border,
|
|
332
|
+
borderRadius: radius.md,
|
|
333
|
+
backgroundColor: colors.bgInput,
|
|
334
|
+
color: colors.text,
|
|
335
|
+
fontSize: type.sm,
|
|
336
|
+
outline: "none",
|
|
337
|
+
":focus": {
|
|
338
|
+
outline: "none",
|
|
339
|
+
borderColor: `color-mix(in srgb, ${colors.textMuted} 45%, ${colors.border})`,
|
|
340
|
+
},
|
|
341
|
+
":focus-visible": {
|
|
342
|
+
outline: "none",
|
|
343
|
+
},
|
|
344
|
+
":disabled": {
|
|
345
|
+
opacity: 0.55,
|
|
346
|
+
},
|
|
347
|
+
"::placeholder": {
|
|
348
|
+
color: colors.textMuted,
|
|
349
|
+
},
|
|
249
350
|
},
|
|
250
|
-
|
|
351
|
+
submit: {
|
|
352
|
+
display: "flex",
|
|
353
|
+
width: "100%",
|
|
354
|
+
alignItems: "center",
|
|
355
|
+
justifyContent: "center",
|
|
356
|
+
height: 28,
|
|
357
|
+
paddingInline: space.xl,
|
|
358
|
+
borderWidth: 1,
|
|
359
|
+
borderStyle: "solid",
|
|
360
|
+
borderColor: colors.accent,
|
|
361
|
+
borderRadius: radius.md,
|
|
362
|
+
backgroundColor: colors.accent,
|
|
363
|
+
color: colors.textInverse,
|
|
364
|
+
fontSize: type.xs,
|
|
365
|
+
fontWeight: 500,
|
|
366
|
+
whiteSpace: "nowrap",
|
|
367
|
+
cursor: "pointer",
|
|
368
|
+
[hover]: {
|
|
369
|
+
":hover": {
|
|
370
|
+
backgroundColor: colors.accentHover,
|
|
371
|
+
borderColor: colors.accentHover,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
":disabled": {
|
|
375
|
+
opacity: 0.55,
|
|
376
|
+
cursor: "not-allowed",
|
|
377
|
+
},
|
|
378
|
+
":active": {
|
|
379
|
+
transform: "scale(0.97)",
|
|
380
|
+
},
|
|
381
|
+
":focus-visible": {
|
|
382
|
+
outlineWidth: 2,
|
|
383
|
+
outlineStyle: "solid",
|
|
384
|
+
outlineColor: colors.focus,
|
|
385
|
+
outlineOffset: 1,
|
|
386
|
+
},
|
|
387
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
388
|
+
":active": {
|
|
389
|
+
transform: "none",
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
hint: {
|
|
394
|
+
margin: 0,
|
|
395
|
+
paddingInline: space.lg,
|
|
396
|
+
paddingBottom: space.sm,
|
|
397
|
+
fontSize: type.xs,
|
|
398
|
+
lineHeight: 1.4,
|
|
399
|
+
color: colors.textMuted,
|
|
400
|
+
},
|
|
401
|
+
error: {
|
|
402
|
+
margin: 0,
|
|
403
|
+
paddingInline: space.lg,
|
|
404
|
+
paddingBottom: space.sm,
|
|
405
|
+
fontSize: type.xs,
|
|
406
|
+
color: colors.danger,
|
|
407
|
+
},
|
|
408
|
+
errorInFooter: {
|
|
409
|
+
paddingInline: 0,
|
|
410
|
+
paddingBottom: 0,
|
|
411
|
+
},
|
|
412
|
+
conflictList: {
|
|
413
|
+
maxHeight: "280px",
|
|
414
|
+
},
|
|
415
|
+
conflictRow: {
|
|
251
416
|
display: "flex",
|
|
252
417
|
flexDirection: "column",
|
|
253
|
-
gap: space.
|
|
418
|
+
gap: space.xs,
|
|
419
|
+
paddingInline: space.md,
|
|
420
|
+
paddingBlock: space.sm,
|
|
421
|
+
fontFamily: type.mono,
|
|
422
|
+
fontSize: type.xs,
|
|
254
423
|
},
|
|
255
|
-
|
|
424
|
+
conflictActions: {
|
|
256
425
|
display: "flex",
|
|
257
|
-
gap: space.
|
|
426
|
+
gap: space.xs,
|
|
427
|
+
},
|
|
428
|
+
conflictBtn: {
|
|
429
|
+
height: 24,
|
|
430
|
+
paddingInline: space.md,
|
|
431
|
+
borderWidth: 1,
|
|
432
|
+
borderStyle: "solid",
|
|
433
|
+
borderColor: colors.border,
|
|
434
|
+
borderRadius: radius.md,
|
|
435
|
+
backgroundColor: colors.bgSurface,
|
|
436
|
+
color: colors.text,
|
|
437
|
+
fontSize: type.xs,
|
|
438
|
+
cursor: "pointer",
|
|
439
|
+
":disabled": {
|
|
440
|
+
opacity: 0.55,
|
|
441
|
+
cursor: "not-allowed",
|
|
442
|
+
},
|
|
443
|
+
":focus-visible": {
|
|
444
|
+
outlineWidth: 2,
|
|
445
|
+
outlineStyle: "solid",
|
|
446
|
+
outlineColor: colors.focus,
|
|
447
|
+
outlineOffset: 1,
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
conflictBtnDanger: {
|
|
451
|
+
borderColor: colors.danger,
|
|
452
|
+
color: colors.danger,
|
|
258
453
|
},
|
|
259
454
|
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { useNavigate, useOutletContext } from "react-router";
|
|
4
4
|
import { bindGithub, fetchGithubRepos, type ApiError } from "../api";
|
|
5
|
+
import { SelectRepositoriesLink } from "./SelectRepositoriesLink";
|
|
5
6
|
import { BackLink } from "../ui/BackLink";
|
|
6
7
|
import { Button, ButtonLink } from "../ui/Button";
|
|
7
8
|
import { Field } from "../ui/Field";
|
|
@@ -30,24 +31,43 @@ export function GithubConnectPage() {
|
|
|
30
31
|
|
|
31
32
|
useEffect(() => {
|
|
32
33
|
if (!workspaceId) return;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
let cancelled = false;
|
|
35
|
+
|
|
36
|
+
function load() {
|
|
37
|
+
void fetchGithubRepos(workspaceId)
|
|
38
|
+
.then((data) => {
|
|
39
|
+
if (cancelled) return;
|
|
40
|
+
setRepos(data.repos);
|
|
41
|
+
setSelected((current) =>
|
|
42
|
+
current && data.repos.some((repo) => repo.fullName === current)
|
|
43
|
+
? current
|
|
44
|
+
: (data.repos[0]?.fullName ?? ""),
|
|
45
|
+
);
|
|
46
|
+
setError(null);
|
|
47
|
+
})
|
|
48
|
+
.catch((err: Error) => {
|
|
49
|
+
if (cancelled) return;
|
|
50
|
+
const code = err.message;
|
|
51
|
+
setError(
|
|
52
|
+
code === "github_not_installed" || code === "github_list_failed"
|
|
53
|
+
? "The saved GitHub installation is missing or this app cannot access it."
|
|
54
|
+
: code,
|
|
55
|
+
);
|
|
56
|
+
})
|
|
57
|
+
.finally(() => {
|
|
58
|
+
if (!cancelled) setLoading(false);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
load();
|
|
63
|
+
function onVisible() {
|
|
64
|
+
if (document.visibilityState === "visible") load();
|
|
65
|
+
}
|
|
66
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
70
|
+
};
|
|
51
71
|
}, [workspaceId]);
|
|
52
72
|
|
|
53
73
|
async function onSubmit(e: React.FormEvent) {
|
|
@@ -100,6 +120,7 @@ export function GithubConnectPage() {
|
|
|
100
120
|
? "GitHub did not return repositories for the saved installation."
|
|
101
121
|
: "No repositories are available for this installation."}
|
|
102
122
|
</Text>
|
|
123
|
+
<SelectRepositoriesLink workspaceId={workspaceId} />
|
|
103
124
|
<div>
|
|
104
125
|
<ButtonLink to={settingsPath(workspace.slug, "github")} variant="secondary">
|
|
105
126
|
Back to GitHub settings
|
|
@@ -109,7 +130,10 @@ export function GithubConnectPage() {
|
|
|
109
130
|
) : (
|
|
110
131
|
<form onSubmit={onSubmit}>
|
|
111
132
|
<Stack>
|
|
112
|
-
<Field
|
|
133
|
+
<Field
|
|
134
|
+
label="Repository"
|
|
135
|
+
hint="Only repositories granted to the GitHub App are listed."
|
|
136
|
+
>
|
|
113
137
|
<Select
|
|
114
138
|
aria-label="Repository"
|
|
115
139
|
value={selected}
|
|
@@ -120,6 +144,7 @@ export function GithubConnectPage() {
|
|
|
120
144
|
}))}
|
|
121
145
|
/>
|
|
122
146
|
</Field>
|
|
147
|
+
<SelectRepositoriesLink workspaceId={workspaceId} />
|
|
123
148
|
<Field
|
|
124
149
|
label="Directory"
|
|
125
150
|
hint="Folder inside the repository that contains this site. Leave empty for the repository root."
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// packages/studio/src/pages/SelectRepositoriesLink.tsx
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { fetchGithubRepositoryAccess } from "../api";
|
|
4
|
+
import { ButtonLink } from "../ui/Button";
|
|
5
|
+
|
|
6
|
+
/** Opens the GitHub page where this installation's repository access is chosen. */
|
|
7
|
+
export function SelectRepositoriesLink({ workspaceId }: { workspaceId: string }) {
|
|
8
|
+
const [url, setUrl] = useState<string | null>(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let cancelled = false;
|
|
12
|
+
void fetchGithubRepositoryAccess(workspaceId)
|
|
13
|
+
.then((data) => {
|
|
14
|
+
if (!cancelled) setUrl(data.url);
|
|
15
|
+
})
|
|
16
|
+
.catch(() => {
|
|
17
|
+
if (!cancelled) setUrl(null);
|
|
18
|
+
});
|
|
19
|
+
return () => {
|
|
20
|
+
cancelled = true;
|
|
21
|
+
};
|
|
22
|
+
}, [workspaceId]);
|
|
23
|
+
|
|
24
|
+
if (!url) return null;
|
|
25
|
+
return (
|
|
26
|
+
<ButtonLink href={url} variant="secondary">
|
|
27
|
+
Select repositories on GitHub
|
|
28
|
+
</ButtonLink>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
rotatePreviewToken,
|
|
9
9
|
} from "../api";
|
|
10
10
|
import { hostEnvSnippet } from "./host-env";
|
|
11
|
+
import { isStudioDev, publicApiUrl } from "../studio-config";
|
|
11
12
|
import { revalidateWebhookUrl } from "./preview-url";
|
|
12
13
|
import { Button } from "../ui/Button";
|
|
13
14
|
import { Field } from "../ui/Field";
|
|
@@ -48,6 +49,7 @@ export function SettingsGeneralPage() {
|
|
|
48
49
|
previewToken: previewTokenOnce,
|
|
49
50
|
hostToken: hostTokenOnce,
|
|
50
51
|
siteUrl: workspace.siteUrls?.[0],
|
|
52
|
+
apiUrl: isStudioDev() ? publicApiUrl() : undefined,
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
async function onSaveSiteUrl(e: React.FormEvent) {
|
|
@@ -18,6 +18,7 @@ import * as stylex from "@stylexjs/stylex";
|
|
|
18
18
|
import { colors, space, type } from "../styles/tokens.stylex";
|
|
19
19
|
import type { WorkspaceOutletContext } from "../workspace/types";
|
|
20
20
|
import { githubConnectPath } from "../workspace-path";
|
|
21
|
+
import { SelectRepositoriesLink } from "./SelectRepositoriesLink";
|
|
21
22
|
|
|
22
23
|
export function SettingsGithubPage() {
|
|
23
24
|
const { workspace, reload } = useOutletContext<WorkspaceOutletContext>();
|
|
@@ -172,6 +173,7 @@ export function SettingsGithubPage() {
|
|
|
172
173
|
>
|
|
173
174
|
Change repository
|
|
174
175
|
</ButtonLink>
|
|
176
|
+
<SelectRepositoriesLink workspaceId={workspace.id} />
|
|
175
177
|
<Button
|
|
176
178
|
variant="danger"
|
|
177
179
|
disabled={disconnecting}
|
|
@@ -185,9 +187,9 @@ export function SettingsGithubPage() {
|
|
|
185
187
|
) : (
|
|
186
188
|
<Stack>
|
|
187
189
|
<Text tone="muted" size="sm">
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
{workspace.githubInstallationId
|
|
191
|
+
? "The list only includes repositories this GitHub App can access. Select repositories on GitHub to grant more. Saving there does not return to Backmatter."
|
|
192
|
+
: "Connect finds the backmatter CMS installation on your GitHub account and opens the repository list here. Saving on GitHub does not return to Backmatter."}
|
|
191
193
|
</Text>
|
|
192
194
|
{canEdit ? (
|
|
193
195
|
<div {...stylex.props(styles.actions)}>
|
|
@@ -210,6 +212,9 @@ export function SettingsGithubPage() {
|
|
|
210
212
|
{connecting ? "Connecting…" : "Connect GitHub"}
|
|
211
213
|
</Button>
|
|
212
214
|
)}
|
|
215
|
+
{workspace.githubInstallationId ? (
|
|
216
|
+
<SelectRepositoriesLink workspaceId={workspace.id} />
|
|
217
|
+
) : null}
|
|
213
218
|
{installUrl ? (
|
|
214
219
|
<ButtonLink href={installUrl} newTab={false} variant="secondary">
|
|
215
220
|
Install on GitHub
|
package/src/pages/host-env.ts
CHANGED
|
@@ -7,13 +7,18 @@ export function hostEnvSnippet(input: {
|
|
|
7
7
|
previewToken?: string | null;
|
|
8
8
|
hostToken?: string | null;
|
|
9
9
|
siteUrl?: string | null;
|
|
10
|
+
/** Local API origin. Passed only while the dashboard is running in dev. */
|
|
11
|
+
apiUrl?: string | null;
|
|
10
12
|
}): string {
|
|
11
13
|
const line = (name: string, value: string | null | undefined) =>
|
|
12
14
|
`${name}=${value?.trim() ?? ""}`;
|
|
15
|
+
const apiUrl = input.apiUrl?.trim();
|
|
13
16
|
return [
|
|
14
17
|
`# backmatter.config.ts → workspaceId: "${input.workspaceId}"`,
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
apiUrl
|
|
19
|
+
? line("BACKMATTER_API_URL", apiUrl)
|
|
20
|
+
: "# API defaults to https://api.backmatter.app in production",
|
|
21
|
+
line("BACKMATTER_WORKSPACE_ID", input.workspaceId),
|
|
17
22
|
line("BACKMATTER_PREVIEW_TOKEN", input.previewToken),
|
|
18
23
|
line("BACKMATTER_HOST_TOKEN", input.hostToken),
|
|
19
24
|
line("BACKMATTER_CONTENT_TOKEN", input.contentToken),
|