@agent-native/core-corpus 0.210.2 → 0.211.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.
Files changed (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
  3. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
  4. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
  5. package/corpus/templates/analytics/CHANGELOG.md +18 -0
  6. package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
  7. package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
  8. package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
  9. package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
  10. package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
  11. package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
  12. package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
  13. package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
  14. package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
  15. package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
  16. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  17. package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
  18. package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
  19. package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
  20. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
  21. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
  22. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
  23. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
  24. package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
  25. package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
  26. package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
  27. package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
  37. package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
  38. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  39. package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
  40. package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
  41. package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
  42. package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
  43. package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
  44. package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
  45. package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
  46. package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
  47. package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
  48. package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
  49. package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
  51. package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
  52. package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
  53. package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
  54. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
  55. package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
  56. package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
  57. package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
  58. package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
  59. package/corpus/templates/analytics/server/plugins/db.ts +9 -1
  60. package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
  61. package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
  62. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
  63. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
  64. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
  65. package/corpus/templates/calendar/actions/delete-event.ts +1 -3
  66. package/corpus/templates/calendar/actions/delete-events.ts +1 -2
  67. package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
  68. package/corpus/templates/calendar/actions/update-event.ts +2 -6
  69. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  70. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
  71. package/corpus/templates/clips/CHANGELOG.md +11 -24
  72. package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
  73. package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
  74. package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
  75. package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
  76. package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
  77. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  78. package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
  79. package/corpus/templates/clips/app/root.tsx +22 -10
  80. package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
  81. package/corpus/templates/clips/app/routes/import.tsx +115 -110
  82. package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
  88. package/corpus/templates/clips/desktop/src/app.tsx +29 -186
  89. package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
  90. package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
  91. package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
  92. package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
  93. package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
  94. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
  95. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
  96. package/corpus/templates/content/CHANGELOG.md +12 -0
  97. package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
  98. package/corpus/templates/content/actions/_database-navigation.ts +3 -3
  99. package/corpus/templates/content/actions/_database-utils.ts +10 -22
  100. package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
  101. package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
  102. package/corpus/templates/content/actions/export-document.ts +2 -14
  103. package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
  104. package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
  105. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
  106. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
  107. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
  108. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
  109. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
  110. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
  111. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
  112. package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
  113. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
  114. package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
  115. package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
  116. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
  117. package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
  118. package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
  119. package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
  120. package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
  121. package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
  122. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
  123. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
  124. package/corpus/templates/content/app/global.css +23 -0
  125. package/corpus/templates/content/app/i18n-data.ts +1 -0
  126. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
  127. package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
  128. package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
  132. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
  133. package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
  134. package/corpus/templates/content/e2e/helpers.ts +10 -0
  135. package/corpus/templates/content/shared/api.ts +18 -12
  136. package/corpus/templates/content/shared/database-query.ts +107 -13
  137. package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
  138. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
  139. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
  140. package/corpus/templates/design/CHANGELOG.md +2 -0
  141. package/corpus/templates/design/actions/delete-file.ts +1 -4
  142. package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
  143. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
  144. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
  145. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
  146. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
  147. package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
  149. package/corpus/templates/design/e2e/global-setup.ts +185 -85
  150. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
  151. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  152. package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
  153. package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
  154. package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
  155. package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
  156. package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
  157. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
  158. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  159. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  160. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  161. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
  162. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
  163. package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
  164. package/corpus/templates/mail/AGENTS.md +1 -1
  165. package/corpus/templates/mail/CHANGELOG.md +14 -0
  166. package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
  167. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
  168. package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
  169. package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
  170. package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
  171. package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
  172. package/corpus/templates/mail/app/root.tsx +10 -2
  173. package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
  174. package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
  175. package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
  176. package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
  177. package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
  178. package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
  179. package/corpus/templates/mail/server/handlers/emails.ts +9 -0
  180. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
  181. package/corpus/templates/plan/CHANGELOG.md +4 -0
  182. package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
  183. package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
  184. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
  185. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
  186. package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
  187. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
  188. package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
  189. package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
  190. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
  191. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
  192. package/package.json +1 -1
@@ -1,33 +1,23 @@
1
1
  // i18n-raw-literal-disable-file -- unused pure helper copy; live database editor owns localized UI.
2
- // Filter/sort logic: apply, upsert, move, match, property value helpers.
2
+ // Filter/sort helpers: apply, upsert, quick filters, property values.
3
3
  // Pure logic — no React, no icons.
4
4
  import type {
5
5
  ContentDatabaseItem,
6
6
  DocumentProperty,
7
7
  DocumentPropertyType,
8
- DocumentPropertyValue,
9
8
  } from "@shared/api";
10
- import { formulaValueText, isComputedPropertyType } from "@shared/properties";
9
+ import { applyContentDatabaseTableQuery } from "@shared/database-query";
10
+ import { formulaValueText } from "@shared/properties";
11
11
 
12
- import { propertyDateValue } from "./calendar-timeline";
13
12
  import {
14
13
  type ColumnKey,
15
- type DatabaseConditionMoveDirection,
16
14
  type DatabaseFilter,
17
15
  type DatabaseFilterMode,
18
16
  type DatabaseQuickFilterOperator,
19
- type DatabaseRowDensity,
20
17
  type FilterOperator,
21
18
  type SortDirection,
22
19
  type DatabaseSort,
23
20
  } from "./types";
24
- export {
25
- normalizeClientDatabaseFilterMode,
26
- normalizeClientDatabaseRowDensity,
27
- normalizeClientDatabaseOpenPagesIn,
28
- } from "./view-config";
29
-
30
- export { type DatabaseConditionMoveDirection };
31
21
 
32
22
  const DATABASE_QUICK_FILTER_OPERATORS: FilterOperator[] = [
33
23
  "is_empty",
@@ -44,104 +34,14 @@ export function applyDatabaseView(
44
34
  sorts: DatabaseSort[],
45
35
  filterMode: DatabaseFilterMode = "and",
46
36
  ) {
47
- const query = searchQuery.trim().toLowerCase();
48
- const searched = query
49
- ? items.filter((item) =>
50
- databaseItemSearchText(item, properties).toLowerCase().includes(query),
51
- )
52
- : items;
53
- const activeFilters = filters.filter(isActiveFilter);
54
- const filtered = activeFilters.length
55
- ? searched.filter((item) =>
56
- databaseItemMatchesFilterTree(
57
- item,
58
- properties,
59
- activeFilters,
60
- filterMode,
61
- ),
62
- )
63
- : searched;
64
-
65
- if (sorts.length === 0) return filtered;
66
-
67
- return [...filtered].sort((a, b) => {
68
- for (const sort of sorts) {
69
- const comparison = compareDatabaseSortValues(
70
- databaseItemSortValue(a, properties, sort.key),
71
- databaseItemSortValue(b, properties, sort.key),
72
- );
73
- if (comparison !== 0) {
74
- return sort.direction === "asc" ? comparison : -comparison;
75
- }
76
- }
77
- return 0;
37
+ return applyContentDatabaseTableQuery(items, properties, {
38
+ search: searchQuery,
39
+ filters,
40
+ sorts,
41
+ filterMode,
78
42
  });
79
43
  }
80
44
 
81
- function databaseItemMatchesFilterTree(
82
- item: ContentDatabaseItem,
83
- properties: DocumentProperty[],
84
- filters: DatabaseFilter[],
85
- filterMode: DatabaseFilterMode,
86
- ) {
87
- const rootFilters = filters.filter((filter) => !filter.parentFilterGroupId);
88
- const nestedGroups = nestedDatabaseFilterGroups(filters);
89
- const matches = [
90
- ...rootFilters.map((filter) =>
91
- databaseItemMatchesFilter(item, properties, filter),
92
- ),
93
- ...nestedGroups.map((group) =>
94
- combineDatabaseFilterMatches(
95
- group.map((filter) =>
96
- databaseItemMatchesFilter(item, properties, filter),
97
- ),
98
- filterMode,
99
- ),
100
- ),
101
- ];
102
-
103
- return combineDatabaseFilterMatches(matches, filterMode);
104
- }
105
-
106
- function nestedDatabaseFilterGroups(filters: DatabaseFilter[]) {
107
- const groups = new Map<string, DatabaseFilter[]>();
108
- for (const filter of filters) {
109
- if (!filter.parentFilterGroupId || !filter.filterGroupId) continue;
110
- groups.set(filter.filterGroupId, [
111
- ...(groups.get(filter.filterGroupId) ?? []),
112
- filter,
113
- ]);
114
- }
115
- return [...groups.values()].filter((group) => group.length > 0);
116
- }
117
-
118
- function combineDatabaseFilterMatches(
119
- matches: boolean[],
120
- filterMode: DatabaseFilterMode,
121
- ) {
122
- if (matches.length === 0) return true;
123
- return filterMode === "or"
124
- ? matches.some((matched) => matched)
125
- : matches.every((matched) => matched);
126
- }
127
-
128
- export function defaultDatabaseSort(): DatabaseSort {
129
- return {
130
- key: "name",
131
- label: "Name",
132
- direction: "asc",
133
- };
134
- }
135
-
136
- export function defaultDatabaseFilter(): DatabaseFilter {
137
- return {
138
- key: "name",
139
- label: "Name",
140
- operator: "contains",
141
- value: "",
142
- };
143
- }
144
-
145
45
  export function upsertDatabaseSort(
146
46
  sorts: DatabaseSort[],
147
47
  key: ColumnKey,
@@ -158,48 +58,6 @@ export function clearDatabaseSort(sorts: DatabaseSort[], key: ColumnKey) {
158
58
  return sorts.filter((sort) => sort.key !== key);
159
59
  }
160
60
 
161
- export function moveDatabaseSort(
162
- sorts: DatabaseSort[],
163
- index: number,
164
- direction: DatabaseConditionMoveDirection,
165
- ) {
166
- return moveDatabaseCondition(sorts, index, direction);
167
- }
168
-
169
- export function appendDatabaseFilter(
170
- filters: DatabaseFilter[],
171
- key: ColumnKey,
172
- label: string,
173
- operator: FilterOperator,
174
- value = "",
175
- ) {
176
- return [...filters, { key, label, operator, value }];
177
- }
178
-
179
- export function moveDatabaseFilter(
180
- filters: DatabaseFilter[],
181
- index: number,
182
- direction: DatabaseConditionMoveDirection,
183
- ) {
184
- return moveDatabaseCondition(filters, index, direction);
185
- }
186
-
187
- function moveDatabaseCondition<T>(
188
- items: T[],
189
- index: number,
190
- direction: DatabaseConditionMoveDirection,
191
- ) {
192
- const targetIndex = direction === "up" ? index - 1 : index + 1;
193
- if (index < 0 || targetIndex < 0 || targetIndex >= items.length) {
194
- return items;
195
- }
196
-
197
- const next = [...items];
198
- next[index] = items[targetIndex];
199
- next[targetIndex] = items[index];
200
- return next;
201
- }
202
-
203
61
  export function databaseQuickFilterOptionsForColumn(
204
62
  propertyType?: DocumentPropertyType,
205
63
  ): Array<{ operator: DatabaseQuickFilterOperator; label: string }> {
@@ -238,357 +96,6 @@ export function clearDatabaseFiltersForColumn(
238
96
  return filters.filter((filter) => filter.key !== key);
239
97
  }
240
98
 
241
- export function databaseColumnHeaderState(
242
- sorts: DatabaseSort[],
243
- filters: DatabaseFilter[],
244
- key: ColumnKey,
245
- ) {
246
- const sort = sorts.find((candidate) => candidate.key === key);
247
- return {
248
- sortDirection: sort?.direction ?? null,
249
- activeFilterCount: filters.filter(
250
- (filter) => filter.key === key && isActiveFilter(filter),
251
- ).length,
252
- };
253
- }
254
-
255
- function databaseColumnHeaderStateLabel(
256
- state: ReturnType<typeof databaseColumnHeaderState>,
257
- ) {
258
- const parts = [
259
- state.sortDirection
260
- ? `Sorted ${state.sortDirection === "asc" ? "ascending" : "descending"}`
261
- : "",
262
- state.activeFilterCount > 0
263
- ? `${state.activeFilterCount} active filter${state.activeFilterCount === 1 ? "" : "s"}`
264
- : "",
265
- ].filter(Boolean);
266
- return parts.join(", ");
267
- }
268
-
269
- export { databaseColumnHeaderStateLabel };
270
-
271
- export function activeDatabaseConstraintCount(
272
- searchQuery: string,
273
- sorts: DatabaseSort[],
274
- filters: DatabaseFilter[],
275
- ) {
276
- return (
277
- (searchQuery.trim() ? 1 : 0) +
278
- sorts.length +
279
- filters.filter(isActiveFilter).length
280
- );
281
- }
282
-
283
- export function isActiveFilter(
284
- filter: DatabaseFilter | null,
285
- ): filter is DatabaseFilter {
286
- if (!filter) return false;
287
- if (filterOperatorNeedsValue(filter.operator)) {
288
- return filter.value.trim().length > 0;
289
- }
290
- return true;
291
- }
292
-
293
- export function databasePropertyValuesForNewItem(
294
- filters: DatabaseFilter[],
295
- properties: DocumentProperty[],
296
- filterMode: DatabaseFilterMode = "and",
297
- ): Record<string, DocumentPropertyValue> {
298
- const propertyValues: Record<string, DocumentPropertyValue> = {};
299
- const activeFilters = filters.filter(isActiveFilter);
300
- if (filterMode === "or" && activeFilters.length > 1) {
301
- return propertyValues;
302
- }
303
-
304
- for (const filter of activeFilters) {
305
- if (filter.key === "name") continue;
306
-
307
- const property = properties.find(
308
- (candidate) => candidate.definition.id === filter.key,
309
- );
310
- if (!property?.editable) continue;
311
- if (isComputedPropertyType(property.definition.type)) continue;
312
- if (propertyValues[property.definition.id] !== undefined) continue;
313
-
314
- const value = databaseFilterDefaultValueForNewItem(filter, property);
315
- if (value !== undefined) {
316
- propertyValues[property.definition.id] = value;
317
- }
318
- }
319
-
320
- return propertyValues;
321
- }
322
-
323
- function databaseFilterDefaultValueForNewItem(
324
- filter: DatabaseFilter,
325
- property: DocumentProperty,
326
- ): DocumentPropertyValue | undefined {
327
- if (filter.operator === "is_checked") {
328
- return property.definition.type === "checkbox" ? true : undefined;
329
- }
330
- if (filter.operator === "is_unchecked") {
331
- return property.definition.type === "checkbox" ? false : undefined;
332
- }
333
- if (property.definition.type === "multi_select") {
334
- if (filter.operator !== "equals" && filter.operator !== "contains") {
335
- return undefined;
336
- }
337
- const values = databaseFilterSelectedValues(filter.value);
338
- if (values.length === 0) return undefined;
339
- return values.map(
340
- (value) =>
341
- databasePropertyOptionIdForFilterValue(property, value) ?? value,
342
- );
343
- }
344
- if (property.definition.type === "person" && filter.operator === "contains") {
345
- const values = databaseFilterSelectedValues(filter.value);
346
- return values.length > 0 ? values : undefined;
347
- }
348
- if (filter.operator !== "equals") return undefined;
349
-
350
- const value = filter.value.trim();
351
- if (!value) return undefined;
352
-
353
- const optionValue = databasePropertyOptionIdForFilterValue(property, value);
354
- if (
355
- property.definition.type === "select" ||
356
- property.definition.type === "status"
357
- ) {
358
- return optionValue ?? value;
359
- }
360
- if (property.definition.type === "date") {
361
- return /^\d{4}-\d{2}-\d{2}$/.test(value)
362
- ? { start: value, includeTime: false }
363
- : undefined;
364
- }
365
- if (property.definition.type === "checkbox") return undefined;
366
-
367
- return value;
368
- }
369
-
370
- function databasePropertyOptionIdForFilterValue(
371
- property: DocumentProperty,
372
- value: string,
373
- ) {
374
- return property.definition.options.options?.find(
375
- (option) =>
376
- option.id === value ||
377
- option.name.trim().toLowerCase() === value.trim().toLowerCase(),
378
- )?.id;
379
- }
380
-
381
- function databaseFilterSelectedValues(value: string) {
382
- const trimmed = value.trim();
383
- if (!trimmed) return [];
384
- try {
385
- const parsed = JSON.parse(trimmed) as unknown;
386
- if (!Array.isArray(parsed)) return [trimmed];
387
- return [
388
- ...new Set(
389
- parsed
390
- .filter((item): item is string => typeof item === "string")
391
- .map((item) => item.trim())
392
- .filter(Boolean),
393
- ),
394
- ];
395
- } catch {
396
- return [trimmed];
397
- }
398
- }
399
-
400
- function databaseFilterDateRangeValues(value: string): [string, string] {
401
- const trimmed = value.trim();
402
- if (!trimmed) return ["", ""];
403
- try {
404
- const parsed = JSON.parse(trimmed) as unknown;
405
- if (!Array.isArray(parsed)) return [trimmed, ""];
406
- const start = typeof parsed[0] === "string" ? parsed[0].trim() : "";
407
- const end = typeof parsed[1] === "string" ? parsed[1].trim() : "";
408
- return [start, end];
409
- } catch {
410
- return [trimmed, ""];
411
- }
412
- }
413
-
414
- function databaseFilterDateRangeValue(value: string): [number, number] | null {
415
- const [startValue, endValue] = databaseFilterDateRangeValues(value);
416
- const start = new Date(startValue).getTime();
417
- const end = new Date(endValue).getTime();
418
- if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
419
- return start <= end ? [start, end] : [end, start];
420
- }
421
-
422
- function databaseItemMatchesFilter(
423
- item: ContentDatabaseItem,
424
- properties: DocumentProperty[],
425
- filter: DatabaseFilter,
426
- ) {
427
- const value = databaseItemFilterValue(item, properties, filter.key);
428
- const property = databaseItemFilterProperty(item, properties, filter.key);
429
-
430
- if (filter.operator === "is_empty") return !value.trim();
431
- if (filter.operator === "is_not_empty") return !!value.trim();
432
-
433
- if (filter.operator === "is_checked") return property?.value === true;
434
- if (filter.operator === "is_unchecked") return property?.value !== true;
435
-
436
- if (filter.operator === "greater_than" || filter.operator === "less_than") {
437
- const current = propertyNumberValue(property);
438
- const target = Number(filter.value.trim());
439
- if (!Number.isFinite(current) || !Number.isFinite(target)) return false;
440
- return filter.operator === "greater_than"
441
- ? current > target
442
- : current < target;
443
- }
444
-
445
- if (
446
- filter.operator === "before" ||
447
- filter.operator === "after" ||
448
- filter.operator === "between"
449
- ) {
450
- const current = propertyDateValue(property);
451
- if (!Number.isFinite(current)) return false;
452
- if (filter.operator === "between") {
453
- const range = databaseFilterDateRangeValue(filter.value);
454
- if (!range) return false;
455
- return current >= range[0] && current <= range[1];
456
- }
457
- const target = new Date(filter.value.trim()).getTime();
458
- if (!Number.isFinite(target)) return false;
459
- return filter.operator === "before" ? current < target : current > target;
460
- }
461
-
462
- const candidateValues = databaseItemFilterCandidateValues(
463
- item,
464
- properties,
465
- filter.key,
466
- ).map((candidate) => candidate.trim().toLowerCase());
467
- const selectedFilterValues = databaseFilterSelectedValues(filter.value).map(
468
- (candidate) => candidate.trim().toLowerCase(),
469
- );
470
- const normalizedValue = value.trim().toLowerCase();
471
- const normalizedFilter = selectedFilterValues[0] ?? "";
472
- const usesDiscreteValues =
473
- property?.definition.type === "select" ||
474
- property?.definition.type === "status" ||
475
- property?.definition.type === "multi_select" ||
476
- property?.definition.type === "person";
477
-
478
- if (
479
- usesDiscreteValues &&
480
- (filter.operator === "equals" || filter.operator === "contains")
481
- ) {
482
- return selectedFilterValues.some((filterValue) =>
483
- candidateValues.includes(filterValue),
484
- );
485
- }
486
- if (usesDiscreteValues && filter.operator === "does_not_equal") {
487
- return selectedFilterValues.every(
488
- (filterValue) => !candidateValues.includes(filterValue),
489
- );
490
- }
491
-
492
- if (filter.operator === "equals") {
493
- return candidateValues.includes(normalizedFilter);
494
- }
495
- if (filter.operator === "does_not_equal") {
496
- return !candidateValues.includes(normalizedFilter);
497
- }
498
- return normalizedValue.includes(normalizedFilter);
499
- }
500
-
501
- function databaseItemSearchText(
502
- item: ContentDatabaseItem,
503
- properties: DocumentProperty[],
504
- ) {
505
- return [
506
- item.document.title || "Untitled",
507
- ...properties.map((property) =>
508
- propertyValueText(
509
- item.properties.find(
510
- (candidate) => candidate.definition.id === property.definition.id,
511
- ) ?? property,
512
- ),
513
- ),
514
- ].join(" ");
515
- }
516
-
517
- function databaseItemSortValue(
518
- item: ContentDatabaseItem,
519
- properties: DocumentProperty[],
520
- key: string,
521
- ) {
522
- if (key === "name") return item.document.title || "";
523
- const property = properties.find(
524
- (candidate) => candidate.definition.id === key,
525
- );
526
- const itemProperty = item.properties.find(
527
- (candidate) => candidate.definition.id === key,
528
- );
529
- return propertyValueText(itemProperty ?? property ?? null);
530
- }
531
-
532
- function databaseItemFilterValue(
533
- item: ContentDatabaseItem,
534
- properties: DocumentProperty[],
535
- key: string,
536
- ) {
537
- if (key === "name") return item.document.title || "";
538
- return propertyValueText(databaseItemFilterProperty(item, properties, key));
539
- }
540
-
541
- function databaseItemFilterProperty(
542
- item: ContentDatabaseItem,
543
- properties: DocumentProperty[],
544
- key: string,
545
- ) {
546
- if (key === "name") return null;
547
- const property = properties.find(
548
- (candidate) => candidate.definition.id === key,
549
- );
550
- const itemProperty = item.properties.find(
551
- (candidate) => candidate.definition.id === key,
552
- );
553
- return itemProperty ?? property ?? null;
554
- }
555
-
556
- function databaseItemFilterCandidateValues(
557
- item: ContentDatabaseItem,
558
- properties: DocumentProperty[],
559
- key: string,
560
- ) {
561
- if (key === "name") return [item.document.title || ""];
562
- const property = databaseItemFilterProperty(item, properties, key);
563
- if (!property) return [""];
564
- const value = property.value;
565
-
566
- if (value === null || value === undefined || value === "") return [""];
567
-
568
- if (Array.isArray(value)) {
569
- return value.flatMap((id) => {
570
- const optionName =
571
- property.definition.options.options?.find((option) => option.id === id)
572
- ?.name ?? id;
573
- return [id, optionName];
574
- });
575
- }
576
-
577
- if (
578
- property.definition.type === "select" ||
579
- property.definition.type === "status"
580
- ) {
581
- const id =
582
- typeof value === "string" ? value : (JSON.stringify(value) ?? "");
583
- const optionName =
584
- property.definition.options.options?.find((option) => option.id === id)
585
- ?.name ?? id;
586
- return [id, optionName];
587
- }
588
-
589
- return [propertyValueText(property)];
590
- }
591
-
592
99
  export function propertyValueText(
593
100
  property: DocumentProperty | null | undefined,
594
101
  ) {
@@ -627,23 +134,6 @@ export function propertyValueText(
627
134
  return formulaValueText(value);
628
135
  }
629
136
 
630
- function compareDatabaseSortValues(left: string, right: string) {
631
- const leftNumber = Number(left);
632
- const rightNumber = Number(right);
633
- if (
634
- left.trim() &&
635
- right.trim() &&
636
- Number.isFinite(leftNumber) &&
637
- Number.isFinite(rightNumber)
638
- ) {
639
- return leftNumber - rightNumber;
640
- }
641
- return left.localeCompare(right, undefined, {
642
- numeric: true,
643
- sensitivity: "base",
644
- });
645
- }
646
-
647
137
  export function propertyNumberValue(
648
138
  property: DocumentProperty | null | undefined,
649
139
  ) {
@@ -666,268 +156,3 @@ export function propertyNumberValue(
666
156
  );
667
157
  return Number.isFinite(value) ? value : Number.NaN;
668
158
  }
669
-
670
- export function databaseFilterOptionChoices(
671
- key: string,
672
- properties: DocumentProperty[],
673
- items: ContentDatabaseItem[] = [],
674
- ) {
675
- const property = databaseFilterPropertyForKey(key, properties);
676
- if (property?.definition.type === "person") {
677
- return databaseFilterPersonChoices(property.definition.id, items);
678
- }
679
- const optionProperty = databaseFilterOptionPropertyForKey(key, properties);
680
- return optionProperty?.definition.options.options ?? [];
681
- }
682
-
683
- function databaseFilterPersonChoices(
684
- propertyId: string,
685
- items: ContentDatabaseItem[],
686
- ) {
687
- const people = new Map<string, string>();
688
- for (const item of items) {
689
- const property = item.properties.find(
690
- (candidate) => candidate.definition.id === propertyId,
691
- );
692
- for (const person of databasePersonItems(property?.value ?? null)) {
693
- const key = person.trim().toLowerCase();
694
- if (!people.has(key)) people.set(key, person);
695
- }
696
- }
697
- return Array.from(people.values()).map((person) => ({
698
- id: person,
699
- name: databasePersonLabel(person),
700
- color: "gray" as const,
701
- }));
702
- }
703
-
704
- function databasePersonItems(value: DocumentProperty["value"]) {
705
- const rawItems = Array.isArray(value)
706
- ? value
707
- : typeof value === "string"
708
- ? value.split(/[\n,]+/)
709
- : [];
710
- const seen = new Set<string>();
711
- return rawItems
712
- .map((item) => item.trim())
713
- .filter((item) => {
714
- if (!item) return false;
715
- const key = item.toLowerCase();
716
- if (seen.has(key)) return false;
717
- seen.add(key);
718
- return true;
719
- });
720
- }
721
-
722
- function databasePersonLabel(value: string) {
723
- const trimmed = value.trim();
724
- if (!trimmed) return "Empty";
725
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)
726
- ? trimmed
727
- .split("@")[0]
728
- .split(/[._-]+/)
729
- .filter(Boolean)
730
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
731
- .join(" ")
732
- : trimmed;
733
- }
734
-
735
- export function databaseFilterOptionPropertyForKey(
736
- key: string,
737
- properties: DocumentProperty[],
738
- ) {
739
- const property = databaseFilterPropertyForKey(key, properties);
740
- if (!property) return null;
741
- if (
742
- property.definition.type !== "select" &&
743
- property.definition.type !== "status" &&
744
- property.definition.type !== "multi_select" &&
745
- property.definition.type !== "person"
746
- ) {
747
- return null;
748
- }
749
- return property;
750
- }
751
-
752
- function databaseFilterValueLabel(
753
- filter: DatabaseFilter,
754
- properties: DocumentProperty[],
755
- ) {
756
- if (filter.operator === "between") {
757
- const [start, end] = databaseFilterDateRangeValues(filter.value);
758
- if (start && end) return `${start} to ${end}`;
759
- return start || end || "Choose dates";
760
- }
761
- const option = databaseFilterOptionChoices(filter.key, properties).find(
762
- (candidate) =>
763
- candidate.id === filter.value || candidate.name === filter.value,
764
- );
765
- return (option?.name ?? filter.value) || "Choose option";
766
- }
767
-
768
- export { databaseFilterValueLabel };
769
-
770
- export function databaseFilterChipLabel(
771
- filter: DatabaseFilter,
772
- properties: DocumentProperty[],
773
- ) {
774
- const operator = FILTER_OPERATOR_LABELS[filter.operator] ?? "Contains";
775
- if (!filterOperatorNeedsValue(filter.operator)) {
776
- return `${filter.label} ${operator.toLowerCase()}`;
777
- }
778
- return `${filter.label} ${operator.toLowerCase()} ${databaseFilterValueLabel(
779
- filter,
780
- properties,
781
- )}`;
782
- }
783
-
784
- function databaseFilterPropertyForKey(
785
- key: string,
786
- properties: DocumentProperty[],
787
- ) {
788
- if (key === "name") return null;
789
- return properties.find((property) => property.definition.id === key) ?? null;
790
- }
791
-
792
- export const FILTER_OPERATOR_LABELS: Record<FilterOperator, string> = {
793
- contains: "Contains",
794
- equals: "Is",
795
- does_not_equal: "Is not",
796
- greater_than: "Greater than",
797
- less_than: "Less than",
798
- before: "Before",
799
- after: "After",
800
- between: "Between",
801
- is_checked: "Checked",
802
- is_unchecked: "Unchecked",
803
- is_empty: "Is empty",
804
- is_not_empty: "Is not empty",
805
- };
806
-
807
- export function filterOperatorsForKey(
808
- key: string,
809
- properties: DocumentProperty[],
810
- ): FilterOperator[] {
811
- const type = filterPropertyTypeForKey(key, properties);
812
-
813
- if (type === "checkbox") {
814
- return ["is_checked", "is_unchecked"];
815
- }
816
-
817
- if (
818
- type === "select" ||
819
- type === "status" ||
820
- type === "multi_select" ||
821
- type === "person"
822
- ) {
823
- return ["contains", "does_not_equal", "is_empty", "is_not_empty"];
824
- }
825
-
826
- if (type === "number") {
827
- return [
828
- "equals",
829
- "does_not_equal",
830
- "greater_than",
831
- "less_than",
832
- "is_empty",
833
- "is_not_empty",
834
- ];
835
- }
836
-
837
- if (
838
- type === "date" ||
839
- type === "created_time" ||
840
- type === "last_edited_time"
841
- ) {
842
- return [
843
- "equals",
844
- "does_not_equal",
845
- "before",
846
- "after",
847
- "between",
848
- "is_empty",
849
- "is_not_empty",
850
- ];
851
- }
852
-
853
- return ["contains", "equals", "does_not_equal", "is_empty", "is_not_empty"];
854
- }
855
-
856
- export function defaultFilterOperatorForKey(
857
- key: string,
858
- properties: DocumentProperty[],
859
- ): FilterOperator {
860
- return filterOperatorsForKey(key, properties)[0] ?? "contains";
861
- }
862
-
863
- export function filterPropertyTypeForKey(
864
- key: string,
865
- properties: DocumentProperty[],
866
- ): DocumentPropertyType {
867
- if (key === "name") return "text";
868
- return (
869
- properties.find((property) => property.definition.id === key)?.definition
870
- .type ?? "text"
871
- );
872
- }
873
-
874
- export function filterOperatorNeedsValue(operator: FilterOperator) {
875
- return !["is_empty", "is_not_empty", "is_checked", "is_unchecked"].includes(
876
- operator,
877
- );
878
- }
879
-
880
- export function filterValuePlaceholder(
881
- key: string,
882
- properties: DocumentProperty[],
883
- ) {
884
- const type = filterPropertyTypeForKey(key, properties);
885
- if (type === "number") return "Number";
886
- if (type === "person") return "Person or email";
887
- if (type === "place") return "City, venue, or address";
888
- if (type === "files_media") return "File or media link";
889
- if (type === "date" || type === "created_time" || type === "last_edited_time")
890
- return "YYYY-MM-DD";
891
- return "Value";
892
- }
893
-
894
- export function filterValueInputType(type: DocumentPropertyType) {
895
- if (type === "number") return "number";
896
- if (type === "date" || type === "created_time" || type === "last_edited_time")
897
- return "date";
898
- return "text";
899
- }
900
-
901
- export function databaseTableRowDensityClass(rowDensity: DatabaseRowDensity) {
902
- if (rowDensity === "compact") return "min-h-8";
903
- if (rowDensity === "comfortable") return "min-h-12";
904
- return "min-h-9";
905
- }
906
-
907
- export function databaseTableCellDensityClass(rowDensity: DatabaseRowDensity) {
908
- if (rowDensity === "compact") return "px-2 py-0.5";
909
- if (rowDensity === "comfortable") return "px-2.5 py-2";
910
- return "px-2 py-1";
911
- }
912
-
913
- export function databaseRowNameCellDensityClass(
914
- rowDensity: DatabaseRowDensity,
915
- ) {
916
- if (rowDensity === "compact") return "px-1 py-0.5";
917
- if (rowDensity === "comfortable") return "px-1.5 py-2";
918
- return "px-1 py-1";
919
- }
920
-
921
- export function databaseTitleButtonDensityClass(
922
- rowDensity: DatabaseRowDensity,
923
- wrapCells: boolean,
924
- ) {
925
- if (wrapCells) {
926
- if (rowDensity === "compact") return "min-h-6 py-0.5";
927
- if (rowDensity === "comfortable") return "min-h-9 py-1.5";
928
- return "min-h-7 py-1";
929
- }
930
- if (rowDensity === "compact") return "h-6";
931
- if (rowDensity === "comfortable") return "h-8";
932
- return "h-7";
933
- }