@agent-native/core 0.91.1 → 0.91.2
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/blueprints/action/crud.md +1 -1
- package/blueprints/channel/discord.md +1 -1
- package/blueprints/provider/stripe.md +1 -1
- package/blueprints/sandbox/docker.md +1 -1
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/blueprints/action/crud.md +1 -1
- package/corpus/core/blueprints/channel/discord.md +1 -1
- package/corpus/core/blueprints/provider/stripe.md +1 -1
- package/corpus/core/blueprints/sandbox/docker.md +1 -1
- package/corpus/core/package.json +6 -5
- package/corpus/core/src/agent/run-manager.ts +23 -2
- package/corpus/core/src/agent/run-store.ts +2 -2
- package/corpus/core/src/cli/add.ts +1 -1
- package/corpus/core/src/cli/create.ts +8 -3
- package/corpus/core/src/cli/index.ts +5 -4
- package/corpus/core/src/client/extensions/ExtensionSlot.tsx +7 -0
- package/corpus/core/src/client/session-replay.ts +97 -3
- package/corpus/core/src/jobs/scheduler.ts +30 -1
- package/corpus/core/src/localization/default-messages.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/corpus/core/src/templates/default/package.json +2 -2
- package/corpus/core/src/templates/headless/package.json +3 -3
- package/corpus/core/src/templates/workspace-core/package.json +6 -6
- package/corpus/core/src/templates/workspace-root/package.json +2 -2
- package/corpus/core/src/triggers/dispatcher.ts +30 -2
- package/corpus/core/src/vite/client.ts +96 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +21 -2
- package/corpus/templates/analytics/app/global.css +26 -10
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +25 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +548 -237
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +285 -294
- package/corpus/templates/analytics/changelog/2026-07-08-console-details-expand-inline-under-each-row-and-the-event-t.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-clamps-ultra-wide-viewports-while-keeping-the.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-keeps-a-usable-stage-when-dev-tools-is-open-a.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-resumes-after-scrubbing-and-expands-console-r.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replays-wait-for-full-load-keep-a-normal-viewport-an.md +6 -0
- package/corpus/templates/analytics/package.json +2 -2
- package/corpus/templates/assets/package.json +2 -2
- package/corpus/templates/brain/package.json +2 -2
- package/corpus/templates/calendar/AGENTS.md +10 -0
- package/corpus/templates/calendar/actions/create-event.ts +5 -1
- package/corpus/templates/calendar/actions/event-action-helpers.ts +41 -0
- package/corpus/templates/calendar/actions/get-attendee-timezones.ts +28 -0
- package/corpus/templates/calendar/actions/set-attendee-timezone.ts +68 -0
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +150 -11
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +36 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +32 -0
- package/corpus/templates/calendar/app/hooks/use-attendee-timezones.ts +24 -0
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/calendar/app/i18n-data.ts +20 -0
- package/corpus/templates/calendar/app/lib/attendee-local-time.ts +122 -0
- package/corpus/templates/calendar/changelog/2026-07-08-event-details-now-support-extension-widgets-and-you-can-see-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-08-when-you-invite-guests-to-a-new-event-you-now-appear-in.md +6 -0
- package/corpus/templates/calendar/package.json +2 -2
- package/corpus/templates/calendar/server/handlers/events.ts +5 -1
- package/corpus/templates/calendar/shared/api.ts +6 -0
- package/corpus/templates/chat/package.json +2 -2
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +6 -2
- package/corpus/templates/clips/actions/create-meeting.ts +8 -3
- package/corpus/templates/clips/actions/list-meetings.ts +31 -3
- package/corpus/templates/clips/changelog/2026-07-08-after-finishing-a-desktop-recording-reopening-clips-restor.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-clips-now-pops-up-to-take-notes-when-you-join-an-adhoc-zoom-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-desktop-clip-recordings-are-louder-after-capture-mic-only.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-meeting-reminders-now-appear-1-minute-before-start-stay-unti.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-the-desktop-meetings-panel-has-room-for-its-drop-shadow-and-.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +2 -2
- package/corpus/templates/clips/desktop/package.json +2 -2
- package/corpus/templates/clips/desktop/src/app.tsx +91 -34
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +15 -5
- package/corpus/templates/clips/desktop/src/lib/meeting-notification-timing.ts +83 -0
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +21 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +98 -20
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +174 -50
- package/corpus/templates/clips/desktop/src/styles.css +128 -53
- package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +364 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +5 -0
- package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +91 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +69 -19
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +36 -1
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +62 -10
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +1 -24
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +36 -1
- package/corpus/templates/clips/package.json +2 -2
- package/corpus/templates/content/actions/_content-database-personal-view.ts +80 -0
- package/corpus/templates/content/actions/get-content-database-personal-view.ts +27 -0
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +31 -0
- package/corpus/templates/content/actions/update-content-database-view.ts +3 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2584 -546
- package/corpus/templates/content/app/components/editor/database/filter-sort.ts +220 -18
- package/corpus/templates/content/app/hooks/use-content-database.ts +32 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +16 -0
- package/corpus/templates/content/app/i18n-data.ts +154 -0
- package/corpus/templates/content/changelog/2026-07-08-database-filters-now-support-notion-style-reset-save-for-eve.md +6 -0
- package/corpus/templates/content/changelog/2026-07-08-date-filters-can-now-target-a-between-range.md +6 -0
- package/corpus/templates/content/package.json +2 -2
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/shared/api.ts +24 -0
- package/corpus/templates/design/package.json +2 -2
- package/corpus/templates/dispatch/app/i18n/zh-TW.ts +34 -1
- package/corpus/templates/dispatch/app/i18n-data.ts +387 -10
- package/corpus/templates/dispatch/app/routes/automations.tsx +1 -0
- package/corpus/templates/dispatch/changelog/2026-07-08-added-an-automations-page-to-review-pause-and-create.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-08-destinations-now-shows-outbound-delivery-queue-health.md +6 -0
- package/corpus/templates/dispatch/package.json +2 -2
- package/corpus/templates/forms/package.json +2 -2
- package/corpus/templates/macros/package.json +2 -2
- package/corpus/templates/mail/app/hooks/use-emails.ts +56 -44
- package/corpus/templates/mail/app/lib/gmail-mutation-queue.ts +325 -0
- package/corpus/templates/mail/changelog/2026-07-08-rapid-archive-and-mark-read-now-batch-gmail-updates-so-the-i.md +6 -0
- package/corpus/templates/mail/package.json +2 -2
- package/corpus/templates/plan/package.json +2 -2
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +46 -29
- package/corpus/templates/slides/app/context/DeckContext.tsx +45 -18
- package/corpus/templates/slides/app/i18n/ar-SA.ts +4 -0
- package/corpus/templates/slides/app/i18n/de-DE.ts +4 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/i18n/es-ES.ts +4 -0
- package/corpus/templates/slides/app/i18n/fr-FR.ts +4 -0
- package/corpus/templates/slides/app/i18n/hi-IN.ts +4 -0
- package/corpus/templates/slides/app/i18n/ja-JP.ts +4 -0
- package/corpus/templates/slides/app/i18n/ko-KR.ts +4 -0
- package/corpus/templates/slides/app/i18n/pt-BR.ts +4 -0
- package/corpus/templates/slides/app/i18n/zh-CN.ts +4 -0
- package/corpus/templates/slides/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +115 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +22 -13
- package/corpus/templates/slides/changelog/2026-07-08-dropping-an-image-onto-a-slide-and-sending-it-to-the-agent-w.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-08-undo-works-after-the-agent-edits-a-slide-so-you-can-reverse.md +6 -0
- package/corpus/templates/slides/package.json +2 -2
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +23 -2
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.js +2 -2
- package/dist/agent/run-store.js.map +1 -1
- package/dist/catalog.json +2 -2
- package/dist/cli/add.js +1 -1
- package/dist/cli/add.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +8 -3
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +5 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/client/extensions/ExtensionSlot.js +6 -0
- package/dist/client/extensions/ExtensionSlot.js.map +1 -1
- package/dist/client/session-replay.d.ts +8 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +80 -3
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/scheduler.js +26 -1
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/localization/default-messages.d.ts +19 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +19 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/templates/default/app/i18n/ar-SA.ts +21 -0
- package/dist/templates/default/app/i18n/de-DE.ts +21 -0
- package/dist/templates/default/app/i18n/en-US.ts +21 -0
- package/dist/templates/default/app/i18n/es-ES.ts +21 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +20 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +21 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +20 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +21 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +20 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
- package/dist/templates/default/package.json +2 -2
- package/dist/templates/headless/package.json +3 -3
- package/dist/templates/workspace-core/package.json +6 -6
- package/dist/templates/workspace-root/package.json +2 -2
- package/dist/triggers/dispatcher.js +27 -2
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +70 -1
- package/dist/vite/client.js.map +1 -1
- package/package.json +7 -6
- package/src/templates/default/app/i18n/ar-SA.ts +21 -0
- package/src/templates/default/app/i18n/de-DE.ts +21 -0
- package/src/templates/default/app/i18n/en-US.ts +21 -0
- package/src/templates/default/app/i18n/es-ES.ts +21 -0
- package/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/src/templates/default/app/i18n/hi-IN.ts +20 -0
- package/src/templates/default/app/i18n/ja-JP.ts +21 -0
- package/src/templates/default/app/i18n/ko-KR.ts +20 -0
- package/src/templates/default/app/i18n/pt-BR.ts +21 -0
- package/src/templates/default/app/i18n/zh-CN.ts +20 -0
- package/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/src/templates/default/package.json +2 -2
- package/src/templates/headless/package.json +3 -3
- package/src/templates/workspace-core/package.json +6 -6
- package/src/templates/workspace-root/package.json +2 -2
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useBuilderConnectFlow,
|
|
5
5
|
useBuilderStatus,
|
|
6
6
|
useCodeMode,
|
|
7
|
+
useSession,
|
|
7
8
|
} from "@agent-native/core/client";
|
|
8
9
|
import {
|
|
9
10
|
AlertDialog,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
type ContentDatabaseSourceJoinRequest,
|
|
57
58
|
type ContentDatabaseSourceReviewPayload,
|
|
58
59
|
type SourceJoinSuggestion,
|
|
60
|
+
type ContentDatabasePersonalViewOverrides,
|
|
59
61
|
type ContentDatabaseView,
|
|
60
62
|
type ContentDatabaseViewConfig,
|
|
61
63
|
type ContentDatabaseColumnCalculation,
|
|
@@ -145,6 +147,7 @@ import {
|
|
|
145
147
|
useBuilderCmsModels,
|
|
146
148
|
useChangeContentDatabaseSourceRole,
|
|
147
149
|
useContentDatabase,
|
|
150
|
+
useContentDatabasePersonalView,
|
|
148
151
|
useContentDatabases,
|
|
149
152
|
contentDatabaseQueryKey,
|
|
150
153
|
useDeleteDatabaseItems,
|
|
@@ -158,6 +161,7 @@ import {
|
|
|
158
161
|
useRefreshContentDatabaseSource,
|
|
159
162
|
useSetContentDatabaseSourceWriteMode,
|
|
160
163
|
useSuggestSourceJoinKey,
|
|
164
|
+
useUpdateContentDatabasePersonalView,
|
|
161
165
|
useUpdateContentDatabaseView,
|
|
162
166
|
} from "@/hooks/use-content-database";
|
|
163
167
|
import {
|
|
@@ -207,6 +211,9 @@ import {
|
|
|
207
211
|
filesMediaItems,
|
|
208
212
|
displayValue,
|
|
209
213
|
nextPropertyOption,
|
|
214
|
+
personItems,
|
|
215
|
+
personLabel,
|
|
216
|
+
PersonPill,
|
|
210
217
|
removePropertyOption,
|
|
211
218
|
renamePropertyOption,
|
|
212
219
|
updatePropertyOptionColor,
|
|
@@ -265,9 +272,14 @@ const DATABASE_OPEN_PAGES_IN: ContentDatabaseOpenPagesIn[] = [
|
|
|
265
272
|
"full_page",
|
|
266
273
|
];
|
|
267
274
|
const DATABASE_FILTER_MODES: DatabaseFilterMode[] = ["and", "or"];
|
|
275
|
+
const DATABASE_ADVANCED_FILTER_GROUP_ID = "advanced";
|
|
276
|
+
export const PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION = 1;
|
|
268
277
|
export const BUILDER_SOURCE_CONTINUATION_STALL_MS = 5_000;
|
|
269
278
|
export const BUILDER_SOURCE_CONTINUATION_MAX_BACKOFF_MS = 30_000;
|
|
270
279
|
|
|
280
|
+
export type PersonalDatabaseViewOverrides =
|
|
281
|
+
ContentDatabasePersonalViewOverrides;
|
|
282
|
+
|
|
271
283
|
type DatabaseMessageKey = keyof (typeof messagesByLocale)["en-US"]["database"];
|
|
272
284
|
|
|
273
285
|
function dbText(
|
|
@@ -526,6 +538,8 @@ function DatabaseTable({
|
|
|
526
538
|
const isLoadingMoreItems =
|
|
527
539
|
database.isFetching && data?.pagination?.limit !== databaseItemLimit;
|
|
528
540
|
const databaseId = data?.database.id ?? expectedDatabaseId;
|
|
541
|
+
const personalView = useContentDatabasePersonalView(databaseId);
|
|
542
|
+
const updatePersonalView = useUpdateContentDatabasePersonalView(databaseId);
|
|
529
543
|
const source = data?.source ?? null;
|
|
530
544
|
const sources = data?.sources ?? (source ? [source] : []);
|
|
531
545
|
const [previewDocumentId, setPreviewDocumentId] = useState<string | null>(
|
|
@@ -540,6 +554,18 @@ function DatabaseTable({
|
|
|
540
554
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
541
555
|
const [searchQuery, setSearchQuery] = useState("");
|
|
542
556
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
557
|
+
const [inlineFilterControlsOpen, setInlineFilterControlsOpen] =
|
|
558
|
+
useState(false);
|
|
559
|
+
const [inlineAddFilterOpen, setInlineAddFilterOpen] = useState(false);
|
|
560
|
+
const [toolbarFilterOpen, setToolbarFilterOpen] = useState(false);
|
|
561
|
+
const [inlineFilterOpenIndex, setInlineFilterOpenIndex] = useState<
|
|
562
|
+
number | null
|
|
563
|
+
>(null);
|
|
564
|
+
const [inlineAdvancedFilterOpen, setInlineAdvancedFilterOpen] =
|
|
565
|
+
useState(false);
|
|
566
|
+
const [inlineSortOpenIndex, setInlineSortOpenIndex] = useState<number | null>(
|
|
567
|
+
null,
|
|
568
|
+
);
|
|
543
569
|
const [builderReviewOpen, setBuilderReviewOpen] = useState(false);
|
|
544
570
|
const [builderReviewResult, setBuilderReviewResult] =
|
|
545
571
|
useState<ContentDatabaseSourceReviewPayload | null>(null);
|
|
@@ -551,6 +577,9 @@ function DatabaseTable({
|
|
|
551
577
|
const [viewConfig, setViewConfig] = useState<ContentDatabaseViewConfig>(
|
|
552
578
|
defaultDatabaseViewConfig(),
|
|
553
579
|
);
|
|
580
|
+
const [savedViewConfig, setSavedViewConfig] =
|
|
581
|
+
useState<ContentDatabaseViewConfig>(defaultDatabaseViewConfig());
|
|
582
|
+
const [personalQueryDirty, setPersonalQueryDirty] = useState(false);
|
|
554
583
|
const [dateViewMonth, setDateViewMonth] = useState(() =>
|
|
555
584
|
startOfMonth(new Date()),
|
|
556
585
|
);
|
|
@@ -584,6 +613,9 @@ function DatabaseTable({
|
|
|
584
613
|
);
|
|
585
614
|
const hydratedViewRef = useRef("");
|
|
586
615
|
const saveViewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
616
|
+
const personalViewSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
617
|
+
null,
|
|
618
|
+
);
|
|
587
619
|
const autoContinueBuilderSourceRef = useRef<string | null>(null);
|
|
588
620
|
const builderContinuationWatchdogRef = useRef<{
|
|
589
621
|
key: string | null;
|
|
@@ -1122,15 +1154,111 @@ function DatabaseTable({
|
|
|
1122
1154
|
}
|
|
1123
1155
|
|
|
1124
1156
|
function setActiveSorts(nextSorts: DatabaseSort[]) {
|
|
1125
|
-
|
|
1157
|
+
updatePersonalQueryView((view) => ({ ...view, sorts: nextSorts }));
|
|
1126
1158
|
}
|
|
1127
1159
|
|
|
1128
1160
|
function setActiveFilters(nextFilters: DatabaseFilter[]) {
|
|
1129
|
-
|
|
1161
|
+
updatePersonalQueryView((view) => ({ ...view, filters: nextFilters }));
|
|
1130
1162
|
}
|
|
1131
1163
|
|
|
1132
1164
|
function setFilterMode(filterMode: DatabaseFilterMode) {
|
|
1133
|
-
|
|
1165
|
+
updatePersonalQueryView((view) => ({ ...view, filterMode }));
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function updatePersonalQueryView(
|
|
1169
|
+
update: (view: ContentDatabaseView) => ContentDatabaseView,
|
|
1170
|
+
) {
|
|
1171
|
+
setViewConfig((current) => {
|
|
1172
|
+
const next = updateActiveDatabaseView(current, update);
|
|
1173
|
+
if (databaseId) {
|
|
1174
|
+
schedulePersonalDatabaseViewOverrideWrite(databaseId, next);
|
|
1175
|
+
}
|
|
1176
|
+
setPersonalQueryDirty(
|
|
1177
|
+
databaseViewHasPersonalQueryChanges(next, savedViewConfig),
|
|
1178
|
+
);
|
|
1179
|
+
return next;
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async function savePersonalQueryForEveryone() {
|
|
1184
|
+
if (!databaseId) return;
|
|
1185
|
+
|
|
1186
|
+
try {
|
|
1187
|
+
const response = await updateView.mutateAsync({
|
|
1188
|
+
databaseId,
|
|
1189
|
+
viewConfig,
|
|
1190
|
+
});
|
|
1191
|
+
const nextViewConfig = normalizeClientDatabaseViewConfig(
|
|
1192
|
+
response.database.viewConfig,
|
|
1193
|
+
);
|
|
1194
|
+
if (personalViewSaveTimerRef.current) {
|
|
1195
|
+
clearTimeout(personalViewSaveTimerRef.current);
|
|
1196
|
+
personalViewSaveTimerRef.current = null;
|
|
1197
|
+
}
|
|
1198
|
+
updatePersonalView.mutate({ databaseId, overrides: null });
|
|
1199
|
+
setSavedViewConfig(nextViewConfig);
|
|
1200
|
+
setViewConfig(nextViewConfig);
|
|
1201
|
+
setPersonalQueryDirty(false);
|
|
1202
|
+
hydratedViewRef.current = databaseViewStateKey(
|
|
1203
|
+
databaseId,
|
|
1204
|
+
nextViewConfig,
|
|
1205
|
+
);
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
toast.error(dbText("failedToSaveView"), {
|
|
1208
|
+
description:
|
|
1209
|
+
err instanceof Error ? err.message : dbText("somethingWentWrong"),
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function resetPersonalQueryChanges() {
|
|
1215
|
+
if (!databaseId) return;
|
|
1216
|
+
if (personalViewSaveTimerRef.current) {
|
|
1217
|
+
clearTimeout(personalViewSaveTimerRef.current);
|
|
1218
|
+
personalViewSaveTimerRef.current = null;
|
|
1219
|
+
}
|
|
1220
|
+
setViewConfig((current) => {
|
|
1221
|
+
const nextViewConfig = databaseViewConfigWithSavedQueryState(
|
|
1222
|
+
current,
|
|
1223
|
+
savedViewConfig,
|
|
1224
|
+
);
|
|
1225
|
+
updatePersonalView.mutate({
|
|
1226
|
+
databaseId,
|
|
1227
|
+
overrides: null,
|
|
1228
|
+
});
|
|
1229
|
+
setPersonalQueryDirty(false);
|
|
1230
|
+
hydratedViewRef.current = databaseViewStateKey(
|
|
1231
|
+
databaseId,
|
|
1232
|
+
nextViewConfig,
|
|
1233
|
+
);
|
|
1234
|
+
return nextViewConfig;
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function schedulePersonalDatabaseViewOverrideWrite(
|
|
1239
|
+
databaseId: string,
|
|
1240
|
+
viewConfig: ContentDatabaseViewConfig,
|
|
1241
|
+
) {
|
|
1242
|
+
if (personalViewSaveTimerRef.current) {
|
|
1243
|
+
clearTimeout(personalViewSaveTimerRef.current);
|
|
1244
|
+
}
|
|
1245
|
+
const overrides = personalDatabaseViewOverridesFromConfig(viewConfig);
|
|
1246
|
+
personalViewSaveTimerRef.current = setTimeout(() => {
|
|
1247
|
+
personalViewSaveTimerRef.current = null;
|
|
1248
|
+
updatePersonalView.mutate(
|
|
1249
|
+
{ databaseId, overrides },
|
|
1250
|
+
{
|
|
1251
|
+
onError: (err) => {
|
|
1252
|
+
toast.error(dbText("failedToSaveView"), {
|
|
1253
|
+
description:
|
|
1254
|
+
err instanceof Error
|
|
1255
|
+
? err.message
|
|
1256
|
+
: dbText("somethingWentWrong"),
|
|
1257
|
+
});
|
|
1258
|
+
},
|
|
1259
|
+
},
|
|
1260
|
+
);
|
|
1261
|
+
}, 300);
|
|
1134
1262
|
}
|
|
1135
1263
|
|
|
1136
1264
|
function clearSearchAndFilters() {
|
|
@@ -1309,38 +1437,71 @@ function DatabaseTable({
|
|
|
1309
1437
|
|
|
1310
1438
|
useEffect(() => {
|
|
1311
1439
|
if (!data?.database.id) return;
|
|
1312
|
-
|
|
1440
|
+
if (personalView.isLoading) return;
|
|
1441
|
+
const nextSavedViewConfig = normalizeClientDatabaseViewConfig(
|
|
1313
1442
|
data.database.viewConfig,
|
|
1314
1443
|
);
|
|
1444
|
+
const nextViewConfig = applyPersonalDatabaseViewOverrides(
|
|
1445
|
+
nextSavedViewConfig,
|
|
1446
|
+
normalizePersonalDatabaseViewOverrides(personalView.data?.overrides),
|
|
1447
|
+
);
|
|
1315
1448
|
const nextKey = databaseViewStateKey(data.database.id, nextViewConfig);
|
|
1316
1449
|
if (hydratedViewRef.current === nextKey) return;
|
|
1317
1450
|
hydratedViewRef.current = nextKey;
|
|
1451
|
+
setSavedViewConfig(nextSavedViewConfig);
|
|
1452
|
+
setPersonalQueryDirty(
|
|
1453
|
+
databaseViewHasPersonalQueryChanges(nextViewConfig, nextSavedViewConfig),
|
|
1454
|
+
);
|
|
1318
1455
|
setViewConfig((current) =>
|
|
1319
1456
|
databaseViewStateKey(data.database.id, current) === nextKey
|
|
1320
1457
|
? current
|
|
1321
1458
|
: nextViewConfig,
|
|
1322
1459
|
);
|
|
1323
|
-
}, [
|
|
1460
|
+
}, [
|
|
1461
|
+
data?.database.id,
|
|
1462
|
+
data?.database.viewConfig,
|
|
1463
|
+
personalView.data?.overrides,
|
|
1464
|
+
personalView.isLoading,
|
|
1465
|
+
]);
|
|
1466
|
+
|
|
1467
|
+
useEffect(() => {
|
|
1468
|
+
return () => {
|
|
1469
|
+
if (personalViewSaveTimerRef.current) {
|
|
1470
|
+
clearTimeout(personalViewSaveTimerRef.current);
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
}, []);
|
|
1324
1474
|
|
|
1325
1475
|
useEffect(() => {
|
|
1326
1476
|
if (!databaseId) return;
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1477
|
+
const sharedViewConfig = personalQueryDirty
|
|
1478
|
+
? databaseViewConfigWithSavedQueryState(viewConfig, savedViewConfig)
|
|
1479
|
+
: viewConfig;
|
|
1480
|
+
const nextKey = databaseViewStateKey(databaseId, sharedViewConfig);
|
|
1481
|
+
if (databaseViewStateKey(databaseId, savedViewConfig) === nextKey) return;
|
|
1329
1482
|
if (!canEdit) return;
|
|
1330
1483
|
if (saveViewTimerRef.current) {
|
|
1331
1484
|
clearTimeout(saveViewTimerRef.current);
|
|
1332
1485
|
}
|
|
1333
1486
|
saveViewTimerRef.current = setTimeout(() => {
|
|
1334
1487
|
updateView.mutate(
|
|
1335
|
-
{ databaseId, viewConfig },
|
|
1488
|
+
{ databaseId, viewConfig: sharedViewConfig },
|
|
1336
1489
|
{
|
|
1337
1490
|
onSuccess: (response) => {
|
|
1338
1491
|
const nextViewConfig = normalizeClientDatabaseViewConfig(
|
|
1339
1492
|
response.database.viewConfig,
|
|
1340
1493
|
);
|
|
1494
|
+
setSavedViewConfig(nextViewConfig);
|
|
1341
1495
|
hydratedViewRef.current = databaseViewStateKey(
|
|
1342
1496
|
response.database.id,
|
|
1343
|
-
|
|
1497
|
+
personalQueryDirty
|
|
1498
|
+
? applyPersonalDatabaseViewOverrides(
|
|
1499
|
+
nextViewConfig,
|
|
1500
|
+
normalizePersonalDatabaseViewOverrides(
|
|
1501
|
+
personalView.data?.overrides,
|
|
1502
|
+
),
|
|
1503
|
+
)
|
|
1504
|
+
: nextViewConfig,
|
|
1344
1505
|
);
|
|
1345
1506
|
},
|
|
1346
1507
|
},
|
|
@@ -1351,7 +1512,15 @@ function DatabaseTable({
|
|
|
1351
1512
|
clearTimeout(saveViewTimerRef.current);
|
|
1352
1513
|
}
|
|
1353
1514
|
};
|
|
1354
|
-
}, [
|
|
1515
|
+
}, [
|
|
1516
|
+
canEdit,
|
|
1517
|
+
databaseId,
|
|
1518
|
+
personalQueryDirty,
|
|
1519
|
+
personalView.data?.overrides,
|
|
1520
|
+
savedViewConfig,
|
|
1521
|
+
updateView,
|
|
1522
|
+
viewConfig,
|
|
1523
|
+
]);
|
|
1355
1524
|
|
|
1356
1525
|
function resizeColumn(
|
|
1357
1526
|
key: ColumnKey,
|
|
@@ -1444,12 +1613,36 @@ function DatabaseTable({
|
|
|
1444
1613
|
onSortsChange={setActiveSorts}
|
|
1445
1614
|
/>
|
|
1446
1615
|
<FilterMenu
|
|
1447
|
-
documentId={document.id}
|
|
1448
|
-
properties={orderedProperties}
|
|
1449
1616
|
filters={filters}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1617
|
+
properties={orderedProperties}
|
|
1618
|
+
inlineOpen={inlineFilterControlsOpen}
|
|
1619
|
+
open={toolbarFilterOpen}
|
|
1620
|
+
onOpenChange={setToolbarFilterOpen}
|
|
1621
|
+
onAddFilter={(key, label) => {
|
|
1622
|
+
setActiveFilters([
|
|
1623
|
+
...filters,
|
|
1624
|
+
createDatabaseFilterForField(key, label, orderedProperties),
|
|
1625
|
+
]);
|
|
1626
|
+
setInlineFilterControlsOpen(true);
|
|
1627
|
+
setInlineAddFilterOpen(false);
|
|
1628
|
+
setInlineFilterOpenIndex(filters.length);
|
|
1629
|
+
setToolbarFilterOpen(false);
|
|
1630
|
+
}}
|
|
1631
|
+
onAddAdvancedFilter={(key, label) => {
|
|
1632
|
+
setActiveFilters([
|
|
1633
|
+
...filters,
|
|
1634
|
+
createDatabaseFilterForField(
|
|
1635
|
+
key,
|
|
1636
|
+
label,
|
|
1637
|
+
orderedProperties,
|
|
1638
|
+
true,
|
|
1639
|
+
),
|
|
1640
|
+
]);
|
|
1641
|
+
setInlineFilterControlsOpen(true);
|
|
1642
|
+
setInlineAddFilterOpen(false);
|
|
1643
|
+
setInlineAdvancedFilterOpen(true);
|
|
1644
|
+
setToolbarFilterOpen(false);
|
|
1645
|
+
}}
|
|
1453
1646
|
/>
|
|
1454
1647
|
{renderMode === "inline" ? (
|
|
1455
1648
|
<Tooltip>
|
|
@@ -1522,29 +1715,58 @@ function DatabaseTable({
|
|
|
1522
1715
|
</div>
|
|
1523
1716
|
|
|
1524
1717
|
<DatabaseActiveConstraintsBar
|
|
1718
|
+
documentId={document.id}
|
|
1719
|
+
items={items}
|
|
1525
1720
|
searchQuery={searchQuery}
|
|
1526
1721
|
sorts={sorts}
|
|
1527
1722
|
filters={filters}
|
|
1723
|
+
filterMode={filterMode}
|
|
1528
1724
|
properties={properties}
|
|
1529
1725
|
constraintCount={activeConstraintCount}
|
|
1726
|
+
forceShow={inlineFilterControlsOpen}
|
|
1727
|
+
addFilterOpen={inlineAddFilterOpen}
|
|
1728
|
+
openSortIndex={inlineSortOpenIndex}
|
|
1729
|
+
openFilterIndex={inlineFilterOpenIndex}
|
|
1730
|
+
advancedFilterOpen={inlineAdvancedFilterOpen}
|
|
1731
|
+
hasPersonalQueryChanges={personalQueryDirty}
|
|
1732
|
+
savePending={updateView.isPending}
|
|
1733
|
+
onAddFilterOpenChange={(open) => {
|
|
1734
|
+
setInlineAddFilterOpen(open);
|
|
1735
|
+
if (open) setInlineFilterControlsOpen(true);
|
|
1736
|
+
}}
|
|
1737
|
+
onSortOpenIndexChange={setInlineSortOpenIndex}
|
|
1738
|
+
onFilterOpenIndexChange={setInlineFilterOpenIndex}
|
|
1739
|
+
onAdvancedFilterOpenChange={setInlineAdvancedFilterOpen}
|
|
1530
1740
|
onClearSearch={() => {
|
|
1531
1741
|
setSearchQuery("");
|
|
1532
1742
|
setSearchOpen(false);
|
|
1533
1743
|
}}
|
|
1534
|
-
onRemoveSort={(index) =>
|
|
1535
|
-
setActiveSorts(sorts.filter((_, sortIndex) => sortIndex !== index))
|
|
1536
|
-
|
|
1537
|
-
|
|
1744
|
+
onRemoveSort={(index) => {
|
|
1745
|
+
setActiveSorts(sorts.filter((_, sortIndex) => sortIndex !== index));
|
|
1746
|
+
setInlineSortOpenIndex(null);
|
|
1747
|
+
}}
|
|
1748
|
+
onRemoveFilter={(index) => {
|
|
1538
1749
|
setActiveFilters(
|
|
1539
1750
|
filters.filter((_, filterIndex) => filterIndex !== index),
|
|
1540
|
-
)
|
|
1541
|
-
|
|
1751
|
+
);
|
|
1752
|
+
setInlineFilterOpenIndex(null);
|
|
1753
|
+
}}
|
|
1754
|
+
onFiltersChange={setActiveFilters}
|
|
1755
|
+
onSortsChange={setActiveSorts}
|
|
1756
|
+
onFilterModeChange={setFilterMode}
|
|
1542
1757
|
onClearAll={() => {
|
|
1543
1758
|
setSearchQuery("");
|
|
1544
1759
|
setSearchOpen(false);
|
|
1545
1760
|
setActiveSorts([]);
|
|
1546
1761
|
setActiveFilters([]);
|
|
1762
|
+
setInlineFilterControlsOpen(false);
|
|
1763
|
+
setInlineAddFilterOpen(false);
|
|
1764
|
+
setInlineFilterOpenIndex(null);
|
|
1765
|
+
setInlineAdvancedFilterOpen(false);
|
|
1766
|
+
setInlineSortOpenIndex(null);
|
|
1547
1767
|
}}
|
|
1768
|
+
onResetPersonalChanges={resetPersonalQueryChanges}
|
|
1769
|
+
onSaveForEveryone={() => void savePersonalQueryForEveryone()}
|
|
1548
1770
|
/>
|
|
1549
1771
|
|
|
1550
1772
|
<BuilderSourceContinuationBar
|
|
@@ -3813,138 +4035,1368 @@ function DatabaseTableView({
|
|
|
3813
4035
|
}
|
|
3814
4036
|
|
|
3815
4037
|
function DatabaseActiveConstraintsBar({
|
|
4038
|
+
documentId,
|
|
4039
|
+
items,
|
|
3816
4040
|
searchQuery,
|
|
3817
4041
|
sorts,
|
|
3818
4042
|
filters,
|
|
4043
|
+
filterMode,
|
|
3819
4044
|
properties,
|
|
3820
4045
|
constraintCount,
|
|
4046
|
+
forceShow,
|
|
4047
|
+
addFilterOpen,
|
|
4048
|
+
openSortIndex,
|
|
4049
|
+
openFilterIndex,
|
|
4050
|
+
advancedFilterOpen,
|
|
4051
|
+
hasPersonalQueryChanges,
|
|
4052
|
+
savePending,
|
|
4053
|
+
onAddFilterOpenChange,
|
|
4054
|
+
onSortOpenIndexChange,
|
|
4055
|
+
onFilterOpenIndexChange,
|
|
4056
|
+
onAdvancedFilterOpenChange,
|
|
3821
4057
|
onClearSearch,
|
|
3822
4058
|
onRemoveSort,
|
|
3823
4059
|
onRemoveFilter,
|
|
4060
|
+
onSortsChange,
|
|
4061
|
+
onFiltersChange,
|
|
4062
|
+
onFilterModeChange,
|
|
3824
4063
|
onClearAll,
|
|
4064
|
+
onResetPersonalChanges,
|
|
4065
|
+
onSaveForEveryone,
|
|
3825
4066
|
}: {
|
|
4067
|
+
documentId: string;
|
|
4068
|
+
items: ContentDatabaseItem[];
|
|
3826
4069
|
searchQuery: string;
|
|
3827
4070
|
sorts: DatabaseSort[];
|
|
3828
4071
|
filters: DatabaseFilter[];
|
|
4072
|
+
filterMode: DatabaseFilterMode;
|
|
3829
4073
|
properties: DocumentProperty[];
|
|
3830
4074
|
constraintCount: number;
|
|
4075
|
+
forceShow: boolean;
|
|
4076
|
+
addFilterOpen: boolean;
|
|
4077
|
+
openSortIndex: number | null;
|
|
4078
|
+
openFilterIndex: number | null;
|
|
4079
|
+
advancedFilterOpen: boolean;
|
|
4080
|
+
hasPersonalQueryChanges: boolean;
|
|
4081
|
+
savePending: boolean;
|
|
4082
|
+
onAddFilterOpenChange: (open: boolean) => void;
|
|
4083
|
+
onSortOpenIndexChange: (index: number | null) => void;
|
|
4084
|
+
onFilterOpenIndexChange: (index: number | null) => void;
|
|
4085
|
+
onAdvancedFilterOpenChange: (open: boolean) => void;
|
|
3831
4086
|
onClearSearch: () => void;
|
|
3832
4087
|
onRemoveSort: (index: number) => void;
|
|
3833
4088
|
onRemoveFilter: (index: number) => void;
|
|
4089
|
+
onSortsChange: (sorts: DatabaseSort[]) => void;
|
|
4090
|
+
onFiltersChange: (filters: DatabaseFilter[]) => void;
|
|
4091
|
+
onFilterModeChange: (filterMode: DatabaseFilterMode) => void;
|
|
3834
4092
|
onClearAll: () => void;
|
|
4093
|
+
onResetPersonalChanges: () => void;
|
|
4094
|
+
onSaveForEveryone: () => void;
|
|
3835
4095
|
}) {
|
|
3836
|
-
if (
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
.
|
|
4096
|
+
if (
|
|
4097
|
+
!forceShow &&
|
|
4098
|
+
constraintCount === 0 &&
|
|
4099
|
+
filters.length === 0 &&
|
|
4100
|
+
!hasPersonalQueryChanges
|
|
4101
|
+
)
|
|
4102
|
+
return null;
|
|
4103
|
+
const hasSearchOrSortConstraints =
|
|
4104
|
+
searchQuery.trim().length > 0 || sorts.length > 0;
|
|
4105
|
+
const showViewActionControls =
|
|
4106
|
+
hasPersonalQueryChanges ||
|
|
4107
|
+
constraintCount > 0 ||
|
|
4108
|
+
filters.length > 0 ||
|
|
4109
|
+
hasSearchOrSortConstraints;
|
|
4110
|
+
const filterEntries = filters.map((filter, index) => ({ filter, index }));
|
|
4111
|
+
const advancedFilterEntries = filterEntries.filter(({ filter }) =>
|
|
4112
|
+
isAdvancedDatabaseFilter(filter),
|
|
4113
|
+
);
|
|
4114
|
+
const regularFilterEntries = filterEntries.filter(
|
|
4115
|
+
({ filter }) => !isAdvancedDatabaseFilter(filter),
|
|
4116
|
+
);
|
|
4117
|
+
const hasSortFilterDivider = sorts.length > 0 && filterEntries.length > 0;
|
|
3840
4118
|
|
|
3841
4119
|
return (
|
|
3842
4120
|
<div className="mb-2 flex min-h-8 flex-wrap items-center gap-1 border-b border-border pb-2 text-xs text-muted-foreground">
|
|
3843
|
-
<span className="px-1.5">
|
|
3844
|
-
Showing {constraintCount} condition{constraintCount === 1 ? "" : "s"}
|
|
3845
|
-
</span>
|
|
3846
|
-
{searchQuery.trim() ? (
|
|
3847
|
-
<DatabaseConstraintChip
|
|
3848
|
-
icon={<IconSearch className="size-3.5" />}
|
|
3849
|
-
label={`Search: ${searchQuery.trim()}`}
|
|
3850
|
-
onRemove={onClearSearch}
|
|
3851
|
-
/>
|
|
3852
|
-
) : null}
|
|
3853
4121
|
{sorts.map((sort, index) => (
|
|
3854
|
-
<
|
|
4122
|
+
<DatabaseInlineSortControl
|
|
3855
4123
|
key={`${sort.key}-${index}`}
|
|
3856
|
-
|
|
3857
|
-
|
|
4124
|
+
sort={sort}
|
|
4125
|
+
sortIndex={index}
|
|
4126
|
+
sorts={sorts}
|
|
4127
|
+
properties={properties}
|
|
4128
|
+
open={openSortIndex === index}
|
|
4129
|
+
showUnsavedIndicator={hasPersonalQueryChanges}
|
|
4130
|
+
onOpenChange={(open) => onSortOpenIndexChange(open ? index : null)}
|
|
4131
|
+
onSortsChange={onSortsChange}
|
|
3858
4132
|
onRemove={() => onRemoveSort(index)}
|
|
3859
4133
|
/>
|
|
3860
4134
|
))}
|
|
3861
|
-
{
|
|
3862
|
-
<
|
|
4135
|
+
{hasSortFilterDivider ? (
|
|
4136
|
+
<div aria-hidden="true" className="mx-1 h-4 w-px shrink-0 bg-border" />
|
|
4137
|
+
) : null}
|
|
4138
|
+
{advancedFilterEntries.length === 0 && regularFilterEntries.length > 1 ? (
|
|
4139
|
+
<DatabaseFilterModeButton
|
|
4140
|
+
filterMode={filterMode}
|
|
4141
|
+
onFilterModeChange={onFilterModeChange}
|
|
4142
|
+
/>
|
|
4143
|
+
) : null}
|
|
4144
|
+
{advancedFilterEntries.length > 0 ? (
|
|
4145
|
+
<DatabaseAdvancedFilterGroupControl
|
|
4146
|
+
documentId={documentId}
|
|
4147
|
+
items={items}
|
|
4148
|
+
filters={filters}
|
|
4149
|
+
filterEntries={advancedFilterEntries}
|
|
4150
|
+
filterMode={filterMode}
|
|
4151
|
+
properties={properties}
|
|
4152
|
+
open={advancedFilterOpen}
|
|
4153
|
+
showUnsavedIndicator={hasPersonalQueryChanges}
|
|
4154
|
+
onOpenChange={onAdvancedFilterOpenChange}
|
|
4155
|
+
onFiltersChange={onFiltersChange}
|
|
4156
|
+
onFilterModeChange={onFilterModeChange}
|
|
4157
|
+
/>
|
|
4158
|
+
) : null}
|
|
4159
|
+
{regularFilterEntries.map(({ filter, index }) => (
|
|
4160
|
+
<DatabaseInlineFilterControl
|
|
3863
4161
|
key={`${filter.key}-${index}`}
|
|
3864
|
-
|
|
3865
|
-
|
|
4162
|
+
documentId={documentId}
|
|
4163
|
+
filter={filter}
|
|
4164
|
+
filterIndex={index}
|
|
4165
|
+
filters={filters}
|
|
4166
|
+
items={items}
|
|
4167
|
+
properties={properties}
|
|
4168
|
+
open={openFilterIndex === index}
|
|
4169
|
+
showUnsavedIndicator={hasPersonalQueryChanges}
|
|
4170
|
+
onOpenChange={(open) => onFilterOpenIndexChange(open ? index : null)}
|
|
4171
|
+
onFiltersChange={onFiltersChange}
|
|
3866
4172
|
onRemove={() => onRemoveFilter(index)}
|
|
3867
4173
|
/>
|
|
3868
4174
|
))}
|
|
3869
|
-
<
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
4175
|
+
<DatabaseAddFilterButton
|
|
4176
|
+
open={addFilterOpen}
|
|
4177
|
+
filters={filters}
|
|
4178
|
+
properties={properties}
|
|
4179
|
+
onOpenChange={onAddFilterOpenChange}
|
|
4180
|
+
onAddFilter={(key, label) => {
|
|
4181
|
+
onFiltersChange([
|
|
4182
|
+
...filters,
|
|
4183
|
+
createDatabaseFilterForField(key, label, properties),
|
|
4184
|
+
]);
|
|
4185
|
+
onAddFilterOpenChange(false);
|
|
4186
|
+
onFilterOpenIndexChange(filters.length);
|
|
4187
|
+
}}
|
|
4188
|
+
onAddAdvancedFilter={(key, label) => {
|
|
4189
|
+
onFiltersChange([
|
|
4190
|
+
...filters,
|
|
4191
|
+
createDatabaseFilterForField(key, label, properties, true),
|
|
4192
|
+
]);
|
|
4193
|
+
onAddFilterOpenChange(false);
|
|
4194
|
+
onAdvancedFilterOpenChange(true);
|
|
4195
|
+
}}
|
|
4196
|
+
/>
|
|
4197
|
+
{searchQuery.trim() ? (
|
|
4198
|
+
<DatabaseConstraintChip
|
|
4199
|
+
icon={<IconSearch className="size-3.5" />}
|
|
4200
|
+
label={`Search: ${searchQuery.trim()}`}
|
|
4201
|
+
onRemove={onClearSearch}
|
|
4202
|
+
/>
|
|
4203
|
+
) : null}
|
|
4204
|
+
<div className="ml-auto flex items-center gap-1 pl-2">
|
|
4205
|
+
{hasSearchOrSortConstraints ? (
|
|
4206
|
+
<Button
|
|
4207
|
+
type="button"
|
|
4208
|
+
size="sm"
|
|
4209
|
+
variant="ghost"
|
|
4210
|
+
className="h-7 px-2 text-xs text-muted-foreground"
|
|
4211
|
+
onClick={onClearAll}
|
|
4212
|
+
>
|
|
4213
|
+
{dbText("clearAll")}
|
|
4214
|
+
</Button>
|
|
4215
|
+
) : null}
|
|
4216
|
+
{showViewActionControls ? (
|
|
4217
|
+
<>
|
|
4218
|
+
<Button
|
|
4219
|
+
type="button"
|
|
4220
|
+
size="sm"
|
|
4221
|
+
variant="ghost"
|
|
4222
|
+
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
|
|
4223
|
+
disabled={savePending}
|
|
4224
|
+
onClick={onResetPersonalChanges}
|
|
4225
|
+
>
|
|
4226
|
+
{dbText("reset")}
|
|
4227
|
+
</Button>
|
|
4228
|
+
<div className="flex h-7 overflow-hidden rounded">
|
|
4229
|
+
<Button
|
|
4230
|
+
type="button"
|
|
4231
|
+
size="sm"
|
|
4232
|
+
className="h-7 rounded-none bg-amber-100 px-2 text-xs font-medium text-amber-900 shadow-none hover:bg-amber-200 dark:bg-amber-500/20 dark:text-amber-100 dark:hover:bg-amber-500/30"
|
|
4233
|
+
disabled={savePending}
|
|
4234
|
+
onClick={onSaveForEveryone}
|
|
4235
|
+
>
|
|
4236
|
+
{savePending ? <Spinner className="mr-1 size-3" /> : null}
|
|
4237
|
+
{dbText("saveForEveryone")}
|
|
4238
|
+
</Button>
|
|
4239
|
+
<DropdownMenu>
|
|
4240
|
+
<DropdownMenuTrigger asChild>
|
|
4241
|
+
<Button
|
|
4242
|
+
type="button"
|
|
4243
|
+
size="sm"
|
|
4244
|
+
className="h-7 w-7 rounded-none border-l border-amber-200 bg-amber-100 p-0 text-amber-900 shadow-none hover:bg-amber-200 dark:border-amber-500/20 dark:bg-amber-500/20 dark:text-amber-100 dark:hover:bg-amber-500/30"
|
|
4245
|
+
disabled={savePending}
|
|
4246
|
+
aria-label={dbText("saveForEveryone")}
|
|
4247
|
+
>
|
|
4248
|
+
<IconChevronDown className="size-3" />
|
|
4249
|
+
</Button>
|
|
4250
|
+
</DropdownMenuTrigger>
|
|
4251
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
4252
|
+
<DropdownMenuItem
|
|
4253
|
+
onSelect={(event) => {
|
|
4254
|
+
event.preventDefault();
|
|
4255
|
+
onSaveForEveryone();
|
|
4256
|
+
}}
|
|
4257
|
+
>
|
|
4258
|
+
<IconCheck className="mr-2 size-4 text-muted-foreground" />
|
|
4259
|
+
{dbText("saveForEveryone")}
|
|
4260
|
+
</DropdownMenuItem>
|
|
4261
|
+
</DropdownMenuContent>
|
|
4262
|
+
</DropdownMenu>
|
|
4263
|
+
</div>
|
|
4264
|
+
</>
|
|
4265
|
+
) : null}
|
|
4266
|
+
</div>
|
|
3878
4267
|
</div>
|
|
3879
4268
|
);
|
|
3880
4269
|
}
|
|
3881
4270
|
|
|
3882
|
-
function
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
| { kind: "model"; model: BuilderCmsModelSummary }
|
|
3912
|
-
| { kind: "addSource" }
|
|
3913
|
-
| { kind: "secondarySource"; sourceId: string; sourceName: string }
|
|
3914
|
-
| { kind: "keyConfirm"; candidate: PendingSourceCandidate }
|
|
3915
|
-
| { kind: "fieldPicker"; sourceId: string; sourceName: string };
|
|
3916
|
-
|
|
3917
|
-
function sourceNavTitle(stack: SourceNavStep[]): string {
|
|
3918
|
-
const top = stack[stack.length - 1];
|
|
3919
|
-
if (!top) return dbText("sources");
|
|
3920
|
-
if (top.kind === "provider") return "Builder";
|
|
3921
|
-
if (top.kind === "space") return top.spaceName;
|
|
3922
|
-
if (top.kind === "addSource") return dbText("addASource");
|
|
3923
|
-
if (top.kind === "secondarySource") return top.sourceName;
|
|
3924
|
-
if (top.kind === "keyConfirm") return dbText("matchExistingItemsToDetails");
|
|
3925
|
-
if (top.kind === "fieldPicker") return dbText("chooseFields");
|
|
3926
|
-
return top.model.displayName;
|
|
3927
|
-
}
|
|
4271
|
+
function DatabaseInlineSortControl({
|
|
4272
|
+
sort,
|
|
4273
|
+
sortIndex,
|
|
4274
|
+
sorts,
|
|
4275
|
+
properties,
|
|
4276
|
+
open,
|
|
4277
|
+
showUnsavedIndicator,
|
|
4278
|
+
onOpenChange,
|
|
4279
|
+
onSortsChange,
|
|
4280
|
+
onRemove,
|
|
4281
|
+
}: {
|
|
4282
|
+
sort: DatabaseSort;
|
|
4283
|
+
sortIndex: number;
|
|
4284
|
+
sorts: DatabaseSort[];
|
|
4285
|
+
properties: DocumentProperty[];
|
|
4286
|
+
open: boolean;
|
|
4287
|
+
showUnsavedIndicator: boolean;
|
|
4288
|
+
onOpenChange: (open: boolean) => void;
|
|
4289
|
+
onSortsChange: (sorts: DatabaseSort[]) => void;
|
|
4290
|
+
onRemove: () => void;
|
|
4291
|
+
}) {
|
|
4292
|
+
function updateSort(next: Partial<DatabaseSort>) {
|
|
4293
|
+
const nextSorts = [...sorts];
|
|
4294
|
+
nextSorts[sortIndex] = {
|
|
4295
|
+
...(nextSorts[sortIndex] ?? defaultDatabaseSort()),
|
|
4296
|
+
...next,
|
|
4297
|
+
};
|
|
4298
|
+
onSortsChange(nextSorts);
|
|
4299
|
+
}
|
|
3928
4300
|
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
return (
|
|
3933
|
-
<svg
|
|
3934
|
-
viewBox="0 0 71 80"
|
|
3935
|
-
fill="currentColor"
|
|
3936
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
3937
|
-
className={className}
|
|
3938
|
-
aria-hidden="true"
|
|
3939
|
-
>
|
|
3940
|
-
<path d="M70.86 24C70.86 10.69 60.06 0 46.86 0H6.31995C2.81995 0 0 2.84031 0 6.32031C0 12.8003 13.71 17.71 13.71 40C13.71 62.29 0 67.2102 0 73.6802C0 77.1602 2.81995 80 6.31995 80H46.86C60.06 80 70.86 69.31 70.86 56C70.86 46.22 64.98 40.25 64.75 40C64.98 39.75 70.86 33.78 70.86 24ZM8.37 6.86035H46.87C51.45 6.86035 55.75 8.64037 58.99 11.8804C62.23 15.1204 64.01 19.42 64.01 24C64.01 28.58 62.3199 32.62 59.3199 35.79L8.37 6.86035ZM58.99 68.1304C55.75 71.3704 51.45 73.1504 46.87 73.1504H8.37L59.3199 44.2202C62.3199 47.3902 64.01 51.5703 64.01 56.0103C64.01 60.4503 62.23 64.8904 58.99 68.1304ZM15.83 61.02C16.24 60.17 20.58 51.74 20.58 40C20.58 28.26 16.24 19.83 15.83 18.98L52.85 40L15.83 61.02Z" />
|
|
3941
|
-
</svg>
|
|
4301
|
+
const existingSortKeys = useMemo(
|
|
4302
|
+
() => new Set(sorts.map((candidate) => candidate.key)),
|
|
4303
|
+
[sorts],
|
|
3942
4304
|
);
|
|
3943
|
-
}
|
|
3944
4305
|
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
4306
|
+
function addSortForField(key: "name" | string, label: string) {
|
|
4307
|
+
onSortsChange([...sorts, { key, label, direction: "asc" }]);
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
return (
|
|
4311
|
+
<DropdownMenu open={open} onOpenChange={onOpenChange}>
|
|
4312
|
+
<DropdownMenuTrigger asChild>
|
|
4313
|
+
<Button
|
|
4314
|
+
type="button"
|
|
4315
|
+
size="sm"
|
|
4316
|
+
variant="ghost"
|
|
4317
|
+
className={cn(
|
|
4318
|
+
"relative h-7 max-w-[16rem] rounded border border-[#2383e2]/30 bg-[#2383e2]/10 px-2 text-xs font-normal text-[#0f5ea8] shadow-none hover:bg-[#2383e2]/15 focus-visible:border-[#2383e2]/40 focus-visible:ring-[#2383e2]/25",
|
|
4319
|
+
open && "border-[#2383e2]/40 bg-[#2383e2]/15",
|
|
4320
|
+
)}
|
|
4321
|
+
>
|
|
4322
|
+
{sort.direction === "asc" ? (
|
|
4323
|
+
<IconArrowUp className="size-3.5 shrink-0" />
|
|
4324
|
+
) : (
|
|
4325
|
+
<IconArrowDown className="size-3.5 shrink-0" />
|
|
4326
|
+
)}
|
|
4327
|
+
<span className="min-w-0 truncate">{sort.label}</span>
|
|
4328
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
4329
|
+
{showUnsavedIndicator ? (
|
|
4330
|
+
<span
|
|
4331
|
+
aria-hidden="true"
|
|
4332
|
+
className="absolute -right-0.5 -top-0.5 size-1.5 rounded-full border border-background bg-amber-500"
|
|
4333
|
+
/>
|
|
4334
|
+
) : null}
|
|
4335
|
+
</Button>
|
|
4336
|
+
</DropdownMenuTrigger>
|
|
4337
|
+
<DropdownMenuContent align="start" className="w-[260px] p-0">
|
|
4338
|
+
<div
|
|
4339
|
+
className="grid gap-1 p-1"
|
|
4340
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
4341
|
+
>
|
|
4342
|
+
<div className="flex items-center gap-0.5">
|
|
4343
|
+
<DropdownMenuSub>
|
|
4344
|
+
<DropdownMenuSubTrigger className="h-8 min-w-0 flex-1 rounded px-1.5 text-xs [&>svg:last-child]:hidden">
|
|
4345
|
+
<SortFieldIcon sort={sort} properties={properties} />
|
|
4346
|
+
<span className="min-w-0 flex-1 truncate">{sort.label}</span>
|
|
4347
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
4348
|
+
</DropdownMenuSubTrigger>
|
|
4349
|
+
<DatabasePropertyPickerSubContent
|
|
4350
|
+
properties={properties}
|
|
4351
|
+
selectedKey={sort.key}
|
|
4352
|
+
includeName
|
|
4353
|
+
onSelect={(key, label) => updateSort({ key, label })}
|
|
4354
|
+
/>
|
|
4355
|
+
</DropdownMenuSub>
|
|
4356
|
+
|
|
4357
|
+
<DropdownMenuSub>
|
|
4358
|
+
<DropdownMenuSubTrigger className="h-8 min-w-0 flex-1 rounded px-1.5 text-xs [&>svg:last-child]:hidden">
|
|
4359
|
+
<span className="min-w-0 flex-1 truncate">
|
|
4360
|
+
{databaseSortDirectionLabel(sort.direction)}
|
|
4361
|
+
</span>
|
|
4362
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
4363
|
+
</DropdownMenuSubTrigger>
|
|
4364
|
+
<DropdownMenuSubContent className="w-40">
|
|
4365
|
+
{(["asc", "desc"] as const).map((direction) => (
|
|
4366
|
+
<DropdownMenuItem
|
|
4367
|
+
key={direction}
|
|
4368
|
+
onSelect={(event) => {
|
|
4369
|
+
event.preventDefault();
|
|
4370
|
+
updateSort({ direction });
|
|
4371
|
+
}}
|
|
4372
|
+
>
|
|
4373
|
+
<span className="flex-1">
|
|
4374
|
+
{databaseSortDirectionLabel(direction)}
|
|
4375
|
+
</span>
|
|
4376
|
+
{sort.direction === direction ? (
|
|
4377
|
+
<IconCheck className="size-4 text-muted-foreground" />
|
|
4378
|
+
) : null}
|
|
4379
|
+
</DropdownMenuItem>
|
|
4380
|
+
))}
|
|
4381
|
+
</DropdownMenuSubContent>
|
|
4382
|
+
</DropdownMenuSub>
|
|
4383
|
+
|
|
4384
|
+
<button
|
|
4385
|
+
type="button"
|
|
4386
|
+
aria-label={`Remove ${sort.label} sort`}
|
|
4387
|
+
className="flex size-8 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
4388
|
+
onClick={onRemove}
|
|
4389
|
+
>
|
|
4390
|
+
<IconX className="size-4" />
|
|
4391
|
+
</button>
|
|
4392
|
+
</div>
|
|
4393
|
+
|
|
4394
|
+
<DropdownMenuSeparator />
|
|
4395
|
+
<DropdownMenuSub>
|
|
4396
|
+
<DropdownMenuSubTrigger className="[&>svg:last-child]:hidden">
|
|
4397
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
4398
|
+
<span className="flex-1">{dbText("addSort")}</span>
|
|
4399
|
+
</DropdownMenuSubTrigger>
|
|
4400
|
+
<DatabasePropertyPickerSubContent
|
|
4401
|
+
properties={properties}
|
|
4402
|
+
includeName
|
|
4403
|
+
excludeKeys={existingSortKeys}
|
|
4404
|
+
placeholder={dbText("searchForAProperty")}
|
|
4405
|
+
onSelect={addSortForField}
|
|
4406
|
+
/>
|
|
4407
|
+
</DropdownMenuSub>
|
|
4408
|
+
<DropdownMenuItem
|
|
4409
|
+
onSelect={(event) => {
|
|
4410
|
+
event.preventDefault();
|
|
4411
|
+
onRemove();
|
|
4412
|
+
}}
|
|
4413
|
+
>
|
|
4414
|
+
<IconTrash className="mr-2 size-4 text-muted-foreground" />
|
|
4415
|
+
{dbText("deleteSort")}
|
|
4416
|
+
</DropdownMenuItem>
|
|
4417
|
+
</div>
|
|
4418
|
+
</DropdownMenuContent>
|
|
4419
|
+
</DropdownMenu>
|
|
4420
|
+
);
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
function DatabaseAddFilterButton({
|
|
4424
|
+
open,
|
|
4425
|
+
filters,
|
|
4426
|
+
properties,
|
|
4427
|
+
onOpenChange,
|
|
4428
|
+
onAddFilter,
|
|
4429
|
+
onAddAdvancedFilter,
|
|
4430
|
+
}: {
|
|
4431
|
+
open: boolean;
|
|
4432
|
+
filters: DatabaseFilter[];
|
|
4433
|
+
properties: DocumentProperty[];
|
|
4434
|
+
onOpenChange: (open: boolean) => void;
|
|
4435
|
+
onAddFilter: (key: "name" | string, label: string) => void;
|
|
4436
|
+
onAddAdvancedFilter: (key: "name" | string, label: string) => void;
|
|
4437
|
+
}) {
|
|
4438
|
+
const excludedFilterKeys = useMemo(
|
|
4439
|
+
() => new Set(filters.filter(isActiveFilter).map((filter) => filter.key)),
|
|
4440
|
+
[filters],
|
|
4441
|
+
);
|
|
4442
|
+
const advancedFilterCandidate =
|
|
4443
|
+
databasePropertyPickerItems(properties, "", {
|
|
4444
|
+
includeName: false,
|
|
4445
|
+
excludeKeys: excludedFilterKeys,
|
|
4446
|
+
})[0] ??
|
|
4447
|
+
databasePropertyPickerItems(properties, "", {
|
|
4448
|
+
includeName: true,
|
|
4449
|
+
excludeKeys: excludedFilterKeys,
|
|
4450
|
+
})[0];
|
|
4451
|
+
|
|
4452
|
+
return (
|
|
4453
|
+
<DropdownMenu open={open} onOpenChange={onOpenChange}>
|
|
4454
|
+
<DropdownMenuTrigger asChild>
|
|
4455
|
+
<Button
|
|
4456
|
+
type="button"
|
|
4457
|
+
size="sm"
|
|
4458
|
+
variant="ghost"
|
|
4459
|
+
className="h-7 rounded px-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
4460
|
+
>
|
|
4461
|
+
<IconPlus className="mr-1 size-3.5" />
|
|
4462
|
+
{dbText("filter")}
|
|
4463
|
+
</Button>
|
|
4464
|
+
</DropdownMenuTrigger>
|
|
4465
|
+
<DropdownMenuContent align="start" className="w-64">
|
|
4466
|
+
<div onKeyDown={(event) => event.stopPropagation()}>
|
|
4467
|
+
<DatabasePropertyPickerMenuItems
|
|
4468
|
+
properties={properties}
|
|
4469
|
+
includeName
|
|
4470
|
+
excludeKeys={excludedFilterKeys}
|
|
4471
|
+
placeholder={dbText("filterBy")}
|
|
4472
|
+
closeOnSelect
|
|
4473
|
+
onSelect={onAddFilter}
|
|
4474
|
+
/>
|
|
4475
|
+
<DropdownMenuSeparator />
|
|
4476
|
+
<DropdownMenuItem
|
|
4477
|
+
disabled={!advancedFilterCandidate}
|
|
4478
|
+
onSelect={(event) => {
|
|
4479
|
+
event.preventDefault();
|
|
4480
|
+
if (!advancedFilterCandidate) return;
|
|
4481
|
+
onAddAdvancedFilter(
|
|
4482
|
+
advancedFilterCandidate.key,
|
|
4483
|
+
advancedFilterCandidate.label,
|
|
4484
|
+
);
|
|
4485
|
+
}}
|
|
4486
|
+
>
|
|
4487
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
4488
|
+
{dbText("addAdvancedFilter")}
|
|
4489
|
+
</DropdownMenuItem>
|
|
4490
|
+
</div>
|
|
4491
|
+
</DropdownMenuContent>
|
|
4492
|
+
</DropdownMenu>
|
|
4493
|
+
);
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4496
|
+
function DatabaseFilterModeButton({
|
|
4497
|
+
filterMode,
|
|
4498
|
+
onFilterModeChange,
|
|
4499
|
+
}: {
|
|
4500
|
+
filterMode: DatabaseFilterMode;
|
|
4501
|
+
onFilterModeChange: (filterMode: DatabaseFilterMode) => void;
|
|
4502
|
+
}) {
|
|
4503
|
+
return (
|
|
4504
|
+
<DropdownMenu>
|
|
4505
|
+
<DropdownMenuTrigger asChild>
|
|
4506
|
+
<Button
|
|
4507
|
+
type="button"
|
|
4508
|
+
size="sm"
|
|
4509
|
+
variant="ghost"
|
|
4510
|
+
className="h-7 rounded border border-border/70 bg-background px-2 text-xs font-normal text-muted-foreground shadow-none hover:bg-muted hover:text-foreground"
|
|
4511
|
+
>
|
|
4512
|
+
{databaseFilterModeLabel(filterMode)}
|
|
4513
|
+
<IconChevronDown className="ml-1 size-3" />
|
|
4514
|
+
</Button>
|
|
4515
|
+
</DropdownMenuTrigger>
|
|
4516
|
+
<DropdownMenuContent align="start" className="w-44">
|
|
4517
|
+
{DATABASE_FILTER_MODES.map((mode) => (
|
|
4518
|
+
<DropdownMenuItem
|
|
4519
|
+
key={mode}
|
|
4520
|
+
onSelect={(event) => {
|
|
4521
|
+
event.preventDefault();
|
|
4522
|
+
onFilterModeChange(mode);
|
|
4523
|
+
}}
|
|
4524
|
+
>
|
|
4525
|
+
<span className="flex-1">{databaseFilterModeLabel(mode)}</span>
|
|
4526
|
+
{filterMode === mode ? (
|
|
4527
|
+
<IconCheck className="size-4 text-muted-foreground" />
|
|
4528
|
+
) : null}
|
|
4529
|
+
</DropdownMenuItem>
|
|
4530
|
+
))}
|
|
4531
|
+
</DropdownMenuContent>
|
|
4532
|
+
</DropdownMenu>
|
|
4533
|
+
);
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
type DatabaseFilterEntry = {
|
|
4537
|
+
filter: DatabaseFilter;
|
|
4538
|
+
index: number;
|
|
4539
|
+
};
|
|
4540
|
+
|
|
4541
|
+
type DatabaseNestedFilterGroupEntry = {
|
|
4542
|
+
id: string;
|
|
4543
|
+
entries: DatabaseFilterEntry[];
|
|
4544
|
+
};
|
|
4545
|
+
|
|
4546
|
+
function DatabaseAdvancedFilterGroupControl({
|
|
4547
|
+
documentId,
|
|
4548
|
+
items,
|
|
4549
|
+
filters,
|
|
4550
|
+
filterEntries,
|
|
4551
|
+
filterMode,
|
|
4552
|
+
properties,
|
|
4553
|
+
open,
|
|
4554
|
+
showUnsavedIndicator,
|
|
4555
|
+
onOpenChange,
|
|
4556
|
+
onFiltersChange,
|
|
4557
|
+
onFilterModeChange,
|
|
4558
|
+
}: {
|
|
4559
|
+
documentId: string;
|
|
4560
|
+
items: ContentDatabaseItem[];
|
|
4561
|
+
filters: DatabaseFilter[];
|
|
4562
|
+
filterEntries: DatabaseFilterEntry[];
|
|
4563
|
+
filterMode: DatabaseFilterMode;
|
|
4564
|
+
properties: DocumentProperty[];
|
|
4565
|
+
open: boolean;
|
|
4566
|
+
showUnsavedIndicator: boolean;
|
|
4567
|
+
onOpenChange: (open: boolean) => void;
|
|
4568
|
+
onFiltersChange: (filters: DatabaseFilter[]) => void;
|
|
4569
|
+
onFilterModeChange: (filterMode: DatabaseFilterMode) => void;
|
|
4570
|
+
}) {
|
|
4571
|
+
const advancedFilters = filterEntries.map((entry) => entry.filter);
|
|
4572
|
+
const rootFilterEntries = filterEntries.filter(
|
|
4573
|
+
({ filter }) => !filter.parentFilterGroupId,
|
|
4574
|
+
);
|
|
4575
|
+
const nestedGroupEntries = advancedNestedFilterGroups(filterEntries);
|
|
4576
|
+
const advancedKeys = useMemo(
|
|
4577
|
+
() => new Set(advancedFilters.map((filter) => filter.key)),
|
|
4578
|
+
[advancedFilters],
|
|
4579
|
+
);
|
|
4580
|
+
const addRuleCandidate =
|
|
4581
|
+
databasePropertyPickerItems(properties, "", {
|
|
4582
|
+
includeName: false,
|
|
4583
|
+
excludeKeys: advancedKeys,
|
|
4584
|
+
})[0] ??
|
|
4585
|
+
databasePropertyPickerItems(properties, "", {
|
|
4586
|
+
includeName: true,
|
|
4587
|
+
excludeKeys: advancedKeys,
|
|
4588
|
+
})[0];
|
|
4589
|
+
|
|
4590
|
+
function updateFilterAt(index: number, next: Partial<DatabaseFilter>) {
|
|
4591
|
+
const nextFilters = [...filters];
|
|
4592
|
+
const currentFilter = nextFilters[index] ?? defaultDatabaseFilter();
|
|
4593
|
+
const nextOperator = next.operator ?? currentFilter.operator;
|
|
4594
|
+
nextFilters[index] = {
|
|
4595
|
+
...currentFilter,
|
|
4596
|
+
...next,
|
|
4597
|
+
filterGroupId: next.filterGroupId ?? currentFilter.filterGroupId,
|
|
4598
|
+
parentFilterGroupId:
|
|
4599
|
+
next.parentFilterGroupId ?? currentFilter.parentFilterGroupId,
|
|
4600
|
+
value: filterOperatorNeedsValue(nextOperator)
|
|
4601
|
+
? (next.value ?? currentFilter.value)
|
|
4602
|
+
: "",
|
|
4603
|
+
};
|
|
4604
|
+
onFiltersChange(nextFilters);
|
|
4605
|
+
}
|
|
4606
|
+
|
|
4607
|
+
function addAdvancedRule(key: "name" | string, label: string) {
|
|
4608
|
+
onFiltersChange([
|
|
4609
|
+
...filters,
|
|
4610
|
+
createDatabaseFilterForField(key, label, properties, true),
|
|
4611
|
+
]);
|
|
4612
|
+
}
|
|
4613
|
+
|
|
4614
|
+
function addNestedFilterGroup(key: "name" | string, label: string) {
|
|
4615
|
+
onFiltersChange([
|
|
4616
|
+
...filters,
|
|
4617
|
+
createDatabaseFilterForField(key, label, properties, {
|
|
4618
|
+
filterGroupId: createAdvancedNestedFilterGroupId(),
|
|
4619
|
+
parentFilterGroupId: DATABASE_ADVANCED_FILTER_GROUP_ID,
|
|
4620
|
+
}),
|
|
4621
|
+
]);
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4624
|
+
function addNestedFilterRule(
|
|
4625
|
+
groupId: string,
|
|
4626
|
+
key: "name" | string,
|
|
4627
|
+
label: string,
|
|
4628
|
+
) {
|
|
4629
|
+
onFiltersChange([
|
|
4630
|
+
...filters,
|
|
4631
|
+
createDatabaseFilterForField(key, label, properties, {
|
|
4632
|
+
filterGroupId: groupId,
|
|
4633
|
+
parentFilterGroupId: DATABASE_ADVANCED_FILTER_GROUP_ID,
|
|
4634
|
+
}),
|
|
4635
|
+
]);
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
function removeFilterAt(index: number) {
|
|
4639
|
+
onFiltersChange(filters.filter((_, filterIndex) => filterIndex !== index));
|
|
4640
|
+
if (filterEntries.length <= 1) onOpenChange(false);
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
function removeNestedFilterGroup(groupId: string) {
|
|
4644
|
+
onFiltersChange(
|
|
4645
|
+
filters.filter((filter) => filter.filterGroupId !== groupId),
|
|
4646
|
+
);
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
return (
|
|
4650
|
+
<DropdownMenu open={open} onOpenChange={onOpenChange}>
|
|
4651
|
+
<DropdownMenuTrigger asChild>
|
|
4652
|
+
<Button
|
|
4653
|
+
type="button"
|
|
4654
|
+
size="sm"
|
|
4655
|
+
variant="ghost"
|
|
4656
|
+
className={cn(
|
|
4657
|
+
"relative h-7 rounded border border-[#2383e2]/30 bg-[#2383e2]/10 px-2 text-xs font-normal text-[#0f5ea8] shadow-none hover:bg-[#2383e2]/15 focus-visible:border-[#2383e2]/40 focus-visible:ring-[#2383e2]/25",
|
|
4658
|
+
open && "border-[#2383e2]/40 bg-[#2383e2]/15",
|
|
4659
|
+
)}
|
|
4660
|
+
>
|
|
4661
|
+
<IconFilter className="size-3.5 shrink-0" />
|
|
4662
|
+
<span>{advancedFilterGroupLabel(filterEntries.length)}</span>
|
|
4663
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
4664
|
+
{showUnsavedIndicator ? (
|
|
4665
|
+
<span
|
|
4666
|
+
aria-hidden="true"
|
|
4667
|
+
className="absolute -right-0.5 -top-0.5 size-1.5 rounded-full border border-background bg-amber-500"
|
|
4668
|
+
/>
|
|
4669
|
+
) : null}
|
|
4670
|
+
</Button>
|
|
4671
|
+
</DropdownMenuTrigger>
|
|
4672
|
+
<DropdownMenuContent
|
|
4673
|
+
align="start"
|
|
4674
|
+
className="w-[640px] max-w-[calc(100vw-2rem)] p-0"
|
|
4675
|
+
>
|
|
4676
|
+
<div
|
|
4677
|
+
className="grid gap-1 rounded-lg bg-background p-1.5"
|
|
4678
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
4679
|
+
>
|
|
4680
|
+
{rootFilterEntries.map(({ filter, index }, ruleIndex) => (
|
|
4681
|
+
<DatabaseAdvancedFilterRuleRow
|
|
4682
|
+
key={`${filter.key}-${index}`}
|
|
4683
|
+
documentId={documentId}
|
|
4684
|
+
filter={filter}
|
|
4685
|
+
filters={filters}
|
|
4686
|
+
filterIndex={index}
|
|
4687
|
+
ruleIndex={ruleIndex}
|
|
4688
|
+
filterMode={filterMode}
|
|
4689
|
+
items={items}
|
|
4690
|
+
properties={properties}
|
|
4691
|
+
onFilterModeChange={onFilterModeChange}
|
|
4692
|
+
onUpdateFilter={(next) => updateFilterAt(index, next)}
|
|
4693
|
+
onRemove={() => removeFilterAt(index)}
|
|
4694
|
+
/>
|
|
4695
|
+
))}
|
|
4696
|
+
{nestedGroupEntries.map((group, groupIndex) => (
|
|
4697
|
+
<DatabaseAdvancedNestedFilterGroup
|
|
4698
|
+
key={group.id}
|
|
4699
|
+
documentId={documentId}
|
|
4700
|
+
items={items}
|
|
4701
|
+
filters={filters}
|
|
4702
|
+
group={group}
|
|
4703
|
+
ruleIndex={rootFilterEntries.length + groupIndex}
|
|
4704
|
+
filterMode={filterMode}
|
|
4705
|
+
properties={properties}
|
|
4706
|
+
onFilterModeChange={onFilterModeChange}
|
|
4707
|
+
onUpdateFilter={updateFilterAt}
|
|
4708
|
+
onAddNestedFilterRule={addNestedFilterRule}
|
|
4709
|
+
onRemoveFilter={removeFilterAt}
|
|
4710
|
+
onRemoveGroup={() => removeNestedFilterGroup(group.id)}
|
|
4711
|
+
/>
|
|
4712
|
+
))}
|
|
4713
|
+
|
|
4714
|
+
<DropdownMenuSeparator className="my-1" />
|
|
4715
|
+
<DropdownMenuSub>
|
|
4716
|
+
<DropdownMenuSubTrigger className="h-9 rounded px-2 text-xs text-muted-foreground [&>svg:last-child]:hidden">
|
|
4717
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
4718
|
+
<span className="flex-1">{dbText("addFilterRule")}</span>
|
|
4719
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
4720
|
+
</DropdownMenuSubTrigger>
|
|
4721
|
+
<DropdownMenuSubContent className="w-56">
|
|
4722
|
+
<DropdownMenuItem
|
|
4723
|
+
disabled={!addRuleCandidate}
|
|
4724
|
+
onSelect={(event) => {
|
|
4725
|
+
event.preventDefault();
|
|
4726
|
+
if (!addRuleCandidate) return;
|
|
4727
|
+
addAdvancedRule(addRuleCandidate.key, addRuleCandidate.label);
|
|
4728
|
+
}}
|
|
4729
|
+
>
|
|
4730
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
4731
|
+
{dbText("addFilterRule")}
|
|
4732
|
+
</DropdownMenuItem>
|
|
4733
|
+
<DropdownMenuItem
|
|
4734
|
+
disabled={!addRuleCandidate}
|
|
4735
|
+
onSelect={(event) => {
|
|
4736
|
+
event.preventDefault();
|
|
4737
|
+
if (!addRuleCandidate) return;
|
|
4738
|
+
addNestedFilterGroup(
|
|
4739
|
+
addRuleCandidate.key,
|
|
4740
|
+
addRuleCandidate.label,
|
|
4741
|
+
);
|
|
4742
|
+
}}
|
|
4743
|
+
>
|
|
4744
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
4745
|
+
{dbText("addFilterGroup")}
|
|
4746
|
+
</DropdownMenuItem>
|
|
4747
|
+
</DropdownMenuSubContent>
|
|
4748
|
+
</DropdownMenuSub>
|
|
4749
|
+
<DropdownMenuItem
|
|
4750
|
+
className="h-9 rounded px-2 text-xs text-muted-foreground"
|
|
4751
|
+
onSelect={(event) => {
|
|
4752
|
+
event.preventDefault();
|
|
4753
|
+
onFiltersChange(
|
|
4754
|
+
filters.filter((filter) => !isAdvancedDatabaseFilter(filter)),
|
|
4755
|
+
);
|
|
4756
|
+
onOpenChange(false);
|
|
4757
|
+
}}
|
|
4758
|
+
>
|
|
4759
|
+
<IconTrash className="mr-2 size-4 text-muted-foreground" />
|
|
4760
|
+
{dbText("deleteFilter")}
|
|
4761
|
+
</DropdownMenuItem>
|
|
4762
|
+
</div>
|
|
4763
|
+
</DropdownMenuContent>
|
|
4764
|
+
</DropdownMenu>
|
|
4765
|
+
);
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
function DatabaseAdvancedFilterRuleRow({
|
|
4769
|
+
documentId,
|
|
4770
|
+
filter,
|
|
4771
|
+
filters,
|
|
4772
|
+
filterIndex,
|
|
4773
|
+
ruleIndex,
|
|
4774
|
+
filterMode,
|
|
4775
|
+
items,
|
|
4776
|
+
properties,
|
|
4777
|
+
onFilterModeChange,
|
|
4778
|
+
onUpdateFilter,
|
|
4779
|
+
onRemove,
|
|
4780
|
+
}: {
|
|
4781
|
+
documentId: string;
|
|
4782
|
+
filter: DatabaseFilter;
|
|
4783
|
+
filters: DatabaseFilter[];
|
|
4784
|
+
filterIndex: number;
|
|
4785
|
+
ruleIndex: number;
|
|
4786
|
+
filterMode: DatabaseFilterMode;
|
|
4787
|
+
items: ContentDatabaseItem[];
|
|
4788
|
+
properties: DocumentProperty[];
|
|
4789
|
+
onFilterModeChange: (filterMode: DatabaseFilterMode) => void;
|
|
4790
|
+
onUpdateFilter: (next: Partial<DatabaseFilter>) => void;
|
|
4791
|
+
onRemove: () => void;
|
|
4792
|
+
}) {
|
|
4793
|
+
function selectField(key: "name" | string, label: string) {
|
|
4794
|
+
onUpdateFilter({
|
|
4795
|
+
key,
|
|
4796
|
+
label,
|
|
4797
|
+
operator: defaultFilterOperatorForKey(key, properties),
|
|
4798
|
+
value: "",
|
|
4799
|
+
});
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
return (
|
|
4803
|
+
<div className="flex items-start gap-2">
|
|
4804
|
+
{ruleIndex === 0 ? (
|
|
4805
|
+
<div className="flex h-9 w-16 shrink-0 items-center px-2 text-xs text-foreground">
|
|
4806
|
+
{dbText("where")}
|
|
4807
|
+
</div>
|
|
4808
|
+
) : (
|
|
4809
|
+
<DropdownMenu>
|
|
4810
|
+
<DropdownMenuTrigger asChild>
|
|
4811
|
+
<button
|
|
4812
|
+
type="button"
|
|
4813
|
+
className="flex h-9 w-16 items-center rounded px-2 text-xs hover:bg-muted"
|
|
4814
|
+
>
|
|
4815
|
+
<span className="flex-1 text-left">
|
|
4816
|
+
{databaseFilterModeConjunctionLabel(filterMode)}
|
|
4817
|
+
</span>
|
|
4818
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
4819
|
+
</button>
|
|
4820
|
+
</DropdownMenuTrigger>
|
|
4821
|
+
<DropdownMenuContent align="start" className="w-32">
|
|
4822
|
+
{DATABASE_FILTER_MODES.map((mode) => (
|
|
4823
|
+
<DropdownMenuItem
|
|
4824
|
+
key={mode}
|
|
4825
|
+
onSelect={(event) => {
|
|
4826
|
+
event.preventDefault();
|
|
4827
|
+
onFilterModeChange(mode);
|
|
4828
|
+
}}
|
|
4829
|
+
>
|
|
4830
|
+
<span className="flex-1">
|
|
4831
|
+
{databaseFilterModeConjunctionLabel(mode)}
|
|
4832
|
+
</span>
|
|
4833
|
+
{filterMode === mode ? (
|
|
4834
|
+
<IconCheck className="size-4 text-muted-foreground" />
|
|
4835
|
+
) : null}
|
|
4836
|
+
</DropdownMenuItem>
|
|
4837
|
+
))}
|
|
4838
|
+
</DropdownMenuContent>
|
|
4839
|
+
</DropdownMenu>
|
|
4840
|
+
)}
|
|
4841
|
+
|
|
4842
|
+
<div className="grid min-w-0 flex-1 gap-1">
|
|
4843
|
+
<div className="flex items-center gap-0.5">
|
|
4844
|
+
<DropdownMenu>
|
|
4845
|
+
<DropdownMenuTrigger asChild>
|
|
4846
|
+
<button
|
|
4847
|
+
type="button"
|
|
4848
|
+
className="flex h-9 min-w-0 flex-1 items-center rounded border border-border bg-background px-2 text-left text-xs font-normal shadow-none hover:bg-muted/50 focus:bg-muted/50 data-[state=open]:bg-muted/50"
|
|
4849
|
+
>
|
|
4850
|
+
<FilterFieldIcon filter={filter} properties={properties} />
|
|
4851
|
+
<span className="min-w-0 flex-1 truncate">{filter.label}</span>
|
|
4852
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
4853
|
+
</button>
|
|
4854
|
+
</DropdownMenuTrigger>
|
|
4855
|
+
<DropdownMenuContent
|
|
4856
|
+
align="start"
|
|
4857
|
+
className="max-h-80 w-64 overflow-auto"
|
|
4858
|
+
>
|
|
4859
|
+
<DatabasePropertyPickerMenuItems
|
|
4860
|
+
properties={properties}
|
|
4861
|
+
selectedKey={filter.key}
|
|
4862
|
+
includeName
|
|
4863
|
+
closeOnSelect
|
|
4864
|
+
onSelect={selectField}
|
|
4865
|
+
/>
|
|
4866
|
+
</DropdownMenuContent>
|
|
4867
|
+
</DropdownMenu>
|
|
4868
|
+
|
|
4869
|
+
<DropdownMenu>
|
|
4870
|
+
<DropdownMenuTrigger asChild>
|
|
4871
|
+
<button
|
|
4872
|
+
type="button"
|
|
4873
|
+
className="flex h-9 min-w-0 flex-1 items-center rounded border border-border bg-background px-2 text-left text-xs font-normal shadow-none hover:bg-muted/50 focus:bg-muted/50 data-[state=open]:bg-muted/50"
|
|
4874
|
+
>
|
|
4875
|
+
<span className="min-w-0 flex-1 truncate">
|
|
4876
|
+
{databaseFilterOperatorInlineLabel(
|
|
4877
|
+
filter.operator,
|
|
4878
|
+
filter.key,
|
|
4879
|
+
properties,
|
|
4880
|
+
)}
|
|
4881
|
+
</span>
|
|
4882
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
4883
|
+
</button>
|
|
4884
|
+
</DropdownMenuTrigger>
|
|
4885
|
+
<DropdownMenuContent align="start" className="w-44">
|
|
4886
|
+
{filterOperatorsForKey(filter.key, properties).map((operator) => (
|
|
4887
|
+
<DropdownMenuItem
|
|
4888
|
+
key={operator}
|
|
4889
|
+
onSelect={(event) => {
|
|
4890
|
+
event.preventDefault();
|
|
4891
|
+
onUpdateFilter({ operator });
|
|
4892
|
+
}}
|
|
4893
|
+
>
|
|
4894
|
+
<span className="flex-1">
|
|
4895
|
+
{databaseFilterOperatorLabel(
|
|
4896
|
+
operator,
|
|
4897
|
+
filter.key,
|
|
4898
|
+
properties,
|
|
4899
|
+
)}
|
|
4900
|
+
</span>
|
|
4901
|
+
{filter.operator === operator ? (
|
|
4902
|
+
<IconCheck className="size-4 text-muted-foreground" />
|
|
4903
|
+
) : null}
|
|
4904
|
+
</DropdownMenuItem>
|
|
4905
|
+
))}
|
|
4906
|
+
</DropdownMenuContent>
|
|
4907
|
+
</DropdownMenu>
|
|
4908
|
+
|
|
4909
|
+
{filterOperatorNeedsValue(filter.operator) ? (
|
|
4910
|
+
<div className="min-w-0 flex-[1.35] [&>div>div:first-child]:px-0 [&>input]:h-9 [&>input]:text-xs">
|
|
4911
|
+
<DatabaseFilterValueControl
|
|
4912
|
+
documentId={documentId}
|
|
4913
|
+
filter={filter}
|
|
4914
|
+
items={items}
|
|
4915
|
+
properties={properties}
|
|
4916
|
+
hideOptionsUntilQuery
|
|
4917
|
+
onValueChange={(value) => onUpdateFilter({ value })}
|
|
4918
|
+
/>
|
|
4919
|
+
</div>
|
|
4920
|
+
) : null}
|
|
4921
|
+
|
|
4922
|
+
<DropdownMenu>
|
|
4923
|
+
<DropdownMenuTrigger asChild>
|
|
4924
|
+
<button
|
|
4925
|
+
type="button"
|
|
4926
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded p-0 text-muted-foreground hover:bg-muted/50 data-[state=open]:bg-muted/50"
|
|
4927
|
+
>
|
|
4928
|
+
<IconDots className="size-3.5" />
|
|
4929
|
+
</button>
|
|
4930
|
+
</DropdownMenuTrigger>
|
|
4931
|
+
<DropdownMenuContent align="end" className="w-36">
|
|
4932
|
+
<DropdownMenuItem
|
|
4933
|
+
onSelect={(event) => {
|
|
4934
|
+
event.preventDefault();
|
|
4935
|
+
onRemove();
|
|
4936
|
+
}}
|
|
4937
|
+
>
|
|
4938
|
+
<IconX className="mr-2 size-4 text-muted-foreground" />
|
|
4939
|
+
Remove
|
|
4940
|
+
</DropdownMenuItem>
|
|
4941
|
+
</DropdownMenuContent>
|
|
4942
|
+
</DropdownMenu>
|
|
4943
|
+
</div>
|
|
4944
|
+
</div>
|
|
4945
|
+
</div>
|
|
4946
|
+
);
|
|
4947
|
+
}
|
|
4948
|
+
|
|
4949
|
+
function DatabaseAdvancedNestedFilterGroup({
|
|
4950
|
+
documentId,
|
|
4951
|
+
items,
|
|
4952
|
+
filters,
|
|
4953
|
+
group,
|
|
4954
|
+
ruleIndex,
|
|
4955
|
+
filterMode,
|
|
4956
|
+
properties,
|
|
4957
|
+
onFilterModeChange,
|
|
4958
|
+
onUpdateFilter,
|
|
4959
|
+
onAddNestedFilterRule,
|
|
4960
|
+
onRemoveFilter,
|
|
4961
|
+
onRemoveGroup,
|
|
4962
|
+
}: {
|
|
4963
|
+
documentId: string;
|
|
4964
|
+
items: ContentDatabaseItem[];
|
|
4965
|
+
filters: DatabaseFilter[];
|
|
4966
|
+
group: DatabaseNestedFilterGroupEntry;
|
|
4967
|
+
ruleIndex: number;
|
|
4968
|
+
filterMode: DatabaseFilterMode;
|
|
4969
|
+
properties: DocumentProperty[];
|
|
4970
|
+
onFilterModeChange: (filterMode: DatabaseFilterMode) => void;
|
|
4971
|
+
onUpdateFilter: (index: number, next: Partial<DatabaseFilter>) => void;
|
|
4972
|
+
onAddNestedFilterRule: (
|
|
4973
|
+
groupId: string,
|
|
4974
|
+
key: "name" | string,
|
|
4975
|
+
label: string,
|
|
4976
|
+
) => void;
|
|
4977
|
+
onRemoveFilter: (index: number) => void;
|
|
4978
|
+
onRemoveGroup: () => void;
|
|
4979
|
+
}) {
|
|
4980
|
+
const nestedKeys = useMemo(
|
|
4981
|
+
() => new Set(group.entries.map(({ filter }) => filter.key)),
|
|
4982
|
+
[group.entries],
|
|
4983
|
+
);
|
|
4984
|
+
const addRuleCandidate =
|
|
4985
|
+
databasePropertyPickerItems(properties, "", {
|
|
4986
|
+
includeName: false,
|
|
4987
|
+
excludeKeys: nestedKeys,
|
|
4988
|
+
})[0] ??
|
|
4989
|
+
databasePropertyPickerItems(properties, "", {
|
|
4990
|
+
includeName: true,
|
|
4991
|
+
excludeKeys: nestedKeys,
|
|
4992
|
+
})[0];
|
|
4993
|
+
|
|
4994
|
+
return (
|
|
4995
|
+
<div className="flex items-start gap-2">
|
|
4996
|
+
<DropdownMenu>
|
|
4997
|
+
<DropdownMenuTrigger asChild>
|
|
4998
|
+
<button
|
|
4999
|
+
type="button"
|
|
5000
|
+
className="flex h-9 w-16 items-center rounded px-2 text-xs hover:bg-muted data-[state=open]:bg-muted"
|
|
5001
|
+
>
|
|
5002
|
+
<span className="flex-1 text-left">
|
|
5003
|
+
{databaseFilterModeConjunctionLabel(filterMode)}
|
|
5004
|
+
</span>
|
|
5005
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
5006
|
+
</button>
|
|
5007
|
+
</DropdownMenuTrigger>
|
|
5008
|
+
<DropdownMenuContent align="start" className="w-32">
|
|
5009
|
+
{DATABASE_FILTER_MODES.map((mode) => (
|
|
5010
|
+
<DropdownMenuItem
|
|
5011
|
+
key={mode}
|
|
5012
|
+
onSelect={(event) => {
|
|
5013
|
+
event.preventDefault();
|
|
5014
|
+
onFilterModeChange(mode);
|
|
5015
|
+
}}
|
|
5016
|
+
>
|
|
5017
|
+
<span className="flex-1">
|
|
5018
|
+
{databaseFilterModeConjunctionLabel(mode)}
|
|
5019
|
+
</span>
|
|
5020
|
+
{filterMode === mode ? (
|
|
5021
|
+
<IconCheck className="size-4 text-muted-foreground" />
|
|
5022
|
+
) : null}
|
|
5023
|
+
</DropdownMenuItem>
|
|
5024
|
+
))}
|
|
5025
|
+
</DropdownMenuContent>
|
|
5026
|
+
</DropdownMenu>
|
|
5027
|
+
|
|
5028
|
+
<div className="grid min-w-0 flex-1 gap-1 rounded border border-border/70 bg-muted/30 p-1.5">
|
|
5029
|
+
{group.entries.map(({ filter, index }, nestedRuleIndex) => (
|
|
5030
|
+
<DatabaseAdvancedFilterRuleRow
|
|
5031
|
+
key={`${filter.key}-${index}`}
|
|
5032
|
+
documentId={documentId}
|
|
5033
|
+
filter={filter}
|
|
5034
|
+
filters={filters}
|
|
5035
|
+
filterIndex={index}
|
|
5036
|
+
ruleIndex={nestedRuleIndex}
|
|
5037
|
+
filterMode={filterMode}
|
|
5038
|
+
items={items}
|
|
5039
|
+
properties={properties}
|
|
5040
|
+
onFilterModeChange={onFilterModeChange}
|
|
5041
|
+
onUpdateFilter={(next) =>
|
|
5042
|
+
onUpdateFilter(index, {
|
|
5043
|
+
...next,
|
|
5044
|
+
filterGroupId: group.id,
|
|
5045
|
+
parentFilterGroupId: DATABASE_ADVANCED_FILTER_GROUP_ID,
|
|
5046
|
+
})
|
|
5047
|
+
}
|
|
5048
|
+
onRemove={() => {
|
|
5049
|
+
if (group.entries.length <= 1) {
|
|
5050
|
+
onRemoveGroup();
|
|
5051
|
+
} else {
|
|
5052
|
+
onRemoveFilter(index);
|
|
5053
|
+
}
|
|
5054
|
+
}}
|
|
5055
|
+
/>
|
|
5056
|
+
))}
|
|
5057
|
+
|
|
5058
|
+
<DropdownMenu>
|
|
5059
|
+
<DropdownMenuTrigger asChild>
|
|
5060
|
+
<button
|
|
5061
|
+
type="button"
|
|
5062
|
+
className="flex h-9 items-center rounded px-2 text-left text-xs text-muted-foreground hover:bg-muted data-[state=open]:bg-muted"
|
|
5063
|
+
>
|
|
5064
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
5065
|
+
<span className="flex-1">{dbText("addFilterRule")}</span>
|
|
5066
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
5067
|
+
</button>
|
|
5068
|
+
</DropdownMenuTrigger>
|
|
5069
|
+
<DropdownMenuContent align="start" className="w-56">
|
|
5070
|
+
<DropdownMenuItem
|
|
5071
|
+
disabled={!addRuleCandidate}
|
|
5072
|
+
onSelect={(event) => {
|
|
5073
|
+
event.preventDefault();
|
|
5074
|
+
if (!addRuleCandidate) return;
|
|
5075
|
+
onAddNestedFilterRule(
|
|
5076
|
+
group.id,
|
|
5077
|
+
addRuleCandidate.key,
|
|
5078
|
+
addRuleCandidate.label,
|
|
5079
|
+
);
|
|
5080
|
+
}}
|
|
5081
|
+
>
|
|
5082
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
5083
|
+
{dbText("addFilterRule")}
|
|
5084
|
+
</DropdownMenuItem>
|
|
5085
|
+
</DropdownMenuContent>
|
|
5086
|
+
</DropdownMenu>
|
|
5087
|
+
</div>
|
|
5088
|
+
|
|
5089
|
+
<DropdownMenu>
|
|
5090
|
+
<DropdownMenuTrigger asChild>
|
|
5091
|
+
<button
|
|
5092
|
+
type="button"
|
|
5093
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded p-0 text-muted-foreground hover:bg-muted/50 data-[state=open]:bg-muted/50"
|
|
5094
|
+
>
|
|
5095
|
+
<IconDots className="size-3.5" />
|
|
5096
|
+
</button>
|
|
5097
|
+
</DropdownMenuTrigger>
|
|
5098
|
+
<DropdownMenuContent align="end" className="w-36">
|
|
5099
|
+
<DropdownMenuItem
|
|
5100
|
+
onSelect={(event) => {
|
|
5101
|
+
event.preventDefault();
|
|
5102
|
+
onRemoveGroup();
|
|
5103
|
+
}}
|
|
5104
|
+
>
|
|
5105
|
+
<IconX className="mr-2 size-4 text-muted-foreground" />
|
|
5106
|
+
Remove
|
|
5107
|
+
</DropdownMenuItem>
|
|
5108
|
+
</DropdownMenuContent>
|
|
5109
|
+
</DropdownMenu>
|
|
5110
|
+
</div>
|
|
5111
|
+
);
|
|
5112
|
+
}
|
|
5113
|
+
|
|
5114
|
+
function DatabaseInlineFilterControl({
|
|
5115
|
+
documentId,
|
|
5116
|
+
filter,
|
|
5117
|
+
filterIndex,
|
|
5118
|
+
filters,
|
|
5119
|
+
items,
|
|
5120
|
+
properties,
|
|
5121
|
+
open,
|
|
5122
|
+
showUnsavedIndicator,
|
|
5123
|
+
onOpenChange,
|
|
5124
|
+
onFiltersChange,
|
|
5125
|
+
onRemove,
|
|
5126
|
+
}: {
|
|
5127
|
+
documentId: string;
|
|
5128
|
+
filter: DatabaseFilter;
|
|
5129
|
+
filterIndex: number;
|
|
5130
|
+
filters: DatabaseFilter[];
|
|
5131
|
+
items: ContentDatabaseItem[];
|
|
5132
|
+
properties: DocumentProperty[];
|
|
5133
|
+
open: boolean;
|
|
5134
|
+
showUnsavedIndicator: boolean;
|
|
5135
|
+
onOpenChange: (open: boolean) => void;
|
|
5136
|
+
onFiltersChange: (filters: DatabaseFilter[]) => void;
|
|
5137
|
+
onRemove: () => void;
|
|
5138
|
+
}) {
|
|
5139
|
+
function updateFilter(next: Partial<DatabaseFilter>) {
|
|
5140
|
+
const nextFilters = [...filters];
|
|
5141
|
+
const currentFilter = nextFilters[filterIndex] ?? defaultDatabaseFilter();
|
|
5142
|
+
const nextOperator = next.operator ?? currentFilter.operator;
|
|
5143
|
+
nextFilters[filterIndex] = {
|
|
5144
|
+
...currentFilter,
|
|
5145
|
+
...next,
|
|
5146
|
+
value: filterOperatorNeedsValue(nextOperator)
|
|
5147
|
+
? (next.value ?? currentFilter.value)
|
|
5148
|
+
: "",
|
|
5149
|
+
};
|
|
5150
|
+
onFiltersChange(nextFilters);
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
function selectField(key: "name" | string, label: string) {
|
|
5154
|
+
updateFilter({
|
|
5155
|
+
key,
|
|
5156
|
+
label,
|
|
5157
|
+
operator: defaultFilterOperatorForKey(key, properties),
|
|
5158
|
+
value: "",
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
const filterIsComplete =
|
|
5163
|
+
!filterOperatorNeedsValue(filter.operator) ||
|
|
5164
|
+
filter.value.trim().length > 0;
|
|
5165
|
+
return (
|
|
5166
|
+
<DropdownMenu open={open} onOpenChange={onOpenChange}>
|
|
5167
|
+
<DropdownMenuTrigger asChild>
|
|
5168
|
+
<Button
|
|
5169
|
+
type="button"
|
|
5170
|
+
size="sm"
|
|
5171
|
+
variant="ghost"
|
|
5172
|
+
className={cn(
|
|
5173
|
+
"relative h-7 max-w-[16rem] rounded border border-border/70 bg-background px-2 text-xs font-normal text-foreground shadow-none hover:bg-muted focus-visible:border-[#2383e2]/40 focus-visible:ring-[#2383e2]/25",
|
|
5174
|
+
filterIsComplete &&
|
|
5175
|
+
"border-[#2383e2]/30 bg-[#2383e2]/10 text-[#0f5ea8] hover:bg-[#2383e2]/15",
|
|
5176
|
+
open && "border-[#2383e2]/40 bg-[#2383e2]/15",
|
|
5177
|
+
!filterIsComplete && "text-muted-foreground",
|
|
5178
|
+
)}
|
|
5179
|
+
>
|
|
5180
|
+
<FilterFieldIcon filter={filter} properties={properties} />
|
|
5181
|
+
<span className="min-w-0 truncate">
|
|
5182
|
+
{databaseInlineFilterLabel(filter, properties)}
|
|
5183
|
+
</span>
|
|
5184
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
5185
|
+
{showUnsavedIndicator ? (
|
|
5186
|
+
<span
|
|
5187
|
+
aria-hidden="true"
|
|
5188
|
+
className="absolute -right-0.5 -top-0.5 size-1.5 rounded-full border border-background bg-amber-500"
|
|
5189
|
+
/>
|
|
5190
|
+
) : null}
|
|
5191
|
+
</Button>
|
|
5192
|
+
</DropdownMenuTrigger>
|
|
5193
|
+
<DropdownMenuContent align="start" className="w-[260px] p-0">
|
|
5194
|
+
<div
|
|
5195
|
+
className="grid gap-1 p-1"
|
|
5196
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
5197
|
+
>
|
|
5198
|
+
<div className="flex items-center gap-0.5">
|
|
5199
|
+
<DropdownMenu>
|
|
5200
|
+
<DropdownMenuTrigger asChild>
|
|
5201
|
+
<button
|
|
5202
|
+
type="button"
|
|
5203
|
+
className="flex h-7 min-w-0 flex-1 items-center rounded px-1.5 text-left text-xs hover:bg-muted data-[state=open]:bg-muted"
|
|
5204
|
+
>
|
|
5205
|
+
<FilterFieldIcon filter={filter} properties={properties} />
|
|
5206
|
+
<span className="min-w-0 flex-1 truncate">
|
|
5207
|
+
{filter.label}
|
|
5208
|
+
</span>
|
|
5209
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
5210
|
+
</button>
|
|
5211
|
+
</DropdownMenuTrigger>
|
|
5212
|
+
<DropdownMenuContent
|
|
5213
|
+
align="start"
|
|
5214
|
+
className="max-h-80 w-64 overflow-auto"
|
|
5215
|
+
>
|
|
5216
|
+
<DatabasePropertyPickerMenuItems
|
|
5217
|
+
properties={properties}
|
|
5218
|
+
selectedKey={filter.key}
|
|
5219
|
+
includeName
|
|
5220
|
+
closeOnSelect
|
|
5221
|
+
onSelect={selectField}
|
|
5222
|
+
/>
|
|
5223
|
+
</DropdownMenuContent>
|
|
5224
|
+
</DropdownMenu>
|
|
5225
|
+
|
|
5226
|
+
<DropdownMenu>
|
|
5227
|
+
<DropdownMenuTrigger asChild>
|
|
5228
|
+
<button
|
|
5229
|
+
type="button"
|
|
5230
|
+
className="flex h-7 min-w-0 flex-1 items-center rounded px-1.5 text-left text-xs hover:bg-muted data-[state=open]:bg-muted"
|
|
5231
|
+
>
|
|
5232
|
+
<span className="min-w-0 flex-1 truncate">
|
|
5233
|
+
{databaseFilterOperatorInlineLabel(
|
|
5234
|
+
filter.operator,
|
|
5235
|
+
filter.key,
|
|
5236
|
+
properties,
|
|
5237
|
+
)}
|
|
5238
|
+
</span>
|
|
5239
|
+
<IconChevronDown className="ml-1 size-3 shrink-0 text-muted-foreground" />
|
|
5240
|
+
</button>
|
|
5241
|
+
</DropdownMenuTrigger>
|
|
5242
|
+
<DropdownMenuContent align="start" className="w-44">
|
|
5243
|
+
{filterOperatorsForKey(filter.key, properties).map(
|
|
5244
|
+
(operator) => (
|
|
5245
|
+
<DropdownMenuItem
|
|
5246
|
+
key={operator}
|
|
5247
|
+
onSelect={(event) => {
|
|
5248
|
+
event.preventDefault();
|
|
5249
|
+
updateFilter({ operator });
|
|
5250
|
+
}}
|
|
5251
|
+
>
|
|
5252
|
+
<span className="flex-1">
|
|
5253
|
+
{databaseFilterOperatorLabel(
|
|
5254
|
+
operator,
|
|
5255
|
+
filter.key,
|
|
5256
|
+
properties,
|
|
5257
|
+
)}
|
|
5258
|
+
</span>
|
|
5259
|
+
{filter.operator === operator ? (
|
|
5260
|
+
<IconCheck className="size-4 text-muted-foreground" />
|
|
5261
|
+
) : null}
|
|
5262
|
+
</DropdownMenuItem>
|
|
5263
|
+
),
|
|
5264
|
+
)}
|
|
5265
|
+
</DropdownMenuContent>
|
|
5266
|
+
</DropdownMenu>
|
|
5267
|
+
|
|
5268
|
+
<DropdownMenu>
|
|
5269
|
+
<DropdownMenuTrigger asChild>
|
|
5270
|
+
<button
|
|
5271
|
+
type="button"
|
|
5272
|
+
className="flex h-7 w-7 items-center justify-center rounded p-0 text-muted-foreground hover:bg-muted data-[state=open]:bg-muted"
|
|
5273
|
+
>
|
|
5274
|
+
<IconDots className="size-3.5" />
|
|
5275
|
+
</button>
|
|
5276
|
+
</DropdownMenuTrigger>
|
|
5277
|
+
<DropdownMenuContent align="end" className="w-36">
|
|
5278
|
+
<DropdownMenuItem
|
|
5279
|
+
disabled={filters.length <= 1 || filterIndex === 0}
|
|
5280
|
+
onSelect={(event) => {
|
|
5281
|
+
event.preventDefault();
|
|
5282
|
+
onFiltersChange(
|
|
5283
|
+
moveDatabaseFilter(filters, filterIndex, "up"),
|
|
5284
|
+
);
|
|
5285
|
+
}}
|
|
5286
|
+
>
|
|
5287
|
+
<IconArrowUp className="mr-2 size-4 text-muted-foreground" />
|
|
5288
|
+
{dbText("moveFilterUp")}
|
|
5289
|
+
</DropdownMenuItem>
|
|
5290
|
+
<DropdownMenuItem
|
|
5291
|
+
disabled={
|
|
5292
|
+
filters.length <= 1 || filterIndex >= filters.length - 1
|
|
5293
|
+
}
|
|
5294
|
+
onSelect={(event) => {
|
|
5295
|
+
event.preventDefault();
|
|
5296
|
+
onFiltersChange(
|
|
5297
|
+
moveDatabaseFilter(filters, filterIndex, "down"),
|
|
5298
|
+
);
|
|
5299
|
+
}}
|
|
5300
|
+
>
|
|
5301
|
+
<IconArrowDown className="mr-2 size-4 text-muted-foreground" />
|
|
5302
|
+
{dbText("moveFilterDown")}
|
|
5303
|
+
</DropdownMenuItem>
|
|
5304
|
+
<DropdownMenuSeparator />
|
|
5305
|
+
<DropdownMenuItem
|
|
5306
|
+
onSelect={(event) => {
|
|
5307
|
+
event.preventDefault();
|
|
5308
|
+
onRemove();
|
|
5309
|
+
}}
|
|
5310
|
+
>
|
|
5311
|
+
<IconX className="mr-2 size-4 text-muted-foreground" />
|
|
5312
|
+
Remove
|
|
5313
|
+
</DropdownMenuItem>
|
|
5314
|
+
</DropdownMenuContent>
|
|
5315
|
+
</DropdownMenu>
|
|
5316
|
+
</div>
|
|
5317
|
+
|
|
5318
|
+
{filterOperatorNeedsValue(filter.operator) ? (
|
|
5319
|
+
<DatabaseFilterValueControl
|
|
5320
|
+
autoFocus={!filter.value}
|
|
5321
|
+
documentId={documentId}
|
|
5322
|
+
filter={filter}
|
|
5323
|
+
items={items}
|
|
5324
|
+
properties={properties}
|
|
5325
|
+
onValueChange={(value) => updateFilter({ value })}
|
|
5326
|
+
/>
|
|
5327
|
+
) : null}
|
|
5328
|
+
</div>
|
|
5329
|
+
</DropdownMenuContent>
|
|
5330
|
+
</DropdownMenu>
|
|
5331
|
+
);
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5334
|
+
function databaseToolbarIconButtonClass(active = false) {
|
|
5335
|
+
return cn(
|
|
5336
|
+
"h-7 w-7 p-0 text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-45",
|
|
5337
|
+
active && "bg-muted text-foreground",
|
|
5338
|
+
);
|
|
5339
|
+
}
|
|
5340
|
+
|
|
5341
|
+
type DatabaseSettingsPanel =
|
|
5342
|
+
| "main"
|
|
5343
|
+
| "source"
|
|
5344
|
+
| "layout"
|
|
5345
|
+
| "property_visibility"
|
|
5346
|
+
| "group";
|
|
5347
|
+
|
|
5348
|
+
// One step in the Sources drill-down: Sources (root, empty stack) → provider
|
|
5349
|
+
// (Builder) → space → model leaf. The model step carries the full summary so
|
|
5350
|
+
// the leaf can attach without re-fetching.
|
|
5351
|
+
// A second source being added, awaiting the canonical-key confirm step.
|
|
5352
|
+
type PendingSourceCandidate = {
|
|
5353
|
+
sourceType: "mock-local" | "builder-cms" | "local-table";
|
|
5354
|
+
sourceName: string;
|
|
5355
|
+
sourceTable: string;
|
|
5356
|
+
displayName: string;
|
|
5357
|
+
existingSourceId?: string;
|
|
5358
|
+
};
|
|
5359
|
+
|
|
5360
|
+
type SourceNavStep =
|
|
5361
|
+
| { kind: "provider"; providerId: "builder" }
|
|
5362
|
+
| { kind: "space"; spaceId: string; spaceName: string }
|
|
5363
|
+
| { kind: "model"; model: BuilderCmsModelSummary }
|
|
5364
|
+
| { kind: "addSource" }
|
|
5365
|
+
| { kind: "secondarySource"; sourceId: string; sourceName: string }
|
|
5366
|
+
| { kind: "keyConfirm"; candidate: PendingSourceCandidate }
|
|
5367
|
+
| { kind: "fieldPicker"; sourceId: string; sourceName: string };
|
|
5368
|
+
|
|
5369
|
+
function sourceNavTitle(stack: SourceNavStep[]): string {
|
|
5370
|
+
const top = stack[stack.length - 1];
|
|
5371
|
+
if (!top) return dbText("sources");
|
|
5372
|
+
if (top.kind === "provider") return "Builder";
|
|
5373
|
+
if (top.kind === "space") return top.spaceName;
|
|
5374
|
+
if (top.kind === "addSource") return dbText("addASource");
|
|
5375
|
+
if (top.kind === "secondarySource") return top.sourceName;
|
|
5376
|
+
if (top.kind === "keyConfirm") return dbText("matchExistingItemsToDetails");
|
|
5377
|
+
if (top.kind === "fieldPicker") return dbText("chooseFields");
|
|
5378
|
+
return top.model.displayName;
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
// The Builder "B" brand mark (first glyph of the wordmark), drawn with
|
|
5382
|
+
// currentColor so it themes against the panel background.
|
|
5383
|
+
function BuilderLogoMark({ className }: { className?: string }) {
|
|
5384
|
+
return (
|
|
5385
|
+
<svg
|
|
5386
|
+
viewBox="0 0 71 80"
|
|
5387
|
+
fill="currentColor"
|
|
5388
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5389
|
+
className={className}
|
|
5390
|
+
aria-hidden="true"
|
|
5391
|
+
>
|
|
5392
|
+
<path d="M70.86 24C70.86 10.69 60.06 0 46.86 0H6.31995C2.81995 0 0 2.84031 0 6.32031C0 12.8003 13.71 17.71 13.71 40C13.71 62.29 0 67.2102 0 73.6802C0 77.1602 2.81995 80 6.31995 80H46.86C60.06 80 70.86 69.31 70.86 56C70.86 46.22 64.98 40.25 64.75 40C64.98 39.75 70.86 33.78 70.86 24ZM8.37 6.86035H46.87C51.45 6.86035 55.75 8.64037 58.99 11.8804C62.23 15.1204 64.01 19.42 64.01 24C64.01 28.58 62.3199 32.62 59.3199 35.79L8.37 6.86035ZM58.99 68.1304C55.75 71.3704 51.45 73.1504 46.87 73.1504H8.37L59.3199 44.2202C62.3199 47.3902 64.01 51.5703 64.01 56.0103C64.01 60.4503 62.23 64.8904 58.99 68.1304ZM15.83 61.02C16.24 60.17 20.58 51.74 20.58 40C20.58 28.26 16.24 19.83 15.83 18.98L52.85 40L15.83 61.02Z" />
|
|
5393
|
+
</svg>
|
|
5394
|
+
);
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5397
|
+
// The Notion logo, reusing the shared `.notion-logo-icon` styling (same mark as
|
|
5398
|
+
// the sidebar's Notion button) so it themes consistently.
|
|
5399
|
+
function NotionLogoMark({ className }: { className?: string }) {
|
|
3948
5400
|
return (
|
|
3949
5401
|
<svg
|
|
3950
5402
|
viewBox="0 0 100 100"
|
|
@@ -7081,7 +8533,10 @@ type DatabasePropertyPickerOption = {
|
|
|
7081
8533
|
export function databasePropertyPickerItems(
|
|
7082
8534
|
properties: DocumentProperty[],
|
|
7083
8535
|
query: string,
|
|
7084
|
-
{
|
|
8536
|
+
{
|
|
8537
|
+
includeName = true,
|
|
8538
|
+
excludeKeys,
|
|
8539
|
+
}: { includeName?: boolean; excludeKeys?: ReadonlySet<string> } = {},
|
|
7085
8540
|
): DatabasePropertyPickerOption[] {
|
|
7086
8541
|
const normalizedQuery = query.trim().toLowerCase();
|
|
7087
8542
|
const items: DatabasePropertyPickerOption[] = [
|
|
@@ -7093,7 +8548,7 @@ export function databasePropertyPickerItems(
|
|
|
7093
8548
|
label: property.definition.name,
|
|
7094
8549
|
type: property.definition.type,
|
|
7095
8550
|
})),
|
|
7096
|
-
];
|
|
8551
|
+
].filter((item) => !excludeKeys?.has(item.key));
|
|
7097
8552
|
|
|
7098
8553
|
if (!normalizedQuery) return items;
|
|
7099
8554
|
return items.filter((item) =>
|
|
@@ -7106,9 +8561,11 @@ export function databasePropertyPickerItems(
|
|
|
7106
8561
|
function DatabasePropertyPickerSearch({
|
|
7107
8562
|
value,
|
|
7108
8563
|
onChange,
|
|
8564
|
+
placeholder = dbText("searchProperties"),
|
|
7109
8565
|
}: {
|
|
7110
8566
|
value: string;
|
|
7111
8567
|
onChange: (value: string) => void;
|
|
8568
|
+
placeholder?: string;
|
|
7112
8569
|
}) {
|
|
7113
8570
|
return (
|
|
7114
8571
|
<div className="border-b border-border/70 p-1">
|
|
@@ -7118,8 +8575,8 @@ function DatabasePropertyPickerSearch({
|
|
|
7118
8575
|
value={value}
|
|
7119
8576
|
onChange={(event) => onChange(event.target.value)}
|
|
7120
8577
|
onKeyDown={(event) => event.stopPropagation()}
|
|
7121
|
-
placeholder={
|
|
7122
|
-
aria-label={
|
|
8578
|
+
placeholder={placeholder}
|
|
8579
|
+
aria-label={placeholder}
|
|
7123
8580
|
className="h-6 border-0 bg-transparent px-0 text-xs shadow-none focus-visible:ring-0"
|
|
7124
8581
|
/>
|
|
7125
8582
|
</div>
|
|
@@ -7131,16 +8588,18 @@ function DatabasePropertyPickerItem({
|
|
|
7131
8588
|
item,
|
|
7132
8589
|
selected,
|
|
7133
8590
|
onSelect,
|
|
8591
|
+
closeOnSelect = false,
|
|
7134
8592
|
}: {
|
|
7135
8593
|
item: DatabasePropertyPickerOption;
|
|
7136
8594
|
selected: boolean;
|
|
7137
8595
|
onSelect: (key: string, label: string) => void;
|
|
8596
|
+
closeOnSelect?: boolean;
|
|
7138
8597
|
}) {
|
|
7139
8598
|
const Icon = item.type === "name" ? IconFileText : TYPE_ICONS[item.type];
|
|
7140
8599
|
return (
|
|
7141
8600
|
<DropdownMenuItem
|
|
7142
8601
|
onSelect={(event) => {
|
|
7143
|
-
event.preventDefault();
|
|
8602
|
+
if (!closeOnSelect) event.preventDefault();
|
|
7144
8603
|
onSelect(item.key, item.label);
|
|
7145
8604
|
}}
|
|
7146
8605
|
>
|
|
@@ -7151,23 +8610,84 @@ function DatabasePropertyPickerItem({
|
|
|
7151
8610
|
);
|
|
7152
8611
|
}
|
|
7153
8612
|
|
|
8613
|
+
function DatabasePropertyPickerMenuItems({
|
|
8614
|
+
properties,
|
|
8615
|
+
selectedKey,
|
|
8616
|
+
includeName,
|
|
8617
|
+
excludeKeys,
|
|
8618
|
+
placeholder,
|
|
8619
|
+
closeOnSelect,
|
|
8620
|
+
onSelect,
|
|
8621
|
+
}: {
|
|
8622
|
+
properties: DocumentProperty[];
|
|
8623
|
+
selectedKey?: string;
|
|
8624
|
+
includeName?: boolean;
|
|
8625
|
+
excludeKeys?: ReadonlySet<string>;
|
|
8626
|
+
placeholder?: string;
|
|
8627
|
+
closeOnSelect?: boolean;
|
|
8628
|
+
onSelect: (key: string, label: string) => void;
|
|
8629
|
+
}) {
|
|
8630
|
+
const [query, setQuery] = useState("");
|
|
8631
|
+
const items = databasePropertyPickerItems(properties, query, {
|
|
8632
|
+
includeName,
|
|
8633
|
+
excludeKeys,
|
|
8634
|
+
});
|
|
8635
|
+
|
|
8636
|
+
return (
|
|
8637
|
+
<>
|
|
8638
|
+
<DatabasePropertyPickerSearch
|
|
8639
|
+
value={query}
|
|
8640
|
+
onChange={setQuery}
|
|
8641
|
+
placeholder={placeholder}
|
|
8642
|
+
/>
|
|
8643
|
+
<div className="max-h-72 overflow-auto p-1">
|
|
8644
|
+
{items.map((item) => (
|
|
8645
|
+
<DatabasePropertyPickerItem
|
|
8646
|
+
key={item.key}
|
|
8647
|
+
item={item}
|
|
8648
|
+
selected={selectedKey === item.key}
|
|
8649
|
+
closeOnSelect={closeOnSelect}
|
|
8650
|
+
onSelect={onSelect}
|
|
8651
|
+
/>
|
|
8652
|
+
))}
|
|
8653
|
+
{items.length === 0 ? (
|
|
8654
|
+
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
|
8655
|
+
{dbText("noPropertiesFound")}
|
|
8656
|
+
</div>
|
|
8657
|
+
) : null}
|
|
8658
|
+
</div>
|
|
8659
|
+
</>
|
|
8660
|
+
);
|
|
8661
|
+
}
|
|
8662
|
+
|
|
7154
8663
|
function DatabasePropertyPickerSubContent({
|
|
7155
8664
|
properties,
|
|
7156
8665
|
selectedKey,
|
|
7157
8666
|
includeName,
|
|
8667
|
+
excludeKeys,
|
|
8668
|
+
placeholder,
|
|
7158
8669
|
onSelect,
|
|
7159
8670
|
}: {
|
|
7160
8671
|
properties: DocumentProperty[];
|
|
7161
|
-
selectedKey
|
|
8672
|
+
selectedKey?: string;
|
|
7162
8673
|
includeName?: boolean;
|
|
8674
|
+
excludeKeys?: ReadonlySet<string>;
|
|
8675
|
+
placeholder?: string;
|
|
7163
8676
|
onSelect: (key: string, label: string) => void;
|
|
7164
8677
|
}) {
|
|
7165
8678
|
const [query, setQuery] = useState("");
|
|
7166
|
-
const items = databasePropertyPickerItems(properties, query, {
|
|
8679
|
+
const items = databasePropertyPickerItems(properties, query, {
|
|
8680
|
+
includeName,
|
|
8681
|
+
excludeKeys,
|
|
8682
|
+
});
|
|
7167
8683
|
|
|
7168
8684
|
return (
|
|
7169
8685
|
<DropdownMenuSubContent className="max-h-80 w-64 overflow-auto">
|
|
7170
|
-
<DatabasePropertyPickerSearch
|
|
8686
|
+
<DatabasePropertyPickerSearch
|
|
8687
|
+
value={query}
|
|
8688
|
+
onChange={setQuery}
|
|
8689
|
+
placeholder={placeholder}
|
|
8690
|
+
/>
|
|
7171
8691
|
{items.map((item) => (
|
|
7172
8692
|
<DatabasePropertyPickerItem
|
|
7173
8693
|
key={item.key}
|
|
@@ -7332,8 +8852,8 @@ function databaseFilterModeLabel(filterMode: DatabaseFilterMode) {
|
|
|
7332
8852
|
return filterMode === "or" ? "Any" : "All";
|
|
7333
8853
|
}
|
|
7334
8854
|
|
|
7335
|
-
function
|
|
7336
|
-
return filterMode === "or" ? "
|
|
8855
|
+
function databaseFilterModeConjunctionLabel(filterMode: DatabaseFilterMode) {
|
|
8856
|
+
return filterMode === "or" ? "Or" : "And";
|
|
7337
8857
|
}
|
|
7338
8858
|
|
|
7339
8859
|
function DatabaseResultCountFooter({
|
|
@@ -10753,6 +12273,143 @@ function databaseViewStateKey(
|
|
|
10753
12273
|
return JSON.stringify({ databaseId, viewConfig });
|
|
10754
12274
|
}
|
|
10755
12275
|
|
|
12276
|
+
function normalizePersonalDatabaseViewOverrides(
|
|
12277
|
+
value: ContentDatabasePersonalViewOverrides | null | undefined,
|
|
12278
|
+
): PersonalDatabaseViewOverrides | null {
|
|
12279
|
+
try {
|
|
12280
|
+
if (!value) return null;
|
|
12281
|
+
const parsed = value as Partial<PersonalDatabaseViewOverrides>;
|
|
12282
|
+
if (
|
|
12283
|
+
parsed.version !== PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION ||
|
|
12284
|
+
!Array.isArray(parsed.views)
|
|
12285
|
+
) {
|
|
12286
|
+
return null;
|
|
12287
|
+
}
|
|
12288
|
+
|
|
12289
|
+
return {
|
|
12290
|
+
version: PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION,
|
|
12291
|
+
activeViewId:
|
|
12292
|
+
typeof parsed.activeViewId === "string"
|
|
12293
|
+
? parsed.activeViewId
|
|
12294
|
+
: undefined,
|
|
12295
|
+
views: parsed.views
|
|
12296
|
+
.filter((view) => typeof view?.id === "string")
|
|
12297
|
+
.map((view) => ({
|
|
12298
|
+
id: view.id,
|
|
12299
|
+
sorts: Array.isArray(view.sorts)
|
|
12300
|
+
? view.sorts.filter(isDatabaseSort)
|
|
12301
|
+
: [],
|
|
12302
|
+
filters: Array.isArray(view.filters)
|
|
12303
|
+
? view.filters.filter(isDatabaseFilter)
|
|
12304
|
+
: [],
|
|
12305
|
+
filterMode: normalizeClientDatabaseFilterMode(view.filterMode),
|
|
12306
|
+
})),
|
|
12307
|
+
};
|
|
12308
|
+
} catch {
|
|
12309
|
+
return null;
|
|
12310
|
+
}
|
|
12311
|
+
}
|
|
12312
|
+
|
|
12313
|
+
function personalDatabaseViewOverridesFromConfig(
|
|
12314
|
+
viewConfig: ContentDatabaseViewConfig,
|
|
12315
|
+
): PersonalDatabaseViewOverrides {
|
|
12316
|
+
const normalized = normalizeClientDatabaseViewConfig(viewConfig);
|
|
12317
|
+
return {
|
|
12318
|
+
version: PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION,
|
|
12319
|
+
activeViewId: normalized.activeViewId,
|
|
12320
|
+
views: normalized.views.map((view) => ({
|
|
12321
|
+
id: view.id,
|
|
12322
|
+
sorts: view.sorts,
|
|
12323
|
+
filters: view.filters,
|
|
12324
|
+
filterMode: view.filterMode ?? "and",
|
|
12325
|
+
})),
|
|
12326
|
+
};
|
|
12327
|
+
}
|
|
12328
|
+
|
|
12329
|
+
export function applyPersonalDatabaseViewOverrides(
|
|
12330
|
+
savedViewConfig: ContentDatabaseViewConfig,
|
|
12331
|
+
overrides: PersonalDatabaseViewOverrides | null,
|
|
12332
|
+
) {
|
|
12333
|
+
const saved = normalizeClientDatabaseViewConfig(savedViewConfig);
|
|
12334
|
+
if (!overrides) return saved;
|
|
12335
|
+
|
|
12336
|
+
const overridesByViewId = new Map(
|
|
12337
|
+
overrides.views.map((view) => [view.id, view]),
|
|
12338
|
+
);
|
|
12339
|
+
return normalizeClientDatabaseViewConfig({
|
|
12340
|
+
...saved,
|
|
12341
|
+
activeViewId: saved.views.some((view) => view.id === overrides.activeViewId)
|
|
12342
|
+
? overrides.activeViewId
|
|
12343
|
+
: saved.activeViewId,
|
|
12344
|
+
views: saved.views.map((view) => {
|
|
12345
|
+
const override = overridesByViewId.get(view.id);
|
|
12346
|
+
if (!override) return view;
|
|
12347
|
+
return {
|
|
12348
|
+
...view,
|
|
12349
|
+
sorts: override.sorts,
|
|
12350
|
+
filters: override.filters,
|
|
12351
|
+
filterMode: override.filterMode,
|
|
12352
|
+
};
|
|
12353
|
+
}),
|
|
12354
|
+
});
|
|
12355
|
+
}
|
|
12356
|
+
|
|
12357
|
+
export function databaseViewConfigWithSavedQueryState(
|
|
12358
|
+
currentViewConfig: ContentDatabaseViewConfig,
|
|
12359
|
+
savedViewConfig: ContentDatabaseViewConfig,
|
|
12360
|
+
) {
|
|
12361
|
+
const current = normalizeClientDatabaseViewConfig(currentViewConfig);
|
|
12362
|
+
const saved = normalizeClientDatabaseViewConfig(savedViewConfig);
|
|
12363
|
+
const savedViewsById = new Map(saved.views.map((view) => [view.id, view]));
|
|
12364
|
+
|
|
12365
|
+
return normalizeClientDatabaseViewConfig({
|
|
12366
|
+
...current,
|
|
12367
|
+
activeViewId: saved.activeViewId,
|
|
12368
|
+
views: current.views.map((view) => {
|
|
12369
|
+
const savedView = savedViewsById.get(view.id);
|
|
12370
|
+
if (!savedView) return view;
|
|
12371
|
+
return {
|
|
12372
|
+
...view,
|
|
12373
|
+
sorts: savedView.sorts,
|
|
12374
|
+
filters: savedView.filters,
|
|
12375
|
+
filterMode: savedView.filterMode,
|
|
12376
|
+
};
|
|
12377
|
+
}),
|
|
12378
|
+
});
|
|
12379
|
+
}
|
|
12380
|
+
|
|
12381
|
+
export function databaseViewConfigWithClearedActiveFilters(
|
|
12382
|
+
viewConfig: ContentDatabaseViewConfig,
|
|
12383
|
+
) {
|
|
12384
|
+
return updateActiveDatabaseView(
|
|
12385
|
+
normalizeClientDatabaseViewConfig(viewConfig),
|
|
12386
|
+
(view) => ({ ...view, filters: [], filterMode: "and" }),
|
|
12387
|
+
);
|
|
12388
|
+
}
|
|
12389
|
+
|
|
12390
|
+
export function databaseViewHasPersonalQueryChanges(
|
|
12391
|
+
currentViewConfig: ContentDatabaseViewConfig,
|
|
12392
|
+
savedViewConfig: ContentDatabaseViewConfig,
|
|
12393
|
+
) {
|
|
12394
|
+
return (
|
|
12395
|
+
databaseViewQueryStateKey(currentViewConfig) !==
|
|
12396
|
+
databaseViewQueryStateKey(savedViewConfig)
|
|
12397
|
+
);
|
|
12398
|
+
}
|
|
12399
|
+
|
|
12400
|
+
function databaseViewQueryStateKey(viewConfig: ContentDatabaseViewConfig) {
|
|
12401
|
+
const normalized = normalizeClientDatabaseViewConfig(viewConfig);
|
|
12402
|
+
return JSON.stringify({
|
|
12403
|
+
activeViewId: normalized.activeViewId,
|
|
12404
|
+
views: normalized.views.map((view) => ({
|
|
12405
|
+
id: view.id,
|
|
12406
|
+
sorts: view.sorts,
|
|
12407
|
+
filters: view.filters,
|
|
12408
|
+
filterMode: view.filterMode ?? "and",
|
|
12409
|
+
})),
|
|
12410
|
+
});
|
|
12411
|
+
}
|
|
12412
|
+
|
|
10756
12413
|
function isTablePropertyVisible(
|
|
10757
12414
|
property: DocumentProperty,
|
|
10758
12415
|
items: ContentDatabaseItem[],
|
|
@@ -13355,13 +15012,12 @@ export function applyDatabaseView(
|
|
|
13355
15012
|
const activeFilters = filters.filter(isActiveFilter);
|
|
13356
15013
|
const filtered = activeFilters.length
|
|
13357
15014
|
? searched.filter((item) =>
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
),
|
|
15015
|
+
databaseItemMatchesFilterTree(
|
|
15016
|
+
item,
|
|
15017
|
+
properties,
|
|
15018
|
+
activeFilters,
|
|
15019
|
+
filterMode,
|
|
15020
|
+
),
|
|
13365
15021
|
)
|
|
13366
15022
|
: searched;
|
|
13367
15023
|
|
|
@@ -13377,25 +15033,137 @@ export function applyDatabaseView(
|
|
|
13377
15033
|
return sort.direction === "asc" ? comparison : -comparison;
|
|
13378
15034
|
}
|
|
13379
15035
|
}
|
|
13380
|
-
return 0;
|
|
13381
|
-
});
|
|
15036
|
+
return 0;
|
|
15037
|
+
});
|
|
15038
|
+
}
|
|
15039
|
+
|
|
15040
|
+
function databaseItemMatchesFilterTree(
|
|
15041
|
+
item: ContentDatabaseItem,
|
|
15042
|
+
properties: DocumentProperty[],
|
|
15043
|
+
filters: DatabaseFilter[],
|
|
15044
|
+
filterMode: DatabaseFilterMode,
|
|
15045
|
+
) {
|
|
15046
|
+
const rootFilters = filters.filter((filter) => !filter.parentFilterGroupId);
|
|
15047
|
+
const nestedGroups = nestedDatabaseFilterGroups(filters);
|
|
15048
|
+
const matches = [
|
|
15049
|
+
...rootFilters.map((filter) =>
|
|
15050
|
+
databaseItemMatchesFilter(item, properties, filter),
|
|
15051
|
+
),
|
|
15052
|
+
...nestedGroups.map((group) =>
|
|
15053
|
+
combineDatabaseFilterMatches(
|
|
15054
|
+
group.map((filter) =>
|
|
15055
|
+
databaseItemMatchesFilter(item, properties, filter),
|
|
15056
|
+
),
|
|
15057
|
+
filterMode,
|
|
15058
|
+
),
|
|
15059
|
+
),
|
|
15060
|
+
];
|
|
15061
|
+
|
|
15062
|
+
return combineDatabaseFilterMatches(matches, filterMode);
|
|
15063
|
+
}
|
|
15064
|
+
|
|
15065
|
+
function nestedDatabaseFilterGroups(filters: DatabaseFilter[]) {
|
|
15066
|
+
const groups = new Map<string, DatabaseFilter[]>();
|
|
15067
|
+
for (const filter of filters) {
|
|
15068
|
+
if (!filter.parentFilterGroupId || !filter.filterGroupId) continue;
|
|
15069
|
+
groups.set(filter.filterGroupId, [
|
|
15070
|
+
...(groups.get(filter.filterGroupId) ?? []),
|
|
15071
|
+
filter,
|
|
15072
|
+
]);
|
|
15073
|
+
}
|
|
15074
|
+
return [...groups.values()].filter((group) => group.length > 0);
|
|
15075
|
+
}
|
|
15076
|
+
|
|
15077
|
+
function combineDatabaseFilterMatches(
|
|
15078
|
+
matches: boolean[],
|
|
15079
|
+
filterMode: DatabaseFilterMode,
|
|
15080
|
+
) {
|
|
15081
|
+
if (matches.length === 0) return true;
|
|
15082
|
+
return filterMode === "or"
|
|
15083
|
+
? matches.some((matched) => matched)
|
|
15084
|
+
: matches.every((matched) => matched);
|
|
15085
|
+
}
|
|
15086
|
+
|
|
15087
|
+
function defaultDatabaseSort(): DatabaseSort {
|
|
15088
|
+
return {
|
|
15089
|
+
key: "name",
|
|
15090
|
+
label: "Name",
|
|
15091
|
+
direction: "asc",
|
|
15092
|
+
};
|
|
15093
|
+
}
|
|
15094
|
+
|
|
15095
|
+
function defaultDatabaseFilter(): DatabaseFilter {
|
|
15096
|
+
return {
|
|
15097
|
+
key: "name",
|
|
15098
|
+
label: "Name",
|
|
15099
|
+
operator: "contains",
|
|
15100
|
+
value: "",
|
|
15101
|
+
};
|
|
15102
|
+
}
|
|
15103
|
+
|
|
15104
|
+
function createDatabaseFilterForField(
|
|
15105
|
+
key: ColumnKey,
|
|
15106
|
+
label: string,
|
|
15107
|
+
properties: DocumentProperty[],
|
|
15108
|
+
advanced:
|
|
15109
|
+
| boolean
|
|
15110
|
+
| { filterGroupId: string; parentFilterGroupId?: string } = false,
|
|
15111
|
+
): DatabaseFilter {
|
|
15112
|
+
const group =
|
|
15113
|
+
typeof advanced === "object"
|
|
15114
|
+
? advanced
|
|
15115
|
+
: advanced
|
|
15116
|
+
? { filterGroupId: DATABASE_ADVANCED_FILTER_GROUP_ID }
|
|
15117
|
+
: null;
|
|
15118
|
+
return {
|
|
15119
|
+
key,
|
|
15120
|
+
label,
|
|
15121
|
+
operator: defaultFilterOperatorForKey(key, properties),
|
|
15122
|
+
value: "",
|
|
15123
|
+
...(group ? { filterGroupId: group.filterGroupId } : {}),
|
|
15124
|
+
...(group?.parentFilterGroupId
|
|
15125
|
+
? { parentFilterGroupId: group.parentFilterGroupId }
|
|
15126
|
+
: {}),
|
|
15127
|
+
};
|
|
15128
|
+
}
|
|
15129
|
+
|
|
15130
|
+
function isAdvancedDatabaseFilter(filter: DatabaseFilter) {
|
|
15131
|
+
return (
|
|
15132
|
+
filter.filterGroupId === DATABASE_ADVANCED_FILTER_GROUP_ID ||
|
|
15133
|
+
filter.parentFilterGroupId === DATABASE_ADVANCED_FILTER_GROUP_ID
|
|
15134
|
+
);
|
|
15135
|
+
}
|
|
15136
|
+
|
|
15137
|
+
function advancedNestedFilterGroups(
|
|
15138
|
+
entries: DatabaseFilterEntry[],
|
|
15139
|
+
): DatabaseNestedFilterGroupEntry[] {
|
|
15140
|
+
const groups = new Map<string, DatabaseFilterEntry[]>();
|
|
15141
|
+
for (const entry of entries) {
|
|
15142
|
+
if (
|
|
15143
|
+
entry.filter.parentFilterGroupId !== DATABASE_ADVANCED_FILTER_GROUP_ID ||
|
|
15144
|
+
!entry.filter.filterGroupId
|
|
15145
|
+
) {
|
|
15146
|
+
continue;
|
|
15147
|
+
}
|
|
15148
|
+
groups.set(entry.filter.filterGroupId, [
|
|
15149
|
+
...(groups.get(entry.filter.filterGroupId) ?? []),
|
|
15150
|
+
entry,
|
|
15151
|
+
]);
|
|
15152
|
+
}
|
|
15153
|
+
return [...groups.entries()].map(([id, groupEntries]) => ({
|
|
15154
|
+
id,
|
|
15155
|
+
entries: groupEntries,
|
|
15156
|
+
}));
|
|
13382
15157
|
}
|
|
13383
15158
|
|
|
13384
|
-
function
|
|
13385
|
-
return {
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
direction: "asc",
|
|
13389
|
-
};
|
|
15159
|
+
function createAdvancedNestedFilterGroupId() {
|
|
15160
|
+
return `advanced-${Date.now().toString(36)}-${Math.random()
|
|
15161
|
+
.toString(36)
|
|
15162
|
+
.slice(2, 8)}`;
|
|
13390
15163
|
}
|
|
13391
15164
|
|
|
13392
|
-
function
|
|
13393
|
-
return {
|
|
13394
|
-
key: "name",
|
|
13395
|
-
label: "Name",
|
|
13396
|
-
operator: "contains",
|
|
13397
|
-
value: "",
|
|
13398
|
-
};
|
|
15165
|
+
function advancedFilterGroupLabel(ruleCount: number) {
|
|
15166
|
+
return `${ruleCount} rule${ruleCount === 1 ? "" : "s"}`;
|
|
13399
15167
|
}
|
|
13400
15168
|
|
|
13401
15169
|
export function upsertDatabaseSort(
|
|
@@ -13553,7 +15321,7 @@ function isActiveFilter(
|
|
|
13553
15321
|
): filter is DatabaseFilter {
|
|
13554
15322
|
if (!filter) return false;
|
|
13555
15323
|
if (filterOperatorNeedsValue(filter.operator)) {
|
|
13556
|
-
return filter.value
|
|
15324
|
+
return databaseFilterSelectedValues(filter.value).length > 0;
|
|
13557
15325
|
}
|
|
13558
15326
|
return true;
|
|
13559
15327
|
}
|
|
@@ -13598,21 +15366,42 @@ function databaseFilterDefaultValueForNewItem(
|
|
|
13598
15366
|
if (filter.operator === "is_unchecked") {
|
|
13599
15367
|
return property.definition.type === "checkbox" ? false : undefined;
|
|
13600
15368
|
}
|
|
13601
|
-
if (
|
|
15369
|
+
if (property.definition.type === "multi_select") {
|
|
15370
|
+
if (filter.operator !== "equals" && filter.operator !== "contains") {
|
|
15371
|
+
return undefined;
|
|
15372
|
+
}
|
|
15373
|
+
const values = databaseFilterSelectedValues(filter.value);
|
|
15374
|
+
if (values.length === 0) return undefined;
|
|
15375
|
+
return values.map(
|
|
15376
|
+
(value) =>
|
|
15377
|
+
databasePropertyOptionIdForFilterValue(property, value) ?? value,
|
|
15378
|
+
);
|
|
15379
|
+
}
|
|
15380
|
+
if (property.definition.type === "person" && filter.operator === "contains") {
|
|
15381
|
+
const values = databaseFilterSelectedValues(filter.value);
|
|
15382
|
+
return values.length > 0 ? values : undefined;
|
|
15383
|
+
}
|
|
13602
15384
|
|
|
13603
15385
|
const value = filter.value.trim();
|
|
13604
15386
|
if (!value) return undefined;
|
|
13605
15387
|
|
|
13606
|
-
const optionValue = databasePropertyOptionIdForFilterValue(property, value);
|
|
13607
|
-
if (property.definition.type === "multi_select") {
|
|
13608
|
-
return [optionValue ?? value];
|
|
13609
|
-
}
|
|
13610
15388
|
if (
|
|
13611
15389
|
property.definition.type === "select" ||
|
|
13612
15390
|
property.definition.type === "status"
|
|
13613
15391
|
) {
|
|
13614
|
-
|
|
15392
|
+
if (filter.operator !== "equals" && filter.operator !== "contains") {
|
|
15393
|
+
return undefined;
|
|
15394
|
+
}
|
|
15395
|
+
const values = databaseFilterSelectedValues(filter.value);
|
|
15396
|
+
const firstValue = values[0]?.trim() ?? "";
|
|
15397
|
+
if (!firstValue) return undefined;
|
|
15398
|
+
return (
|
|
15399
|
+
databasePropertyOptionIdForFilterValue(property, firstValue) ?? firstValue
|
|
15400
|
+
);
|
|
13615
15401
|
}
|
|
15402
|
+
|
|
15403
|
+
if (filter.operator !== "equals") return undefined;
|
|
15404
|
+
|
|
13616
15405
|
if (property.definition.type === "date") {
|
|
13617
15406
|
return /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
13618
15407
|
? { start: value, includeTime: false }
|
|
@@ -13657,10 +15446,20 @@ function databaseItemMatchesFilter(
|
|
|
13657
15446
|
: current < target;
|
|
13658
15447
|
}
|
|
13659
15448
|
|
|
13660
|
-
if (
|
|
15449
|
+
if (
|
|
15450
|
+
filter.operator === "before" ||
|
|
15451
|
+
filter.operator === "after" ||
|
|
15452
|
+
filter.operator === "between"
|
|
15453
|
+
) {
|
|
13661
15454
|
const current = propertyDateValue(property);
|
|
15455
|
+
if (!Number.isFinite(current)) return false;
|
|
15456
|
+
if (filter.operator === "between") {
|
|
15457
|
+
const range = databaseFilterDateRangeValue(filter.value);
|
|
15458
|
+
if (!range) return false;
|
|
15459
|
+
return current >= range[0] && current <= range[1];
|
|
15460
|
+
}
|
|
13662
15461
|
const target = new Date(filter.value.trim()).getTime();
|
|
13663
|
-
if (!Number.isFinite(
|
|
15462
|
+
if (!Number.isFinite(target)) return false;
|
|
13664
15463
|
return filter.operator === "before" ? current < target : current > target;
|
|
13665
15464
|
}
|
|
13666
15465
|
|
|
@@ -13669,8 +15468,31 @@ function databaseItemMatchesFilter(
|
|
|
13669
15468
|
properties,
|
|
13670
15469
|
filter.key,
|
|
13671
15470
|
).map((candidate) => candidate.trim().toLowerCase());
|
|
15471
|
+
const selectedFilterValues = databaseFilterSelectedValues(filter.value).map(
|
|
15472
|
+
(candidate) => candidate.trim().toLowerCase(),
|
|
15473
|
+
);
|
|
13672
15474
|
const normalizedValue = value.trim().toLowerCase();
|
|
13673
|
-
const normalizedFilter =
|
|
15475
|
+
const normalizedFilter = selectedFilterValues[0] ?? "";
|
|
15476
|
+
const usesDiscreteValues =
|
|
15477
|
+
property?.definition.type === "select" ||
|
|
15478
|
+
property?.definition.type === "status" ||
|
|
15479
|
+
property?.definition.type === "multi_select" ||
|
|
15480
|
+
property?.definition.type === "person";
|
|
15481
|
+
|
|
15482
|
+
if (
|
|
15483
|
+
usesDiscreteValues &&
|
|
15484
|
+
(filter.operator === "equals" || filter.operator === "contains")
|
|
15485
|
+
) {
|
|
15486
|
+
return selectedFilterValues.some((filterValue) =>
|
|
15487
|
+
candidateValues.includes(filterValue),
|
|
15488
|
+
);
|
|
15489
|
+
}
|
|
15490
|
+
if (usesDiscreteValues && filter.operator === "does_not_equal") {
|
|
15491
|
+
return selectedFilterValues.every(
|
|
15492
|
+
(filterValue) => !candidateValues.includes(filterValue),
|
|
15493
|
+
);
|
|
15494
|
+
}
|
|
15495
|
+
|
|
13674
15496
|
if (filter.operator === "equals") {
|
|
13675
15497
|
return candidateValues.includes(normalizedFilter);
|
|
13676
15498
|
}
|
|
@@ -13844,6 +15666,10 @@ function propertyDateValue(property: DocumentProperty | null | undefined) {
|
|
|
13844
15666
|
return Number.isFinite(value) ? value : Number.NaN;
|
|
13845
15667
|
}
|
|
13846
15668
|
|
|
15669
|
+
function databaseSortDirectionLabel(direction: SortDirection) {
|
|
15670
|
+
return direction === "asc" ? "Ascending" : "Descending";
|
|
15671
|
+
}
|
|
15672
|
+
|
|
13847
15673
|
function SortMenu({
|
|
13848
15674
|
properties,
|
|
13849
15675
|
sorts,
|
|
@@ -13879,6 +15705,10 @@ function SortMenu({
|
|
|
13879
15705
|
onSortsChange([...sorts, defaultDatabaseSort()]);
|
|
13880
15706
|
}
|
|
13881
15707
|
|
|
15708
|
+
function addSortForField(key: "name" | string, label: string) {
|
|
15709
|
+
onSortsChange([...sorts, { key, label, direction: "asc" }]);
|
|
15710
|
+
}
|
|
15711
|
+
|
|
13882
15712
|
function removeSort(index: number) {
|
|
13883
15713
|
onSortsChange(sorts.filter((_, sortIndex) => sortIndex !== index));
|
|
13884
15714
|
}
|
|
@@ -13911,363 +15741,210 @@ function SortMenu({
|
|
|
13911
15741
|
) : null}
|
|
13912
15742
|
</Button>
|
|
13913
15743
|
</DropdownMenuTrigger>
|
|
13914
|
-
<DropdownMenuContent
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
<div
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
<SortFieldIcon sort={sort} properties={properties} />
|
|
13928
|
-
<span className="min-w-0 flex-1 truncate">
|
|
13929
|
-
{sort.label}
|
|
13930
|
-
</span>
|
|
13931
|
-
</DropdownMenuSubTrigger>
|
|
13932
|
-
<DatabasePropertyPickerSubContent
|
|
13933
|
-
properties={properties}
|
|
13934
|
-
selectedKey={sort.key}
|
|
13935
|
-
includeName
|
|
13936
|
-
onSelect={(key, label) => selectSort(index, key, label)}
|
|
13937
|
-
/>
|
|
13938
|
-
</DropdownMenuSub>
|
|
13939
|
-
<button
|
|
13940
|
-
type="button"
|
|
13941
|
-
className="flex h-8 items-center rounded px-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
13942
|
-
onClick={() => toggleDirection(index)}
|
|
13943
|
-
>
|
|
13944
|
-
{sort.direction === "asc" ? "Asc" : "Desc"}
|
|
13945
|
-
</button>
|
|
13946
|
-
<div className="flex items-center">
|
|
13947
|
-
<button
|
|
13948
|
-
type="button"
|
|
13949
|
-
aria-label={`Move sort ${index + 1} earlier`}
|
|
13950
|
-
className="flex size-8 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40"
|
|
13951
|
-
disabled={sorts.length <= 1 || index === 0}
|
|
13952
|
-
onClick={() => moveSort(index, "up")}
|
|
13953
|
-
>
|
|
13954
|
-
<IconArrowUp className="size-3.5" />
|
|
13955
|
-
</button>
|
|
13956
|
-
<button
|
|
13957
|
-
type="button"
|
|
13958
|
-
aria-label={`Move sort ${index + 1} later`}
|
|
13959
|
-
className="flex size-8 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40"
|
|
13960
|
-
disabled={
|
|
13961
|
-
sorts.length <= 1 || index >= displayedSorts.length - 1
|
|
13962
|
-
}
|
|
13963
|
-
onClick={() => moveSort(index, "down")}
|
|
13964
|
-
>
|
|
13965
|
-
<IconArrowDown className="size-3.5" />
|
|
13966
|
-
</button>
|
|
13967
|
-
</div>
|
|
13968
|
-
<button
|
|
13969
|
-
type="button"
|
|
13970
|
-
aria-label={`Remove sort ${index + 1}`}
|
|
13971
|
-
className="flex size-8 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40"
|
|
13972
|
-
disabled={sorts.length === 0}
|
|
13973
|
-
onClick={() => removeSort(index)}
|
|
13974
|
-
>
|
|
13975
|
-
<IconX className="size-4" />
|
|
13976
|
-
</button>
|
|
13977
|
-
</div>
|
|
13978
|
-
))}
|
|
13979
|
-
</div>
|
|
13980
|
-
|
|
13981
|
-
<div className="flex justify-between gap-2 border-t pt-2">
|
|
13982
|
-
<Button
|
|
13983
|
-
type="button"
|
|
13984
|
-
size="sm"
|
|
13985
|
-
variant="ghost"
|
|
13986
|
-
className="h-8 px-2 text-xs"
|
|
13987
|
-
onClick={addSort}
|
|
13988
|
-
>
|
|
13989
|
-
<IconPlus className="mr-1 size-3.5" />
|
|
13990
|
-
{dbText("addSort")}
|
|
13991
|
-
</Button>
|
|
13992
|
-
<Button
|
|
13993
|
-
type="button"
|
|
13994
|
-
size="sm"
|
|
13995
|
-
variant="ghost"
|
|
13996
|
-
className="h-8 px-2 text-xs"
|
|
13997
|
-
disabled={sorts.length === 0}
|
|
13998
|
-
onClick={() => onSortsChange([])}
|
|
13999
|
-
>
|
|
14000
|
-
{dbText("clearSorts")}
|
|
14001
|
-
</Button>
|
|
14002
|
-
</div>
|
|
14003
|
-
</div>
|
|
14004
|
-
</DropdownMenuContent>
|
|
14005
|
-
</DropdownMenu>
|
|
14006
|
-
);
|
|
14007
|
-
}
|
|
14008
|
-
|
|
14009
|
-
function SortFieldIcon({
|
|
14010
|
-
sort,
|
|
14011
|
-
properties,
|
|
14012
|
-
}: {
|
|
14013
|
-
sort: DatabaseSort;
|
|
14014
|
-
properties: DocumentProperty[];
|
|
14015
|
-
}) {
|
|
14016
|
-
if (sort.key === "name") {
|
|
14017
|
-
return <IconFileText className="mr-2 size-4 text-muted-foreground" />;
|
|
14018
|
-
}
|
|
14019
|
-
const property = properties.find(
|
|
14020
|
-
(candidate) => candidate.definition.id === sort.key,
|
|
14021
|
-
);
|
|
14022
|
-
const Icon = property ? TYPE_ICONS[property.definition.type] : IconFileText;
|
|
14023
|
-
return <Icon className="mr-2 size-4 text-muted-foreground" />;
|
|
14024
|
-
}
|
|
14025
|
-
|
|
14026
|
-
function FilterMenu({
|
|
14027
|
-
documentId,
|
|
14028
|
-
properties,
|
|
14029
|
-
filters,
|
|
14030
|
-
filterMode,
|
|
14031
|
-
onFiltersChange,
|
|
14032
|
-
onFilterModeChange,
|
|
14033
|
-
}: {
|
|
14034
|
-
documentId: string;
|
|
14035
|
-
properties: DocumentProperty[];
|
|
14036
|
-
filters: DatabaseFilter[];
|
|
14037
|
-
filterMode: DatabaseFilterMode;
|
|
14038
|
-
onFiltersChange: (filters: DatabaseFilter[]) => void;
|
|
14039
|
-
onFilterModeChange: (filterMode: DatabaseFilterMode) => void;
|
|
14040
|
-
}) {
|
|
14041
|
-
const activeFilters = filters.filter(isActiveFilter);
|
|
14042
|
-
const active = activeFilters.length > 0;
|
|
14043
|
-
const displayedFilters =
|
|
14044
|
-
filters.length > 0 ? filters : [defaultDatabaseFilter()];
|
|
14045
|
-
|
|
14046
|
-
function updateFilter(index: number, next: Partial<DatabaseFilter>) {
|
|
14047
|
-
const baseFilters =
|
|
14048
|
-
filters.length > 0 ? [...filters] : [defaultDatabaseFilter()];
|
|
14049
|
-
const currentFilter = baseFilters[index] ?? defaultDatabaseFilter();
|
|
14050
|
-
const nextOperator = next.operator ?? currentFilter.operator;
|
|
14051
|
-
baseFilters[index] = {
|
|
14052
|
-
...currentFilter,
|
|
14053
|
-
...next,
|
|
14054
|
-
value: filterOperatorNeedsValue(nextOperator)
|
|
14055
|
-
? (next.value ?? currentFilter.value)
|
|
14056
|
-
: "",
|
|
14057
|
-
};
|
|
14058
|
-
onFiltersChange(baseFilters);
|
|
14059
|
-
}
|
|
14060
|
-
|
|
14061
|
-
function selectField(index: number, key: "name" | string, label: string) {
|
|
14062
|
-
updateFilter(index, {
|
|
14063
|
-
key,
|
|
14064
|
-
label,
|
|
14065
|
-
operator: defaultFilterOperatorForKey(key, properties),
|
|
14066
|
-
value: "",
|
|
14067
|
-
});
|
|
14068
|
-
}
|
|
14069
|
-
|
|
14070
|
-
function selectOperator(index: number, operator: FilterOperator) {
|
|
14071
|
-
updateFilter(index, { operator });
|
|
14072
|
-
}
|
|
14073
|
-
|
|
14074
|
-
function addFilter() {
|
|
14075
|
-
onFiltersChange([...filters, defaultDatabaseFilter()]);
|
|
14076
|
-
}
|
|
14077
|
-
|
|
14078
|
-
function removeFilter(index: number) {
|
|
14079
|
-
onFiltersChange(filters.filter((_, filterIndex) => filterIndex !== index));
|
|
14080
|
-
}
|
|
14081
|
-
|
|
14082
|
-
function moveFilter(
|
|
14083
|
-
index: number,
|
|
14084
|
-
direction: DatabaseConditionMoveDirection,
|
|
14085
|
-
) {
|
|
14086
|
-
onFiltersChange(moveDatabaseFilter(filters, index, direction));
|
|
14087
|
-
}
|
|
14088
|
-
|
|
14089
|
-
return (
|
|
14090
|
-
<DropdownMenu>
|
|
14091
|
-
<DropdownMenuTrigger asChild>
|
|
14092
|
-
<Button
|
|
14093
|
-
type="button"
|
|
14094
|
-
variant="ghost"
|
|
14095
|
-
size="sm"
|
|
14096
|
-
aria-label={
|
|
14097
|
-
active ? `${activeFilters.length} active filters` : "Filter"
|
|
14098
|
-
}
|
|
14099
|
-
title="Filter"
|
|
14100
|
-
className={cn(databaseToolbarIconButtonClass(active), "relative")}
|
|
14101
|
-
>
|
|
14102
|
-
<IconFilter className="size-3.5" />
|
|
14103
|
-
{active ? (
|
|
14104
|
-
<span className="absolute -right-0.5 -top-0.5 flex h-3.5 min-w-3.5 shrink-0 items-center justify-center rounded-full bg-foreground px-1 text-[9px] leading-none text-background">
|
|
14105
|
-
{formatCompactCountBadge(activeFilters.length)}
|
|
14106
|
-
</span>
|
|
14107
|
-
) : null}
|
|
14108
|
-
</Button>
|
|
14109
|
-
</DropdownMenuTrigger>
|
|
14110
|
-
<DropdownMenuContent align="end" className="w-[360px]">
|
|
14111
|
-
<div
|
|
14112
|
-
className="grid gap-2 p-2"
|
|
14113
|
-
onKeyDown={(event) => event.stopPropagation()}
|
|
14114
|
-
>
|
|
14115
|
-
<div className="text-xs font-medium text-muted-foreground">
|
|
14116
|
-
{dbText("filterRowsWhere")}
|
|
15744
|
+
<DropdownMenuContent
|
|
15745
|
+
align="end"
|
|
15746
|
+
className={cn("w-[340px]", sorts.length === 0 && "w-64 p-0")}
|
|
15747
|
+
>
|
|
15748
|
+
{sorts.length === 0 ? (
|
|
15749
|
+
<div onKeyDown={(event) => event.stopPropagation()}>
|
|
15750
|
+
<DatabasePropertyPickerMenuItems
|
|
15751
|
+
properties={properties}
|
|
15752
|
+
includeName
|
|
15753
|
+
placeholder={dbText("sortBy")}
|
|
15754
|
+
closeOnSelect
|
|
15755
|
+
onSelect={addSortForField}
|
|
15756
|
+
/>
|
|
14117
15757
|
</div>
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14122
|
-
|
|
14123
|
-
</
|
|
14124
|
-
<
|
|
14125
|
-
{
|
|
14126
|
-
<
|
|
14127
|
-
key={
|
|
14128
|
-
|
|
14129
|
-
event.preventDefault();
|
|
14130
|
-
onFilterModeChange(mode);
|
|
14131
|
-
}}
|
|
15758
|
+
) : null}
|
|
15759
|
+
{sorts.length > 0 ? (
|
|
15760
|
+
<div className="grid gap-2 p-2">
|
|
15761
|
+
<div className="text-xs font-medium text-muted-foreground">
|
|
15762
|
+
{dbText("sortRowsBy")}
|
|
15763
|
+
</div>
|
|
15764
|
+
<div className="grid gap-2">
|
|
15765
|
+
{displayedSorts.map((sort, index) => (
|
|
15766
|
+
<div
|
|
15767
|
+
key={`${index}-${sort.key}`}
|
|
15768
|
+
className="grid grid-cols-[minmax(0,1fr)_auto_auto_auto] gap-1 rounded border border-border/70 bg-background p-1.5"
|
|
14132
15769
|
>
|
|
14133
|
-
<span className="flex-1">
|
|
14134
|
-
{databaseFilterModeLabel(mode)}
|
|
14135
|
-
</span>
|
|
14136
|
-
{filterMode === mode ? (
|
|
14137
|
-
<IconCheck className="size-4 text-muted-foreground" />
|
|
14138
|
-
) : null}
|
|
14139
|
-
</DropdownMenuItem>
|
|
14140
|
-
))}
|
|
14141
|
-
</DropdownMenuSubContent>
|
|
14142
|
-
</DropdownMenuSub>
|
|
14143
|
-
<div className="grid gap-2">
|
|
14144
|
-
{displayedFilters.map((currentFilter, index) => (
|
|
14145
|
-
<div
|
|
14146
|
-
key={`${index}-${currentFilter.key}`}
|
|
14147
|
-
className="grid gap-1 rounded border border-border/70 bg-background p-1.5"
|
|
14148
|
-
>
|
|
14149
|
-
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_auto] gap-1">
|
|
14150
15770
|
<DropdownMenuSub>
|
|
14151
15771
|
<DropdownMenuSubTrigger className="min-w-0">
|
|
14152
|
-
<
|
|
14153
|
-
filter={currentFilter}
|
|
14154
|
-
properties={properties}
|
|
14155
|
-
/>
|
|
15772
|
+
<SortFieldIcon sort={sort} properties={properties} />
|
|
14156
15773
|
<span className="min-w-0 flex-1 truncate">
|
|
14157
|
-
{
|
|
15774
|
+
{sort.label}
|
|
14158
15775
|
</span>
|
|
14159
15776
|
</DropdownMenuSubTrigger>
|
|
14160
15777
|
<DatabasePropertyPickerSubContent
|
|
14161
15778
|
properties={properties}
|
|
14162
|
-
selectedKey={
|
|
15779
|
+
selectedKey={sort.key}
|
|
14163
15780
|
includeName
|
|
14164
|
-
onSelect={(key, label) =>
|
|
15781
|
+
onSelect={(key, label) => selectSort(index, key, label)}
|
|
14165
15782
|
/>
|
|
14166
15783
|
</DropdownMenuSub>
|
|
14167
|
-
|
|
14168
|
-
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
</span>
|
|
14175
|
-
</DropdownMenuSubTrigger>
|
|
14176
|
-
<DropdownMenuSubContent className="w-44">
|
|
14177
|
-
{filterOperatorsForKey(currentFilter.key, properties).map(
|
|
14178
|
-
(operator) => (
|
|
14179
|
-
<DropdownMenuItem
|
|
14180
|
-
key={operator}
|
|
14181
|
-
onSelect={(event) => {
|
|
14182
|
-
event.preventDefault();
|
|
14183
|
-
selectOperator(index, operator);
|
|
14184
|
-
}}
|
|
14185
|
-
>
|
|
14186
|
-
<span className="flex-1">
|
|
14187
|
-
{FILTER_OPERATOR_LABELS[operator]}
|
|
14188
|
-
</span>
|
|
14189
|
-
{currentFilter.operator === operator ? (
|
|
14190
|
-
<IconCheck className="size-4 text-muted-foreground" />
|
|
14191
|
-
) : null}
|
|
14192
|
-
</DropdownMenuItem>
|
|
14193
|
-
),
|
|
14194
|
-
)}
|
|
14195
|
-
</DropdownMenuSubContent>
|
|
14196
|
-
</DropdownMenuSub>
|
|
14197
|
-
|
|
15784
|
+
<button
|
|
15785
|
+
type="button"
|
|
15786
|
+
className="flex h-8 items-center rounded px-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
15787
|
+
onClick={() => toggleDirection(index)}
|
|
15788
|
+
>
|
|
15789
|
+
{sort.direction === "asc" ? "Asc" : "Desc"}
|
|
15790
|
+
</button>
|
|
14198
15791
|
<div className="flex items-center">
|
|
14199
15792
|
<button
|
|
14200
15793
|
type="button"
|
|
14201
|
-
aria-label={`Move
|
|
15794
|
+
aria-label={`Move sort ${index + 1} earlier`}
|
|
14202
15795
|
className="flex size-8 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40"
|
|
14203
|
-
disabled={
|
|
14204
|
-
onClick={() =>
|
|
15796
|
+
disabled={sorts.length <= 1 || index === 0}
|
|
15797
|
+
onClick={() => moveSort(index, "up")}
|
|
14205
15798
|
>
|
|
14206
15799
|
<IconArrowUp className="size-3.5" />
|
|
14207
15800
|
</button>
|
|
14208
15801
|
<button
|
|
14209
15802
|
type="button"
|
|
14210
|
-
aria-label={`Move
|
|
15803
|
+
aria-label={`Move sort ${index + 1} later`}
|
|
14211
15804
|
className="flex size-8 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40"
|
|
14212
15805
|
disabled={
|
|
14213
|
-
|
|
14214
|
-
index >= displayedFilters.length - 1
|
|
15806
|
+
sorts.length <= 1 || index >= displayedSorts.length - 1
|
|
14215
15807
|
}
|
|
14216
|
-
onClick={() =>
|
|
15808
|
+
onClick={() => moveSort(index, "down")}
|
|
14217
15809
|
>
|
|
14218
15810
|
<IconArrowDown className="size-3.5" />
|
|
14219
15811
|
</button>
|
|
14220
15812
|
</div>
|
|
14221
|
-
|
|
14222
15813
|
<button
|
|
14223
15814
|
type="button"
|
|
14224
|
-
aria-label={`Remove
|
|
15815
|
+
aria-label={`Remove sort ${index + 1}`}
|
|
14225
15816
|
className="flex size-8 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40"
|
|
14226
|
-
disabled={
|
|
14227
|
-
onClick={() =>
|
|
15817
|
+
disabled={sorts.length === 0}
|
|
15818
|
+
onClick={() => removeSort(index)}
|
|
14228
15819
|
>
|
|
14229
15820
|
<IconX className="size-4" />
|
|
14230
15821
|
</button>
|
|
14231
15822
|
</div>
|
|
15823
|
+
))}
|
|
15824
|
+
</div>
|
|
15825
|
+
|
|
15826
|
+
<div className="flex justify-between gap-2 border-t pt-2">
|
|
15827
|
+
<Button
|
|
15828
|
+
type="button"
|
|
15829
|
+
size="sm"
|
|
15830
|
+
variant="ghost"
|
|
15831
|
+
className="h-8 px-2 text-xs"
|
|
15832
|
+
onClick={addSort}
|
|
15833
|
+
>
|
|
15834
|
+
<IconPlus className="mr-1 size-3.5" />
|
|
15835
|
+
{dbText("addSort")}
|
|
15836
|
+
</Button>
|
|
15837
|
+
<Button
|
|
15838
|
+
type="button"
|
|
15839
|
+
size="sm"
|
|
15840
|
+
variant="ghost"
|
|
15841
|
+
className="h-8 px-2 text-xs"
|
|
15842
|
+
disabled={sorts.length === 0}
|
|
15843
|
+
onClick={() => onSortsChange([])}
|
|
15844
|
+
>
|
|
15845
|
+
{dbText("clearSorts")}
|
|
15846
|
+
</Button>
|
|
15847
|
+
</div>
|
|
15848
|
+
</div>
|
|
15849
|
+
) : null}
|
|
15850
|
+
</DropdownMenuContent>
|
|
15851
|
+
</DropdownMenu>
|
|
15852
|
+
);
|
|
15853
|
+
}
|
|
15854
|
+
|
|
15855
|
+
function SortFieldIcon({
|
|
15856
|
+
sort,
|
|
15857
|
+
properties,
|
|
15858
|
+
}: {
|
|
15859
|
+
sort: DatabaseSort;
|
|
15860
|
+
properties: DocumentProperty[];
|
|
15861
|
+
}) {
|
|
15862
|
+
if (sort.key === "name") {
|
|
15863
|
+
return <IconFileText className="mr-2 size-4 text-muted-foreground" />;
|
|
15864
|
+
}
|
|
15865
|
+
const property = properties.find(
|
|
15866
|
+
(candidate) => candidate.definition.id === sort.key,
|
|
15867
|
+
);
|
|
15868
|
+
const Icon = property ? TYPE_ICONS[property.definition.type] : IconFileText;
|
|
15869
|
+
return <Icon className="mr-2 size-4 text-muted-foreground" />;
|
|
15870
|
+
}
|
|
14232
15871
|
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
15872
|
+
function FilterMenu({
|
|
15873
|
+
filters,
|
|
15874
|
+
properties,
|
|
15875
|
+
inlineOpen,
|
|
15876
|
+
open,
|
|
15877
|
+
onOpenChange,
|
|
15878
|
+
onAddFilter,
|
|
15879
|
+
onAddAdvancedFilter,
|
|
15880
|
+
}: {
|
|
15881
|
+
filters: DatabaseFilter[];
|
|
15882
|
+
properties: DocumentProperty[];
|
|
15883
|
+
inlineOpen: boolean;
|
|
15884
|
+
open: boolean;
|
|
15885
|
+
onOpenChange: (open: boolean) => void;
|
|
15886
|
+
onAddFilter: (key: "name" | string, label: string) => void;
|
|
15887
|
+
onAddAdvancedFilter: (key: "name" | string, label: string) => void;
|
|
15888
|
+
}) {
|
|
15889
|
+
const activeFilters = filters.filter(isActiveFilter);
|
|
15890
|
+
const active = activeFilters.length > 0 || inlineOpen;
|
|
15891
|
+
const excludedFilterKeys = useMemo(
|
|
15892
|
+
() => new Set(activeFilters.map((filter) => filter.key)),
|
|
15893
|
+
[activeFilters],
|
|
15894
|
+
);
|
|
15895
|
+
const advancedFilterCandidate =
|
|
15896
|
+
databasePropertyPickerItems(properties, "", {
|
|
15897
|
+
includeName: false,
|
|
15898
|
+
excludeKeys: excludedFilterKeys,
|
|
15899
|
+
})[0] ??
|
|
15900
|
+
databasePropertyPickerItems(properties, "", {
|
|
15901
|
+
includeName: true,
|
|
15902
|
+
excludeKeys: excludedFilterKeys,
|
|
15903
|
+
})[0];
|
|
14245
15904
|
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
14249
|
-
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
|
|
14253
|
-
|
|
14254
|
-
|
|
14255
|
-
{
|
|
14256
|
-
|
|
14257
|
-
|
|
14258
|
-
|
|
14259
|
-
|
|
14260
|
-
|
|
14261
|
-
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14265
|
-
|
|
14266
|
-
|
|
14267
|
-
|
|
14268
|
-
|
|
14269
|
-
{
|
|
14270
|
-
|
|
15905
|
+
return (
|
|
15906
|
+
<DropdownMenu open={open} onOpenChange={onOpenChange}>
|
|
15907
|
+
<DropdownMenuTrigger asChild>
|
|
15908
|
+
<Button
|
|
15909
|
+
type="button"
|
|
15910
|
+
variant="ghost"
|
|
15911
|
+
size="sm"
|
|
15912
|
+
aria-label={
|
|
15913
|
+
activeFilters.length > 0
|
|
15914
|
+
? `${activeFilters.length} active filters`
|
|
15915
|
+
: "Filter"
|
|
15916
|
+
}
|
|
15917
|
+
title="Filter"
|
|
15918
|
+
className={databaseToolbarIconButtonClass(active || open)}
|
|
15919
|
+
>
|
|
15920
|
+
<IconFilter className="size-3.5" />
|
|
15921
|
+
</Button>
|
|
15922
|
+
</DropdownMenuTrigger>
|
|
15923
|
+
<DropdownMenuContent align="end" className="w-64">
|
|
15924
|
+
<div onKeyDown={(event) => event.stopPropagation()}>
|
|
15925
|
+
<DatabasePropertyPickerMenuItems
|
|
15926
|
+
properties={properties}
|
|
15927
|
+
includeName
|
|
15928
|
+
excludeKeys={excludedFilterKeys}
|
|
15929
|
+
placeholder={dbText("filterBy")}
|
|
15930
|
+
closeOnSelect
|
|
15931
|
+
onSelect={onAddFilter}
|
|
15932
|
+
/>
|
|
15933
|
+
<DropdownMenuSeparator />
|
|
15934
|
+
<DropdownMenuItem
|
|
15935
|
+
disabled={!advancedFilterCandidate}
|
|
15936
|
+
onSelect={(event) => {
|
|
15937
|
+
event.preventDefault();
|
|
15938
|
+
if (!advancedFilterCandidate) return;
|
|
15939
|
+
onAddAdvancedFilter(
|
|
15940
|
+
advancedFilterCandidate.key,
|
|
15941
|
+
advancedFilterCandidate.label,
|
|
15942
|
+
);
|
|
15943
|
+
}}
|
|
15944
|
+
>
|
|
15945
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
15946
|
+
{dbText("addAdvancedFilter")}
|
|
15947
|
+
</DropdownMenuItem>
|
|
14271
15948
|
</div>
|
|
14272
15949
|
</DropdownMenuContent>
|
|
14273
15950
|
</DropdownMenu>
|
|
@@ -14277,18 +15954,29 @@ function FilterMenu({
|
|
|
14277
15954
|
function DatabaseFilterValueControl({
|
|
14278
15955
|
documentId,
|
|
14279
15956
|
filter,
|
|
15957
|
+
items,
|
|
14280
15958
|
properties,
|
|
14281
15959
|
autoFocus,
|
|
15960
|
+
hideOptionsUntilQuery = false,
|
|
14282
15961
|
onValueChange,
|
|
14283
15962
|
}: {
|
|
14284
15963
|
documentId: string;
|
|
14285
15964
|
filter: DatabaseFilter;
|
|
15965
|
+
items: ContentDatabaseItem[];
|
|
14286
15966
|
properties: DocumentProperty[];
|
|
14287
15967
|
autoFocus?: boolean;
|
|
15968
|
+
hideOptionsUntilQuery?: boolean;
|
|
14288
15969
|
onValueChange: (value: string) => void;
|
|
14289
15970
|
}) {
|
|
14290
15971
|
const configureProperty = useConfigureDocumentProperty(documentId);
|
|
14291
|
-
const
|
|
15972
|
+
const { session } = useSession();
|
|
15973
|
+
const currentUserEmail = session?.email?.trim() ?? "";
|
|
15974
|
+
const options = databaseFilterOptionChoices(
|
|
15975
|
+
filter.key,
|
|
15976
|
+
properties,
|
|
15977
|
+
items,
|
|
15978
|
+
currentUserEmail,
|
|
15979
|
+
);
|
|
14292
15980
|
const type = filterPropertyTypeForKey(filter.key, properties);
|
|
14293
15981
|
const [optionQuery, setOptionQuery] = useState("");
|
|
14294
15982
|
const filteredOptions = filterPropertyOptions(options, optionQuery);
|
|
@@ -14297,7 +15985,57 @@ function DatabaseFilterValueControl({
|
|
|
14297
15985
|
properties,
|
|
14298
15986
|
);
|
|
14299
15987
|
const canCreateOption =
|
|
14300
|
-
!!optionProperty &&
|
|
15988
|
+
!!optionProperty &&
|
|
15989
|
+
optionProperty.definition.type !== "person" &&
|
|
15990
|
+
canCreatePropertyOption(options, optionQuery);
|
|
15991
|
+
const usesOptionFilterValues =
|
|
15992
|
+
optionProperty?.definition.type === "select" ||
|
|
15993
|
+
optionProperty?.definition.type === "status" ||
|
|
15994
|
+
optionProperty?.definition.type === "multi_select" ||
|
|
15995
|
+
optionProperty?.definition.type === "person";
|
|
15996
|
+
const allowsMultipleValues =
|
|
15997
|
+
usesOptionFilterValues &&
|
|
15998
|
+
(filter.operator === "contains" || filter.operator === "does_not_equal");
|
|
15999
|
+
const selectedValues = databaseFilterSelectedValues(filter.value);
|
|
16000
|
+
const selectedOptions = selectedValues
|
|
16001
|
+
.map((value) =>
|
|
16002
|
+
options.find(
|
|
16003
|
+
(option) =>
|
|
16004
|
+
databaseFilterChoiceValue(option) === value ||
|
|
16005
|
+
option.id === value ||
|
|
16006
|
+
option.name.trim().toLowerCase() === value.trim().toLowerCase(),
|
|
16007
|
+
),
|
|
16008
|
+
)
|
|
16009
|
+
.filter((option): option is DatabaseFilterChoice => !!option);
|
|
16010
|
+
const showClearValue = selectedValues.length > 0;
|
|
16011
|
+
const showOptionChoices =
|
|
16012
|
+
!hideOptionsUntilQuery ||
|
|
16013
|
+
optionQuery.trim().length > 0 ||
|
|
16014
|
+
selectedValues.length > 0;
|
|
16015
|
+
const showCompactOptionValueInput =
|
|
16016
|
+
hideOptionsUntilQuery && selectedOptions.length === 0;
|
|
16017
|
+
|
|
16018
|
+
function setSelectedValues(values: string[]) {
|
|
16019
|
+
onValueChange(databaseFilterEncodeSelectedValues(values));
|
|
16020
|
+
}
|
|
16021
|
+
|
|
16022
|
+
function chooseFilterOption(option: DatabaseFilterChoice) {
|
|
16023
|
+
const optionValue = databaseFilterChoiceValue(option);
|
|
16024
|
+
if (!allowsMultipleValues) {
|
|
16025
|
+
onValueChange(optionValue);
|
|
16026
|
+
return;
|
|
16027
|
+
}
|
|
16028
|
+
const selected = databaseFilterOptionIsSelected(option, selectedValues);
|
|
16029
|
+
setSelectedValues(
|
|
16030
|
+
selected
|
|
16031
|
+
? selectedValues.filter(
|
|
16032
|
+
(value) =>
|
|
16033
|
+
value !== optionValue &&
|
|
16034
|
+
value.trim().toLowerCase() !== option.name.trim().toLowerCase(),
|
|
16035
|
+
)
|
|
16036
|
+
: [...selectedValues, optionValue],
|
|
16037
|
+
);
|
|
16038
|
+
}
|
|
14301
16039
|
|
|
14302
16040
|
async function createFilterOption() {
|
|
14303
16041
|
if (!optionProperty || !canCreateOption) return;
|
|
@@ -14311,37 +16049,82 @@ function DatabaseFilterValueControl({
|
|
|
14311
16049
|
options: { options: [...options, option] },
|
|
14312
16050
|
});
|
|
14313
16051
|
setOptionQuery("");
|
|
14314
|
-
|
|
16052
|
+
if (allowsMultipleValues) {
|
|
16053
|
+
setSelectedValues([...selectedValues, option.id]);
|
|
16054
|
+
} else {
|
|
16055
|
+
onValueChange(option.id);
|
|
16056
|
+
}
|
|
16057
|
+
}
|
|
16058
|
+
|
|
16059
|
+
if (
|
|
16060
|
+
filter.operator === "between" &&
|
|
16061
|
+
(type === "date" || type === "created_time" || type === "last_edited_time")
|
|
16062
|
+
) {
|
|
16063
|
+
return (
|
|
16064
|
+
<DatabaseBetweenDateFilterValueControl
|
|
16065
|
+
filter={filter}
|
|
16066
|
+
onValueChange={onValueChange}
|
|
16067
|
+
/>
|
|
16068
|
+
);
|
|
14315
16069
|
}
|
|
14316
16070
|
|
|
14317
16071
|
if (optionProperty) {
|
|
14318
16072
|
return (
|
|
14319
|
-
<
|
|
14320
|
-
<
|
|
14321
|
-
<
|
|
14322
|
-
{
|
|
14323
|
-
</span>
|
|
14324
|
-
</DropdownMenuSubTrigger>
|
|
14325
|
-
<DropdownMenuSubContent className="max-h-72 w-56 overflow-auto">
|
|
14326
|
-
<div
|
|
14327
|
-
className="sticky top-0 z-10 border-b border-border bg-popover p-2"
|
|
14328
|
-
onKeyDown={(event) => event.stopPropagation()}
|
|
14329
|
-
>
|
|
14330
|
-
<div className="flex h-8 items-center gap-1 rounded border border-border bg-background px-2">
|
|
16073
|
+
<div className="overflow-hidden rounded-sm">
|
|
16074
|
+
<div className="px-1 pb-1">
|
|
16075
|
+
<div className="flex min-h-9 flex-wrap items-center gap-1 rounded border border-input bg-background px-2 py-1 focus-within:border-[#2383e2] focus-within:ring-2 focus-within:ring-[#2383e2]/25">
|
|
16076
|
+
{showCompactOptionValueInput ? null : (
|
|
14331
16077
|
<IconSearch className="size-3.5 shrink-0 text-muted-foreground" />
|
|
14332
|
-
|
|
14333
|
-
|
|
14334
|
-
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
|
|
16078
|
+
)}
|
|
16079
|
+
{selectedOptions.map((option) => (
|
|
16080
|
+
<button
|
|
16081
|
+
key={option.id}
|
|
16082
|
+
type="button"
|
|
16083
|
+
className={cn(
|
|
16084
|
+
"inline-flex max-w-[9rem] items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium",
|
|
16085
|
+
optionProperty?.definition.type === "person"
|
|
16086
|
+
? "bg-muted text-foreground"
|
|
16087
|
+
: OPTION_COLOR_CLASSES[option.color ?? "gray"],
|
|
16088
|
+
)}
|
|
16089
|
+
onClick={(event) => {
|
|
16090
|
+
event.preventDefault();
|
|
16091
|
+
event.stopPropagation();
|
|
16092
|
+
setSelectedValues(
|
|
16093
|
+
selectedValues.filter(
|
|
16094
|
+
(value) => value !== databaseFilterChoiceValue(option),
|
|
16095
|
+
),
|
|
16096
|
+
);
|
|
16097
|
+
}}
|
|
16098
|
+
>
|
|
16099
|
+
{optionProperty?.definition.type === "person" ? (
|
|
16100
|
+
<PersonPill value={databaseFilterChoiceValue(option)} />
|
|
16101
|
+
) : (
|
|
16102
|
+
<span className="truncate">{option.name}</span>
|
|
16103
|
+
)}
|
|
16104
|
+
<IconX className="size-3 opacity-70" />
|
|
16105
|
+
</button>
|
|
16106
|
+
))}
|
|
16107
|
+
<Input
|
|
16108
|
+
autoFocus={autoFocus}
|
|
16109
|
+
value={optionQuery}
|
|
16110
|
+
placeholder={
|
|
16111
|
+
showCompactOptionValueInput
|
|
16112
|
+
? filterValuePlaceholder(filter.key, properties)
|
|
16113
|
+
: selectedOptions.length > 0
|
|
16114
|
+
? ""
|
|
16115
|
+
: filterOptionSearchPlaceholder(filter.key, properties)
|
|
16116
|
+
}
|
|
16117
|
+
aria-label={`Search ${filter.label} filter options`}
|
|
16118
|
+
onChange={(event) => setOptionQuery(event.target.value)}
|
|
16119
|
+
className="h-7 min-w-[7rem] flex-1 border-0 bg-transparent px-0 text-xs shadow-none focus-visible:ring-0"
|
|
16120
|
+
/>
|
|
14341
16121
|
</div>
|
|
14342
|
-
|
|
14343
|
-
|
|
16122
|
+
</div>
|
|
16123
|
+
{showOptionChoices ? (
|
|
16124
|
+
<div className="max-h-60 overflow-auto py-0.5">
|
|
16125
|
+
{showClearValue ? (
|
|
14344
16126
|
<DropdownMenuItem
|
|
16127
|
+
className="text-xs"
|
|
14345
16128
|
onSelect={(event) => {
|
|
14346
16129
|
event.preventDefault();
|
|
14347
16130
|
onValueChange("");
|
|
@@ -14350,45 +16133,56 @@ function DatabaseFilterValueControl({
|
|
|
14350
16133
|
<IconX className="mr-2 size-4 text-muted-foreground" />
|
|
14351
16134
|
{dbText("clearValue")}
|
|
14352
16135
|
</DropdownMenuItem>
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
) : null}
|
|
14361
|
-
{filteredOptions.map((option) => (
|
|
14362
|
-
<DropdownMenuItem
|
|
14363
|
-
key={option.id}
|
|
14364
|
-
onSelect={(event) => {
|
|
14365
|
-
event.preventDefault();
|
|
14366
|
-
onValueChange(option.id);
|
|
14367
|
-
}}
|
|
14368
|
-
>
|
|
14369
|
-
<span className="min-w-0 flex-1 truncate">{option.name}</span>
|
|
14370
|
-
{filter.value === option.id || filter.value === option.name ? (
|
|
14371
|
-
<IconCheck className="size-4 text-muted-foreground" />
|
|
14372
|
-
) : null}
|
|
14373
|
-
</DropdownMenuItem>
|
|
14374
|
-
))}
|
|
14375
|
-
{canCreateOption ? (
|
|
14376
|
-
<>
|
|
14377
|
-
<DropdownMenuSeparator />
|
|
16136
|
+
) : null}
|
|
16137
|
+
{filteredOptions.length === 0 && !canCreateOption ? (
|
|
16138
|
+
<div className="px-2 py-3 text-xs text-muted-foreground">
|
|
16139
|
+
{dbText("noMatchingOptions")}
|
|
16140
|
+
</div>
|
|
16141
|
+
) : null}
|
|
16142
|
+
{filteredOptions.map((option) => (
|
|
14378
16143
|
<DropdownMenuItem
|
|
14379
|
-
|
|
16144
|
+
key={option.id}
|
|
16145
|
+
className="text-xs"
|
|
14380
16146
|
onSelect={(event) => {
|
|
14381
16147
|
event.preventDefault();
|
|
14382
|
-
|
|
16148
|
+
chooseFilterOption(option);
|
|
14383
16149
|
}}
|
|
14384
16150
|
>
|
|
14385
|
-
<
|
|
14386
|
-
|
|
16151
|
+
<span className="min-w-0 flex-1 truncate">
|
|
16152
|
+
{optionProperty?.definition.type === "person" ? (
|
|
16153
|
+
option.name === "Me" ? (
|
|
16154
|
+
"Me"
|
|
16155
|
+
) : (
|
|
16156
|
+
<PersonPill value={databaseFilterChoiceValue(option)} />
|
|
16157
|
+
)
|
|
16158
|
+
) : (
|
|
16159
|
+
option.name
|
|
16160
|
+
)}
|
|
16161
|
+
</span>
|
|
16162
|
+
{databaseFilterOptionIsSelected(option, selectedValues) ? (
|
|
16163
|
+
<IconCheck className="size-4 text-muted-foreground" />
|
|
16164
|
+
) : null}
|
|
14387
16165
|
</DropdownMenuItem>
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
16166
|
+
))}
|
|
16167
|
+
{canCreateOption ? (
|
|
16168
|
+
<>
|
|
16169
|
+
<DropdownMenuSeparator />
|
|
16170
|
+
<DropdownMenuItem
|
|
16171
|
+
disabled={configureProperty.isPending}
|
|
16172
|
+
className="text-xs"
|
|
16173
|
+
onSelect={(event) => {
|
|
16174
|
+
event.preventDefault();
|
|
16175
|
+
void createFilterOption();
|
|
16176
|
+
}}
|
|
16177
|
+
>
|
|
16178
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
16179
|
+
Create “{optionQuery.trim()}”
|
|
16180
|
+
</DropdownMenuItem>
|
|
16181
|
+
</>
|
|
16182
|
+
) : null}
|
|
16183
|
+
</div>
|
|
16184
|
+
) : null}
|
|
16185
|
+
</div>
|
|
14392
16186
|
);
|
|
14393
16187
|
}
|
|
14394
16188
|
|
|
@@ -14400,7 +16194,7 @@ function DatabaseFilterValueControl({
|
|
|
14400
16194
|
value={filter.value}
|
|
14401
16195
|
placeholder={filterValuePlaceholder(filter.key, properties)}
|
|
14402
16196
|
onChange={(event) => onValueChange(event.target.value)}
|
|
14403
|
-
className="h-
|
|
16197
|
+
className="h-9 text-sm focus-visible:border-[#2383e2] focus-visible:ring-[#2383e2]/25"
|
|
14404
16198
|
/>
|
|
14405
16199
|
);
|
|
14406
16200
|
}
|
|
@@ -14430,12 +16224,39 @@ const FILTER_OPERATOR_LABELS: Record<FilterOperator, string> = {
|
|
|
14430
16224
|
less_than: "Less than",
|
|
14431
16225
|
before: "Before",
|
|
14432
16226
|
after: "After",
|
|
16227
|
+
between: "Between",
|
|
14433
16228
|
is_checked: "Checked",
|
|
14434
16229
|
is_unchecked: "Unchecked",
|
|
14435
16230
|
is_empty: "Is empty",
|
|
14436
16231
|
is_not_empty: "Is not empty",
|
|
14437
16232
|
};
|
|
14438
16233
|
|
|
16234
|
+
function databaseFilterOperatorLabel(
|
|
16235
|
+
operator: FilterOperator,
|
|
16236
|
+
key: string,
|
|
16237
|
+
properties: DocumentProperty[],
|
|
16238
|
+
) {
|
|
16239
|
+
const type = filterPropertyTypeForKey(key, properties);
|
|
16240
|
+
if (
|
|
16241
|
+
operator === "does_not_equal" &&
|
|
16242
|
+
(type === "select" ||
|
|
16243
|
+
type === "status" ||
|
|
16244
|
+
type === "multi_select" ||
|
|
16245
|
+
type === "person")
|
|
16246
|
+
) {
|
|
16247
|
+
return "Does not contain";
|
|
16248
|
+
}
|
|
16249
|
+
return FILTER_OPERATOR_LABELS[operator] ?? "Contains";
|
|
16250
|
+
}
|
|
16251
|
+
|
|
16252
|
+
function databaseFilterOperatorInlineLabel(
|
|
16253
|
+
operator: FilterOperator,
|
|
16254
|
+
key: string,
|
|
16255
|
+
properties: DocumentProperty[],
|
|
16256
|
+
) {
|
|
16257
|
+
return databaseFilterOperatorLabel(operator, key, properties).toLowerCase();
|
|
16258
|
+
}
|
|
16259
|
+
|
|
14439
16260
|
function filterOperatorsForKey(
|
|
14440
16261
|
key: string,
|
|
14441
16262
|
properties: DocumentProperty[],
|
|
@@ -14446,8 +16267,13 @@ function filterOperatorsForKey(
|
|
|
14446
16267
|
return ["is_checked", "is_unchecked"];
|
|
14447
16268
|
}
|
|
14448
16269
|
|
|
14449
|
-
if (
|
|
14450
|
-
|
|
16270
|
+
if (
|
|
16271
|
+
type === "select" ||
|
|
16272
|
+
type === "status" ||
|
|
16273
|
+
type === "multi_select" ||
|
|
16274
|
+
type === "person"
|
|
16275
|
+
) {
|
|
16276
|
+
return ["contains", "does_not_equal", "is_empty", "is_not_empty"];
|
|
14451
16277
|
}
|
|
14452
16278
|
|
|
14453
16279
|
if (type === "number") {
|
|
@@ -14471,6 +16297,7 @@ function filterOperatorsForKey(
|
|
|
14471
16297
|
"does_not_equal",
|
|
14472
16298
|
"before",
|
|
14473
16299
|
"after",
|
|
16300
|
+
"between",
|
|
14474
16301
|
"is_empty",
|
|
14475
16302
|
"is_not_empty",
|
|
14476
16303
|
];
|
|
@@ -14514,6 +16341,18 @@ function filterValuePlaceholder(key: string, properties: DocumentProperty[]) {
|
|
|
14514
16341
|
return "Value";
|
|
14515
16342
|
}
|
|
14516
16343
|
|
|
16344
|
+
function filterOptionSearchPlaceholder(
|
|
16345
|
+
key: string,
|
|
16346
|
+
properties: DocumentProperty[],
|
|
16347
|
+
) {
|
|
16348
|
+
const type = filterPropertyTypeForKey(key, properties);
|
|
16349
|
+
if (type === "person") return "Search for one or more people...";
|
|
16350
|
+
if (type === "select" || type === "status" || type === "multi_select") {
|
|
16351
|
+
return "Search for one or more options...";
|
|
16352
|
+
}
|
|
16353
|
+
return "Search for an option...";
|
|
16354
|
+
}
|
|
16355
|
+
|
|
14517
16356
|
function filterValueInputType(type: DocumentPropertyType) {
|
|
14518
16357
|
if (type === "number") return "number";
|
|
14519
16358
|
if (type === "date" || type === "created_time" || type === "last_edited_time")
|
|
@@ -14521,12 +16360,106 @@ function filterValueInputType(type: DocumentPropertyType) {
|
|
|
14521
16360
|
return "text";
|
|
14522
16361
|
}
|
|
14523
16362
|
|
|
16363
|
+
function DatabaseBetweenDateFilterValueControl({
|
|
16364
|
+
filter,
|
|
16365
|
+
onValueChange,
|
|
16366
|
+
}: {
|
|
16367
|
+
filter: DatabaseFilter;
|
|
16368
|
+
onValueChange: (value: string) => void;
|
|
16369
|
+
}) {
|
|
16370
|
+
const [start, end] = databaseFilterDateRangeValues(filter.value);
|
|
16371
|
+
const updateRange = (nextStart: string, nextEnd: string) => {
|
|
16372
|
+
onValueChange(databaseFilterEncodeDateRangeValues(nextStart, nextEnd));
|
|
16373
|
+
};
|
|
16374
|
+
|
|
16375
|
+
return (
|
|
16376
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2">
|
|
16377
|
+
<Input
|
|
16378
|
+
type="date"
|
|
16379
|
+
value={start}
|
|
16380
|
+
aria-label={`Start ${filter.label} filter date`}
|
|
16381
|
+
onChange={(event) => updateRange(event.target.value, end)}
|
|
16382
|
+
className="h-9 text-sm focus-visible:border-[#2383e2] focus-visible:ring-[#2383e2]/25"
|
|
16383
|
+
/>
|
|
16384
|
+
<span className="text-xs text-muted-foreground">to</span>
|
|
16385
|
+
<Input
|
|
16386
|
+
type="date"
|
|
16387
|
+
value={end}
|
|
16388
|
+
aria-label={`End ${filter.label} filter date`}
|
|
16389
|
+
onChange={(event) => updateRange(start, event.target.value)}
|
|
16390
|
+
className="h-9 text-sm focus-visible:border-[#2383e2] focus-visible:ring-[#2383e2]/25"
|
|
16391
|
+
/>
|
|
16392
|
+
</div>
|
|
16393
|
+
);
|
|
16394
|
+
}
|
|
16395
|
+
|
|
14524
16396
|
export function databaseFilterOptionChoices(
|
|
14525
16397
|
key: string,
|
|
14526
16398
|
properties: DocumentProperty[],
|
|
16399
|
+
items: ContentDatabaseItem[] = [],
|
|
16400
|
+
currentUserEmail = "",
|
|
16401
|
+
): DatabaseFilterChoice[] {
|
|
16402
|
+
const property = databaseFilterPropertyForKey(key, properties);
|
|
16403
|
+
if (property?.definition.type === "person") {
|
|
16404
|
+
return databaseFilterPersonChoices(
|
|
16405
|
+
property.definition.id,
|
|
16406
|
+
items,
|
|
16407
|
+
currentUserEmail,
|
|
16408
|
+
);
|
|
16409
|
+
}
|
|
16410
|
+
const optionProperty = databaseFilterOptionPropertyForKey(key, properties);
|
|
16411
|
+
return optionProperty?.definition.options.options ?? [];
|
|
16412
|
+
}
|
|
16413
|
+
|
|
16414
|
+
type DatabaseFilterChoice = DocumentPropertyOption & {
|
|
16415
|
+
filterValue?: string;
|
|
16416
|
+
};
|
|
16417
|
+
|
|
16418
|
+
function databaseFilterChoiceValue(option: DatabaseFilterChoice) {
|
|
16419
|
+
return option.filterValue ?? option.id;
|
|
16420
|
+
}
|
|
16421
|
+
|
|
16422
|
+
function databaseFilterPersonChoices(
|
|
16423
|
+
propertyId: string,
|
|
16424
|
+
items: ContentDatabaseItem[],
|
|
16425
|
+
currentUserEmail = "",
|
|
14527
16426
|
) {
|
|
14528
|
-
const
|
|
14529
|
-
|
|
16427
|
+
const people = new Map<string, string>();
|
|
16428
|
+
for (const item of items) {
|
|
16429
|
+
const property = item.properties.find(
|
|
16430
|
+
(candidate) => candidate.definition.id === propertyId,
|
|
16431
|
+
);
|
|
16432
|
+
for (const person of personItems(property?.value ?? null)) {
|
|
16433
|
+
const key = person.trim().toLowerCase();
|
|
16434
|
+
if (!people.has(key)) people.set(key, person);
|
|
16435
|
+
}
|
|
16436
|
+
}
|
|
16437
|
+
const choices: DatabaseFilterChoice[] = [];
|
|
16438
|
+
const currentUserLabel = currentUserEmail
|
|
16439
|
+
? personLabel(currentUserEmail)
|
|
16440
|
+
: "";
|
|
16441
|
+
if (currentUserEmail) {
|
|
16442
|
+
const matchingPerson =
|
|
16443
|
+
Array.from(people.values()).find(
|
|
16444
|
+
(person) =>
|
|
16445
|
+
person.toLowerCase() === currentUserEmail.toLowerCase() ||
|
|
16446
|
+
personLabel(person).toLowerCase() === currentUserLabel.toLowerCase(),
|
|
16447
|
+
) ?? currentUserEmail;
|
|
16448
|
+
choices.push({
|
|
16449
|
+
id: `__current_user__:${matchingPerson}`,
|
|
16450
|
+
name: "Me",
|
|
16451
|
+
color: "gray" as const,
|
|
16452
|
+
filterValue: matchingPerson,
|
|
16453
|
+
});
|
|
16454
|
+
}
|
|
16455
|
+
choices.push(
|
|
16456
|
+
...Array.from(people.values()).map((person) => ({
|
|
16457
|
+
id: person,
|
|
16458
|
+
name: personLabel(person),
|
|
16459
|
+
color: "gray" as const,
|
|
16460
|
+
})),
|
|
16461
|
+
);
|
|
16462
|
+
return choices;
|
|
14530
16463
|
}
|
|
14531
16464
|
|
|
14532
16465
|
export function databaseFilterOptionPropertyForKey(
|
|
@@ -14538,7 +16471,8 @@ export function databaseFilterOptionPropertyForKey(
|
|
|
14538
16471
|
if (
|
|
14539
16472
|
property.definition.type !== "select" &&
|
|
14540
16473
|
property.definition.type !== "status" &&
|
|
14541
|
-
property.definition.type !== "multi_select"
|
|
16474
|
+
property.definition.type !== "multi_select" &&
|
|
16475
|
+
property.definition.type !== "person"
|
|
14542
16476
|
) {
|
|
14543
16477
|
return null;
|
|
14544
16478
|
}
|
|
@@ -14549,18 +16483,105 @@ function databaseFilterValueLabel(
|
|
|
14549
16483
|
filter: DatabaseFilter,
|
|
14550
16484
|
properties: DocumentProperty[],
|
|
14551
16485
|
) {
|
|
14552
|
-
|
|
14553
|
-
(
|
|
14554
|
-
|
|
16486
|
+
if (filter.operator === "between") {
|
|
16487
|
+
const [start, end] = databaseFilterDateRangeValues(filter.value);
|
|
16488
|
+
if (start && end) return `${start} to ${end}`;
|
|
16489
|
+
return start || end || "Choose dates";
|
|
16490
|
+
}
|
|
16491
|
+
const values = databaseFilterSelectedValues(filter.value);
|
|
16492
|
+
const options = databaseFilterOptionChoices(filter.key, properties);
|
|
16493
|
+
if (options.length > 0) {
|
|
16494
|
+
const labels = values.map((value) => {
|
|
16495
|
+
const option = options.find(
|
|
16496
|
+
(candidate) =>
|
|
16497
|
+
candidate.id === value ||
|
|
16498
|
+
candidate.name.trim().toLowerCase() === value.trim().toLowerCase(),
|
|
16499
|
+
);
|
|
16500
|
+
return option?.name ?? value;
|
|
16501
|
+
});
|
|
16502
|
+
return labels.length > 0 ? labels.join(", ") : "Choose option";
|
|
16503
|
+
}
|
|
16504
|
+
return values[0] || "Choose option";
|
|
16505
|
+
}
|
|
16506
|
+
|
|
16507
|
+
function databaseFilterSelectedValues(value: string) {
|
|
16508
|
+
const trimmed = value.trim();
|
|
16509
|
+
if (!trimmed) return [];
|
|
16510
|
+
try {
|
|
16511
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
16512
|
+
if (!Array.isArray(parsed)) return [trimmed];
|
|
16513
|
+
return [
|
|
16514
|
+
...new Set(
|
|
16515
|
+
parsed
|
|
16516
|
+
.filter((item): item is string => typeof item === "string")
|
|
16517
|
+
.map((item) => item.trim())
|
|
16518
|
+
.filter(Boolean),
|
|
16519
|
+
),
|
|
16520
|
+
];
|
|
16521
|
+
} catch {
|
|
16522
|
+
return [trimmed];
|
|
16523
|
+
}
|
|
16524
|
+
}
|
|
16525
|
+
|
|
16526
|
+
function databaseFilterDateRangeValues(value: string): [string, string] {
|
|
16527
|
+
const trimmed = value.trim();
|
|
16528
|
+
if (!trimmed) return ["", ""];
|
|
16529
|
+
try {
|
|
16530
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
16531
|
+
if (!Array.isArray(parsed)) return [trimmed, ""];
|
|
16532
|
+
const start = typeof parsed[0] === "string" ? parsed[0].trim() : "";
|
|
16533
|
+
const end = typeof parsed[1] === "string" ? parsed[1].trim() : "";
|
|
16534
|
+
return [start, end];
|
|
16535
|
+
} catch {
|
|
16536
|
+
return [trimmed, ""];
|
|
16537
|
+
}
|
|
16538
|
+
}
|
|
16539
|
+
|
|
16540
|
+
function databaseFilterEncodeDateRangeValues(start: string, end: string) {
|
|
16541
|
+
const normalizedStart = start.trim();
|
|
16542
|
+
const normalizedEnd = end.trim();
|
|
16543
|
+
if (!normalizedStart && !normalizedEnd) return "";
|
|
16544
|
+
return JSON.stringify([normalizedStart, normalizedEnd]);
|
|
16545
|
+
}
|
|
16546
|
+
|
|
16547
|
+
function databaseFilterDateRangeValue(value: string): [number, number] | null {
|
|
16548
|
+
const [startValue, endValue] = databaseFilterDateRangeValues(value);
|
|
16549
|
+
const start = new Date(startValue).getTime();
|
|
16550
|
+
const end = new Date(endValue).getTime();
|
|
16551
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
16552
|
+
return start <= end ? [start, end] : [end, start];
|
|
16553
|
+
}
|
|
16554
|
+
|
|
16555
|
+
function databaseFilterEncodeSelectedValues(values: string[]) {
|
|
16556
|
+
const normalized = [
|
|
16557
|
+
...new Set(values.map((value) => value.trim()).filter(Boolean)),
|
|
16558
|
+
];
|
|
16559
|
+
if (normalized.length === 0) return "";
|
|
16560
|
+
if (normalized.length === 1) return normalized[0];
|
|
16561
|
+
return JSON.stringify(normalized);
|
|
16562
|
+
}
|
|
16563
|
+
|
|
16564
|
+
function databaseFilterOptionIsSelected(
|
|
16565
|
+
option: DatabaseFilterChoice,
|
|
16566
|
+
values: string[],
|
|
16567
|
+
) {
|
|
16568
|
+
const optionValue = databaseFilterChoiceValue(option);
|
|
16569
|
+
const optionName = option.name.trim().toLowerCase();
|
|
16570
|
+
return values.some(
|
|
16571
|
+
(value) =>
|
|
16572
|
+
value === optionValue || value.trim().toLowerCase() === optionName,
|
|
14555
16573
|
);
|
|
14556
|
-
return (option?.name ?? filter.value) || "Choose option";
|
|
14557
16574
|
}
|
|
14558
16575
|
|
|
14559
16576
|
function databaseFilterChipLabel(
|
|
14560
16577
|
filter: DatabaseFilter,
|
|
14561
16578
|
properties: DocumentProperty[],
|
|
14562
16579
|
) {
|
|
14563
|
-
const operator =
|
|
16580
|
+
const operator = databaseFilterOperatorLabel(
|
|
16581
|
+
filter.operator,
|
|
16582
|
+
filter.key,
|
|
16583
|
+
properties,
|
|
16584
|
+
);
|
|
14564
16585
|
if (!filterOperatorNeedsValue(filter.operator)) {
|
|
14565
16586
|
return `${filter.label} ${operator.toLowerCase()}`;
|
|
14566
16587
|
}
|
|
@@ -14570,6 +16591,23 @@ function databaseFilterChipLabel(
|
|
|
14570
16591
|
)}`;
|
|
14571
16592
|
}
|
|
14572
16593
|
|
|
16594
|
+
export function databaseInlineFilterLabel(
|
|
16595
|
+
filter: DatabaseFilter,
|
|
16596
|
+
properties: DocumentProperty[],
|
|
16597
|
+
) {
|
|
16598
|
+
if (!filterOperatorNeedsValue(filter.operator)) {
|
|
16599
|
+
return `${filter.label}: ${databaseFilterOperatorLabel(
|
|
16600
|
+
filter.operator,
|
|
16601
|
+
filter.key,
|
|
16602
|
+
properties,
|
|
16603
|
+
)}`;
|
|
16604
|
+
}
|
|
16605
|
+
if (databaseFilterSelectedValues(filter.value).length === 0) {
|
|
16606
|
+
return filter.label;
|
|
16607
|
+
}
|
|
16608
|
+
return `${filter.label}: ${databaseFilterValueLabel(filter, properties)}`;
|
|
16609
|
+
}
|
|
16610
|
+
|
|
14573
16611
|
function databaseFilterPropertyForKey(
|
|
14574
16612
|
key: string,
|
|
14575
16613
|
properties: DocumentProperty[],
|