@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
- package/corpus/templates/analytics/CHANGELOG.md +18 -0
- package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
- package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
- package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
- package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
- package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
- package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
- package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
- package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
- package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
- package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
- package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
- package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
- package/corpus/templates/analytics/server/db/schema.ts +1 -0
- package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
- package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
- package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
- package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
- package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
- package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
- package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
- package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
- package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
- package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
- package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
- package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
- package/corpus/templates/analytics/server/plugins/db.ts +9 -1
- package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
- package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
- package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/actions/delete-event.ts +1 -3
- package/corpus/templates/calendar/actions/delete-events.ts +1 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
- package/corpus/templates/calendar/actions/update-event.ts +2 -6
- package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/CHANGELOG.md +11 -24
- package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
- package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
- package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
- package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
- package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
- package/corpus/templates/clips/app/root.tsx +22 -10
- package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
- package/corpus/templates/clips/app/routes/import.tsx +115 -110
- package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +29 -186
- package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
- package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
- package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
- package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
- package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
- package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/content/CHANGELOG.md +12 -0
- package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
- package/corpus/templates/content/actions/_database-navigation.ts +3 -3
- package/corpus/templates/content/actions/_database-utils.ts +10 -22
- package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
- package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
- package/corpus/templates/content/actions/export-document.ts +2 -14
- package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
- package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
- package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
- package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
- package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
- package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
- package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
- package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
- package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
- package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
- package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
- package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
- package/corpus/templates/content/app/global.css +23 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
- 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
- package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
- package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
- package/corpus/templates/content/e2e/helpers.ts +10 -0
- package/corpus/templates/content/shared/api.ts +18 -12
- package/corpus/templates/content/shared/database-query.ts +107 -13
- package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
- package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/CHANGELOG.md +2 -0
- package/corpus/templates/design/actions/delete-file.ts +1 -4
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
- 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
- package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +185 -85
- package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
- package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
- package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
- package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
- package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
- package/corpus/templates/mail/AGENTS.md +1 -1
- package/corpus/templates/mail/CHANGELOG.md +14 -0
- package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
- package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
- package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
- package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
- package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
- package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
- package/corpus/templates/mail/app/root.tsx +10 -2
- package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
- package/corpus/templates/mail/server/handlers/emails.ts +9 -0
- package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/plan/CHANGELOG.md +4 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
- package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
- package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
- package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
- package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
- package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
- 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
|
|
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 {
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
}
|