@agent-native/core-corpus 0.212.1 → 0.214.0
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/.agents/skills/document-editing/references/databases.md +15 -1
- package/corpus/templates/content/CHANGELOG.md +13 -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/_database-view-setup.ts +305 -11
- 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/_property-utils.ts +3 -0
- 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/actions/update-content-database-view.ts +18 -2
- package/corpus/templates/content/actions/view-screen.ts +12 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +263 -35
- 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/ListItemLayout.tsx +410 -0
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
- package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
- 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/editor/database/navigation-state.ts +3 -0
- package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
- package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
- package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
- package/corpus/templates/content/app/global.css +13 -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 +186 -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-19-list-rows-can-be-customized.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.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 +21 -0
- package/corpus/templates/content/shared/database-item-layout.ts +591 -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,147 @@
|
|
|
1
|
+
import type { ContentDatabaseSource } from "@shared/api";
|
|
2
|
+
import { GITHUB_SOURCE_TYPE, githubFolderName } from "@shared/github-source";
|
|
3
|
+
|
|
4
|
+
import { githubRelativeTime } from "./github-source-text";
|
|
5
|
+
import { dbText } from "./text";
|
|
6
|
+
|
|
7
|
+
/** The collection the Sources panel is about, as GitHub linking needs it. */
|
|
8
|
+
export interface GitHubCollectionPlace {
|
|
9
|
+
databaseId: string;
|
|
10
|
+
documentId: string;
|
|
11
|
+
/** Where a new collection beside this one goes. */
|
|
12
|
+
spaceId?: string;
|
|
13
|
+
parentId: string | null;
|
|
14
|
+
systemRole: string | null;
|
|
15
|
+
pageCount: number;
|
|
16
|
+
canEdit: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type GitHubBlockedReason =
|
|
20
|
+
| "files"
|
|
21
|
+
| "system"
|
|
22
|
+
| "editors-only"
|
|
23
|
+
| "other-source"
|
|
24
|
+
| "has-pages";
|
|
25
|
+
|
|
26
|
+
/** Whether this collection can take a GitHub folder, and if not, why. */
|
|
27
|
+
export type GitHubAvailability =
|
|
28
|
+
| { kind: "linked"; source: ContentDatabaseSource }
|
|
29
|
+
| { kind: "available" }
|
|
30
|
+
| { kind: "connect" }
|
|
31
|
+
| {
|
|
32
|
+
kind: "blocked";
|
|
33
|
+
reason: GitHubBlockedReason;
|
|
34
|
+
/** The other app the collection's pages come from. */
|
|
35
|
+
sourceName?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The app a source's pages come from, as people call it. */
|
|
39
|
+
export function sourceAppName(source: ContentDatabaseSource): string {
|
|
40
|
+
if (source.sourceType === "builder-cms") return "Builder";
|
|
41
|
+
if (source.sourceType === "notion-database") return "Notion";
|
|
42
|
+
return source.sourceName;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Checks the collection before the GitHub connection, so nobody is sent
|
|
47
|
+
* through GitHub's sign-in for a collection that could never link. The server
|
|
48
|
+
* checks again when linking, for changes made in between.
|
|
49
|
+
*/
|
|
50
|
+
export function githubAvailability(
|
|
51
|
+
place: GitHubCollectionPlace,
|
|
52
|
+
sources: ContentDatabaseSource[],
|
|
53
|
+
connected: boolean | null | undefined,
|
|
54
|
+
): GitHubAvailability {
|
|
55
|
+
const linked = sources.find(
|
|
56
|
+
(source) => source.sourceType === GITHUB_SOURCE_TYPE,
|
|
57
|
+
);
|
|
58
|
+
if (linked) return { kind: "linked", source: linked };
|
|
59
|
+
if (place.systemRole === "files") return { kind: "blocked", reason: "files" };
|
|
60
|
+
if (place.systemRole) return { kind: "blocked", reason: "system" };
|
|
61
|
+
if (!place.canEdit) return { kind: "blocked", reason: "editors-only" };
|
|
62
|
+
const other = sources[0];
|
|
63
|
+
if (other) {
|
|
64
|
+
return {
|
|
65
|
+
kind: "blocked",
|
|
66
|
+
reason: "other-source",
|
|
67
|
+
sourceName: sourceAppName(other),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (place.pageCount > 0) return { kind: "blocked", reason: "has-pages" };
|
|
71
|
+
// An unknown connection is left to the dialog, which asks again.
|
|
72
|
+
return connected === false ? { kind: "connect" } : { kind: "available" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether a blocked collection can point to a new one instead. */
|
|
76
|
+
export function githubOffersNewCollection(
|
|
77
|
+
availability: GitHubAvailability,
|
|
78
|
+
place: GitHubCollectionPlace,
|
|
79
|
+
): boolean {
|
|
80
|
+
return (
|
|
81
|
+
availability.kind === "blocked" &&
|
|
82
|
+
availability.reason !== "editors-only" &&
|
|
83
|
+
!!place.spaceId
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The line under "GitHub" in Sources, always visible. */
|
|
88
|
+
export function githubRowDescription(
|
|
89
|
+
availability: GitHubAvailability,
|
|
90
|
+
now = Date.now(),
|
|
91
|
+
): string {
|
|
92
|
+
switch (availability.kind) {
|
|
93
|
+
case "linked": {
|
|
94
|
+
const status = availability.source.metadata.github;
|
|
95
|
+
const folder = status
|
|
96
|
+
? githubFolderName(status)
|
|
97
|
+
: availability.source.sourceName;
|
|
98
|
+
return status?.syncedAt
|
|
99
|
+
? dbText("githubRowLinked", {
|
|
100
|
+
folder,
|
|
101
|
+
time: githubRelativeTime(status.syncedAt, now),
|
|
102
|
+
})
|
|
103
|
+
: dbText("githubRowLinkedNotUpdated", { folder });
|
|
104
|
+
}
|
|
105
|
+
case "available":
|
|
106
|
+
return dbText("githubRowAvailable");
|
|
107
|
+
case "connect":
|
|
108
|
+
return dbText("githubRowConnectFirst");
|
|
109
|
+
case "blocked":
|
|
110
|
+
switch (availability.reason) {
|
|
111
|
+
case "files":
|
|
112
|
+
return dbText("githubRowFiles");
|
|
113
|
+
case "system":
|
|
114
|
+
return dbText("githubRowSystem");
|
|
115
|
+
case "editors-only":
|
|
116
|
+
return dbText("githubRowEditorsOnly");
|
|
117
|
+
case "other-source":
|
|
118
|
+
return dbText("githubRowOtherSource", {
|
|
119
|
+
source: availability.sourceName ?? "",
|
|
120
|
+
});
|
|
121
|
+
case "has-pages":
|
|
122
|
+
return dbText("githubRowEmptyOnly");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The full reason a blocked collection can't take a folder. */
|
|
128
|
+
export function githubBlockedExplanation(
|
|
129
|
+
reason: GitHubBlockedReason,
|
|
130
|
+
place: GitHubCollectionPlace,
|
|
131
|
+
sourceName?: string,
|
|
132
|
+
): string {
|
|
133
|
+
switch (reason) {
|
|
134
|
+
case "files":
|
|
135
|
+
return dbText("githubExplainFiles");
|
|
136
|
+
case "system":
|
|
137
|
+
return dbText("githubExplainSystem");
|
|
138
|
+
case "editors-only":
|
|
139
|
+
return dbText("githubExplainEditorsOnly");
|
|
140
|
+
case "other-source":
|
|
141
|
+
return dbText("githubExplainOtherSource", { source: sourceName ?? "" });
|
|
142
|
+
case "has-pages":
|
|
143
|
+
return place.pageCount === 1
|
|
144
|
+
? dbText("githubExplainHasPagesOne")
|
|
145
|
+
: dbText("githubExplainHasPages", { count: place.pageCount });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { actionErrorMessage } from "@agent-native/core/client/hooks";
|
|
2
|
+
import {
|
|
3
|
+
GITHUB_SOURCE_MAX_FILES,
|
|
4
|
+
parseGitHubFolderUrl,
|
|
5
|
+
type GitHubLinkBlockedReason,
|
|
6
|
+
type GitHubSourceProblemCode,
|
|
7
|
+
type GitHubSourceSkipReason,
|
|
8
|
+
} from "@shared/github-source";
|
|
9
|
+
|
|
10
|
+
import { dbText, type DatabaseMessageKey } from "./text";
|
|
11
|
+
|
|
12
|
+
const PROBLEM_KEYS: Record<GitHubSourceProblemCode, DatabaseMessageKey> = {
|
|
13
|
+
connection: "githubProblemConnection",
|
|
14
|
+
forbidden: "githubProblemForbidden",
|
|
15
|
+
repository_not_found: "githubProblemRepositoryNotFound",
|
|
16
|
+
branch_not_found: "githubProblemBranchNotFound",
|
|
17
|
+
folder_not_found: "githubProblemFolderNotFound",
|
|
18
|
+
rate_limited: "githubProblemPaused",
|
|
19
|
+
too_large: "githubProblemTooLarge",
|
|
20
|
+
unavailable: "githubProblemPaused",
|
|
21
|
+
sync_failed: "githubProblemPaused",
|
|
22
|
+
storage_unavailable: "githubProblemStorageUnavailable",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SKIP_KEYS: Record<GitHubSourceSkipReason, DatabaseMessageKey> = {
|
|
26
|
+
mdx: "githubSkipMdx",
|
|
27
|
+
"too-large": "githubSkipTooLarge",
|
|
28
|
+
"not-text": "githubSkipNotText",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const BLOCKED_KEYS: Record<GitHubLinkBlockedReason, DatabaseMessageKey> = {
|
|
32
|
+
"collection-has-rows": "githubLinkBlockedHasRows",
|
|
33
|
+
"already-linked": "githubLinkBlockedLinked",
|
|
34
|
+
"system-collection": "githubLinkBlockedSystem",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Where a linked folder lives, for problem text that names it. */
|
|
38
|
+
export interface GitHubProblemPlace {
|
|
39
|
+
branch?: string;
|
|
40
|
+
path?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A linked folder's problem, as the status line and update toasts say it. */
|
|
44
|
+
export function githubProblemText(
|
|
45
|
+
code: GitHubSourceProblemCode,
|
|
46
|
+
place: GitHubProblemPlace = {},
|
|
47
|
+
): string {
|
|
48
|
+
return dbText(PROBLEM_KEYS[code] ?? "githubProblemPaused", {
|
|
49
|
+
branch: place.branch ?? "",
|
|
50
|
+
folder: lastSegment(place.path ?? ""),
|
|
51
|
+
max: GITHUB_SOURCE_MAX_FILES.toLocaleString(pageLocale()),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Problems that mean nothing comes in until someone acts. */
|
|
56
|
+
export function githubProblemStopsUpdates(
|
|
57
|
+
code: GitHubSourceProblemCode,
|
|
58
|
+
): boolean {
|
|
59
|
+
return (
|
|
60
|
+
code === "connection" ||
|
|
61
|
+
code === "forbidden" ||
|
|
62
|
+
code === "repository_not_found" ||
|
|
63
|
+
code === "branch_not_found" ||
|
|
64
|
+
code === "folder_not_found" ||
|
|
65
|
+
code === "too_large"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The problem a `GITHUB_<CODE>` action error stands for, so the dialog can say
|
|
71
|
+
* it in the reader's language. Null for any other error.
|
|
72
|
+
*/
|
|
73
|
+
export function githubProblemFromErrorCode(
|
|
74
|
+
errorCode: unknown,
|
|
75
|
+
): GitHubSourceProblemCode | null {
|
|
76
|
+
if (typeof errorCode !== "string" || !errorCode.startsWith("GITHUB_")) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const code = errorCode.slice("GITHUB_".length).toLowerCase();
|
|
80
|
+
return code in PROBLEM_KEYS ? (code as GitHubSourceProblemCode) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether an action error means GitHub needs connecting first. */
|
|
84
|
+
export function githubErrorNeedsConnection(error: unknown): boolean {
|
|
85
|
+
const code = (error as { errorCode?: unknown } | null)?.errorCode;
|
|
86
|
+
// A missing or ungranted connection comes from the shared provider runtime
|
|
87
|
+
// as `connection_required`; a rejected one, from GitHub.
|
|
88
|
+
return (
|
|
89
|
+
code === "connection_required" ||
|
|
90
|
+
githubProblemFromErrorCode(code) === "connection"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The collection-level reason a link was refused, from its action error. */
|
|
95
|
+
export function githubBlockedFromErrorCode(
|
|
96
|
+
errorCode: unknown,
|
|
97
|
+
): GitHubLinkBlockedReason | null {
|
|
98
|
+
if (errorCode === "COLLECTION_NOT_EMPTY") return "collection-has-rows";
|
|
99
|
+
if (errorCode === "SOURCE_ALREADY_LINKED") return "already-linked";
|
|
100
|
+
if (errorCode === "COLLECTION_NOT_LINKABLE") return "system-collection";
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A link-time error in plain words, naming what the person typed: a typo in
|
|
106
|
+
* the link reads as a typo, not as something that went missing.
|
|
107
|
+
*/
|
|
108
|
+
export function githubLinkErrorText(error: unknown, url: string): string {
|
|
109
|
+
const errorCode = (error as { errorCode?: unknown } | null)?.errorCode;
|
|
110
|
+
const blocked = githubBlockedFromErrorCode(errorCode);
|
|
111
|
+
if (blocked) return githubBlockedText(blocked);
|
|
112
|
+
const problem = githubProblemFromErrorCode(errorCode);
|
|
113
|
+
if (!problem) {
|
|
114
|
+
return (
|
|
115
|
+
actionErrorMessage(error) ??
|
|
116
|
+
(error instanceof Error ? error.message : dbText("githubLinkUnavailable"))
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const parsed = parseGitHubFolderUrl(url);
|
|
120
|
+
const repo = parsed.ok ? parsed.repo : "";
|
|
121
|
+
const [branch = "", ...folder] = parsed.ok ? parsed.refPath : [];
|
|
122
|
+
switch (problem) {
|
|
123
|
+
case "repository_not_found":
|
|
124
|
+
return dbText("githubLinkRepositoryNotFound", { repo });
|
|
125
|
+
case "forbidden":
|
|
126
|
+
return dbText("githubLinkForbidden", { repo });
|
|
127
|
+
case "branch_not_found":
|
|
128
|
+
return dbText("githubLinkBranchNotFound", { branch, repo });
|
|
129
|
+
case "folder_not_found":
|
|
130
|
+
return dbText("githubLinkFolderNotFound", {
|
|
131
|
+
folder: folder.join("/"),
|
|
132
|
+
repo,
|
|
133
|
+
});
|
|
134
|
+
case "too_large":
|
|
135
|
+
return dbText("githubLinkTooLarge", {
|
|
136
|
+
max: GITHUB_SOURCE_MAX_FILES.toLocaleString(pageLocale()),
|
|
137
|
+
});
|
|
138
|
+
default:
|
|
139
|
+
return dbText("githubLinkUnavailable");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** What's wrong with a link before anything is sent, if anything. */
|
|
144
|
+
export function githubUrlProblem(
|
|
145
|
+
url: string,
|
|
146
|
+
): { text: string; folderUrl: string | null; folder: string | null } | null {
|
|
147
|
+
const parsed = parseGitHubFolderUrl(url);
|
|
148
|
+
if (parsed.ok) return null;
|
|
149
|
+
if (parsed.reason !== "file") {
|
|
150
|
+
return {
|
|
151
|
+
text: dbText("githubLinkInvalidUrl"),
|
|
152
|
+
folderUrl: null,
|
|
153
|
+
folder: null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const folder = fileLinkFolder(url);
|
|
157
|
+
return {
|
|
158
|
+
text: dbText("githubLinkFileUrl"),
|
|
159
|
+
folderUrl: folder?.url ?? null,
|
|
160
|
+
folder: folder?.name ?? null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The folder a file link points into: `…/blob/main/docs/faq.md` becomes
|
|
166
|
+
* `…/tree/main/docs`, named "docs", or the repository at its root.
|
|
167
|
+
*/
|
|
168
|
+
export function fileLinkFolder(
|
|
169
|
+
url: string,
|
|
170
|
+
): { url: string; name: string } | null {
|
|
171
|
+
let parsed: URL;
|
|
172
|
+
try {
|
|
173
|
+
parsed = new URL(
|
|
174
|
+
/^[a-z][a-z0-9+.-]*:\/\//i.test(url) ? url : `https://${url}`,
|
|
175
|
+
);
|
|
176
|
+
// coercion-ok: a link that isn't a URL has no folder to offer; the caller shows the invalid-link text.
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
181
|
+
const [owner, repo, kind, ...rest] = segments;
|
|
182
|
+
if (!owner || !repo || kind !== "blob" || rest.length < 2) return null;
|
|
183
|
+
const folderSegments = rest.slice(0, -1);
|
|
184
|
+
const folderUrl = `https://github.com/${owner}/${repo}/tree/${folderSegments.join("/")}`;
|
|
185
|
+
// The first segment is the branch, so a file at the root names the repository.
|
|
186
|
+
const name =
|
|
187
|
+
folderSegments.length > 1
|
|
188
|
+
? decodeURIComponent(folderSegments[folderSegments.length - 1]!)
|
|
189
|
+
: decodeURIComponent(repo);
|
|
190
|
+
return parseGitHubFolderUrl(folderUrl).ok ? { url: folderUrl, name } : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function githubSkipReasonText(reason: GitHubSourceSkipReason): string {
|
|
194
|
+
return dbText(SKIP_KEYS[reason] ?? "githubSkipNotText");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function githubBlockedText(reason: GitHubLinkBlockedReason): string {
|
|
198
|
+
return dbText(BLOCKED_KEYS[reason] ?? "githubLinkBlockedLinked");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The seven-character commit name GitHub shows. */
|
|
202
|
+
export function shortCommit(sha: string): string {
|
|
203
|
+
return sha.slice(0, 7);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function lastSegment(path: string): string {
|
|
207
|
+
const trimmed = path.replace(/\/+$/, "");
|
|
208
|
+
return trimmed.slice(trimmed.lastIndexOf("/") + 1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The page's language, for numbers, dates and lists. */
|
|
212
|
+
export function pageLocale(): string {
|
|
213
|
+
return typeof document === "undefined" || !document.documentElement.lang
|
|
214
|
+
? "en-US"
|
|
215
|
+
: document.documentElement.lang;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Not in the TypeScript lib this app targets, though every supported
|
|
219
|
+
// browser has it.
|
|
220
|
+
const ListFormat = (
|
|
221
|
+
Intl as typeof Intl & {
|
|
222
|
+
ListFormat?: new (
|
|
223
|
+
locale: string,
|
|
224
|
+
options: { style: "long"; type: "conjunction" },
|
|
225
|
+
) => { format(items: string[]): string };
|
|
226
|
+
}
|
|
227
|
+
).ListFormat;
|
|
228
|
+
|
|
229
|
+
/** "API reference, FAQ and Getting started", in the page's language. */
|
|
230
|
+
export function githubList(items: string[]): string {
|
|
231
|
+
if (!ListFormat) return items.join(", ");
|
|
232
|
+
return new ListFormat(pageLocale(), {
|
|
233
|
+
style: "long",
|
|
234
|
+
type: "conjunction",
|
|
235
|
+
}).format(items);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const RELATIVE_UNITS: Array<[Intl.RelativeTimeFormatUnit, number]> = [
|
|
239
|
+
["year", 365 * 24 * 60 * 60],
|
|
240
|
+
["month", 30 * 24 * 60 * 60],
|
|
241
|
+
["week", 7 * 24 * 60 * 60],
|
|
242
|
+
["day", 24 * 60 * 60],
|
|
243
|
+
["hour", 60 * 60],
|
|
244
|
+
["minute", 60],
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
/** "2 minutes ago", "just now" and the like, in the page's language. */
|
|
248
|
+
export function githubRelativeTime(iso: string, now = Date.now()): string {
|
|
249
|
+
const at = Date.parse(iso);
|
|
250
|
+
if (!Number.isFinite(at)) return "";
|
|
251
|
+
const format = new Intl.RelativeTimeFormat(pageLocale(), {
|
|
252
|
+
numeric: "auto",
|
|
253
|
+
});
|
|
254
|
+
const seconds = Math.round((at - now) / 1000);
|
|
255
|
+
for (const [unit, size] of RELATIVE_UNITS) {
|
|
256
|
+
if (Math.abs(seconds) >= size) {
|
|
257
|
+
return format.format(Math.round(seconds / size), unit);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return dbText("githubJustNow");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A frontmatter example the way a writer reads it: "Yes", "Sep 15, 2026". */
|
|
264
|
+
export function githubExampleText(type: string, example: string): string {
|
|
265
|
+
if (type === "checkbox") {
|
|
266
|
+
if (example === "true") return dbText("githubYes");
|
|
267
|
+
if (example === "false") return dbText("githubNo");
|
|
268
|
+
}
|
|
269
|
+
if (type === "date") {
|
|
270
|
+
const at = Date.parse(example);
|
|
271
|
+
if (Number.isFinite(at)) {
|
|
272
|
+
return new Intl.DateTimeFormat(pageLocale(), {
|
|
273
|
+
dateStyle: "medium",
|
|
274
|
+
// Frontmatter dates are calendar days, so they mustn't shift by zone.
|
|
275
|
+
timeZone: /^\d{4}-\d{2}-\d{2}$/.test(example) ? "UTC" : undefined,
|
|
276
|
+
}).format(at);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return example;
|
|
280
|
+
}
|
|
@@ -172,6 +172,7 @@ export function databaseNavigationState({
|
|
|
172
172
|
| "rowDensity"
|
|
173
173
|
| "openPagesIn"
|
|
174
174
|
| "formQuestions"
|
|
175
|
+
| "itemLayout"
|
|
175
176
|
>;
|
|
176
177
|
searchQuery?: string;
|
|
177
178
|
sorts?: DatabaseSort[];
|
|
@@ -286,6 +287,8 @@ export function databaseNavigationState({
|
|
|
286
287
|
: undefined,
|
|
287
288
|
databaseFormQuestions:
|
|
288
289
|
activeView.type === "form" ? activeView.formQuestions : undefined,
|
|
290
|
+
databaseItemLayout:
|
|
291
|
+
activeView.type === "list" ? (activeView.itemLayout ?? null) : undefined,
|
|
289
292
|
databaseVisibleItemCount: visibleItemCount,
|
|
290
293
|
databaseTotalItemCount: totalItemCount,
|
|
291
294
|
databaseVisibleItems: databaseVisibleItemSummaries(
|
|
@@ -2066,7 +2066,7 @@ function SourceDetailsFieldPicker({
|
|
|
2066
2066
|
className={cn(
|
|
2067
2067
|
"flex size-4 shrink-0 items-center justify-center rounded border",
|
|
2068
2068
|
selected.has(field.id)
|
|
2069
|
-
? "border-
|
|
2069
|
+
? "border-database-blue bg-database-blue text-database-blue-foreground"
|
|
2070
2070
|
: "border-muted-foreground/40 text-transparent",
|
|
2071
2071
|
)}
|
|
2072
2072
|
>
|
|
@@ -2644,7 +2644,7 @@ function DatabaseSettingsSwitch({
|
|
|
2644
2644
|
<span
|
|
2645
2645
|
className={cn(
|
|
2646
2646
|
"relative h-5 w-9 rounded-full transition-colors",
|
|
2647
|
-
checked ? "bg-
|
|
2647
|
+
checked ? "bg-database-blue" : "bg-muted-foreground/25",
|
|
2648
2648
|
)}
|
|
2649
2649
|
>
|
|
2650
2650
|
<span
|
|
@@ -2683,7 +2683,7 @@ function DatabaseSettingsLayoutPanel({
|
|
|
2683
2683
|
className={cn(
|
|
2684
2684
|
"flex h-16 flex-col items-center justify-center gap-1 rounded-md border text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2685
2685
|
activeView.type === type
|
|
2686
|
-
? "border-
|
|
2686
|
+
? "border-database-blue bg-database-blue/5 text-database-blue"
|
|
2687
2687
|
: "border-border text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
2688
2688
|
)}
|
|
2689
2689
|
onClick={() => onViewTypeChange(type)}
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ContentDatabaseViewConfig,
|
|
4
4
|
ContentDatabaseViewType,
|
|
5
5
|
} from "@shared/api";
|
|
6
|
+
import { contentDatabaseItemLayoutField } from "@shared/database-item-layout";
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
type DatabaseColumnCalculation,
|
|
@@ -60,6 +61,7 @@ export function createDatabaseView(
|
|
|
60
61
|
rowDensity: normalizeClientDatabaseRowDensity(values.rowDensity),
|
|
61
62
|
openPagesIn: normalizeClientDatabaseOpenPagesIn(values.openPagesIn),
|
|
62
63
|
formQuestions: normalizeClientDatabaseFormQuestions(values.formQuestions),
|
|
64
|
+
...contentDatabaseItemLayoutField(values.itemLayout),
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -219,6 +221,7 @@ export function duplicateDatabaseView(
|
|
|
219
221
|
rowDensity: view.rowDensity,
|
|
220
222
|
openPagesIn: view.openPagesIn,
|
|
221
223
|
formQuestions: view.formQuestions,
|
|
224
|
+
itemLayout: view.itemLayout,
|
|
222
225
|
},
|
|
223
226
|
view.type,
|
|
224
227
|
);
|
|
@@ -360,6 +363,7 @@ function normalizeClientDatabaseView(
|
|
|
360
363
|
rowDensity: normalizeClientDatabaseRowDensity(value.rowDensity),
|
|
361
364
|
openPagesIn: normalizeClientDatabaseOpenPagesIn(value.openPagesIn),
|
|
362
365
|
formQuestions: normalizeClientDatabaseFormQuestions(value.formQuestions),
|
|
366
|
+
itemLayout: value.itemLayout,
|
|
363
367
|
},
|
|
364
368
|
type,
|
|
365
369
|
);
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { useLocation, useNavigation } from "react-router";
|
|
27
27
|
import { toast } from "sonner";
|
|
28
28
|
|
|
29
|
+
import { GitHubLinkDialogHost } from "@/components/editor/database/GitHubLinkDialog";
|
|
29
30
|
import { DocumentEditor } from "@/components/editor/DocumentEditor";
|
|
30
31
|
import { DocumentEditorSkeleton } from "@/components/editor/DocumentEditorSkeleton";
|
|
31
32
|
import { DocumentSidebar } from "@/components/sidebar/DocumentSidebar";
|
|
@@ -497,6 +498,7 @@ export function Layout({ children }: LayoutProps) {
|
|
|
497
498
|
</>
|
|
498
499
|
)}
|
|
499
500
|
</div>
|
|
501
|
+
<GitHubLinkDialogHost />
|
|
500
502
|
</HeaderActionsProvider>
|
|
501
503
|
);
|
|
502
504
|
}
|
|
@@ -70,10 +70,20 @@
|
|
|
70
70
|
--block-bg-purple: #ad46ff19;
|
|
71
71
|
--block-bg-pink: #f6339a19;
|
|
72
72
|
--block-bg-red: #fb2c3619;
|
|
73
|
+
|
|
74
|
+
/* Active filter and sort chips in collection views. */
|
|
75
|
+
--database-chip: #2383e2;
|
|
76
|
+
--database-chip-foreground: #0f5ea8;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
@theme {
|
|
76
80
|
--color-sidebar-muted: hsl(var(--sidebar-muted));
|
|
81
|
+
/* Checked, selected, and focused controls in collection views, the same in
|
|
82
|
+
light and dark mode. */
|
|
83
|
+
--color-database-blue: #2383e2;
|
|
84
|
+
--color-database-blue-foreground: #fff;
|
|
85
|
+
--color-database-chip: var(--database-chip);
|
|
86
|
+
--color-database-chip-foreground: var(--database-chip-foreground);
|
|
77
87
|
/* Comment cards: resting in the margin, floating as a popover, and the
|
|
78
88
|
lifted state for the hovered, focused, or selected thread. */
|
|
79
89
|
--shadow-comment-card:
|
|
@@ -143,6 +153,9 @@
|
|
|
143
153
|
--block-bg-purple: #ad46ff26;
|
|
144
154
|
--block-bg-pink: #f6339a26;
|
|
145
155
|
--block-bg-red: #fb2c3626;
|
|
156
|
+
|
|
157
|
+
--database-chip: #529cca;
|
|
158
|
+
--database-chip-foreground: #8ec7ff;
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
@layer base {
|
|
@@ -30,12 +30,14 @@ const DOCUMENT_MUTATIONS = new Set([
|
|
|
30
30
|
"delete-document",
|
|
31
31
|
"delete-document-property",
|
|
32
32
|
"delete-content-database",
|
|
33
|
+
"disconnect-content-database-source",
|
|
33
34
|
"execute-content-trash-purge",
|
|
34
35
|
"permanently-delete-document",
|
|
35
36
|
"duplicate-document-property",
|
|
36
37
|
"edit-document",
|
|
37
38
|
"execute-builder-source-batch",
|
|
38
39
|
"execute-builder-source-execution",
|
|
40
|
+
"create-github-collection",
|
|
39
41
|
"import-content-source",
|
|
40
42
|
"link-github-folder",
|
|
41
43
|
"migrate-content-database-rows",
|
|
@@ -72,12 +74,14 @@ const DATABASE_RESULT_MUTATIONS = new Set([
|
|
|
72
74
|
"delete-content-database",
|
|
73
75
|
"delete-document",
|
|
74
76
|
"delete-document-property",
|
|
77
|
+
"disconnect-content-database-source",
|
|
75
78
|
"duplicate-database-item",
|
|
76
79
|
"duplicate-database-items",
|
|
77
80
|
"duplicate-document-property",
|
|
78
81
|
"edit-document",
|
|
79
82
|
"execute-builder-source-batch",
|
|
80
83
|
"execute-builder-source-execution",
|
|
84
|
+
"create-github-collection",
|
|
81
85
|
"import-content-source",
|
|
82
86
|
"link-github-folder",
|
|
83
87
|
"migrate-content-database-rows",
|
|
@@ -103,6 +107,7 @@ const DATABASE_PRESENTATION_MUTATIONS = new Set([
|
|
|
103
107
|
]);
|
|
104
108
|
|
|
105
109
|
const DATABASE_LIFECYCLE_MUTATIONS = new Set([
|
|
110
|
+
"create-github-collection",
|
|
106
111
|
"delete-content-database",
|
|
107
112
|
"execute-content-trash-purge",
|
|
108
113
|
"permanently-delete-document",
|
|
@@ -115,6 +120,7 @@ const DATABASE_LIFECYCLE_MUTATIONS = new Set([
|
|
|
115
120
|
|
|
116
121
|
const DOCUMENT_DISCOVERY_MUTATIONS = new Set([
|
|
117
122
|
"create-document",
|
|
123
|
+
"create-github-collection",
|
|
118
124
|
"import-content",
|
|
119
125
|
"link-github-folder",
|
|
120
126
|
"refresh-content-database-source",
|
|
@@ -1837,7 +1837,9 @@ export function useRefreshContentDatabaseSource(documentId: string) {
|
|
|
1837
1837
|
RefreshContentDatabaseSourceRequest
|
|
1838
1838
|
>("refresh-content-database-source", {
|
|
1839
1839
|
skipActionQueryInvalidation: true,
|
|
1840
|
-
|
|
1840
|
+
// A GitHub refresh that fails stores the problem on the source first, so
|
|
1841
|
+
// the status line rereads after a failure too.
|
|
1842
|
+
onSettled: () => {
|
|
1841
1843
|
invalidateContentDatabaseSourceRefreshQueries(queryClient, documentId);
|
|
1842
1844
|
},
|
|
1843
1845
|
});
|