@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
|
@@ -8,11 +8,30 @@ import { syncPrivateViewIconReferences } from "../server/lib/private-icon-refere
|
|
|
8
8
|
import {
|
|
9
9
|
CONTENT_DATABASE_FILTER_OPERATORS,
|
|
10
10
|
type ContentDatabaseFilter,
|
|
11
|
+
type ContentDatabaseItemLayout,
|
|
11
12
|
type ContentDatabaseView,
|
|
12
13
|
type ContentDatabaseViewConfig,
|
|
13
14
|
} from "../shared/api.js";
|
|
15
|
+
import {
|
|
16
|
+
CONTENT_DATABASE_ITEM_LAYOUT_PRESET_IDS,
|
|
17
|
+
CONTENT_DATABASE_ITEM_LEADING_KINDS,
|
|
18
|
+
CONTENT_DATABASE_ITEM_TITLE_WEIGHTS,
|
|
19
|
+
MAX_ITEM_LAYOUT_META_PROPERTIES,
|
|
20
|
+
contentDatabaseItemLayoutPresets,
|
|
21
|
+
contentDatabaseItemLayoutReferenceErrors,
|
|
22
|
+
dropDeletedContentDatabaseItemLayoutReferences,
|
|
23
|
+
itemLayoutLeadingCandidates,
|
|
24
|
+
normalizeContentDatabaseItemLayout,
|
|
25
|
+
pruneContentDatabaseItemLayout,
|
|
26
|
+
savableContentDatabaseItemLayout,
|
|
27
|
+
startingContentDatabaseItemLayout,
|
|
28
|
+
} from "../shared/database-item-layout.js";
|
|
14
29
|
import { contentDatabaseFilterOperatorsForType } from "../shared/database-query.js";
|
|
15
|
-
import
|
|
30
|
+
import {
|
|
31
|
+
normalizePropertyVisibility,
|
|
32
|
+
parsePropertyOptions,
|
|
33
|
+
type DocumentPropertyType,
|
|
34
|
+
} from "../shared/properties.js";
|
|
16
35
|
import {
|
|
17
36
|
runDatabaseSetupMutation,
|
|
18
37
|
setupError,
|
|
@@ -76,6 +95,77 @@ const tablePatchFields = {
|
|
|
76
95
|
openPagesIn: z.enum(["preview", "full_page"]).optional(),
|
|
77
96
|
};
|
|
78
97
|
|
|
98
|
+
const itemLayoutPropertyIdSchema = z.string().trim().min(1).max(200);
|
|
99
|
+
const itemLayoutLinesSchema = z.union([
|
|
100
|
+
z.literal(1),
|
|
101
|
+
z.literal(2),
|
|
102
|
+
z.literal(3),
|
|
103
|
+
]);
|
|
104
|
+
const itemLayoutMetaSchema = z
|
|
105
|
+
.array(itemLayoutPropertyIdSchema)
|
|
106
|
+
.max(MAX_ITEM_LAYOUT_META_PROPERTIES);
|
|
107
|
+
|
|
108
|
+
export const contentDatabaseItemLayoutSchema = z
|
|
109
|
+
.object({
|
|
110
|
+
leading: z
|
|
111
|
+
.object({
|
|
112
|
+
kind: z.enum(CONTENT_DATABASE_ITEM_LEADING_KINDS),
|
|
113
|
+
propertyId: itemLayoutPropertyIdSchema.nullable(),
|
|
114
|
+
})
|
|
115
|
+
.strict(),
|
|
116
|
+
titleWeight: z.enum(CONTENT_DATABASE_ITEM_TITLE_WEIGHTS),
|
|
117
|
+
line2: z
|
|
118
|
+
.object({
|
|
119
|
+
propertyId: itemLayoutPropertyIdSchema.nullable(),
|
|
120
|
+
lines: itemLayoutLinesSchema,
|
|
121
|
+
})
|
|
122
|
+
.strict(),
|
|
123
|
+
line3: itemLayoutMetaSchema,
|
|
124
|
+
trailing: itemLayoutMetaSchema,
|
|
125
|
+
})
|
|
126
|
+
.strict();
|
|
127
|
+
|
|
128
|
+
const itemLayoutPatchSchema = z
|
|
129
|
+
.object({
|
|
130
|
+
leading: z
|
|
131
|
+
.object({
|
|
132
|
+
kind: z.enum(CONTENT_DATABASE_ITEM_LEADING_KINDS),
|
|
133
|
+
propertyId: itemLayoutPropertyIdSchema.nullable().optional(),
|
|
134
|
+
})
|
|
135
|
+
.strict()
|
|
136
|
+
.optional(),
|
|
137
|
+
titleWeight: z.enum(CONTENT_DATABASE_ITEM_TITLE_WEIGHTS).optional(),
|
|
138
|
+
line2: z
|
|
139
|
+
.object({
|
|
140
|
+
propertyId: itemLayoutPropertyIdSchema.nullable().optional(),
|
|
141
|
+
lines: itemLayoutLinesSchema.optional(),
|
|
142
|
+
})
|
|
143
|
+
.strict()
|
|
144
|
+
.optional(),
|
|
145
|
+
line3: itemLayoutMetaSchema.optional(),
|
|
146
|
+
trailing: itemLayoutMetaSchema.optional(),
|
|
147
|
+
})
|
|
148
|
+
.strict()
|
|
149
|
+
.describe(
|
|
150
|
+
"List views only. Slots to change in each row; omitted slots keep their saved value. leading.kind checkbox or status uses the first property of that type unless propertyId names one. line2 shows a text, URL, or Blocks property; line3 and trailing list property IDs in order. Trailing values move under the title on narrow screens. Pass null to restore the default rows.",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const listPatchFields = {
|
|
154
|
+
itemLayout: itemLayoutPatchSchema.nullable().optional(),
|
|
155
|
+
itemLayoutPreset: z
|
|
156
|
+
.enum(CONTENT_DATABASE_ITEM_LAYOUT_PRESET_IDS)
|
|
157
|
+
.optional()
|
|
158
|
+
.describe(
|
|
159
|
+
"List views only. Starts the rows from a preset before itemLayout applies: checklist (checkbox, bold title, two lines of text), reminders (checkbox, one line of text, date, person and tag under it), tracker (status dot, tag, person and date on the right), digest (page icon, bold title, three lines of text, details under it), or default.",
|
|
160
|
+
),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const LIST_VIEW_PATCH_KEYS = new Set([
|
|
164
|
+
"name",
|
|
165
|
+
"icon",
|
|
166
|
+
...Object.keys(listPatchFields),
|
|
167
|
+
]);
|
|
168
|
+
|
|
79
169
|
const boundedTablePatchSchema = z
|
|
80
170
|
.object(tablePatchFields)
|
|
81
171
|
.strict()
|
|
@@ -91,11 +181,12 @@ const boundedTablePatchSchema = z
|
|
|
91
181
|
}
|
|
92
182
|
});
|
|
93
183
|
|
|
94
|
-
const
|
|
184
|
+
const updateViewPatchSchema = z
|
|
95
185
|
.object({
|
|
96
186
|
name: z.string().trim().min(1).max(500).optional(),
|
|
97
187
|
icon: iconValueSchema.nullable().optional(),
|
|
98
188
|
...tablePatchFields,
|
|
189
|
+
...listPatchFields,
|
|
99
190
|
})
|
|
100
191
|
.strict()
|
|
101
192
|
.refine(
|
|
@@ -125,7 +216,7 @@ export const updateContentDatabaseViewAgentSchema = z.discriminatedUnion(
|
|
|
125
216
|
expectedConfigurationRevision: z.string().min(1),
|
|
126
217
|
operation: z.literal("update"),
|
|
127
218
|
viewId: z.string().trim().min(1).max(200),
|
|
128
|
-
patch:
|
|
219
|
+
patch: updateViewPatchSchema,
|
|
129
220
|
})
|
|
130
221
|
.strict(),
|
|
131
222
|
],
|
|
@@ -147,7 +238,7 @@ function uniqueReferences(values: readonly string[], field: string) {
|
|
|
147
238
|
|
|
148
239
|
function validateTablePatch(
|
|
149
240
|
patch:
|
|
150
|
-
| z.infer<typeof
|
|
241
|
+
| z.infer<typeof updateViewPatchSchema>
|
|
151
242
|
| z.infer<typeof boundedTablePatchSchema>,
|
|
152
243
|
definitions: readonly (typeof schema.documentPropertyDefinitions.$inferSelect)[],
|
|
153
244
|
) {
|
|
@@ -228,6 +319,174 @@ function validateTablePatch(
|
|
|
228
319
|
}
|
|
229
320
|
}
|
|
230
321
|
|
|
322
|
+
type PropertyDefinitionRow =
|
|
323
|
+
typeof schema.documentPropertyDefinitions.$inferSelect;
|
|
324
|
+
|
|
325
|
+
function itemLayoutProperties(definitions: readonly PropertyDefinitionRow[]) {
|
|
326
|
+
return definitions.map((definition) => ({
|
|
327
|
+
definition: {
|
|
328
|
+
id: definition.id,
|
|
329
|
+
type: definition.type as DocumentPropertyType,
|
|
330
|
+
options: parsePropertyOptions(definition.optionsJson),
|
|
331
|
+
visibility: normalizePropertyVisibility(definition.visibility),
|
|
332
|
+
},
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Checks the layout a full-view save carries against the view's saved one:
|
|
338
|
+
* new slot references the rows can't show are dropped, saved ones stay.
|
|
339
|
+
*/
|
|
340
|
+
export function withSavableItemLayout<
|
|
341
|
+
T extends { itemLayout?: unknown; hiddenPropertyIds?: unknown },
|
|
342
|
+
>(
|
|
343
|
+
view: T,
|
|
344
|
+
savedLayout: unknown,
|
|
345
|
+
definitions: readonly PropertyDefinitionRow[],
|
|
346
|
+
): T {
|
|
347
|
+
const layout = normalizeContentDatabaseItemLayout(view.itemLayout);
|
|
348
|
+
return layout
|
|
349
|
+
? {
|
|
350
|
+
...view,
|
|
351
|
+
itemLayout: savableContentDatabaseItemLayout(
|
|
352
|
+
layout,
|
|
353
|
+
normalizeContentDatabaseItemLayout(savedLayout),
|
|
354
|
+
itemLayoutProperties(definitions),
|
|
355
|
+
Array.isArray(view.hiddenPropertyIds) ? view.hiddenPropertyIds : [],
|
|
356
|
+
),
|
|
357
|
+
}
|
|
358
|
+
: view;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* A view's properties in the order its UI lists them, so presets and slot
|
|
363
|
+
* defaults pick the same first checkbox, status or text field the UI does.
|
|
364
|
+
*/
|
|
365
|
+
function viewOrderedDefinitions(
|
|
366
|
+
view: ContentDatabaseView,
|
|
367
|
+
definitions: readonly PropertyDefinitionRow[],
|
|
368
|
+
) {
|
|
369
|
+
const order = view.propertyOrderIds ?? [];
|
|
370
|
+
const rank = (id: string) => {
|
|
371
|
+
const index = order.indexOf(id);
|
|
372
|
+
return index < 0 ? order.length : index;
|
|
373
|
+
};
|
|
374
|
+
return [...definitions].sort(
|
|
375
|
+
(a, b) => rank(a.id) - rank(b.id) || a.position - b.position,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The default rows' properties as the view's settings define them: those not
|
|
381
|
+
* hidden, in its order. The rows also leave out a "hide when empty" property
|
|
382
|
+
* that none of the loaded rows fills; the server doesn't see which rows the
|
|
383
|
+
* view loads, so it keeps those, and their blank values take no space.
|
|
384
|
+
*/
|
|
385
|
+
function defaultListRowProperties(
|
|
386
|
+
view: ContentDatabaseView,
|
|
387
|
+
definitions: readonly PropertyDefinitionRow[],
|
|
388
|
+
) {
|
|
389
|
+
const hidden = new Set(view.hiddenPropertyIds ?? []);
|
|
390
|
+
return itemLayoutProperties(
|
|
391
|
+
viewOrderedDefinitions(view, definitions).filter(
|
|
392
|
+
(definition) =>
|
|
393
|
+
definition.visibility !== "always_hide" && !hidden.has(definition.id),
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Applies an agent's item layout patch to a list view. Returns undefined
|
|
400
|
+
* when the patch leaves the layout alone and null to restore default rows.
|
|
401
|
+
*/
|
|
402
|
+
function nextListItemLayout(
|
|
403
|
+
patch: z.infer<typeof updateViewPatchSchema>,
|
|
404
|
+
view: ContentDatabaseView,
|
|
405
|
+
definitions: readonly PropertyDefinitionRow[],
|
|
406
|
+
): ContentDatabaseItemLayout | null | undefined {
|
|
407
|
+
const { itemLayout, itemLayoutPreset } = patch;
|
|
408
|
+
if (itemLayout === undefined && itemLayoutPreset === undefined) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
if (itemLayout === null && itemLayoutPreset !== undefined) {
|
|
412
|
+
setupError(
|
|
413
|
+
"INVALID_VIEW_REFERENCE",
|
|
414
|
+
"Pass itemLayoutPreset or itemLayout: null, not both.",
|
|
415
|
+
400,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (itemLayout === null) return null;
|
|
419
|
+
const allProperties = itemLayoutProperties(
|
|
420
|
+
viewOrderedDefinitions(view, definitions),
|
|
421
|
+
);
|
|
422
|
+
// Properties hidden in this view can't be put in a slot, as in the UI.
|
|
423
|
+
const hidden = new Set(view.hiddenPropertyIds ?? []);
|
|
424
|
+
const properties = allProperties.filter(
|
|
425
|
+
(property) => !hidden.has(property.definition.id),
|
|
426
|
+
);
|
|
427
|
+
let base = view.itemLayout ?? null;
|
|
428
|
+
if (itemLayoutPreset !== undefined) {
|
|
429
|
+
const preset = contentDatabaseItemLayoutPresets(properties).find(
|
|
430
|
+
(candidate) => candidate.id === itemLayoutPreset,
|
|
431
|
+
)!;
|
|
432
|
+
if (preset.missingPropertyType) {
|
|
433
|
+
setupError(
|
|
434
|
+
"INVALID_VIEW_REFERENCE",
|
|
435
|
+
`The ${preset.id} preset needs a ${preset.missingPropertyType} property.`,
|
|
436
|
+
400,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
base = preset.layout;
|
|
440
|
+
}
|
|
441
|
+
if (!itemLayout) {
|
|
442
|
+
return (
|
|
443
|
+
base &&
|
|
444
|
+
dropDeletedContentDatabaseItemLayoutReferences(base, allProperties)
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
base ??= startingContentDatabaseItemLayout(
|
|
448
|
+
defaultListRowProperties(view, definitions),
|
|
449
|
+
);
|
|
450
|
+
for (const slot of ["line3", "trailing"] as const) {
|
|
451
|
+
const ids = itemLayout[slot];
|
|
452
|
+
if (ids) uniqueReferences(ids, slot);
|
|
453
|
+
}
|
|
454
|
+
const leading = itemLayout.leading && {
|
|
455
|
+
kind: itemLayout.leading.kind,
|
|
456
|
+
propertyId:
|
|
457
|
+
itemLayout.leading.propertyId ??
|
|
458
|
+
itemLayoutLeadingCandidates(properties, itemLayout.leading.kind)[0]
|
|
459
|
+
?.definition.id ??
|
|
460
|
+
null,
|
|
461
|
+
};
|
|
462
|
+
const withPatch = (from: ContentDatabaseItemLayout) =>
|
|
463
|
+
normalizeContentDatabaseItemLayout({
|
|
464
|
+
leading: leading ?? from.leading,
|
|
465
|
+
titleWeight: itemLayout.titleWeight ?? from.titleWeight,
|
|
466
|
+
line2: {
|
|
467
|
+
propertyId:
|
|
468
|
+
itemLayout.line2?.propertyId !== undefined
|
|
469
|
+
? itemLayout.line2.propertyId
|
|
470
|
+
: from.line2.propertyId,
|
|
471
|
+
lines: itemLayout.line2?.lines ?? from.line2.lines,
|
|
472
|
+
},
|
|
473
|
+
line3: itemLayout.line3 ?? from.line3,
|
|
474
|
+
trailing: itemLayout.trailing ?? from.trailing,
|
|
475
|
+
})!;
|
|
476
|
+
// Only the slots this patch names are checked. Saved slots that name a
|
|
477
|
+
// hidden property keep it for when it's shown again, and saved slots that
|
|
478
|
+
// name a deleted one drop it; neither fails an unrelated edit.
|
|
479
|
+
const [error] = contentDatabaseItemLayoutReferenceErrors(
|
|
480
|
+
withPatch(pruneContentDatabaseItemLayout(base, properties)),
|
|
481
|
+
properties,
|
|
482
|
+
);
|
|
483
|
+
if (error) setupError("INVALID_VIEW_REFERENCE", error, 400);
|
|
484
|
+
return dropDeletedContentDatabaseItemLayoutReferences(
|
|
485
|
+
withPatch(base),
|
|
486
|
+
allProperties,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
231
490
|
function parseRawConfig(value: string | null) {
|
|
232
491
|
if (!value) return {} as Record<string, unknown>;
|
|
233
492
|
try {
|
|
@@ -373,14 +632,43 @@ export async function runUpdateContentDatabaseView(
|
|
|
373
632
|
404,
|
|
374
633
|
);
|
|
375
634
|
}
|
|
376
|
-
|
|
635
|
+
const patchKeys = Object.keys(input.patch);
|
|
636
|
+
let patch: Record<string, unknown> = input.patch;
|
|
637
|
+
if (existing.type === "list") {
|
|
638
|
+
const unsupported = patchKeys.find(
|
|
639
|
+
(key) => !LIST_VIEW_PATCH_KEYS.has(key),
|
|
640
|
+
);
|
|
641
|
+
if (unsupported) {
|
|
642
|
+
setupError(
|
|
643
|
+
"UNSUPPORTED_SCHEMA_CHANGE",
|
|
644
|
+
`List views accept only name, icon, itemLayout, and itemLayoutPreset here, not ${unsupported}.`,
|
|
645
|
+
400,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
const { itemLayoutPreset: _preset, ...rest } = input.patch;
|
|
649
|
+
const itemLayout = nextListItemLayout(
|
|
650
|
+
input.patch,
|
|
651
|
+
existing,
|
|
652
|
+
context.definitions,
|
|
653
|
+
);
|
|
654
|
+
patch = itemLayout === undefined ? rest : { ...rest, itemLayout };
|
|
655
|
+
} else if (existing.type === "table") {
|
|
656
|
+
const listKey = patchKeys.find((key) => key in listPatchFields);
|
|
657
|
+
if (listKey) {
|
|
658
|
+
setupError(
|
|
659
|
+
"UNSUPPORTED_SCHEMA_CHANGE",
|
|
660
|
+
`${listKey} applies only to list views.`,
|
|
661
|
+
400,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
validateTablePatch(input.patch, context.definitions);
|
|
665
|
+
} else {
|
|
377
666
|
setupError(
|
|
378
667
|
"UNSUPPORTED_SCHEMA_CHANGE",
|
|
379
|
-
"Ordinary database setup can update only table views.",
|
|
668
|
+
"Ordinary database setup can update only table and list views.",
|
|
380
669
|
400,
|
|
381
670
|
);
|
|
382
671
|
}
|
|
383
|
-
validateTablePatch(input.patch, context.definitions);
|
|
384
672
|
viewId = existing.id;
|
|
385
673
|
const index = rawViews.findIndex(
|
|
386
674
|
(view) =>
|
|
@@ -398,7 +686,7 @@ export async function runUpdateContentDatabaseView(
|
|
|
398
686
|
nextViews = [...rawViews];
|
|
399
687
|
nextViews[index] = {
|
|
400
688
|
...(rawViews[index] as Record<string, unknown>),
|
|
401
|
-
...
|
|
689
|
+
...patch,
|
|
402
690
|
};
|
|
403
691
|
}
|
|
404
692
|
|
|
@@ -480,9 +768,15 @@ export async function runReplaceContentDatabaseViews(input: {
|
|
|
480
768
|
typeof candidate === "object" &&
|
|
481
769
|
(candidate as { id?: unknown }).id === view.id,
|
|
482
770
|
);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
771
|
+
const savedView =
|
|
772
|
+
saved && typeof saved === "object"
|
|
773
|
+
? (saved as Record<string, unknown>)
|
|
774
|
+
: null;
|
|
775
|
+
return withSavableItemLayout(
|
|
776
|
+
savedView ? { ...savedView, ...view } : view,
|
|
777
|
+
savedView?.itemLayout,
|
|
778
|
+
context.definitions,
|
|
779
|
+
);
|
|
486
780
|
});
|
|
487
781
|
const nextRaw: Record<string, unknown> = {
|
|
488
782
|
...rawConfig,
|
|
@@ -65,19 +65,56 @@ function checkedWithin(lastRefreshedAt: string | null, ms: number) {
|
|
|
65
65
|
return Number.isFinite(at) && Date.now() - at < ms;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function linkState(
|
|
68
|
+
function linkState(metadataJson: string | undefined) {
|
|
69
69
|
let metadata: unknown;
|
|
70
70
|
try {
|
|
71
|
-
metadata = JSON.parse(
|
|
71
|
+
metadata = JSON.parse(metadataJson ?? "");
|
|
72
72
|
} catch {
|
|
73
|
-
// coercion-ok: a link whose metadata can't be read has no
|
|
73
|
+
// coercion-ok: a link whose metadata can't be read has no state; the sweep passes it by.
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
return readGitHubSourceState(metadata);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function linkedBy(link: GitHubLink): string | null {
|
|
80
|
-
return linkState(link)?.linkedBy || null;
|
|
80
|
+
return linkState(link.metadataJson)?.linkedBy || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** What the collection's status line shows about a link, as one comparable string. */
|
|
84
|
+
function statusShown(metadataJson: string | undefined) {
|
|
85
|
+
const state = linkState(metadataJson);
|
|
86
|
+
return JSON.stringify([
|
|
87
|
+
state?.syncedCommitSha ?? null,
|
|
88
|
+
state?.pendingCommitSha ?? null,
|
|
89
|
+
state?.progress?.done ?? null,
|
|
90
|
+
state?.progress?.total ?? null,
|
|
91
|
+
state?.problem?.code ?? null,
|
|
92
|
+
state?.skippedCount ?? 0,
|
|
93
|
+
state?.skipped.map((file) => `${file.reason}:${file.path}`) ?? [],
|
|
94
|
+
state?.imagesWaiting ?? 0,
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reads what a link's status line shows now. Compare a reading from before a
|
|
100
|
+
* sync with one from after to learn whether the sync changed it, like a new
|
|
101
|
+
* problem or a few more files done.
|
|
102
|
+
*/
|
|
103
|
+
export async function readGitHubStatusShown(sourceId: string) {
|
|
104
|
+
const [source] = await getDb()
|
|
105
|
+
.select({ metadataJson: schema.contentDatabaseSources.metadataJson })
|
|
106
|
+
.from(schema.contentDatabaseSources)
|
|
107
|
+
.where(eq(schema.contentDatabaseSources.id, sourceId))
|
|
108
|
+
.limit(1);
|
|
109
|
+
return statusShown(source?.metadataJson);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether a sync changed what the status line shows, like a new problem. */
|
|
113
|
+
async function statusChanged(link: GitHubLink) {
|
|
114
|
+
return (
|
|
115
|
+
statusShown(link.metadataJson) !==
|
|
116
|
+
(await readGitHubStatusShown(link.sourceId))
|
|
117
|
+
);
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
export function githubSyncChangedRows(result: GitHubSyncResult) {
|
|
@@ -85,30 +122,40 @@ export function githubSyncChangedRows(result: GitHubSyncResult) {
|
|
|
85
122
|
}
|
|
86
123
|
|
|
87
124
|
/**
|
|
88
|
-
* Tells everyone who can read the collection that
|
|
89
|
-
*
|
|
90
|
-
*
|
|
125
|
+
* Tells everyone who can read the collection that its rows or status changed.
|
|
126
|
+
* It is the event the sync action sends, so open pages, tables, page lists,
|
|
127
|
+
* Trash and the status line update in place. An action whose sync failed
|
|
128
|
+
* sends it too, since a failed action sends no change event of its own.
|
|
91
129
|
*/
|
|
92
|
-
function
|
|
93
|
-
|
|
130
|
+
export function announceGitHubSyncChange(
|
|
131
|
+
databaseDocumentId: string,
|
|
94
132
|
owner: string | null,
|
|
95
|
-
result: GitHubSyncResult,
|
|
96
133
|
) {
|
|
97
|
-
if (!githubSyncChangedRows(result)) return;
|
|
98
134
|
recordChange({
|
|
99
135
|
source: "action",
|
|
100
136
|
type: "change",
|
|
101
137
|
key: "sync-github-folder",
|
|
102
138
|
...(owner ? { owner } : {}),
|
|
103
|
-
...documentChangeResource(
|
|
139
|
+
...documentChangeResource(databaseDocumentId),
|
|
104
140
|
});
|
|
105
141
|
}
|
|
106
142
|
|
|
143
|
+
/** For a sync nobody asked for that changed rows or the status line. */
|
|
144
|
+
async function announceSync(
|
|
145
|
+
link: GitHubLink,
|
|
146
|
+
owner: string | null,
|
|
147
|
+
result: GitHubSyncResult,
|
|
148
|
+
) {
|
|
149
|
+
if (result.state === "busy") return;
|
|
150
|
+
if (!githubSyncChangedRows(result) && !(await statusChanged(link))) return;
|
|
151
|
+
announceGitHubSyncChange(link.databaseDocumentId, owner);
|
|
152
|
+
}
|
|
153
|
+
|
|
107
154
|
async function syncBeforeRead(link: GitHubLink | undefined) {
|
|
108
155
|
if (!link || checkedWithin(link.lastRefreshedAt, GITHUB_SOURCE_STALE_MS)) {
|
|
109
156
|
return false;
|
|
110
157
|
}
|
|
111
|
-
const state = linkState(link);
|
|
158
|
+
const state = linkState(link.metadataJson);
|
|
112
159
|
if (
|
|
113
160
|
state?.pendingCommitSha &&
|
|
114
161
|
checkedWithin(state.checkedAt, PUSH_LEFT_TO_SWEEP_MS)
|
|
@@ -116,8 +163,9 @@ async function syncBeforeRead(link: GitHubLink | undefined) {
|
|
|
116
163
|
return false;
|
|
117
164
|
}
|
|
118
165
|
const until = Date.now() + READ_SYNC_BUDGET_MS;
|
|
166
|
+
let result: GitHubSyncResult;
|
|
119
167
|
try {
|
|
120
|
-
|
|
168
|
+
result = await syncGitHubSource({
|
|
121
169
|
sourceId: link.sourceId,
|
|
122
170
|
budgetMs: READ_SYNC_BUDGET_MS,
|
|
123
171
|
});
|
|
@@ -129,8 +177,6 @@ async function syncBeforeRead(link: GitHubLink | undefined) {
|
|
|
129
177
|
if (!result.commitSha) await waitForGitHubSync(link.sourceId, until);
|
|
130
178
|
return true;
|
|
131
179
|
}
|
|
132
|
-
announceSync(link, linkedBy(link), result);
|
|
133
|
-
return githubSyncChangedRows(result);
|
|
134
180
|
} catch (error) {
|
|
135
181
|
// coercion-ok: the read goes on with the rows Content has; the next read or background sync tries GitHub again.
|
|
136
182
|
console.warn(
|
|
@@ -139,6 +185,15 @@ async function syncBeforeRead(link: GitHubLink | undefined) {
|
|
|
139
185
|
);
|
|
140
186
|
return false;
|
|
141
187
|
}
|
|
188
|
+
// The rows changed either way, so the read rereads them even when telling
|
|
189
|
+
// open pages fails.
|
|
190
|
+
await announceSync(link, linkedBy(link), result).catch((error: unknown) => {
|
|
191
|
+
console.warn(
|
|
192
|
+
"[github-source] change event failed:",
|
|
193
|
+
error instanceof Error ? error.message : String(error),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
return githubSyncChangedRows(result);
|
|
142
197
|
}
|
|
143
198
|
|
|
144
199
|
/**
|
|
@@ -237,7 +292,7 @@ export async function runGitHubSourceSweep(context: RecurringSweepContext) {
|
|
|
237
292
|
signal: context.signal,
|
|
238
293
|
}),
|
|
239
294
|
);
|
|
240
|
-
announceSync(link, owner, result);
|
|
295
|
+
await announceSync(link, owner, result);
|
|
241
296
|
} catch (error) {
|
|
242
297
|
// coercion-ok: one link's failure is recorded on that link by the sync; the rest still run.
|
|
243
298
|
console.warn(
|
|
@@ -14,6 +14,17 @@ import {
|
|
|
14
14
|
/** Most skipped files a link remembers for its status line. */
|
|
15
15
|
const MAX_REMEMBERED_SKIPS = 50;
|
|
16
16
|
|
|
17
|
+
/** A file the folder has that no row shows, and why. */
|
|
18
|
+
export interface GitHubSkippedFile {
|
|
19
|
+
path: string;
|
|
20
|
+
reason: GitHubSourceSkipReason;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A file skipped once it was read, with the blob it was read at. */
|
|
24
|
+
export interface GitHubReadSkip extends GitHubSkippedFile {
|
|
25
|
+
blobSha: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
29
|
* Everything a GitHub link keeps between syncs, stored under `github` in the
|
|
19
30
|
* source's metadata. Rows keep their own blob SHA, so a sync that stops
|
|
@@ -40,7 +51,15 @@ export interface GitHubSourceState extends GitHubFolderLocation {
|
|
|
40
51
|
*/
|
|
41
52
|
cutShort: string[];
|
|
42
53
|
files: number;
|
|
43
|
-
|
|
54
|
+
/** Up to 50 skipped files; `skippedCount` says how many there are. */
|
|
55
|
+
skipped: GitHubSkippedFile[];
|
|
56
|
+
skippedCount: number;
|
|
57
|
+
/**
|
|
58
|
+
* Every file skipped once it was read, so later syncs pass each by while
|
|
59
|
+
* its blob is unchanged rather than reading it again. One per Markdown
|
|
60
|
+
* file at most, so a link never keeps more than 2,000.
|
|
61
|
+
*/
|
|
62
|
+
readSkips: GitHubReadSkip[];
|
|
44
63
|
/** Images rows couldn't store yet because file storage isn't set up. */
|
|
45
64
|
imagesWaiting: number;
|
|
46
65
|
problem: {
|
|
@@ -103,6 +122,24 @@ function stringOrNull(value: unknown): string | null {
|
|
|
103
122
|
return typeof value === "string" && value ? value : null;
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
function readSkippedFiles(
|
|
126
|
+
value: unknown,
|
|
127
|
+
): Array<GitHubSkippedFile & { blobSha?: unknown }> {
|
|
128
|
+
return (Array.isArray(value) ? value : []).flatMap((raw) => {
|
|
129
|
+
const skip = asRecord(raw);
|
|
130
|
+
if (
|
|
131
|
+
!skip ||
|
|
132
|
+
typeof skip.path !== "string" ||
|
|
133
|
+
(skip.reason !== "mdx" &&
|
|
134
|
+
skip.reason !== "too-large" &&
|
|
135
|
+
skip.reason !== "not-text")
|
|
136
|
+
) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
return [{ path: skip.path, reason: skip.reason, blobSha: skip.blobSha }];
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
export function readGitHubSourceState(
|
|
107
144
|
metadata: unknown,
|
|
108
145
|
): GitHubSourceState | null {
|
|
@@ -119,6 +156,16 @@ export function readGitHubSourceState(
|
|
|
119
156
|
const progress = asRecord(github.progress);
|
|
120
157
|
const problem = asRecord(github.problem);
|
|
121
158
|
const claim = asRecord(github.claim);
|
|
159
|
+
const skipped = readSkippedFiles(github.skipped).map(({ path, reason }) => ({
|
|
160
|
+
path,
|
|
161
|
+
reason,
|
|
162
|
+
}));
|
|
163
|
+
const readSkips = readSkippedFiles(github.readSkips).flatMap(
|
|
164
|
+
(skip): GitHubReadSkip[] =>
|
|
165
|
+
typeof skip.blobSha === "string"
|
|
166
|
+
? [{ path: skip.path, reason: skip.reason, blobSha: skip.blobSha }]
|
|
167
|
+
: [],
|
|
168
|
+
);
|
|
122
169
|
return {
|
|
123
170
|
owner: github.owner,
|
|
124
171
|
repo: github.repo,
|
|
@@ -143,18 +190,12 @@ export function readGitHubSourceState(
|
|
|
143
190
|
(path): path is string => typeof path === "string",
|
|
144
191
|
),
|
|
145
192
|
files: typeof github.files === "number" ? github.files : 0,
|
|
146
|
-
skipped
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
skip.reason === "too-large" ||
|
|
153
|
-
skip.reason === "not-text")
|
|
154
|
-
? [{ path: skip.path, reason: skip.reason }]
|
|
155
|
-
: [];
|
|
156
|
-
},
|
|
157
|
-
),
|
|
193
|
+
skipped,
|
|
194
|
+
skippedCount:
|
|
195
|
+
typeof github.skippedCount === "number"
|
|
196
|
+
? github.skippedCount
|
|
197
|
+
: skipped.length,
|
|
198
|
+
readSkips,
|
|
158
199
|
imagesWaiting:
|
|
159
200
|
typeof github.imagesWaiting === "number" ? github.imagesWaiting : 0,
|
|
160
201
|
problem:
|
|
@@ -174,10 +215,14 @@ export function readGitHubSourceState(
|
|
|
174
215
|
};
|
|
175
216
|
}
|
|
176
217
|
|
|
218
|
+
/** The first 50 skipped files in path order, which the status line lists. */
|
|
177
219
|
export function rememberedSkips(
|
|
178
|
-
skipped:
|
|
179
|
-
) {
|
|
180
|
-
return skipped
|
|
220
|
+
skipped: GitHubSkippedFile[],
|
|
221
|
+
): GitHubSkippedFile[] {
|
|
222
|
+
return [...skipped]
|
|
223
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
224
|
+
.slice(0, MAX_REMEMBERED_SKIPS)
|
|
225
|
+
.map(({ path, reason }) => ({ path, reason }));
|
|
181
226
|
}
|
|
182
227
|
|
|
183
228
|
/** The status agents and the status line read; never includes the claim. */
|
|
@@ -199,7 +244,8 @@ export function githubSourceStatus(
|
|
|
199
244
|
pendingCommitSha: state.pendingCommitSha,
|
|
200
245
|
progress: state.progress,
|
|
201
246
|
files: state.files,
|
|
202
|
-
skipped: state.skipped,
|
|
247
|
+
skipped: state.skipped.map(({ path, reason }) => ({ path, reason })),
|
|
248
|
+
skippedCount: state.skippedCount,
|
|
203
249
|
imagesWaiting: state.imagesWaiting,
|
|
204
250
|
problem: state.problem,
|
|
205
251
|
};
|