@agent-native/core-corpus 0.212.1 → 0.213.1
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +29 -14
- package/corpus/templates/analytics/CHANGELOG.md +11 -0
- package/corpus/templates/analytics/actions/estimate-onboarding-journey-cost.ts +23 -6
- package/corpus/templates/analytics/actions/get-onboarding-journey.ts +20 -5
- package/corpus/templates/analytics/actions/provider-api-register.ts +2 -7
- package/corpus/templates/analytics/changelog/2026-10-21-analytics-recognizes-questions-about-connected-data-product-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-21-onboarding-journeys-now-honor-larger-event-row-budgets.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-21-preserve-unknown-replay-availability-in-partial-journeys.md +6 -0
- package/corpus/templates/analytics/package.json +6 -0
- package/corpus/templates/analytics/scripts/journey-capture-plan.ts +18 -7
- package/corpus/templates/analytics/scripts/journey-capture.ts +3 -1
- package/corpus/templates/analytics/server/lib/db-admin-connections.ts +19 -12
- package/corpus/templates/analytics/server/lib/journey-tree.ts +24 -9
- package/corpus/templates/analytics/server/lib/onboarding-journey-cost.ts +163 -94
- package/corpus/templates/analytics/server/lib/onboarding-journey.ts +162 -74
- package/corpus/templates/assets/server/lib/org-admin.ts +20 -16
- package/corpus/templates/brain/server/lib/zoom-governance.ts +11 -6
- package/corpus/templates/clips/CHANGELOG.md +1 -0
- package/corpus/templates/clips/changelog/2026-10-16-rewind-status-says-when-it-remembers-audio.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +7 -4
- package/corpus/templates/clips/desktop/src/components/RewindCaptureStatus.tsx +25 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +6 -1
- package/corpus/templates/clips/desktop/src/shared/config.ts +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2 -0
- package/corpus/templates/clips/server/lib/recordings.ts +4 -19
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/CHANGELOG.md +8 -0
- package/corpus/templates/content/actions/_content-space-access.ts +95 -6
- package/corpus/templates/content/actions/_database-utils.ts +20 -16
- package/corpus/templates/content/actions/_github-source-freshness.ts +72 -17
- package/corpus/templates/content/actions/_github-source-metadata.ts +63 -17
- package/corpus/templates/content/actions/_github-source.ts +141 -77
- package/corpus/templates/content/actions/check-github-connection.ts +15 -0
- package/corpus/templates/content/actions/create-github-collection.ts +180 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +5 -0
- package/corpus/templates/content/actions/link-github-folder.ts +104 -36
- package/corpus/templates/content/actions/refresh-content-database-source.ts +37 -4
- package/corpus/templates/content/actions/sync-github-folder.ts +23 -5
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +172 -19
- package/corpus/templates/content/app/components/editor/database/GitHubLinkDialog.tsx +938 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourcePanel.tsx +228 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +561 -0
- package/corpus/templates/content/app/components/editor/database/GitHubUnlinkDialog.tsx +96 -0
- package/corpus/templates/content/app/components/editor/database/github-availability.ts +147 -0
- package/corpus/templates/content/app/components/editor/database/github-source-text.ts +280 -0
- package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
- package/corpus/templates/content/app/hooks/content-action-refresh.ts +6 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +3 -1
- package/corpus/templates/content/app/hooks/use-github-folder.ts +199 -0
- package/corpus/templates/content/app/i18n-data.ts +154 -1
- package/corpus/templates/content/app/lib/github-link-dialog.ts +86 -0
- package/corpus/templates/content/changelog/2026-10-18-connected-ai-assistants-can-link-a-new-collection-to-a-github.md +1 -1
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-organization-favorites-include-documents-shared-with-the-cur.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +32 -7
- package/corpus/templates/content/docs/product/encyclopedia.md +3 -3
- package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +2 -2
- package/corpus/templates/content/docs/product/roadmap.md +1 -1
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +14 -0
- package/corpus/templates/content/server/lib/github-source-client.ts +50 -1
- package/corpus/templates/content/shared/api.ts +3 -0
- package/corpus/templates/content/shared/github-source.ts +68 -0
- package/corpus/templates/design/CHANGELOG.md +1 -0
- package/corpus/templates/design/actions/connect-assets-mcp.ts +5 -9
- package/corpus/templates/design/changelog/2026-10-21-journey-canvases-accept-partial-analytics-samples-when-repla.md +6 -0
- package/corpus/templates/design/shared/journey-canvas.ts +6 -4
- package/corpus/templates/dispatch/.agents/skills/a2a-protocol/SKILL.md +16 -13
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-customer-ai-credit-questions-route-to-account-health-instead.md +6 -0
- package/corpus/templates/factory/server/lib/require-workspace-member.ts +5 -17
- package/corpus/templates/mail/server/lib/queued-drafts.ts +9 -4
- package/corpus/templates/slides/server/workspace-defaults.ts +12 -15
- package/package.json +1 -1
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { startWorkspaceProviderOAuth } from "@agent-native/core/client/integrations";
|
|
3
|
+
import type { ContentDatabaseSource } from "@shared/api";
|
|
4
|
+
import {
|
|
5
|
+
githubFolderName,
|
|
6
|
+
type GitHubFolderPreview,
|
|
7
|
+
type GitHubLinkBlockedReason,
|
|
8
|
+
} from "@shared/github-source";
|
|
9
|
+
import {
|
|
10
|
+
IconBrandGithub,
|
|
11
|
+
IconExternalLink,
|
|
12
|
+
IconFileText,
|
|
13
|
+
IconFolder,
|
|
14
|
+
IconGitBranch,
|
|
15
|
+
} from "@tabler/icons-react";
|
|
16
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
17
|
+
import {
|
|
18
|
+
useEffect,
|
|
19
|
+
useRef,
|
|
20
|
+
useState,
|
|
21
|
+
type ClipboardEvent,
|
|
22
|
+
type FormEvent,
|
|
23
|
+
type ReactNode,
|
|
24
|
+
} from "react";
|
|
25
|
+
import { useLocation, useNavigate } from "react-router";
|
|
26
|
+
import { toast } from "sonner";
|
|
27
|
+
|
|
28
|
+
import { TYPE_ICONS } from "@/components/editor/DocumentProperties";
|
|
29
|
+
import { Button } from "@/components/ui/button";
|
|
30
|
+
import {
|
|
31
|
+
Dialog,
|
|
32
|
+
DialogContent,
|
|
33
|
+
DialogDescription,
|
|
34
|
+
DialogFooter,
|
|
35
|
+
DialogHeader,
|
|
36
|
+
DialogTitle,
|
|
37
|
+
} from "@/components/ui/dialog";
|
|
38
|
+
import { Input } from "@/components/ui/input";
|
|
39
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
40
|
+
import {
|
|
41
|
+
startPageOpenDocumentReads,
|
|
42
|
+
useUpdateDocument,
|
|
43
|
+
} from "@/hooks/use-documents";
|
|
44
|
+
import {
|
|
45
|
+
useCreateGitHubCollection,
|
|
46
|
+
useGitHubConnection,
|
|
47
|
+
useLinkGitHubFolder,
|
|
48
|
+
useRefreshAfterGitHubFolder,
|
|
49
|
+
type GitHubFolderSyncSummary,
|
|
50
|
+
} from "@/hooks/use-github-folder";
|
|
51
|
+
import { documentQueryFilter } from "@/lib/document-query";
|
|
52
|
+
import {
|
|
53
|
+
GITHUB_LINK_DIALOG_OPEN_EVENT,
|
|
54
|
+
githubLinkRequestFromSearch,
|
|
55
|
+
githubLinkReturnPath,
|
|
56
|
+
searchWithoutGitHubLink,
|
|
57
|
+
type GitHubLinkDialogRequest,
|
|
58
|
+
type GitHubLinkTarget,
|
|
59
|
+
} from "@/lib/github-link-dialog";
|
|
60
|
+
|
|
61
|
+
import {
|
|
62
|
+
githubBlockedFromErrorCode,
|
|
63
|
+
githubBlockedText,
|
|
64
|
+
githubErrorNeedsConnection,
|
|
65
|
+
githubExampleText,
|
|
66
|
+
githubLinkErrorText,
|
|
67
|
+
githubList,
|
|
68
|
+
githubProblemText,
|
|
69
|
+
githubSkipReasonText,
|
|
70
|
+
githubUrlProblem,
|
|
71
|
+
shortCommit,
|
|
72
|
+
} from "./github-source-text";
|
|
73
|
+
import { dbText } from "./text";
|
|
74
|
+
|
|
75
|
+
/** Pages, columns or files a details list shows before "and N more". */
|
|
76
|
+
const DETAILS_LIST_LIMIT = 8;
|
|
77
|
+
/** Page titles the summary lists before "N more". */
|
|
78
|
+
const SUMMARY_TITLES = 3;
|
|
79
|
+
/** Columns the summary shows before "N more". */
|
|
80
|
+
const SUMMARY_COLUMNS = 8;
|
|
81
|
+
|
|
82
|
+
interface CheckedFolder {
|
|
83
|
+
url: string;
|
|
84
|
+
preview: GitHubFolderPreview;
|
|
85
|
+
blockedReason: GitHubLinkBlockedReason | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface LinkProblem {
|
|
89
|
+
text: string;
|
|
90
|
+
/** The folder a file link points into, to use instead. */
|
|
91
|
+
folder?: { url: string; name: string } | null;
|
|
92
|
+
/** The collection can't take a folder, so offer a new one. */
|
|
93
|
+
offerNewCollection?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Whether the folder link would fill in fewer pages than it has files. */
|
|
97
|
+
function pagesAreEstimate(preview: GitHubFolderPreview): boolean {
|
|
98
|
+
// Only the first files are read before linking; the others can still turn
|
|
99
|
+
// out unreadable, so the count is a ceiling until then.
|
|
100
|
+
return preview.sampledFiles < preview.markdownFiles;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pagesHeading(preview: GitHubFolderPreview): string {
|
|
104
|
+
const count = preview.markdownFiles;
|
|
105
|
+
if (pagesAreEstimate(preview)) {
|
|
106
|
+
return dbText("githubPreviewPagesUpTo", { count });
|
|
107
|
+
}
|
|
108
|
+
return count === 1
|
|
109
|
+
? dbText("githubPreviewPagesOne")
|
|
110
|
+
: dbText("githubPreviewPages", { count });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function linkButtonText(preview: GitHubFolderPreview): string {
|
|
114
|
+
const count = preview.markdownFiles;
|
|
115
|
+
if (pagesAreEstimate(preview)) {
|
|
116
|
+
return dbText("githubLinkSubmitUpTo", { count });
|
|
117
|
+
}
|
|
118
|
+
return count === 1
|
|
119
|
+
? dbText("githubLinkSubmitOne")
|
|
120
|
+
: dbText("githubLinkSubmit", { count });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Says how the link went, once the dialog closes. */
|
|
124
|
+
function announceLink(
|
|
125
|
+
preview: GitHubFolderPreview,
|
|
126
|
+
sync: GitHubFolderSyncSummary,
|
|
127
|
+
source: ContentDatabaseSource | null,
|
|
128
|
+
) {
|
|
129
|
+
const folder = githubFolderName(preview.location);
|
|
130
|
+
// The link is saved even when its first update fails; say both, and the
|
|
131
|
+
// status line keeps showing the problem after this closes.
|
|
132
|
+
if (sync.state === "error") {
|
|
133
|
+
toast.error(dbText("githubLinkedSyncFailed", { folder }), {
|
|
134
|
+
description: githubProblemText(
|
|
135
|
+
sync.problem?.code ?? "unavailable",
|
|
136
|
+
preview.location,
|
|
137
|
+
),
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (sync.state === "partial") {
|
|
142
|
+
const progress = source?.metadata.github?.progress;
|
|
143
|
+
toast.success(
|
|
144
|
+
dbText("githubLinkedPartialToast", {
|
|
145
|
+
done: progress?.done ?? sync.created,
|
|
146
|
+
total: progress?.total ?? sync.created + sync.remaining,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const count = sync.created || preview.markdownFiles;
|
|
152
|
+
toast.success(
|
|
153
|
+
count === 1
|
|
154
|
+
? dbText("githubLinkedToastOne", { folder })
|
|
155
|
+
: dbText("githubLinkedToast", { count, folder }),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function DetailsSection({
|
|
160
|
+
title,
|
|
161
|
+
children,
|
|
162
|
+
}: {
|
|
163
|
+
title: string;
|
|
164
|
+
children: ReactNode;
|
|
165
|
+
}) {
|
|
166
|
+
return (
|
|
167
|
+
<section className="grid min-w-0 gap-1">
|
|
168
|
+
<h3 className="text-xs font-medium text-foreground">{title}</h3>
|
|
169
|
+
{children}
|
|
170
|
+
</section>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Everything the summary leaves out, for the person who wants to look. */
|
|
175
|
+
function GitHubFolderDetails({ preview }: { preview: GitHubFolderPreview }) {
|
|
176
|
+
const t = useT();
|
|
177
|
+
const morePages = preview.markdownFiles - DETAILS_LIST_LIMIT;
|
|
178
|
+
const moreSkipped = preview.skipped.length - DETAILS_LIST_LIMIT;
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
id="github-folder-details"
|
|
182
|
+
className="grid min-w-0 gap-3 border-t border-border pt-3 text-xs"
|
|
183
|
+
data-github-folder-details
|
|
184
|
+
>
|
|
185
|
+
{preview.sample.length > 0 ? (
|
|
186
|
+
<DetailsSection title={dbText("githubDetailsPages")}>
|
|
187
|
+
<ul className="grid min-w-0 gap-0.5">
|
|
188
|
+
{preview.sample.slice(0, DETAILS_LIST_LIMIT).map((page) => (
|
|
189
|
+
<li key={page.path} className="flex min-w-0 items-baseline gap-2">
|
|
190
|
+
<span className="min-w-0 truncate">{page.title}</span>
|
|
191
|
+
<span className="min-w-0 truncate text-muted-foreground">
|
|
192
|
+
{page.path}
|
|
193
|
+
</span>
|
|
194
|
+
</li>
|
|
195
|
+
))}
|
|
196
|
+
{morePages > 0 ? (
|
|
197
|
+
<li className="text-muted-foreground">
|
|
198
|
+
{dbText("githubLinkMore", { count: morePages })}
|
|
199
|
+
</li>
|
|
200
|
+
) : null}
|
|
201
|
+
</ul>
|
|
202
|
+
</DetailsSection>
|
|
203
|
+
) : null}
|
|
204
|
+
{preview.fields.length > 0 ? (
|
|
205
|
+
<DetailsSection title={dbText("githubDetailsColumns")}>
|
|
206
|
+
<ul className="grid min-w-0 gap-0.5">
|
|
207
|
+
{preview.fields.map((field) => (
|
|
208
|
+
<li key={field.key} className="flex min-w-0 items-baseline gap-2">
|
|
209
|
+
<span className="shrink-0">{field.key}</span>
|
|
210
|
+
<span className="shrink-0 text-muted-foreground">
|
|
211
|
+
{t(`editor.propertyTypes.${field.type}`)}
|
|
212
|
+
</span>
|
|
213
|
+
{field.examples.length > 0 ? (
|
|
214
|
+
<span className="min-w-0 truncate text-muted-foreground/80">
|
|
215
|
+
{githubList(
|
|
216
|
+
[...new Set(field.examples)]
|
|
217
|
+
.slice(0, 3)
|
|
218
|
+
.map((example) =>
|
|
219
|
+
githubExampleText(field.type, example),
|
|
220
|
+
),
|
|
221
|
+
)}
|
|
222
|
+
</span>
|
|
223
|
+
) : null}
|
|
224
|
+
</li>
|
|
225
|
+
))}
|
|
226
|
+
</ul>
|
|
227
|
+
{pagesAreEstimate(preview) ? (
|
|
228
|
+
<p className="text-muted-foreground">
|
|
229
|
+
{dbText("githubDetailsColumnsSampled", {
|
|
230
|
+
count: preview.sampledFiles,
|
|
231
|
+
total: preview.markdownFiles,
|
|
232
|
+
})}
|
|
233
|
+
</p>
|
|
234
|
+
) : null}
|
|
235
|
+
</DetailsSection>
|
|
236
|
+
) : null}
|
|
237
|
+
{preview.skipped.length > 0 ? (
|
|
238
|
+
<DetailsSection title={dbText("githubDetailsNotShown")}>
|
|
239
|
+
<ul className="grid min-w-0 gap-0.5">
|
|
240
|
+
{preview.skipped.slice(0, DETAILS_LIST_LIMIT).map((file) => (
|
|
241
|
+
<li key={file.path} className="flex min-w-0 items-baseline gap-2">
|
|
242
|
+
<span className="min-w-0 truncate">{file.path}</span>
|
|
243
|
+
<span className="shrink-0 text-muted-foreground">
|
|
244
|
+
{githubSkipReasonText(file.reason)}
|
|
245
|
+
</span>
|
|
246
|
+
</li>
|
|
247
|
+
))}
|
|
248
|
+
{moreSkipped > 0 ? (
|
|
249
|
+
<li className="text-muted-foreground">
|
|
250
|
+
{dbText("githubLinkMore", { count: moreSkipped })}
|
|
251
|
+
</li>
|
|
252
|
+
) : null}
|
|
253
|
+
</ul>
|
|
254
|
+
</DetailsSection>
|
|
255
|
+
) : null}
|
|
256
|
+
<DetailsSection title={dbText("githubDetailsFrom")}>
|
|
257
|
+
<p className="text-muted-foreground">
|
|
258
|
+
{dbText("githubDetailsBranchVersion", {
|
|
259
|
+
branch: preview.location.branch,
|
|
260
|
+
commit: shortCommit(preview.commitSha),
|
|
261
|
+
})}
|
|
262
|
+
</p>
|
|
263
|
+
</DetailsSection>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A heading and its list in the folder preview. */
|
|
269
|
+
function SummarySection({
|
|
270
|
+
title,
|
|
271
|
+
children,
|
|
272
|
+
}: {
|
|
273
|
+
title: string;
|
|
274
|
+
children: ReactNode;
|
|
275
|
+
}) {
|
|
276
|
+
return (
|
|
277
|
+
<section className="grid min-w-0 gap-1.5">
|
|
278
|
+
<h3 className="text-xs font-medium text-muted-foreground">{title}</h3>
|
|
279
|
+
{children}
|
|
280
|
+
</section>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* What linking the folder would do: which folder, a few of its pages, the
|
|
286
|
+
* columns they'll get, and what won't show. The rest waits behind "Show
|
|
287
|
+
* details".
|
|
288
|
+
*/
|
|
289
|
+
export function GitHubFolderSummary({
|
|
290
|
+
preview,
|
|
291
|
+
blockedReason,
|
|
292
|
+
onNewCollection,
|
|
293
|
+
}: {
|
|
294
|
+
preview: GitHubFolderPreview;
|
|
295
|
+
blockedReason: GitHubLinkBlockedReason | null;
|
|
296
|
+
onNewCollection?: () => void;
|
|
297
|
+
}) {
|
|
298
|
+
const t = useT();
|
|
299
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
300
|
+
const { location } = preview;
|
|
301
|
+
const hasPages = preview.markdownFiles > 0;
|
|
302
|
+
const titles = preview.sample.slice(0, SUMMARY_TITLES);
|
|
303
|
+
const morePages = preview.markdownFiles - titles.length;
|
|
304
|
+
const columns = preview.fields.slice(0, SUMMARY_COLUMNS);
|
|
305
|
+
const moreColumns = preview.fields.length - columns.length;
|
|
306
|
+
const skipped = preview.skipped.length;
|
|
307
|
+
return (
|
|
308
|
+
<div
|
|
309
|
+
className="grid min-w-0 overflow-hidden rounded-lg border border-border text-sm"
|
|
310
|
+
data-github-folder-preview
|
|
311
|
+
>
|
|
312
|
+
<a
|
|
313
|
+
href={preview.folderUrl}
|
|
314
|
+
target="_blank"
|
|
315
|
+
rel="noreferrer"
|
|
316
|
+
className="group flex min-w-0 items-center gap-2.5 border-b border-border bg-muted/30 px-3 py-2.5 transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
317
|
+
>
|
|
318
|
+
<IconFolder
|
|
319
|
+
className="size-5 shrink-0 text-muted-foreground"
|
|
320
|
+
aria-hidden
|
|
321
|
+
/>
|
|
322
|
+
<span className="grid min-w-0 flex-1">
|
|
323
|
+
<span className="truncate font-medium">
|
|
324
|
+
{githubFolderName(location)}
|
|
325
|
+
</span>
|
|
326
|
+
<span className="flex min-w-0 items-center gap-1 text-xs text-muted-foreground">
|
|
327
|
+
<span className="truncate">
|
|
328
|
+
{location.owner}/{location.repo}
|
|
329
|
+
</span>
|
|
330
|
+
<IconGitBranch className="ms-1.5 size-3 shrink-0" aria-hidden />
|
|
331
|
+
<span className="truncate">{location.branch}</span>
|
|
332
|
+
</span>
|
|
333
|
+
</span>
|
|
334
|
+
<IconExternalLink
|
|
335
|
+
className="size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
|
|
336
|
+
aria-hidden
|
|
337
|
+
/>
|
|
338
|
+
</a>
|
|
339
|
+
<div className="grid min-w-0 gap-3 p-3">
|
|
340
|
+
{hasPages ? (
|
|
341
|
+
<SummarySection title={pagesHeading(preview)}>
|
|
342
|
+
<ul className="grid min-w-0 gap-1">
|
|
343
|
+
{titles.map((page) => (
|
|
344
|
+
<li key={page.path} className="flex min-w-0 items-center gap-2">
|
|
345
|
+
<IconFileText
|
|
346
|
+
className="size-3.5 shrink-0 text-muted-foreground"
|
|
347
|
+
aria-hidden
|
|
348
|
+
/>
|
|
349
|
+
<span className="min-w-0 truncate">{page.title}</span>
|
|
350
|
+
</li>
|
|
351
|
+
))}
|
|
352
|
+
{morePages > 0 ? (
|
|
353
|
+
<li className="flex min-w-0 items-center gap-2 text-xs text-muted-foreground">
|
|
354
|
+
<span className="size-3.5 shrink-0" aria-hidden />
|
|
355
|
+
{dbText("githubLinkMore", { count: morePages })}
|
|
356
|
+
</li>
|
|
357
|
+
) : null}
|
|
358
|
+
</ul>
|
|
359
|
+
</SummarySection>
|
|
360
|
+
) : (
|
|
361
|
+
<p className="break-words text-muted-foreground">
|
|
362
|
+
{dbText("githubLinkNoFiles")}
|
|
363
|
+
</p>
|
|
364
|
+
)}
|
|
365
|
+
{hasPages && preview.fields.length > 0 ? (
|
|
366
|
+
<SummarySection
|
|
367
|
+
title={
|
|
368
|
+
preview.fields.length === 1
|
|
369
|
+
? dbText("githubPreviewColumnsOne")
|
|
370
|
+
: dbText("githubPreviewColumns", {
|
|
371
|
+
count: preview.fields.length,
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
>
|
|
375
|
+
<ul className="flex min-w-0 flex-wrap gap-1">
|
|
376
|
+
{columns.map((field) => {
|
|
377
|
+
const TypeIcon = TYPE_ICONS[field.type];
|
|
378
|
+
return (
|
|
379
|
+
<li
|
|
380
|
+
key={field.key}
|
|
381
|
+
className="inline-flex min-w-0 max-w-full items-center gap-1 rounded-md bg-muted px-1.5 py-0.5 text-xs"
|
|
382
|
+
title={t(`editor.propertyTypes.${field.type}`)}
|
|
383
|
+
>
|
|
384
|
+
<TypeIcon
|
|
385
|
+
className="size-3 shrink-0 text-muted-foreground"
|
|
386
|
+
aria-hidden
|
|
387
|
+
/>
|
|
388
|
+
<span className="min-w-0 truncate">{field.key}</span>
|
|
389
|
+
</li>
|
|
390
|
+
);
|
|
391
|
+
})}
|
|
392
|
+
{moreColumns > 0 ? (
|
|
393
|
+
<li className="inline-flex items-center px-1 py-0.5 text-xs text-muted-foreground">
|
|
394
|
+
{dbText("githubLinkMore", { count: moreColumns })}
|
|
395
|
+
</li>
|
|
396
|
+
) : null}
|
|
397
|
+
</ul>
|
|
398
|
+
</SummarySection>
|
|
399
|
+
) : hasPages ? (
|
|
400
|
+
<p className="break-words text-muted-foreground">
|
|
401
|
+
{dbText("githubLinkNoColumns")}
|
|
402
|
+
</p>
|
|
403
|
+
) : null}
|
|
404
|
+
{preview.images > 0 && !preview.storageReady ? (
|
|
405
|
+
<p className="break-words text-xs text-muted-foreground">
|
|
406
|
+
{dbText("githubLinkPicturesWaiting")}
|
|
407
|
+
</p>
|
|
408
|
+
) : null}
|
|
409
|
+
<div className="flex min-w-0 flex-wrap items-baseline justify-between gap-x-3 gap-y-1 text-xs">
|
|
410
|
+
<span className="break-words text-muted-foreground">
|
|
411
|
+
{skipped === 0
|
|
412
|
+
? null
|
|
413
|
+
: skipped === 1
|
|
414
|
+
? dbText("githubLinkNotShownOne")
|
|
415
|
+
: dbText("githubLinkNotShown", { count: skipped })}
|
|
416
|
+
</span>
|
|
417
|
+
<button
|
|
418
|
+
type="button"
|
|
419
|
+
className="ms-auto rounded-sm text-muted-foreground underline underline-offset-2 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
420
|
+
aria-expanded={showDetails}
|
|
421
|
+
aria-controls="github-folder-details"
|
|
422
|
+
onClick={() => setShowDetails((open) => !open)}
|
|
423
|
+
>
|
|
424
|
+
{showDetails
|
|
425
|
+
? dbText("githubHideDetails")
|
|
426
|
+
: dbText("githubShowDetails")}
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
{blockedReason ? (
|
|
430
|
+
<div
|
|
431
|
+
className="grid min-w-0 gap-2 rounded-md border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive"
|
|
432
|
+
role="alert"
|
|
433
|
+
>
|
|
434
|
+
<span className="break-words">
|
|
435
|
+
{githubBlockedText(blockedReason)}
|
|
436
|
+
</span>
|
|
437
|
+
{onNewCollection ? (
|
|
438
|
+
<Button
|
|
439
|
+
type="button"
|
|
440
|
+
size="xs"
|
|
441
|
+
variant="outline"
|
|
442
|
+
className="justify-self-start"
|
|
443
|
+
onClick={onNewCollection}
|
|
444
|
+
>
|
|
445
|
+
{dbText("githubNewCollectionInstead")}
|
|
446
|
+
</Button>
|
|
447
|
+
) : null}
|
|
448
|
+
</div>
|
|
449
|
+
) : null}
|
|
450
|
+
{showDetails ? <GitHubFolderDetails preview={preview} /> : null}
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Links a collection to a GitHub folder, or makes a new collection from one:
|
|
458
|
+
* connect GitHub if needed, paste the folder's link, check what it becomes,
|
|
459
|
+
* then link it.
|
|
460
|
+
*/
|
|
461
|
+
export function GitHubLinkDialog({
|
|
462
|
+
request,
|
|
463
|
+
open,
|
|
464
|
+
onOpenChange,
|
|
465
|
+
}: {
|
|
466
|
+
request: GitHubLinkDialogRequest;
|
|
467
|
+
open: boolean;
|
|
468
|
+
onOpenChange: (open: boolean) => void;
|
|
469
|
+
}) {
|
|
470
|
+
const t = useT();
|
|
471
|
+
const navigate = useNavigate();
|
|
472
|
+
const queryClient = useQueryClient();
|
|
473
|
+
const updateDocument = useUpdateDocument();
|
|
474
|
+
const refreshAfterGitHubFolder = useRefreshAfterGitHubFolder();
|
|
475
|
+
const linkFolder = useLinkGitHubFolder();
|
|
476
|
+
const createCollection = useCreateGitHubCollection();
|
|
477
|
+
const connection = useGitHubConnection(open);
|
|
478
|
+
const [target, setTarget] = useState<GitHubLinkTarget>(request.target);
|
|
479
|
+
const [url, setUrl] = useState(request.url ?? "");
|
|
480
|
+
const [checked, setChecked] = useState<CheckedFolder | null>(null);
|
|
481
|
+
const [problem, setProblem] = useState<LinkProblem | null>(null);
|
|
482
|
+
const [checking, setChecking] = useState(false);
|
|
483
|
+
const [linking, setLinking] = useState(false);
|
|
484
|
+
const [needsConnection, setNeedsConnection] = useState(false);
|
|
485
|
+
const [triedOnce, setTriedOnce] = useState(false);
|
|
486
|
+
// Counts checks and edits, so a check that answers after the link changed
|
|
487
|
+
// is dropped rather than shown under the new link.
|
|
488
|
+
const checkRun = useRef(0);
|
|
489
|
+
const startedFromUrl = useRef(false);
|
|
490
|
+
|
|
491
|
+
const isNew = target.kind === "new";
|
|
492
|
+
const newCollectionPlace =
|
|
493
|
+
target.kind === "new"
|
|
494
|
+
? target
|
|
495
|
+
: target.spaceId
|
|
496
|
+
? { spaceId: target.spaceId, parentId: target.parentId ?? null }
|
|
497
|
+
: null;
|
|
498
|
+
const connected = connection.data?.connected;
|
|
499
|
+
const showConnect =
|
|
500
|
+
needsConnection || (connected === false && !checked && !linking);
|
|
501
|
+
const waitingForConnection =
|
|
502
|
+
!needsConnection && connection.isPending && !checked;
|
|
503
|
+
const ready =
|
|
504
|
+
!!checked &&
|
|
505
|
+
!checked.blockedReason &&
|
|
506
|
+
checked.preview.markdownFiles > 0 &&
|
|
507
|
+
checked.url === url.trim();
|
|
508
|
+
|
|
509
|
+
function clearCheck() {
|
|
510
|
+
checkRun.current += 1;
|
|
511
|
+
setChecked(null);
|
|
512
|
+
setProblem(null);
|
|
513
|
+
setChecking(false);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function handleError(error: unknown, typed: string) {
|
|
517
|
+
if (githubErrorNeedsConnection(error)) {
|
|
518
|
+
setNeedsConnection(true);
|
|
519
|
+
setProblem(null);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const blocked = githubBlockedFromErrorCode(
|
|
523
|
+
(error as { errorCode?: unknown } | null)?.errorCode,
|
|
524
|
+
);
|
|
525
|
+
setProblem({
|
|
526
|
+
text: githubLinkErrorText(error, typed),
|
|
527
|
+
offerNewCollection: !!blocked && !!newCollectionPlace,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function check(nextUrl = url, into: GitHubLinkTarget = target) {
|
|
532
|
+
const typed = nextUrl.trim();
|
|
533
|
+
clearCheck();
|
|
534
|
+
if (!typed) return;
|
|
535
|
+
setTriedOnce(true);
|
|
536
|
+
const urlProblem = githubUrlProblem(typed);
|
|
537
|
+
if (urlProblem) {
|
|
538
|
+
setProblem({
|
|
539
|
+
text: urlProblem.text,
|
|
540
|
+
folder:
|
|
541
|
+
urlProblem.folderUrl && urlProblem.folder
|
|
542
|
+
? { url: urlProblem.folderUrl, name: urlProblem.folder }
|
|
543
|
+
: null,
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const run = checkRun.current;
|
|
548
|
+
setChecking(true);
|
|
549
|
+
try {
|
|
550
|
+
const response =
|
|
551
|
+
into.kind === "collection"
|
|
552
|
+
? await linkFolder.mutateAsync({
|
|
553
|
+
databaseId: into.databaseId,
|
|
554
|
+
url: typed,
|
|
555
|
+
dryRun: true,
|
|
556
|
+
})
|
|
557
|
+
: await createCollection.mutateAsync({
|
|
558
|
+
url: typed,
|
|
559
|
+
spaceId: into.spaceId,
|
|
560
|
+
parentId: into.parentId,
|
|
561
|
+
dryRun: true,
|
|
562
|
+
});
|
|
563
|
+
if (run !== checkRun.current || !response.dryRun) return;
|
|
564
|
+
setChecked({
|
|
565
|
+
url: typed,
|
|
566
|
+
preview: response.preview,
|
|
567
|
+
blockedReason:
|
|
568
|
+
"blockedReason" in response ? response.blockedReason : null,
|
|
569
|
+
});
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (run === checkRun.current) handleError(error, typed);
|
|
572
|
+
} finally {
|
|
573
|
+
if (run === checkRun.current) setChecking(false);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function link() {
|
|
578
|
+
if (!checked || !ready) return;
|
|
579
|
+
const { preview } = checked;
|
|
580
|
+
setLinking(true);
|
|
581
|
+
setProblem(null);
|
|
582
|
+
try {
|
|
583
|
+
if (target.kind === "collection") {
|
|
584
|
+
const response = await linkFolder.mutateAsync({
|
|
585
|
+
databaseId: target.databaseId,
|
|
586
|
+
url: checked.url,
|
|
587
|
+
});
|
|
588
|
+
if (response.dryRun) return;
|
|
589
|
+
announceLink(preview, response.sync, response.source);
|
|
590
|
+
nameUntitledCollection(
|
|
591
|
+
target.documentId,
|
|
592
|
+
githubFolderName(preview.location),
|
|
593
|
+
);
|
|
594
|
+
onOpenChange(false);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const response = await createCollection.mutateAsync({
|
|
598
|
+
url: checked.url,
|
|
599
|
+
spaceId: target.spaceId,
|
|
600
|
+
parentId: target.parentId,
|
|
601
|
+
});
|
|
602
|
+
if (response.dryRun) return;
|
|
603
|
+
announceLink(preview, response.sync, response.source);
|
|
604
|
+
onOpenChange(false);
|
|
605
|
+
const documentId = response.database.documentId;
|
|
606
|
+
startPageOpenDocumentReads(queryClient, documentId);
|
|
607
|
+
void navigate(`/page/${documentId}`);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
handleError(error, checked.url);
|
|
610
|
+
} finally {
|
|
611
|
+
setLinking(false);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Gives a collection still called "Untitled collection" the folder's name,
|
|
617
|
+
* like one made from the folder gets. A name someone typed stays.
|
|
618
|
+
*/
|
|
619
|
+
function nameUntitledCollection(documentId: string, folder: string) {
|
|
620
|
+
const cached = queryClient
|
|
621
|
+
.getQueriesData<{ title?: string }>(documentQueryFilter(documentId))
|
|
622
|
+
.map(([, document]) => document?.title)
|
|
623
|
+
.find((title) => title !== undefined);
|
|
624
|
+
const title = cached?.trim();
|
|
625
|
+
if (title === undefined) return;
|
|
626
|
+
if (title !== "" && title !== t("editor.untitledDatabase")) return;
|
|
627
|
+
// The link already worked; a name that didn't save is only cosmetic.
|
|
628
|
+
// Renaming cancels reads in flight, the link's refresh among them, so the
|
|
629
|
+
// rows refresh again once it settles.
|
|
630
|
+
updateDocument
|
|
631
|
+
.mutateAsync({ id: documentId, title: folder })
|
|
632
|
+
.catch(() => undefined)
|
|
633
|
+
.finally(() => refreshAfterGitHubFolder("link-github-folder"));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function connectGitHub() {
|
|
637
|
+
startWorkspaceProviderOAuth("github", {
|
|
638
|
+
appId: "content",
|
|
639
|
+
returnPath: githubLinkReturnPath(window.location, target, url),
|
|
640
|
+
scope: "user",
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Keeps the link and checks it again, for a new collection beside this one. */
|
|
645
|
+
function switchToNewCollection() {
|
|
646
|
+
if (!newCollectionPlace) return;
|
|
647
|
+
const next: GitHubLinkTarget = { kind: "new", ...newCollectionPlace };
|
|
648
|
+
setTarget(next);
|
|
649
|
+
void check(url, next);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// A link carried back from GitHub's sign-in is checked once the
|
|
653
|
+
// connection allows it.
|
|
654
|
+
const canCheckStartUrl =
|
|
655
|
+
!!request.url?.trim() && !waitingForConnection && !showConnect;
|
|
656
|
+
useEffect(() => {
|
|
657
|
+
if (startedFromUrl.current || !canCheckStartUrl) return;
|
|
658
|
+
startedFromUrl.current = true;
|
|
659
|
+
void check(request.url);
|
|
660
|
+
// Runs once, when the dialog can first check; `check` is the latest.
|
|
661
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
662
|
+
}, [canCheckStartUrl]);
|
|
663
|
+
|
|
664
|
+
function submit(event: FormEvent) {
|
|
665
|
+
event.preventDefault();
|
|
666
|
+
if (ready) void link();
|
|
667
|
+
else if (!checking) void check();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function pasteLink(event: ClipboardEvent<HTMLInputElement>) {
|
|
671
|
+
const input = event.currentTarget;
|
|
672
|
+
const replacesAll =
|
|
673
|
+
input.value === "" ||
|
|
674
|
+
(input.selectionStart === 0 && input.selectionEnd === input.value.length);
|
|
675
|
+
const pasted = event.clipboardData.getData("text").trim();
|
|
676
|
+
if (!replacesAll || !pasted) return;
|
|
677
|
+
event.preventDefault();
|
|
678
|
+
setUrl(pasted);
|
|
679
|
+
void check(pasted);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const primaryText = linking
|
|
683
|
+
? dbText("githubLinking")
|
|
684
|
+
: ready && checked
|
|
685
|
+
? linkButtonText(checked.preview)
|
|
686
|
+
: dbText("githubLinkCheck");
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
<Dialog
|
|
690
|
+
open={open}
|
|
691
|
+
onOpenChange={(next) => {
|
|
692
|
+
if (linking && !next) return;
|
|
693
|
+
onOpenChange(next);
|
|
694
|
+
}}
|
|
695
|
+
>
|
|
696
|
+
<DialogContent
|
|
697
|
+
hideClose={linking}
|
|
698
|
+
onCloseAutoFocus={(event) => {
|
|
699
|
+
const returnTo = request.returnFocusTo;
|
|
700
|
+
if (returnTo?.isConnected) {
|
|
701
|
+
event.preventDefault();
|
|
702
|
+
returnTo.focus();
|
|
703
|
+
}
|
|
704
|
+
}}
|
|
705
|
+
>
|
|
706
|
+
<DialogHeader>
|
|
707
|
+
<DialogTitle>
|
|
708
|
+
<span className="flex items-center gap-2">
|
|
709
|
+
<IconBrandGithub className="size-4" aria-hidden />
|
|
710
|
+
{isNew
|
|
711
|
+
? dbText("githubNewCollectionTitle")
|
|
712
|
+
: dbText("githubLinkTitle")}
|
|
713
|
+
</span>
|
|
714
|
+
</DialogTitle>
|
|
715
|
+
<DialogDescription>
|
|
716
|
+
{isNew
|
|
717
|
+
? dbText("githubNewCollectionDescription")
|
|
718
|
+
: dbText("githubLinkDescription")}
|
|
719
|
+
</DialogDescription>
|
|
720
|
+
</DialogHeader>
|
|
721
|
+
|
|
722
|
+
{waitingForConnection ? (
|
|
723
|
+
<div
|
|
724
|
+
className="flex items-center gap-2 text-sm text-muted-foreground"
|
|
725
|
+
role="status"
|
|
726
|
+
>
|
|
727
|
+
<Spinner className="size-3.5" />
|
|
728
|
+
{dbText("githubConnectChecking")}
|
|
729
|
+
</div>
|
|
730
|
+
) : showConnect ? (
|
|
731
|
+
<p className="break-words text-sm" data-github-connect-step>
|
|
732
|
+
{dbText("githubConnectPrompt")}
|
|
733
|
+
</p>
|
|
734
|
+
) : (
|
|
735
|
+
<form
|
|
736
|
+
id="github-link-form"
|
|
737
|
+
className="grid min-w-0 gap-1.5"
|
|
738
|
+
onSubmit={submit}
|
|
739
|
+
>
|
|
740
|
+
<label
|
|
741
|
+
htmlFor="github-folder-url"
|
|
742
|
+
className="text-xs font-medium text-muted-foreground"
|
|
743
|
+
>
|
|
744
|
+
{dbText("githubLinkUrlLabel")}
|
|
745
|
+
</label>
|
|
746
|
+
<Input
|
|
747
|
+
id="github-folder-url"
|
|
748
|
+
value={url}
|
|
749
|
+
placeholder="https://github.com/your-team/website/tree/main/docs"
|
|
750
|
+
autoComplete="off"
|
|
751
|
+
spellCheck={false}
|
|
752
|
+
autoFocus
|
|
753
|
+
disabled={linking}
|
|
754
|
+
aria-describedby={
|
|
755
|
+
triedOnce ? undefined : "github-folder-url-help"
|
|
756
|
+
}
|
|
757
|
+
aria-invalid={problem ? true : undefined}
|
|
758
|
+
onPaste={pasteLink}
|
|
759
|
+
onChange={(event) => {
|
|
760
|
+
setUrl(event.target.value);
|
|
761
|
+
clearCheck();
|
|
762
|
+
}}
|
|
763
|
+
/>
|
|
764
|
+
{triedOnce ? null : (
|
|
765
|
+
<p
|
|
766
|
+
id="github-folder-url-help"
|
|
767
|
+
className="text-xs text-muted-foreground"
|
|
768
|
+
>
|
|
769
|
+
{dbText("githubLinkUrlHelp")}
|
|
770
|
+
</p>
|
|
771
|
+
)}
|
|
772
|
+
</form>
|
|
773
|
+
)}
|
|
774
|
+
|
|
775
|
+
{checking ? (
|
|
776
|
+
<div
|
|
777
|
+
className="flex items-center gap-2 text-xs text-muted-foreground"
|
|
778
|
+
role="status"
|
|
779
|
+
>
|
|
780
|
+
<Spinner className="size-3.5" />
|
|
781
|
+
{dbText("githubLinkReading")}
|
|
782
|
+
</div>
|
|
783
|
+
) : null}
|
|
784
|
+
|
|
785
|
+
{problem ? (
|
|
786
|
+
<div className="grid min-w-0 gap-2" role="alert">
|
|
787
|
+
<p className="break-words text-xs text-destructive">
|
|
788
|
+
{problem.text}
|
|
789
|
+
</p>
|
|
790
|
+
{problem.folder ? (
|
|
791
|
+
<Button
|
|
792
|
+
type="button"
|
|
793
|
+
size="xs"
|
|
794
|
+
variant="outline"
|
|
795
|
+
className="justify-self-start"
|
|
796
|
+
onClick={() => {
|
|
797
|
+
const folderUrl = problem.folder!.url;
|
|
798
|
+
setUrl(folderUrl);
|
|
799
|
+
void check(folderUrl);
|
|
800
|
+
}}
|
|
801
|
+
>
|
|
802
|
+
{dbText("githubLinkUseFolder", { folder: problem.folder.name })}
|
|
803
|
+
</Button>
|
|
804
|
+
) : null}
|
|
805
|
+
{problem.offerNewCollection ? (
|
|
806
|
+
<Button
|
|
807
|
+
type="button"
|
|
808
|
+
size="xs"
|
|
809
|
+
variant="outline"
|
|
810
|
+
className="justify-self-start"
|
|
811
|
+
onClick={switchToNewCollection}
|
|
812
|
+
>
|
|
813
|
+
{dbText("githubNewCollectionInstead")}
|
|
814
|
+
</Button>
|
|
815
|
+
) : null}
|
|
816
|
+
</div>
|
|
817
|
+
) : null}
|
|
818
|
+
|
|
819
|
+
{checked && !checking && !showConnect ? (
|
|
820
|
+
<GitHubFolderSummary
|
|
821
|
+
preview={checked.preview}
|
|
822
|
+
blockedReason={checked.blockedReason}
|
|
823
|
+
onNewCollection={
|
|
824
|
+
newCollectionPlace ? switchToNewCollection : undefined
|
|
825
|
+
}
|
|
826
|
+
/>
|
|
827
|
+
) : null}
|
|
828
|
+
|
|
829
|
+
{linking && checked ? (
|
|
830
|
+
<div
|
|
831
|
+
className="flex items-center gap-2 text-xs text-muted-foreground"
|
|
832
|
+
role="status"
|
|
833
|
+
>
|
|
834
|
+
<Spinner className="size-3.5" />
|
|
835
|
+
{checked.preview.markdownFiles === 1
|
|
836
|
+
? dbText("githubLinkingProgressOne")
|
|
837
|
+
: dbText("githubLinkingProgress", {
|
|
838
|
+
count: checked.preview.markdownFiles,
|
|
839
|
+
})}
|
|
840
|
+
</div>
|
|
841
|
+
) : null}
|
|
842
|
+
|
|
843
|
+
<DialogFooter>
|
|
844
|
+
<Button
|
|
845
|
+
type="button"
|
|
846
|
+
variant="ghost"
|
|
847
|
+
disabled={linking}
|
|
848
|
+
onClick={() => onOpenChange(false)}
|
|
849
|
+
>
|
|
850
|
+
{dbText("cancel")}
|
|
851
|
+
</Button>
|
|
852
|
+
{waitingForConnection ? null : showConnect ? (
|
|
853
|
+
<Button type="button" onClick={connectGitHub}>
|
|
854
|
+
<IconBrandGithub className="mr-1 size-3.5" aria-hidden />
|
|
855
|
+
{dbText("githubConnect")}
|
|
856
|
+
</Button>
|
|
857
|
+
) : (
|
|
858
|
+
<Button
|
|
859
|
+
type="submit"
|
|
860
|
+
form="github-link-form"
|
|
861
|
+
disabled={
|
|
862
|
+
linking ||
|
|
863
|
+
checking ||
|
|
864
|
+
!url.trim() ||
|
|
865
|
+
(!!checked && !ready && checked.url === url.trim())
|
|
866
|
+
}
|
|
867
|
+
>
|
|
868
|
+
{linking ? <Spinner className="mr-1 size-3.5" /> : null}
|
|
869
|
+
{primaryText}
|
|
870
|
+
</Button>
|
|
871
|
+
)}
|
|
872
|
+
</DialogFooter>
|
|
873
|
+
</DialogContent>
|
|
874
|
+
</Dialog>
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* The one GitHub folder dialog for the app. Anything can open it with
|
|
880
|
+
* `openGitHubLinkDialog`, and it reopens after GitHub's sign-in from the
|
|
881
|
+
* page's address.
|
|
882
|
+
*/
|
|
883
|
+
export function GitHubLinkDialogHost() {
|
|
884
|
+
const location = useLocation();
|
|
885
|
+
const navigate = useNavigate();
|
|
886
|
+
const [state, setState] = useState<{
|
|
887
|
+
request: GitHubLinkDialogRequest;
|
|
888
|
+
open: boolean;
|
|
889
|
+
key: number;
|
|
890
|
+
} | null>(null);
|
|
891
|
+
|
|
892
|
+
useEffect(() => {
|
|
893
|
+
const onOpen = (event: Event) => {
|
|
894
|
+
const request = (event as CustomEvent<GitHubLinkDialogRequest>).detail;
|
|
895
|
+
if (!request?.target) return;
|
|
896
|
+
setState((current) => ({
|
|
897
|
+
request,
|
|
898
|
+
open: true,
|
|
899
|
+
key: (current?.key ?? 0) + 1,
|
|
900
|
+
}));
|
|
901
|
+
};
|
|
902
|
+
window.addEventListener(GITHUB_LINK_DIALOG_OPEN_EVENT, onOpen);
|
|
903
|
+
return () =>
|
|
904
|
+
window.removeEventListener(GITHUB_LINK_DIALOG_OPEN_EVENT, onOpen);
|
|
905
|
+
}, []);
|
|
906
|
+
|
|
907
|
+
// Back from GitHub's sign-in: reopen the dialog the address names, then
|
|
908
|
+
// drop it from the address so a reload doesn't open it again.
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
const request = githubLinkRequestFromSearch(location.search);
|
|
911
|
+
if (!request) return;
|
|
912
|
+
setState((current) => ({
|
|
913
|
+
request,
|
|
914
|
+
open: true,
|
|
915
|
+
key: (current?.key ?? 0) + 1,
|
|
916
|
+
}));
|
|
917
|
+
void navigate(
|
|
918
|
+
{
|
|
919
|
+
pathname: location.pathname,
|
|
920
|
+
search: searchWithoutGitHubLink(location.search),
|
|
921
|
+
hash: location.hash,
|
|
922
|
+
},
|
|
923
|
+
{ replace: true },
|
|
924
|
+
);
|
|
925
|
+
}, [location.hash, location.pathname, location.search, navigate]);
|
|
926
|
+
|
|
927
|
+
if (!state) return null;
|
|
928
|
+
return (
|
|
929
|
+
<GitHubLinkDialog
|
|
930
|
+
key={state.key}
|
|
931
|
+
request={state.request}
|
|
932
|
+
open={state.open}
|
|
933
|
+
onOpenChange={(open) =>
|
|
934
|
+
setState((current) => (current ? { ...current, open } : current))
|
|
935
|
+
}
|
|
936
|
+
/>
|
|
937
|
+
);
|
|
938
|
+
}
|