@agent-native/core 0.91.1 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +2 -2
- package/corpus/core/CHANGELOG.md +31 -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/docs/content/faq.mdx +17 -0
- package/corpus/core/docs/content/notifications.mdx +12 -6
- package/corpus/core/package.json +6 -5
- package/corpus/core/src/agent/production-agent.ts +140 -46
- package/corpus/core/src/agent/run-manager.ts +23 -2
- package/corpus/core/src/agent/run-store.ts +2 -2
- package/corpus/core/src/agent/tool-search.ts +71 -4
- 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 +25 -4
- package/corpus/core/src/cli/upgrade.ts +798 -0
- package/corpus/core/src/client/AssistantChat.tsx +1 -1
- package/corpus/core/src/client/HighlightedCodeBlock.tsx +20 -3
- package/corpus/core/src/client/agent-chat-adapter.ts +31 -6
- package/corpus/core/src/client/chat/message-components.tsx +85 -8
- package/corpus/core/src/client/chat/tool-call-display.tsx +355 -144
- package/corpus/core/src/client/code-agent-chat-adapter.ts +2 -9
- package/corpus/core/src/client/extensions/ExtensionSlot.tsx +7 -0
- package/corpus/core/src/client/settings/SettingsPanel.tsx +533 -389
- package/corpus/core/src/client/settings/SettingsSection.tsx +154 -17
- package/corpus/core/src/client/sse-event-processor.ts +29 -2
- package/corpus/core/src/extensions/actions.ts +113 -5
- package/corpus/core/src/jobs/scheduler.ts +30 -1
- package/corpus/core/src/localization/default-messages.ts +21 -0
- package/corpus/core/src/notifications/channels.ts +88 -25
- package/corpus/core/src/notifications/registry.ts +17 -3
- package/corpus/core/src/notifications/types.ts +4 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +7 -3
- package/corpus/core/src/server/request-context.ts +7 -0
- package/corpus/core/src/styles/agent-conversation.css +1 -1
- package/corpus/core/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/core/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/core/src/templates/default/AGENTS.md +10 -3
- 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 +4 -3
- package/corpus/core/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/core/src/templates/headless/AGENTS.md +9 -3
- package/corpus/core/src/templates/headless/package.json +4 -3
- package/corpus/core/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/core/src/templates/workspace-core/package.json +6 -6
- package/corpus/core/src/templates/workspace-root/AGENTS.md +10 -4
- package/corpus/core/src/templates/workspace-root/package.json +3 -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/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +20 -2
- package/corpus/templates/analytics/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/analytics/AGENTS.md +7 -5
- package/corpus/templates/analytics/actions/save-monitor.ts +14 -0
- package/corpus/templates/analytics/app/components/dashboard/DataTable.tsx +41 -6
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +122 -11
- 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/monitoring/MonitoringPage.tsx +8 -2
- package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorFormPage.tsx +151 -32
- package/corpus/templates/analytics/app/pages/monitoring/uptime/i18n.ts +17 -2
- package/corpus/templates/analytics/app/pages/monitoring/uptime/types.ts +4 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +461 -286
- 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-dashboard-tables-keep-their-size-while-loading.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-monitoring-uptime-and-errors-tabs-now-clearly-show-which-view-is-active.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-replay-uses-stock-rrweb-sizing-again-so-recorded.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/changelog/2026-07-08-sidebar-dashboard-menus-now-open-above-the-dashboard-canvas.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-uptime-alerting-clarifies-in-app-inbox-vs-email-and-lets-you-paste-slack-or-webhook-urls-per-monitor.md +6 -0
- package/corpus/templates/analytics/docs/uptime-monitoring.md +13 -11
- package/corpus/templates/analytics/package.json +2 -2
- package/corpus/templates/analytics/server/db/schema-monitoring.ts +4 -0
- package/corpus/templates/analytics/server/lib/uptime-monitors.ts +60 -12
- package/corpus/templates/analytics/server/plugins/db.ts +10 -0
- package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/assets/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/assets/package.json +2 -2
- package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/brain/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/brain/package.json +2 -2
- package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/calendar/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- 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/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/chat/package.json +2 -2
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +6 -2
- package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/clips/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- 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-desktop-meeting-notes-stay-active-from-tray-and-shortcut-controls.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-desktop-meeting-popovers-can-start-notes-and-join-the-meetin.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-meeting-notes-keep-their-recording-state-when-the-desktop.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-meeting-reminder-dropdowns-now-expand-fully-instead-of-being.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 +214 -37
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +30 -6
- 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 +197 -50
- package/corpus/templates/clips/desktop/src/styles.css +150 -53
- package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +362 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +41 -7
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +9 -2
- package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +89 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +71 -17
- 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/shortcuts.rs +3 -3
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +1 -25
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +5 -0
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +18 -6
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +36 -1
- package/corpus/templates/clips/package.json +2 -2
- package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/content/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/content/actions/_batch-utils.ts +8 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +7 -5
- package/corpus/templates/content/actions/_content-database-personal-view.ts +80 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +776 -319
- package/corpus/templates/content/actions/_database-utils.ts +6 -1
- package/corpus/templates/content/actions/_property-utils.ts +168 -0
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +19 -12
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +19 -12
- package/corpus/templates/content/actions/delete-document.ts +9 -13
- package/corpus/templates/content/actions/get-content-database-personal-view.ts +27 -0
- package/corpus/templates/content/actions/refresh-content-database-source.ts +5 -5
- 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/body-hydration.ts +35 -11
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2631 -551
- 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/hooks/use-documents.ts +8 -2
- 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-builder-source-sync-now-completes-reliably-on-hosted-databases.md +6 -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/changelog/2026-07-08-fixed-builder-database-sync-so-large-sources-continue-past-t.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/server/plugins/db.ts +19 -0
- package/corpus/templates/content/shared/api.ts +24 -0
- package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/design/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/design/package.json +2 -2
- package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/dispatch/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- 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/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/forms/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/forms/package.json +2 -2
- package/corpus/templates/macros/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/macros/package.json +2 -2
- package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/mail/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- 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/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/plan/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/plan/package.json +2 -2
- package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/corpus/templates/slides/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- 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/corpus/templates/videos/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/voice/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/dist/agent/production-agent.d.ts +5 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +112 -42
- package/dist/agent/production-agent.js.map +1 -1
- 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/agent/tool-search.d.ts +2 -0
- package/dist/agent/tool-search.d.ts.map +1 -1
- package/dist/agent/tool-search.js +60 -4
- package/dist/agent/tool-search.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 +24 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/upgrade.d.ts +100 -0
- package/dist/cli/upgrade.d.ts.map +1 -0
- package/dist/cli/upgrade.js +678 -0
- package/dist/cli/upgrade.js.map +1 -0
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/HighlightedCodeBlock.d.ts +2 -0
- package/dist/client/HighlightedCodeBlock.d.ts.map +1 -1
- package/dist/client/HighlightedCodeBlock.js +10 -1
- package/dist/client/HighlightedCodeBlock.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +32 -4
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +61 -8
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +12 -0
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +136 -42
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/code-agent-chat-adapter.js +2 -9
- package/dist/client/code-agent-chat-adapter.js.map +1 -1
- package/dist/client/extensions/ExtensionSlot.js +6 -0
- package/dist/client/extensions/ExtensionSlot.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +52 -19
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsSection.d.ts +19 -4
- package/dist/client/settings/SettingsSection.d.ts.map +1 -1
- package/dist/client/settings/SettingsSection.js +33 -4
- package/dist/client/settings/SettingsSection.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +9 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +20 -2
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +85 -5
- package/dist/extensions/actions.js.map +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/notifications/channels.d.ts +8 -3
- package/dist/notifications/channels.d.ts.map +1 -1
- package/dist/notifications/channels.js +61 -22
- package/dist/notifications/channels.js.map +1 -1
- package/dist/notifications/registry.d.ts.map +1 -1
- package/dist/notifications/registry.js +14 -3
- package/dist/notifications/registry.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/notifications/types.d.ts +4 -1
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/notifications/types.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts +5 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +2 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/request-context.d.ts +7 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/styles/agent-conversation.css +1 -1
- package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/dist/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/dist/templates/default/AGENTS.md +10 -3
- 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 +4 -3
- package/dist/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/dist/templates/headless/AGENTS.md +9 -3
- package/dist/templates/headless/package.json +4 -3
- package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/dist/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/dist/templates/workspace-core/package.json +6 -6
- package/dist/templates/workspace-root/AGENTS.md +10 -4
- package/dist/templates/workspace-root/package.json +3 -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/docs/content/faq.mdx +17 -0
- package/docs/content/notifications.mdx +12 -6
- package/package.json +7 -6
- package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/src/templates/default/AGENTS.md +10 -3
- 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 +4 -3
- package/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/src/templates/headless/AGENTS.md +9 -3
- package/src/templates/headless/package.json +4 -3
- package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +20 -2
- package/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/src/templates/workspace-core/package.json +6 -6
- package/src/templates/workspace-root/AGENTS.md +10 -4
- package/src/templates/workspace-root/package.json +3 -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"
|
|
@@ -5707,6 +7159,11 @@ function BuilderSourceContinuationBar({
|
|
|
5707
7159
|
)
|
|
5708
7160
|
: rawFetchedCount;
|
|
5709
7161
|
const hasMore = source.metadata.lastReadHasMore === true && !rowsComplete;
|
|
7162
|
+
const fetchedCountDetail = builderSourceContinuationFetchedCountDetail(
|
|
7163
|
+
source,
|
|
7164
|
+
fetchedCount,
|
|
7165
|
+
rowsComplete,
|
|
7166
|
+
);
|
|
5710
7167
|
const bodyHydrationActive =
|
|
5711
7168
|
!hasMore &&
|
|
5712
7169
|
!!bodyHydration &&
|
|
@@ -5760,7 +7217,9 @@ function BuilderSourceContinuationBar({
|
|
|
5760
7217
|
failed: bodyHydration.error,
|
|
5761
7218
|
},
|
|
5762
7219
|
)
|
|
5763
|
-
: dbText("builderRowsFetchedSoFar", {
|
|
7220
|
+
: dbText("builderRowsFetchedSoFar", {
|
|
7221
|
+
count: fetchedCountDetail ?? fetchedCount,
|
|
7222
|
+
});
|
|
5764
7223
|
|
|
5765
7224
|
return (
|
|
5766
7225
|
<div
|
|
@@ -6578,6 +8037,24 @@ export function builderSourceContinuationProgressPercent(
|
|
|
6578
8037
|
: rawPercent;
|
|
6579
8038
|
}
|
|
6580
8039
|
|
|
8040
|
+
export function builderSourceContinuationFetchedCountDetail(
|
|
8041
|
+
source: Pick<ContentDatabaseSource, "metadata">,
|
|
8042
|
+
fetchedCount: number | null,
|
|
8043
|
+
rowsComplete = false,
|
|
8044
|
+
) {
|
|
8045
|
+
const knownFetchTotal =
|
|
8046
|
+
!rowsComplete &&
|
|
8047
|
+
source.metadata.lastReadHasMore === true &&
|
|
8048
|
+
typeof source.metadata.lastReadLimit === "number"
|
|
8049
|
+
? source.metadata.lastReadLimit
|
|
8050
|
+
: null;
|
|
8051
|
+
return fetchedCount !== null &&
|
|
8052
|
+
knownFetchTotal !== null &&
|
|
8053
|
+
knownFetchTotal > fetchedCount
|
|
8054
|
+
? `${fetchedCount} of ${knownFetchTotal}`
|
|
8055
|
+
: fetchedCount;
|
|
8056
|
+
}
|
|
8057
|
+
|
|
6581
8058
|
export function builderSourceContinuationWatchdogDecision(refires: number) {
|
|
6582
8059
|
return "refire";
|
|
6583
8060
|
}
|
|
@@ -7081,7 +8558,10 @@ type DatabasePropertyPickerOption = {
|
|
|
7081
8558
|
export function databasePropertyPickerItems(
|
|
7082
8559
|
properties: DocumentProperty[],
|
|
7083
8560
|
query: string,
|
|
7084
|
-
{
|
|
8561
|
+
{
|
|
8562
|
+
includeName = true,
|
|
8563
|
+
excludeKeys,
|
|
8564
|
+
}: { includeName?: boolean; excludeKeys?: ReadonlySet<string> } = {},
|
|
7085
8565
|
): DatabasePropertyPickerOption[] {
|
|
7086
8566
|
const normalizedQuery = query.trim().toLowerCase();
|
|
7087
8567
|
const items: DatabasePropertyPickerOption[] = [
|
|
@@ -7093,7 +8573,7 @@ export function databasePropertyPickerItems(
|
|
|
7093
8573
|
label: property.definition.name,
|
|
7094
8574
|
type: property.definition.type,
|
|
7095
8575
|
})),
|
|
7096
|
-
];
|
|
8576
|
+
].filter((item) => !excludeKeys?.has(item.key));
|
|
7097
8577
|
|
|
7098
8578
|
if (!normalizedQuery) return items;
|
|
7099
8579
|
return items.filter((item) =>
|
|
@@ -7106,9 +8586,11 @@ export function databasePropertyPickerItems(
|
|
|
7106
8586
|
function DatabasePropertyPickerSearch({
|
|
7107
8587
|
value,
|
|
7108
8588
|
onChange,
|
|
8589
|
+
placeholder = dbText("searchProperties"),
|
|
7109
8590
|
}: {
|
|
7110
8591
|
value: string;
|
|
7111
8592
|
onChange: (value: string) => void;
|
|
8593
|
+
placeholder?: string;
|
|
7112
8594
|
}) {
|
|
7113
8595
|
return (
|
|
7114
8596
|
<div className="border-b border-border/70 p-1">
|
|
@@ -7118,8 +8600,8 @@ function DatabasePropertyPickerSearch({
|
|
|
7118
8600
|
value={value}
|
|
7119
8601
|
onChange={(event) => onChange(event.target.value)}
|
|
7120
8602
|
onKeyDown={(event) => event.stopPropagation()}
|
|
7121
|
-
placeholder={
|
|
7122
|
-
aria-label={
|
|
8603
|
+
placeholder={placeholder}
|
|
8604
|
+
aria-label={placeholder}
|
|
7123
8605
|
className="h-6 border-0 bg-transparent px-0 text-xs shadow-none focus-visible:ring-0"
|
|
7124
8606
|
/>
|
|
7125
8607
|
</div>
|
|
@@ -7131,16 +8613,18 @@ function DatabasePropertyPickerItem({
|
|
|
7131
8613
|
item,
|
|
7132
8614
|
selected,
|
|
7133
8615
|
onSelect,
|
|
8616
|
+
closeOnSelect = false,
|
|
7134
8617
|
}: {
|
|
7135
8618
|
item: DatabasePropertyPickerOption;
|
|
7136
8619
|
selected: boolean;
|
|
7137
8620
|
onSelect: (key: string, label: string) => void;
|
|
8621
|
+
closeOnSelect?: boolean;
|
|
7138
8622
|
}) {
|
|
7139
8623
|
const Icon = item.type === "name" ? IconFileText : TYPE_ICONS[item.type];
|
|
7140
8624
|
return (
|
|
7141
8625
|
<DropdownMenuItem
|
|
7142
8626
|
onSelect={(event) => {
|
|
7143
|
-
event.preventDefault();
|
|
8627
|
+
if (!closeOnSelect) event.preventDefault();
|
|
7144
8628
|
onSelect(item.key, item.label);
|
|
7145
8629
|
}}
|
|
7146
8630
|
>
|
|
@@ -7151,23 +8635,84 @@ function DatabasePropertyPickerItem({
|
|
|
7151
8635
|
);
|
|
7152
8636
|
}
|
|
7153
8637
|
|
|
8638
|
+
function DatabasePropertyPickerMenuItems({
|
|
8639
|
+
properties,
|
|
8640
|
+
selectedKey,
|
|
8641
|
+
includeName,
|
|
8642
|
+
excludeKeys,
|
|
8643
|
+
placeholder,
|
|
8644
|
+
closeOnSelect,
|
|
8645
|
+
onSelect,
|
|
8646
|
+
}: {
|
|
8647
|
+
properties: DocumentProperty[];
|
|
8648
|
+
selectedKey?: string;
|
|
8649
|
+
includeName?: boolean;
|
|
8650
|
+
excludeKeys?: ReadonlySet<string>;
|
|
8651
|
+
placeholder?: string;
|
|
8652
|
+
closeOnSelect?: boolean;
|
|
8653
|
+
onSelect: (key: string, label: string) => void;
|
|
8654
|
+
}) {
|
|
8655
|
+
const [query, setQuery] = useState("");
|
|
8656
|
+
const items = databasePropertyPickerItems(properties, query, {
|
|
8657
|
+
includeName,
|
|
8658
|
+
excludeKeys,
|
|
8659
|
+
});
|
|
8660
|
+
|
|
8661
|
+
return (
|
|
8662
|
+
<>
|
|
8663
|
+
<DatabasePropertyPickerSearch
|
|
8664
|
+
value={query}
|
|
8665
|
+
onChange={setQuery}
|
|
8666
|
+
placeholder={placeholder}
|
|
8667
|
+
/>
|
|
8668
|
+
<div className="max-h-72 overflow-auto p-1">
|
|
8669
|
+
{items.map((item) => (
|
|
8670
|
+
<DatabasePropertyPickerItem
|
|
8671
|
+
key={item.key}
|
|
8672
|
+
item={item}
|
|
8673
|
+
selected={selectedKey === item.key}
|
|
8674
|
+
closeOnSelect={closeOnSelect}
|
|
8675
|
+
onSelect={onSelect}
|
|
8676
|
+
/>
|
|
8677
|
+
))}
|
|
8678
|
+
{items.length === 0 ? (
|
|
8679
|
+
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
|
8680
|
+
{dbText("noPropertiesFound")}
|
|
8681
|
+
</div>
|
|
8682
|
+
) : null}
|
|
8683
|
+
</div>
|
|
8684
|
+
</>
|
|
8685
|
+
);
|
|
8686
|
+
}
|
|
8687
|
+
|
|
7154
8688
|
function DatabasePropertyPickerSubContent({
|
|
7155
8689
|
properties,
|
|
7156
8690
|
selectedKey,
|
|
7157
8691
|
includeName,
|
|
8692
|
+
excludeKeys,
|
|
8693
|
+
placeholder,
|
|
7158
8694
|
onSelect,
|
|
7159
8695
|
}: {
|
|
7160
8696
|
properties: DocumentProperty[];
|
|
7161
|
-
selectedKey
|
|
8697
|
+
selectedKey?: string;
|
|
7162
8698
|
includeName?: boolean;
|
|
8699
|
+
excludeKeys?: ReadonlySet<string>;
|
|
8700
|
+
placeholder?: string;
|
|
7163
8701
|
onSelect: (key: string, label: string) => void;
|
|
7164
8702
|
}) {
|
|
7165
8703
|
const [query, setQuery] = useState("");
|
|
7166
|
-
const items = databasePropertyPickerItems(properties, query, {
|
|
8704
|
+
const items = databasePropertyPickerItems(properties, query, {
|
|
8705
|
+
includeName,
|
|
8706
|
+
excludeKeys,
|
|
8707
|
+
});
|
|
7167
8708
|
|
|
7168
8709
|
return (
|
|
7169
8710
|
<DropdownMenuSubContent className="max-h-80 w-64 overflow-auto">
|
|
7170
|
-
<DatabasePropertyPickerSearch
|
|
8711
|
+
<DatabasePropertyPickerSearch
|
|
8712
|
+
value={query}
|
|
8713
|
+
onChange={setQuery}
|
|
8714
|
+
placeholder={placeholder}
|
|
8715
|
+
/>
|
|
7171
8716
|
{items.map((item) => (
|
|
7172
8717
|
<DatabasePropertyPickerItem
|
|
7173
8718
|
key={item.key}
|
|
@@ -7332,8 +8877,8 @@ function databaseFilterModeLabel(filterMode: DatabaseFilterMode) {
|
|
|
7332
8877
|
return filterMode === "or" ? "Any" : "All";
|
|
7333
8878
|
}
|
|
7334
8879
|
|
|
7335
|
-
function
|
|
7336
|
-
return filterMode === "or" ? "
|
|
8880
|
+
function databaseFilterModeConjunctionLabel(filterMode: DatabaseFilterMode) {
|
|
8881
|
+
return filterMode === "or" ? "Or" : "And";
|
|
7337
8882
|
}
|
|
7338
8883
|
|
|
7339
8884
|
function DatabaseResultCountFooter({
|
|
@@ -10753,6 +12298,143 @@ function databaseViewStateKey(
|
|
|
10753
12298
|
return JSON.stringify({ databaseId, viewConfig });
|
|
10754
12299
|
}
|
|
10755
12300
|
|
|
12301
|
+
function normalizePersonalDatabaseViewOverrides(
|
|
12302
|
+
value: ContentDatabasePersonalViewOverrides | null | undefined,
|
|
12303
|
+
): PersonalDatabaseViewOverrides | null {
|
|
12304
|
+
try {
|
|
12305
|
+
if (!value) return null;
|
|
12306
|
+
const parsed = value as Partial<PersonalDatabaseViewOverrides>;
|
|
12307
|
+
if (
|
|
12308
|
+
parsed.version !== PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION ||
|
|
12309
|
+
!Array.isArray(parsed.views)
|
|
12310
|
+
) {
|
|
12311
|
+
return null;
|
|
12312
|
+
}
|
|
12313
|
+
|
|
12314
|
+
return {
|
|
12315
|
+
version: PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION,
|
|
12316
|
+
activeViewId:
|
|
12317
|
+
typeof parsed.activeViewId === "string"
|
|
12318
|
+
? parsed.activeViewId
|
|
12319
|
+
: undefined,
|
|
12320
|
+
views: parsed.views
|
|
12321
|
+
.filter((view) => typeof view?.id === "string")
|
|
12322
|
+
.map((view) => ({
|
|
12323
|
+
id: view.id,
|
|
12324
|
+
sorts: Array.isArray(view.sorts)
|
|
12325
|
+
? view.sorts.filter(isDatabaseSort)
|
|
12326
|
+
: [],
|
|
12327
|
+
filters: Array.isArray(view.filters)
|
|
12328
|
+
? view.filters.filter(isDatabaseFilter)
|
|
12329
|
+
: [],
|
|
12330
|
+
filterMode: normalizeClientDatabaseFilterMode(view.filterMode),
|
|
12331
|
+
})),
|
|
12332
|
+
};
|
|
12333
|
+
} catch {
|
|
12334
|
+
return null;
|
|
12335
|
+
}
|
|
12336
|
+
}
|
|
12337
|
+
|
|
12338
|
+
function personalDatabaseViewOverridesFromConfig(
|
|
12339
|
+
viewConfig: ContentDatabaseViewConfig,
|
|
12340
|
+
): PersonalDatabaseViewOverrides {
|
|
12341
|
+
const normalized = normalizeClientDatabaseViewConfig(viewConfig);
|
|
12342
|
+
return {
|
|
12343
|
+
version: PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION,
|
|
12344
|
+
activeViewId: normalized.activeViewId,
|
|
12345
|
+
views: normalized.views.map((view) => ({
|
|
12346
|
+
id: view.id,
|
|
12347
|
+
sorts: view.sorts,
|
|
12348
|
+
filters: view.filters,
|
|
12349
|
+
filterMode: view.filterMode ?? "and",
|
|
12350
|
+
})),
|
|
12351
|
+
};
|
|
12352
|
+
}
|
|
12353
|
+
|
|
12354
|
+
export function applyPersonalDatabaseViewOverrides(
|
|
12355
|
+
savedViewConfig: ContentDatabaseViewConfig,
|
|
12356
|
+
overrides: PersonalDatabaseViewOverrides | null,
|
|
12357
|
+
) {
|
|
12358
|
+
const saved = normalizeClientDatabaseViewConfig(savedViewConfig);
|
|
12359
|
+
if (!overrides) return saved;
|
|
12360
|
+
|
|
12361
|
+
const overridesByViewId = new Map(
|
|
12362
|
+
overrides.views.map((view) => [view.id, view]),
|
|
12363
|
+
);
|
|
12364
|
+
return normalizeClientDatabaseViewConfig({
|
|
12365
|
+
...saved,
|
|
12366
|
+
activeViewId: saved.views.some((view) => view.id === overrides.activeViewId)
|
|
12367
|
+
? overrides.activeViewId
|
|
12368
|
+
: saved.activeViewId,
|
|
12369
|
+
views: saved.views.map((view) => {
|
|
12370
|
+
const override = overridesByViewId.get(view.id);
|
|
12371
|
+
if (!override) return view;
|
|
12372
|
+
return {
|
|
12373
|
+
...view,
|
|
12374
|
+
sorts: override.sorts,
|
|
12375
|
+
filters: override.filters,
|
|
12376
|
+
filterMode: override.filterMode,
|
|
12377
|
+
};
|
|
12378
|
+
}),
|
|
12379
|
+
});
|
|
12380
|
+
}
|
|
12381
|
+
|
|
12382
|
+
export function databaseViewConfigWithSavedQueryState(
|
|
12383
|
+
currentViewConfig: ContentDatabaseViewConfig,
|
|
12384
|
+
savedViewConfig: ContentDatabaseViewConfig,
|
|
12385
|
+
) {
|
|
12386
|
+
const current = normalizeClientDatabaseViewConfig(currentViewConfig);
|
|
12387
|
+
const saved = normalizeClientDatabaseViewConfig(savedViewConfig);
|
|
12388
|
+
const savedViewsById = new Map(saved.views.map((view) => [view.id, view]));
|
|
12389
|
+
|
|
12390
|
+
return normalizeClientDatabaseViewConfig({
|
|
12391
|
+
...current,
|
|
12392
|
+
activeViewId: saved.activeViewId,
|
|
12393
|
+
views: current.views.map((view) => {
|
|
12394
|
+
const savedView = savedViewsById.get(view.id);
|
|
12395
|
+
if (!savedView) return view;
|
|
12396
|
+
return {
|
|
12397
|
+
...view,
|
|
12398
|
+
sorts: savedView.sorts,
|
|
12399
|
+
filters: savedView.filters,
|
|
12400
|
+
filterMode: savedView.filterMode,
|
|
12401
|
+
};
|
|
12402
|
+
}),
|
|
12403
|
+
});
|
|
12404
|
+
}
|
|
12405
|
+
|
|
12406
|
+
export function databaseViewConfigWithClearedActiveFilters(
|
|
12407
|
+
viewConfig: ContentDatabaseViewConfig,
|
|
12408
|
+
) {
|
|
12409
|
+
return updateActiveDatabaseView(
|
|
12410
|
+
normalizeClientDatabaseViewConfig(viewConfig),
|
|
12411
|
+
(view) => ({ ...view, filters: [], filterMode: "and" }),
|
|
12412
|
+
);
|
|
12413
|
+
}
|
|
12414
|
+
|
|
12415
|
+
export function databaseViewHasPersonalQueryChanges(
|
|
12416
|
+
currentViewConfig: ContentDatabaseViewConfig,
|
|
12417
|
+
savedViewConfig: ContentDatabaseViewConfig,
|
|
12418
|
+
) {
|
|
12419
|
+
return (
|
|
12420
|
+
databaseViewQueryStateKey(currentViewConfig) !==
|
|
12421
|
+
databaseViewQueryStateKey(savedViewConfig)
|
|
12422
|
+
);
|
|
12423
|
+
}
|
|
12424
|
+
|
|
12425
|
+
function databaseViewQueryStateKey(viewConfig: ContentDatabaseViewConfig) {
|
|
12426
|
+
const normalized = normalizeClientDatabaseViewConfig(viewConfig);
|
|
12427
|
+
return JSON.stringify({
|
|
12428
|
+
activeViewId: normalized.activeViewId,
|
|
12429
|
+
views: normalized.views.map((view) => ({
|
|
12430
|
+
id: view.id,
|
|
12431
|
+
sorts: view.sorts,
|
|
12432
|
+
filters: view.filters,
|
|
12433
|
+
filterMode: view.filterMode ?? "and",
|
|
12434
|
+
})),
|
|
12435
|
+
});
|
|
12436
|
+
}
|
|
12437
|
+
|
|
10756
12438
|
function isTablePropertyVisible(
|
|
10757
12439
|
property: DocumentProperty,
|
|
10758
12440
|
items: ContentDatabaseItem[],
|
|
@@ -10770,12 +12452,24 @@ function isTablePropertyVisible(
|
|
|
10770
12452
|
});
|
|
10771
12453
|
}
|
|
10772
12454
|
|
|
10773
|
-
function databaseTableCellDisplayValue(
|
|
12455
|
+
function databaseTableCellDisplayValue(
|
|
12456
|
+
property: DocumentProperty,
|
|
12457
|
+
item?: Pick<ContentDatabaseItem, "bodyHydration" | "document">,
|
|
12458
|
+
) {
|
|
10774
12459
|
// Blocks columns show a word count, never the dumped body content.
|
|
10775
12460
|
if (property.definition.type === "blocks") {
|
|
10776
12461
|
const content = typeof property.value === "string" ? property.value : "";
|
|
10777
12462
|
const words = countWords(content);
|
|
10778
|
-
if (words === 0)
|
|
12463
|
+
if (words === 0) {
|
|
12464
|
+
if (item && databaseItemBodyHydrationIsPending(item)) {
|
|
12465
|
+
return (
|
|
12466
|
+
<span className="text-muted-foreground">
|
|
12467
|
+
{dbText("builderBodySyncing")}
|
|
12468
|
+
</span>
|
|
12469
|
+
);
|
|
12470
|
+
}
|
|
12471
|
+
return <span aria-hidden="true"> </span>;
|
|
12472
|
+
}
|
|
10779
12473
|
return (
|
|
10780
12474
|
<span className="text-muted-foreground">{formatWordCount(content)}</span>
|
|
10781
12475
|
);
|
|
@@ -13355,13 +15049,12 @@ export function applyDatabaseView(
|
|
|
13355
15049
|
const activeFilters = filters.filter(isActiveFilter);
|
|
13356
15050
|
const filtered = activeFilters.length
|
|
13357
15051
|
? searched.filter((item) =>
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
),
|
|
15052
|
+
databaseItemMatchesFilterTree(
|
|
15053
|
+
item,
|
|
15054
|
+
properties,
|
|
15055
|
+
activeFilters,
|
|
15056
|
+
filterMode,
|
|
15057
|
+
),
|
|
13365
15058
|
)
|
|
13366
15059
|
: searched;
|
|
13367
15060
|
|
|
@@ -13377,25 +15070,137 @@ export function applyDatabaseView(
|
|
|
13377
15070
|
return sort.direction === "asc" ? comparison : -comparison;
|
|
13378
15071
|
}
|
|
13379
15072
|
}
|
|
13380
|
-
return 0;
|
|
13381
|
-
});
|
|
15073
|
+
return 0;
|
|
15074
|
+
});
|
|
15075
|
+
}
|
|
15076
|
+
|
|
15077
|
+
function databaseItemMatchesFilterTree(
|
|
15078
|
+
item: ContentDatabaseItem,
|
|
15079
|
+
properties: DocumentProperty[],
|
|
15080
|
+
filters: DatabaseFilter[],
|
|
15081
|
+
filterMode: DatabaseFilterMode,
|
|
15082
|
+
) {
|
|
15083
|
+
const rootFilters = filters.filter((filter) => !filter.parentFilterGroupId);
|
|
15084
|
+
const nestedGroups = nestedDatabaseFilterGroups(filters);
|
|
15085
|
+
const matches = [
|
|
15086
|
+
...rootFilters.map((filter) =>
|
|
15087
|
+
databaseItemMatchesFilter(item, properties, filter),
|
|
15088
|
+
),
|
|
15089
|
+
...nestedGroups.map((group) =>
|
|
15090
|
+
combineDatabaseFilterMatches(
|
|
15091
|
+
group.map((filter) =>
|
|
15092
|
+
databaseItemMatchesFilter(item, properties, filter),
|
|
15093
|
+
),
|
|
15094
|
+
filterMode,
|
|
15095
|
+
),
|
|
15096
|
+
),
|
|
15097
|
+
];
|
|
15098
|
+
|
|
15099
|
+
return combineDatabaseFilterMatches(matches, filterMode);
|
|
15100
|
+
}
|
|
15101
|
+
|
|
15102
|
+
function nestedDatabaseFilterGroups(filters: DatabaseFilter[]) {
|
|
15103
|
+
const groups = new Map<string, DatabaseFilter[]>();
|
|
15104
|
+
for (const filter of filters) {
|
|
15105
|
+
if (!filter.parentFilterGroupId || !filter.filterGroupId) continue;
|
|
15106
|
+
groups.set(filter.filterGroupId, [
|
|
15107
|
+
...(groups.get(filter.filterGroupId) ?? []),
|
|
15108
|
+
filter,
|
|
15109
|
+
]);
|
|
15110
|
+
}
|
|
15111
|
+
return [...groups.values()].filter((group) => group.length > 0);
|
|
15112
|
+
}
|
|
15113
|
+
|
|
15114
|
+
function combineDatabaseFilterMatches(
|
|
15115
|
+
matches: boolean[],
|
|
15116
|
+
filterMode: DatabaseFilterMode,
|
|
15117
|
+
) {
|
|
15118
|
+
if (matches.length === 0) return true;
|
|
15119
|
+
return filterMode === "or"
|
|
15120
|
+
? matches.some((matched) => matched)
|
|
15121
|
+
: matches.every((matched) => matched);
|
|
15122
|
+
}
|
|
15123
|
+
|
|
15124
|
+
function defaultDatabaseSort(): DatabaseSort {
|
|
15125
|
+
return {
|
|
15126
|
+
key: "name",
|
|
15127
|
+
label: "Name",
|
|
15128
|
+
direction: "asc",
|
|
15129
|
+
};
|
|
15130
|
+
}
|
|
15131
|
+
|
|
15132
|
+
function defaultDatabaseFilter(): DatabaseFilter {
|
|
15133
|
+
return {
|
|
15134
|
+
key: "name",
|
|
15135
|
+
label: "Name",
|
|
15136
|
+
operator: "contains",
|
|
15137
|
+
value: "",
|
|
15138
|
+
};
|
|
15139
|
+
}
|
|
15140
|
+
|
|
15141
|
+
function createDatabaseFilterForField(
|
|
15142
|
+
key: ColumnKey,
|
|
15143
|
+
label: string,
|
|
15144
|
+
properties: DocumentProperty[],
|
|
15145
|
+
advanced:
|
|
15146
|
+
| boolean
|
|
15147
|
+
| { filterGroupId: string; parentFilterGroupId?: string } = false,
|
|
15148
|
+
): DatabaseFilter {
|
|
15149
|
+
const group =
|
|
15150
|
+
typeof advanced === "object"
|
|
15151
|
+
? advanced
|
|
15152
|
+
: advanced
|
|
15153
|
+
? { filterGroupId: DATABASE_ADVANCED_FILTER_GROUP_ID }
|
|
15154
|
+
: null;
|
|
15155
|
+
return {
|
|
15156
|
+
key,
|
|
15157
|
+
label,
|
|
15158
|
+
operator: defaultFilterOperatorForKey(key, properties),
|
|
15159
|
+
value: "",
|
|
15160
|
+
...(group ? { filterGroupId: group.filterGroupId } : {}),
|
|
15161
|
+
...(group?.parentFilterGroupId
|
|
15162
|
+
? { parentFilterGroupId: group.parentFilterGroupId }
|
|
15163
|
+
: {}),
|
|
15164
|
+
};
|
|
15165
|
+
}
|
|
15166
|
+
|
|
15167
|
+
function isAdvancedDatabaseFilter(filter: DatabaseFilter) {
|
|
15168
|
+
return (
|
|
15169
|
+
filter.filterGroupId === DATABASE_ADVANCED_FILTER_GROUP_ID ||
|
|
15170
|
+
filter.parentFilterGroupId === DATABASE_ADVANCED_FILTER_GROUP_ID
|
|
15171
|
+
);
|
|
15172
|
+
}
|
|
15173
|
+
|
|
15174
|
+
function advancedNestedFilterGroups(
|
|
15175
|
+
entries: DatabaseFilterEntry[],
|
|
15176
|
+
): DatabaseNestedFilterGroupEntry[] {
|
|
15177
|
+
const groups = new Map<string, DatabaseFilterEntry[]>();
|
|
15178
|
+
for (const entry of entries) {
|
|
15179
|
+
if (
|
|
15180
|
+
entry.filter.parentFilterGroupId !== DATABASE_ADVANCED_FILTER_GROUP_ID ||
|
|
15181
|
+
!entry.filter.filterGroupId
|
|
15182
|
+
) {
|
|
15183
|
+
continue;
|
|
15184
|
+
}
|
|
15185
|
+
groups.set(entry.filter.filterGroupId, [
|
|
15186
|
+
...(groups.get(entry.filter.filterGroupId) ?? []),
|
|
15187
|
+
entry,
|
|
15188
|
+
]);
|
|
15189
|
+
}
|
|
15190
|
+
return [...groups.entries()].map(([id, groupEntries]) => ({
|
|
15191
|
+
id,
|
|
15192
|
+
entries: groupEntries,
|
|
15193
|
+
}));
|
|
13382
15194
|
}
|
|
13383
15195
|
|
|
13384
|
-
function
|
|
13385
|
-
return {
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
direction: "asc",
|
|
13389
|
-
};
|
|
15196
|
+
function createAdvancedNestedFilterGroupId() {
|
|
15197
|
+
return `advanced-${Date.now().toString(36)}-${Math.random()
|
|
15198
|
+
.toString(36)
|
|
15199
|
+
.slice(2, 8)}`;
|
|
13390
15200
|
}
|
|
13391
15201
|
|
|
13392
|
-
function
|
|
13393
|
-
return {
|
|
13394
|
-
key: "name",
|
|
13395
|
-
label: "Name",
|
|
13396
|
-
operator: "contains",
|
|
13397
|
-
value: "",
|
|
13398
|
-
};
|
|
15202
|
+
function advancedFilterGroupLabel(ruleCount: number) {
|
|
15203
|
+
return `${ruleCount} rule${ruleCount === 1 ? "" : "s"}`;
|
|
13399
15204
|
}
|
|
13400
15205
|
|
|
13401
15206
|
export function upsertDatabaseSort(
|
|
@@ -13553,7 +15358,7 @@ function isActiveFilter(
|
|
|
13553
15358
|
): filter is DatabaseFilter {
|
|
13554
15359
|
if (!filter) return false;
|
|
13555
15360
|
if (filterOperatorNeedsValue(filter.operator)) {
|
|
13556
|
-
return filter.value
|
|
15361
|
+
return databaseFilterSelectedValues(filter.value).length > 0;
|
|
13557
15362
|
}
|
|
13558
15363
|
return true;
|
|
13559
15364
|
}
|
|
@@ -13598,21 +15403,42 @@ function databaseFilterDefaultValueForNewItem(
|
|
|
13598
15403
|
if (filter.operator === "is_unchecked") {
|
|
13599
15404
|
return property.definition.type === "checkbox" ? false : undefined;
|
|
13600
15405
|
}
|
|
13601
|
-
if (
|
|
15406
|
+
if (property.definition.type === "multi_select") {
|
|
15407
|
+
if (filter.operator !== "equals" && filter.operator !== "contains") {
|
|
15408
|
+
return undefined;
|
|
15409
|
+
}
|
|
15410
|
+
const values = databaseFilterSelectedValues(filter.value);
|
|
15411
|
+
if (values.length === 0) return undefined;
|
|
15412
|
+
return values.map(
|
|
15413
|
+
(value) =>
|
|
15414
|
+
databasePropertyOptionIdForFilterValue(property, value) ?? value,
|
|
15415
|
+
);
|
|
15416
|
+
}
|
|
15417
|
+
if (property.definition.type === "person" && filter.operator === "contains") {
|
|
15418
|
+
const values = databaseFilterSelectedValues(filter.value);
|
|
15419
|
+
return values.length > 0 ? values : undefined;
|
|
15420
|
+
}
|
|
13602
15421
|
|
|
13603
15422
|
const value = filter.value.trim();
|
|
13604
15423
|
if (!value) return undefined;
|
|
13605
15424
|
|
|
13606
|
-
const optionValue = databasePropertyOptionIdForFilterValue(property, value);
|
|
13607
|
-
if (property.definition.type === "multi_select") {
|
|
13608
|
-
return [optionValue ?? value];
|
|
13609
|
-
}
|
|
13610
15425
|
if (
|
|
13611
15426
|
property.definition.type === "select" ||
|
|
13612
15427
|
property.definition.type === "status"
|
|
13613
15428
|
) {
|
|
13614
|
-
|
|
15429
|
+
if (filter.operator !== "equals" && filter.operator !== "contains") {
|
|
15430
|
+
return undefined;
|
|
15431
|
+
}
|
|
15432
|
+
const values = databaseFilterSelectedValues(filter.value);
|
|
15433
|
+
const firstValue = values[0]?.trim() ?? "";
|
|
15434
|
+
if (!firstValue) return undefined;
|
|
15435
|
+
return (
|
|
15436
|
+
databasePropertyOptionIdForFilterValue(property, firstValue) ?? firstValue
|
|
15437
|
+
);
|
|
13615
15438
|
}
|
|
15439
|
+
|
|
15440
|
+
if (filter.operator !== "equals") return undefined;
|
|
15441
|
+
|
|
13616
15442
|
if (property.definition.type === "date") {
|
|
13617
15443
|
return /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
13618
15444
|
? { start: value, includeTime: false }
|
|
@@ -13657,10 +15483,20 @@ function databaseItemMatchesFilter(
|
|
|
13657
15483
|
: current < target;
|
|
13658
15484
|
}
|
|
13659
15485
|
|
|
13660
|
-
if (
|
|
15486
|
+
if (
|
|
15487
|
+
filter.operator === "before" ||
|
|
15488
|
+
filter.operator === "after" ||
|
|
15489
|
+
filter.operator === "between"
|
|
15490
|
+
) {
|
|
13661
15491
|
const current = propertyDateValue(property);
|
|
15492
|
+
if (!Number.isFinite(current)) return false;
|
|
15493
|
+
if (filter.operator === "between") {
|
|
15494
|
+
const range = databaseFilterDateRangeValue(filter.value);
|
|
15495
|
+
if (!range) return false;
|
|
15496
|
+
return current >= range[0] && current <= range[1];
|
|
15497
|
+
}
|
|
13662
15498
|
const target = new Date(filter.value.trim()).getTime();
|
|
13663
|
-
if (!Number.isFinite(
|
|
15499
|
+
if (!Number.isFinite(target)) return false;
|
|
13664
15500
|
return filter.operator === "before" ? current < target : current > target;
|
|
13665
15501
|
}
|
|
13666
15502
|
|
|
@@ -13669,8 +15505,31 @@ function databaseItemMatchesFilter(
|
|
|
13669
15505
|
properties,
|
|
13670
15506
|
filter.key,
|
|
13671
15507
|
).map((candidate) => candidate.trim().toLowerCase());
|
|
15508
|
+
const selectedFilterValues = databaseFilterSelectedValues(filter.value).map(
|
|
15509
|
+
(candidate) => candidate.trim().toLowerCase(),
|
|
15510
|
+
);
|
|
13672
15511
|
const normalizedValue = value.trim().toLowerCase();
|
|
13673
|
-
const normalizedFilter =
|
|
15512
|
+
const normalizedFilter = selectedFilterValues[0] ?? "";
|
|
15513
|
+
const usesDiscreteValues =
|
|
15514
|
+
property?.definition.type === "select" ||
|
|
15515
|
+
property?.definition.type === "status" ||
|
|
15516
|
+
property?.definition.type === "multi_select" ||
|
|
15517
|
+
property?.definition.type === "person";
|
|
15518
|
+
|
|
15519
|
+
if (
|
|
15520
|
+
usesDiscreteValues &&
|
|
15521
|
+
(filter.operator === "equals" || filter.operator === "contains")
|
|
15522
|
+
) {
|
|
15523
|
+
return selectedFilterValues.some((filterValue) =>
|
|
15524
|
+
candidateValues.includes(filterValue),
|
|
15525
|
+
);
|
|
15526
|
+
}
|
|
15527
|
+
if (usesDiscreteValues && filter.operator === "does_not_equal") {
|
|
15528
|
+
return selectedFilterValues.every(
|
|
15529
|
+
(filterValue) => !candidateValues.includes(filterValue),
|
|
15530
|
+
);
|
|
15531
|
+
}
|
|
15532
|
+
|
|
13674
15533
|
if (filter.operator === "equals") {
|
|
13675
15534
|
return candidateValues.includes(normalizedFilter);
|
|
13676
15535
|
}
|
|
@@ -13844,6 +15703,10 @@ function propertyDateValue(property: DocumentProperty | null | undefined) {
|
|
|
13844
15703
|
return Number.isFinite(value) ? value : Number.NaN;
|
|
13845
15704
|
}
|
|
13846
15705
|
|
|
15706
|
+
function databaseSortDirectionLabel(direction: SortDirection) {
|
|
15707
|
+
return direction === "asc" ? "Ascending" : "Descending";
|
|
15708
|
+
}
|
|
15709
|
+
|
|
13847
15710
|
function SortMenu({
|
|
13848
15711
|
properties,
|
|
13849
15712
|
sorts,
|
|
@@ -13879,6 +15742,10 @@ function SortMenu({
|
|
|
13879
15742
|
onSortsChange([...sorts, defaultDatabaseSort()]);
|
|
13880
15743
|
}
|
|
13881
15744
|
|
|
15745
|
+
function addSortForField(key: "name" | string, label: string) {
|
|
15746
|
+
onSortsChange([...sorts, { key, label, direction: "asc" }]);
|
|
15747
|
+
}
|
|
15748
|
+
|
|
13882
15749
|
function removeSort(index: number) {
|
|
13883
15750
|
onSortsChange(sorts.filter((_, sortIndex) => sortIndex !== index));
|
|
13884
15751
|
}
|
|
@@ -13911,363 +15778,210 @@ function SortMenu({
|
|
|
13911
15778
|
) : null}
|
|
13912
15779
|
</Button>
|
|
13913
15780
|
</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")}
|
|
15781
|
+
<DropdownMenuContent
|
|
15782
|
+
align="end"
|
|
15783
|
+
className={cn("w-[340px]", sorts.length === 0 && "w-64 p-0")}
|
|
15784
|
+
>
|
|
15785
|
+
{sorts.length === 0 ? (
|
|
15786
|
+
<div onKeyDown={(event) => event.stopPropagation()}>
|
|
15787
|
+
<DatabasePropertyPickerMenuItems
|
|
15788
|
+
properties={properties}
|
|
15789
|
+
includeName
|
|
15790
|
+
placeholder={dbText("sortBy")}
|
|
15791
|
+
closeOnSelect
|
|
15792
|
+
onSelect={addSortForField}
|
|
15793
|
+
/>
|
|
14117
15794
|
</div>
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14122
|
-
|
|
14123
|
-
</
|
|
14124
|
-
<
|
|
14125
|
-
{
|
|
14126
|
-
<
|
|
14127
|
-
key={
|
|
14128
|
-
|
|
14129
|
-
event.preventDefault();
|
|
14130
|
-
onFilterModeChange(mode);
|
|
14131
|
-
}}
|
|
15795
|
+
) : null}
|
|
15796
|
+
{sorts.length > 0 ? (
|
|
15797
|
+
<div className="grid gap-2 p-2">
|
|
15798
|
+
<div className="text-xs font-medium text-muted-foreground">
|
|
15799
|
+
{dbText("sortRowsBy")}
|
|
15800
|
+
</div>
|
|
15801
|
+
<div className="grid gap-2">
|
|
15802
|
+
{displayedSorts.map((sort, index) => (
|
|
15803
|
+
<div
|
|
15804
|
+
key={`${index}-${sort.key}`}
|
|
15805
|
+
className="grid grid-cols-[minmax(0,1fr)_auto_auto_auto] gap-1 rounded border border-border/70 bg-background p-1.5"
|
|
14132
15806
|
>
|
|
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
15807
|
<DropdownMenuSub>
|
|
14151
15808
|
<DropdownMenuSubTrigger className="min-w-0">
|
|
14152
|
-
<
|
|
14153
|
-
filter={currentFilter}
|
|
14154
|
-
properties={properties}
|
|
14155
|
-
/>
|
|
15809
|
+
<SortFieldIcon sort={sort} properties={properties} />
|
|
14156
15810
|
<span className="min-w-0 flex-1 truncate">
|
|
14157
|
-
{
|
|
15811
|
+
{sort.label}
|
|
14158
15812
|
</span>
|
|
14159
15813
|
</DropdownMenuSubTrigger>
|
|
14160
15814
|
<DatabasePropertyPickerSubContent
|
|
14161
15815
|
properties={properties}
|
|
14162
|
-
selectedKey={
|
|
15816
|
+
selectedKey={sort.key}
|
|
14163
15817
|
includeName
|
|
14164
|
-
onSelect={(key, label) =>
|
|
15818
|
+
onSelect={(key, label) => selectSort(index, key, label)}
|
|
14165
15819
|
/>
|
|
14166
15820
|
</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
|
-
|
|
15821
|
+
<button
|
|
15822
|
+
type="button"
|
|
15823
|
+
className="flex h-8 items-center rounded px-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
15824
|
+
onClick={() => toggleDirection(index)}
|
|
15825
|
+
>
|
|
15826
|
+
{sort.direction === "asc" ? "Asc" : "Desc"}
|
|
15827
|
+
</button>
|
|
14198
15828
|
<div className="flex items-center">
|
|
14199
15829
|
<button
|
|
14200
15830
|
type="button"
|
|
14201
|
-
aria-label={`Move
|
|
15831
|
+
aria-label={`Move sort ${index + 1} earlier`}
|
|
14202
15832
|
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={() =>
|
|
15833
|
+
disabled={sorts.length <= 1 || index === 0}
|
|
15834
|
+
onClick={() => moveSort(index, "up")}
|
|
14205
15835
|
>
|
|
14206
15836
|
<IconArrowUp className="size-3.5" />
|
|
14207
15837
|
</button>
|
|
14208
15838
|
<button
|
|
14209
15839
|
type="button"
|
|
14210
|
-
aria-label={`Move
|
|
15840
|
+
aria-label={`Move sort ${index + 1} later`}
|
|
14211
15841
|
className="flex size-8 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40"
|
|
14212
15842
|
disabled={
|
|
14213
|
-
|
|
14214
|
-
index >= displayedFilters.length - 1
|
|
15843
|
+
sorts.length <= 1 || index >= displayedSorts.length - 1
|
|
14215
15844
|
}
|
|
14216
|
-
onClick={() =>
|
|
15845
|
+
onClick={() => moveSort(index, "down")}
|
|
14217
15846
|
>
|
|
14218
15847
|
<IconArrowDown className="size-3.5" />
|
|
14219
15848
|
</button>
|
|
14220
15849
|
</div>
|
|
14221
|
-
|
|
14222
15850
|
<button
|
|
14223
15851
|
type="button"
|
|
14224
|
-
aria-label={`Remove
|
|
15852
|
+
aria-label={`Remove sort ${index + 1}`}
|
|
14225
15853
|
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={() =>
|
|
15854
|
+
disabled={sorts.length === 0}
|
|
15855
|
+
onClick={() => removeSort(index)}
|
|
14228
15856
|
>
|
|
14229
15857
|
<IconX className="size-4" />
|
|
14230
15858
|
</button>
|
|
14231
15859
|
</div>
|
|
15860
|
+
))}
|
|
15861
|
+
</div>
|
|
15862
|
+
|
|
15863
|
+
<div className="flex justify-between gap-2 border-t pt-2">
|
|
15864
|
+
<Button
|
|
15865
|
+
type="button"
|
|
15866
|
+
size="sm"
|
|
15867
|
+
variant="ghost"
|
|
15868
|
+
className="h-8 px-2 text-xs"
|
|
15869
|
+
onClick={addSort}
|
|
15870
|
+
>
|
|
15871
|
+
<IconPlus className="mr-1 size-3.5" />
|
|
15872
|
+
{dbText("addSort")}
|
|
15873
|
+
</Button>
|
|
15874
|
+
<Button
|
|
15875
|
+
type="button"
|
|
15876
|
+
size="sm"
|
|
15877
|
+
variant="ghost"
|
|
15878
|
+
className="h-8 px-2 text-xs"
|
|
15879
|
+
disabled={sorts.length === 0}
|
|
15880
|
+
onClick={() => onSortsChange([])}
|
|
15881
|
+
>
|
|
15882
|
+
{dbText("clearSorts")}
|
|
15883
|
+
</Button>
|
|
15884
|
+
</div>
|
|
15885
|
+
</div>
|
|
15886
|
+
) : null}
|
|
15887
|
+
</DropdownMenuContent>
|
|
15888
|
+
</DropdownMenu>
|
|
15889
|
+
);
|
|
15890
|
+
}
|
|
15891
|
+
|
|
15892
|
+
function SortFieldIcon({
|
|
15893
|
+
sort,
|
|
15894
|
+
properties,
|
|
15895
|
+
}: {
|
|
15896
|
+
sort: DatabaseSort;
|
|
15897
|
+
properties: DocumentProperty[];
|
|
15898
|
+
}) {
|
|
15899
|
+
if (sort.key === "name") {
|
|
15900
|
+
return <IconFileText className="mr-2 size-4 text-muted-foreground" />;
|
|
15901
|
+
}
|
|
15902
|
+
const property = properties.find(
|
|
15903
|
+
(candidate) => candidate.definition.id === sort.key,
|
|
15904
|
+
);
|
|
15905
|
+
const Icon = property ? TYPE_ICONS[property.definition.type] : IconFileText;
|
|
15906
|
+
return <Icon className="mr-2 size-4 text-muted-foreground" />;
|
|
15907
|
+
}
|
|
14232
15908
|
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
15909
|
+
function FilterMenu({
|
|
15910
|
+
filters,
|
|
15911
|
+
properties,
|
|
15912
|
+
inlineOpen,
|
|
15913
|
+
open,
|
|
15914
|
+
onOpenChange,
|
|
15915
|
+
onAddFilter,
|
|
15916
|
+
onAddAdvancedFilter,
|
|
15917
|
+
}: {
|
|
15918
|
+
filters: DatabaseFilter[];
|
|
15919
|
+
properties: DocumentProperty[];
|
|
15920
|
+
inlineOpen: boolean;
|
|
15921
|
+
open: boolean;
|
|
15922
|
+
onOpenChange: (open: boolean) => void;
|
|
15923
|
+
onAddFilter: (key: "name" | string, label: string) => void;
|
|
15924
|
+
onAddAdvancedFilter: (key: "name" | string, label: string) => void;
|
|
15925
|
+
}) {
|
|
15926
|
+
const activeFilters = filters.filter(isActiveFilter);
|
|
15927
|
+
const active = activeFilters.length > 0 || inlineOpen;
|
|
15928
|
+
const excludedFilterKeys = useMemo(
|
|
15929
|
+
() => new Set(activeFilters.map((filter) => filter.key)),
|
|
15930
|
+
[activeFilters],
|
|
15931
|
+
);
|
|
15932
|
+
const advancedFilterCandidate =
|
|
15933
|
+
databasePropertyPickerItems(properties, "", {
|
|
15934
|
+
includeName: false,
|
|
15935
|
+
excludeKeys: excludedFilterKeys,
|
|
15936
|
+
})[0] ??
|
|
15937
|
+
databasePropertyPickerItems(properties, "", {
|
|
15938
|
+
includeName: true,
|
|
15939
|
+
excludeKeys: excludedFilterKeys,
|
|
15940
|
+
})[0];
|
|
14245
15941
|
|
|
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
|
-
|
|
15942
|
+
return (
|
|
15943
|
+
<DropdownMenu open={open} onOpenChange={onOpenChange}>
|
|
15944
|
+
<DropdownMenuTrigger asChild>
|
|
15945
|
+
<Button
|
|
15946
|
+
type="button"
|
|
15947
|
+
variant="ghost"
|
|
15948
|
+
size="sm"
|
|
15949
|
+
aria-label={
|
|
15950
|
+
activeFilters.length > 0
|
|
15951
|
+
? `${activeFilters.length} active filters`
|
|
15952
|
+
: "Filter"
|
|
15953
|
+
}
|
|
15954
|
+
title="Filter"
|
|
15955
|
+
className={databaseToolbarIconButtonClass(active || open)}
|
|
15956
|
+
>
|
|
15957
|
+
<IconFilter className="size-3.5" />
|
|
15958
|
+
</Button>
|
|
15959
|
+
</DropdownMenuTrigger>
|
|
15960
|
+
<DropdownMenuContent align="end" className="w-64">
|
|
15961
|
+
<div onKeyDown={(event) => event.stopPropagation()}>
|
|
15962
|
+
<DatabasePropertyPickerMenuItems
|
|
15963
|
+
properties={properties}
|
|
15964
|
+
includeName
|
|
15965
|
+
excludeKeys={excludedFilterKeys}
|
|
15966
|
+
placeholder={dbText("filterBy")}
|
|
15967
|
+
closeOnSelect
|
|
15968
|
+
onSelect={onAddFilter}
|
|
15969
|
+
/>
|
|
15970
|
+
<DropdownMenuSeparator />
|
|
15971
|
+
<DropdownMenuItem
|
|
15972
|
+
disabled={!advancedFilterCandidate}
|
|
15973
|
+
onSelect={(event) => {
|
|
15974
|
+
event.preventDefault();
|
|
15975
|
+
if (!advancedFilterCandidate) return;
|
|
15976
|
+
onAddAdvancedFilter(
|
|
15977
|
+
advancedFilterCandidate.key,
|
|
15978
|
+
advancedFilterCandidate.label,
|
|
15979
|
+
);
|
|
15980
|
+
}}
|
|
15981
|
+
>
|
|
15982
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
15983
|
+
{dbText("addAdvancedFilter")}
|
|
15984
|
+
</DropdownMenuItem>
|
|
14271
15985
|
</div>
|
|
14272
15986
|
</DropdownMenuContent>
|
|
14273
15987
|
</DropdownMenu>
|
|
@@ -14277,18 +15991,29 @@ function FilterMenu({
|
|
|
14277
15991
|
function DatabaseFilterValueControl({
|
|
14278
15992
|
documentId,
|
|
14279
15993
|
filter,
|
|
15994
|
+
items,
|
|
14280
15995
|
properties,
|
|
14281
15996
|
autoFocus,
|
|
15997
|
+
hideOptionsUntilQuery = false,
|
|
14282
15998
|
onValueChange,
|
|
14283
15999
|
}: {
|
|
14284
16000
|
documentId: string;
|
|
14285
16001
|
filter: DatabaseFilter;
|
|
16002
|
+
items: ContentDatabaseItem[];
|
|
14286
16003
|
properties: DocumentProperty[];
|
|
14287
16004
|
autoFocus?: boolean;
|
|
16005
|
+
hideOptionsUntilQuery?: boolean;
|
|
14288
16006
|
onValueChange: (value: string) => void;
|
|
14289
16007
|
}) {
|
|
14290
16008
|
const configureProperty = useConfigureDocumentProperty(documentId);
|
|
14291
|
-
const
|
|
16009
|
+
const { session } = useSession();
|
|
16010
|
+
const currentUserEmail = session?.email?.trim() ?? "";
|
|
16011
|
+
const options = databaseFilterOptionChoices(
|
|
16012
|
+
filter.key,
|
|
16013
|
+
properties,
|
|
16014
|
+
items,
|
|
16015
|
+
currentUserEmail,
|
|
16016
|
+
);
|
|
14292
16017
|
const type = filterPropertyTypeForKey(filter.key, properties);
|
|
14293
16018
|
const [optionQuery, setOptionQuery] = useState("");
|
|
14294
16019
|
const filteredOptions = filterPropertyOptions(options, optionQuery);
|
|
@@ -14297,7 +16022,57 @@ function DatabaseFilterValueControl({
|
|
|
14297
16022
|
properties,
|
|
14298
16023
|
);
|
|
14299
16024
|
const canCreateOption =
|
|
14300
|
-
!!optionProperty &&
|
|
16025
|
+
!!optionProperty &&
|
|
16026
|
+
optionProperty.definition.type !== "person" &&
|
|
16027
|
+
canCreatePropertyOption(options, optionQuery);
|
|
16028
|
+
const usesOptionFilterValues =
|
|
16029
|
+
optionProperty?.definition.type === "select" ||
|
|
16030
|
+
optionProperty?.definition.type === "status" ||
|
|
16031
|
+
optionProperty?.definition.type === "multi_select" ||
|
|
16032
|
+
optionProperty?.definition.type === "person";
|
|
16033
|
+
const allowsMultipleValues =
|
|
16034
|
+
usesOptionFilterValues &&
|
|
16035
|
+
(filter.operator === "contains" || filter.operator === "does_not_equal");
|
|
16036
|
+
const selectedValues = databaseFilterSelectedValues(filter.value);
|
|
16037
|
+
const selectedOptions = selectedValues
|
|
16038
|
+
.map((value) =>
|
|
16039
|
+
options.find(
|
|
16040
|
+
(option) =>
|
|
16041
|
+
databaseFilterChoiceValue(option) === value ||
|
|
16042
|
+
option.id === value ||
|
|
16043
|
+
option.name.trim().toLowerCase() === value.trim().toLowerCase(),
|
|
16044
|
+
),
|
|
16045
|
+
)
|
|
16046
|
+
.filter((option): option is DatabaseFilterChoice => !!option);
|
|
16047
|
+
const showClearValue = selectedValues.length > 0;
|
|
16048
|
+
const showOptionChoices =
|
|
16049
|
+
!hideOptionsUntilQuery ||
|
|
16050
|
+
optionQuery.trim().length > 0 ||
|
|
16051
|
+
selectedValues.length > 0;
|
|
16052
|
+
const showCompactOptionValueInput =
|
|
16053
|
+
hideOptionsUntilQuery && selectedOptions.length === 0;
|
|
16054
|
+
|
|
16055
|
+
function setSelectedValues(values: string[]) {
|
|
16056
|
+
onValueChange(databaseFilterEncodeSelectedValues(values));
|
|
16057
|
+
}
|
|
16058
|
+
|
|
16059
|
+
function chooseFilterOption(option: DatabaseFilterChoice) {
|
|
16060
|
+
const optionValue = databaseFilterChoiceValue(option);
|
|
16061
|
+
if (!allowsMultipleValues) {
|
|
16062
|
+
onValueChange(optionValue);
|
|
16063
|
+
return;
|
|
16064
|
+
}
|
|
16065
|
+
const selected = databaseFilterOptionIsSelected(option, selectedValues);
|
|
16066
|
+
setSelectedValues(
|
|
16067
|
+
selected
|
|
16068
|
+
? selectedValues.filter(
|
|
16069
|
+
(value) =>
|
|
16070
|
+
value !== optionValue &&
|
|
16071
|
+
value.trim().toLowerCase() !== option.name.trim().toLowerCase(),
|
|
16072
|
+
)
|
|
16073
|
+
: [...selectedValues, optionValue],
|
|
16074
|
+
);
|
|
16075
|
+
}
|
|
14301
16076
|
|
|
14302
16077
|
async function createFilterOption() {
|
|
14303
16078
|
if (!optionProperty || !canCreateOption) return;
|
|
@@ -14311,37 +16086,82 @@ function DatabaseFilterValueControl({
|
|
|
14311
16086
|
options: { options: [...options, option] },
|
|
14312
16087
|
});
|
|
14313
16088
|
setOptionQuery("");
|
|
14314
|
-
|
|
16089
|
+
if (allowsMultipleValues) {
|
|
16090
|
+
setSelectedValues([...selectedValues, option.id]);
|
|
16091
|
+
} else {
|
|
16092
|
+
onValueChange(option.id);
|
|
16093
|
+
}
|
|
16094
|
+
}
|
|
16095
|
+
|
|
16096
|
+
if (
|
|
16097
|
+
filter.operator === "between" &&
|
|
16098
|
+
(type === "date" || type === "created_time" || type === "last_edited_time")
|
|
16099
|
+
) {
|
|
16100
|
+
return (
|
|
16101
|
+
<DatabaseBetweenDateFilterValueControl
|
|
16102
|
+
filter={filter}
|
|
16103
|
+
onValueChange={onValueChange}
|
|
16104
|
+
/>
|
|
16105
|
+
);
|
|
14315
16106
|
}
|
|
14316
16107
|
|
|
14317
16108
|
if (optionProperty) {
|
|
14318
16109
|
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">
|
|
16110
|
+
<div className="overflow-hidden rounded-sm">
|
|
16111
|
+
<div className="px-1 pb-1">
|
|
16112
|
+
<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">
|
|
16113
|
+
{showCompactOptionValueInput ? null : (
|
|
14331
16114
|
<IconSearch className="size-3.5 shrink-0 text-muted-foreground" />
|
|
14332
|
-
|
|
14333
|
-
|
|
14334
|
-
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
|
|
16115
|
+
)}
|
|
16116
|
+
{selectedOptions.map((option) => (
|
|
16117
|
+
<button
|
|
16118
|
+
key={option.id}
|
|
16119
|
+
type="button"
|
|
16120
|
+
className={cn(
|
|
16121
|
+
"inline-flex max-w-[9rem] items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium",
|
|
16122
|
+
optionProperty?.definition.type === "person"
|
|
16123
|
+
? "bg-muted text-foreground"
|
|
16124
|
+
: OPTION_COLOR_CLASSES[option.color ?? "gray"],
|
|
16125
|
+
)}
|
|
16126
|
+
onClick={(event) => {
|
|
16127
|
+
event.preventDefault();
|
|
16128
|
+
event.stopPropagation();
|
|
16129
|
+
setSelectedValues(
|
|
16130
|
+
selectedValues.filter(
|
|
16131
|
+
(value) => value !== databaseFilterChoiceValue(option),
|
|
16132
|
+
),
|
|
16133
|
+
);
|
|
16134
|
+
}}
|
|
16135
|
+
>
|
|
16136
|
+
{optionProperty?.definition.type === "person" ? (
|
|
16137
|
+
<PersonPill value={databaseFilterChoiceValue(option)} />
|
|
16138
|
+
) : (
|
|
16139
|
+
<span className="truncate">{option.name}</span>
|
|
16140
|
+
)}
|
|
16141
|
+
<IconX className="size-3 opacity-70" />
|
|
16142
|
+
</button>
|
|
16143
|
+
))}
|
|
16144
|
+
<Input
|
|
16145
|
+
autoFocus={autoFocus}
|
|
16146
|
+
value={optionQuery}
|
|
16147
|
+
placeholder={
|
|
16148
|
+
showCompactOptionValueInput
|
|
16149
|
+
? filterValuePlaceholder(filter.key, properties)
|
|
16150
|
+
: selectedOptions.length > 0
|
|
16151
|
+
? ""
|
|
16152
|
+
: filterOptionSearchPlaceholder(filter.key, properties)
|
|
16153
|
+
}
|
|
16154
|
+
aria-label={`Search ${filter.label} filter options`}
|
|
16155
|
+
onChange={(event) => setOptionQuery(event.target.value)}
|
|
16156
|
+
className="h-7 min-w-[7rem] flex-1 border-0 bg-transparent px-0 text-xs shadow-none focus-visible:ring-0"
|
|
16157
|
+
/>
|
|
14341
16158
|
</div>
|
|
14342
|
-
|
|
14343
|
-
|
|
16159
|
+
</div>
|
|
16160
|
+
{showOptionChoices ? (
|
|
16161
|
+
<div className="max-h-60 overflow-auto py-0.5">
|
|
16162
|
+
{showClearValue ? (
|
|
14344
16163
|
<DropdownMenuItem
|
|
16164
|
+
className="text-xs"
|
|
14345
16165
|
onSelect={(event) => {
|
|
14346
16166
|
event.preventDefault();
|
|
14347
16167
|
onValueChange("");
|
|
@@ -14350,45 +16170,56 @@ function DatabaseFilterValueControl({
|
|
|
14350
16170
|
<IconX className="mr-2 size-4 text-muted-foreground" />
|
|
14351
16171
|
{dbText("clearValue")}
|
|
14352
16172
|
</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 />
|
|
16173
|
+
) : null}
|
|
16174
|
+
{filteredOptions.length === 0 && !canCreateOption ? (
|
|
16175
|
+
<div className="px-2 py-3 text-xs text-muted-foreground">
|
|
16176
|
+
{dbText("noMatchingOptions")}
|
|
16177
|
+
</div>
|
|
16178
|
+
) : null}
|
|
16179
|
+
{filteredOptions.map((option) => (
|
|
14378
16180
|
<DropdownMenuItem
|
|
14379
|
-
|
|
16181
|
+
key={option.id}
|
|
16182
|
+
className="text-xs"
|
|
14380
16183
|
onSelect={(event) => {
|
|
14381
16184
|
event.preventDefault();
|
|
14382
|
-
|
|
16185
|
+
chooseFilterOption(option);
|
|
14383
16186
|
}}
|
|
14384
16187
|
>
|
|
14385
|
-
<
|
|
14386
|
-
|
|
16188
|
+
<span className="min-w-0 flex-1 truncate">
|
|
16189
|
+
{optionProperty?.definition.type === "person" ? (
|
|
16190
|
+
option.name === "Me" ? (
|
|
16191
|
+
"Me"
|
|
16192
|
+
) : (
|
|
16193
|
+
<PersonPill value={databaseFilterChoiceValue(option)} />
|
|
16194
|
+
)
|
|
16195
|
+
) : (
|
|
16196
|
+
option.name
|
|
16197
|
+
)}
|
|
16198
|
+
</span>
|
|
16199
|
+
{databaseFilterOptionIsSelected(option, selectedValues) ? (
|
|
16200
|
+
<IconCheck className="size-4 text-muted-foreground" />
|
|
16201
|
+
) : null}
|
|
14387
16202
|
</DropdownMenuItem>
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
16203
|
+
))}
|
|
16204
|
+
{canCreateOption ? (
|
|
16205
|
+
<>
|
|
16206
|
+
<DropdownMenuSeparator />
|
|
16207
|
+
<DropdownMenuItem
|
|
16208
|
+
disabled={configureProperty.isPending}
|
|
16209
|
+
className="text-xs"
|
|
16210
|
+
onSelect={(event) => {
|
|
16211
|
+
event.preventDefault();
|
|
16212
|
+
void createFilterOption();
|
|
16213
|
+
}}
|
|
16214
|
+
>
|
|
16215
|
+
<IconPlus className="mr-2 size-4 text-muted-foreground" />
|
|
16216
|
+
Create “{optionQuery.trim()}”
|
|
16217
|
+
</DropdownMenuItem>
|
|
16218
|
+
</>
|
|
16219
|
+
) : null}
|
|
16220
|
+
</div>
|
|
16221
|
+
) : null}
|
|
16222
|
+
</div>
|
|
14392
16223
|
);
|
|
14393
16224
|
}
|
|
14394
16225
|
|
|
@@ -14400,7 +16231,7 @@ function DatabaseFilterValueControl({
|
|
|
14400
16231
|
value={filter.value}
|
|
14401
16232
|
placeholder={filterValuePlaceholder(filter.key, properties)}
|
|
14402
16233
|
onChange={(event) => onValueChange(event.target.value)}
|
|
14403
|
-
className="h-
|
|
16234
|
+
className="h-9 text-sm focus-visible:border-[#2383e2] focus-visible:ring-[#2383e2]/25"
|
|
14404
16235
|
/>
|
|
14405
16236
|
);
|
|
14406
16237
|
}
|
|
@@ -14430,12 +16261,39 @@ const FILTER_OPERATOR_LABELS: Record<FilterOperator, string> = {
|
|
|
14430
16261
|
less_than: "Less than",
|
|
14431
16262
|
before: "Before",
|
|
14432
16263
|
after: "After",
|
|
16264
|
+
between: "Between",
|
|
14433
16265
|
is_checked: "Checked",
|
|
14434
16266
|
is_unchecked: "Unchecked",
|
|
14435
16267
|
is_empty: "Is empty",
|
|
14436
16268
|
is_not_empty: "Is not empty",
|
|
14437
16269
|
};
|
|
14438
16270
|
|
|
16271
|
+
function databaseFilterOperatorLabel(
|
|
16272
|
+
operator: FilterOperator,
|
|
16273
|
+
key: string,
|
|
16274
|
+
properties: DocumentProperty[],
|
|
16275
|
+
) {
|
|
16276
|
+
const type = filterPropertyTypeForKey(key, properties);
|
|
16277
|
+
if (
|
|
16278
|
+
operator === "does_not_equal" &&
|
|
16279
|
+
(type === "select" ||
|
|
16280
|
+
type === "status" ||
|
|
16281
|
+
type === "multi_select" ||
|
|
16282
|
+
type === "person")
|
|
16283
|
+
) {
|
|
16284
|
+
return "Does not contain";
|
|
16285
|
+
}
|
|
16286
|
+
return FILTER_OPERATOR_LABELS[operator] ?? "Contains";
|
|
16287
|
+
}
|
|
16288
|
+
|
|
16289
|
+
function databaseFilterOperatorInlineLabel(
|
|
16290
|
+
operator: FilterOperator,
|
|
16291
|
+
key: string,
|
|
16292
|
+
properties: DocumentProperty[],
|
|
16293
|
+
) {
|
|
16294
|
+
return databaseFilterOperatorLabel(operator, key, properties).toLowerCase();
|
|
16295
|
+
}
|
|
16296
|
+
|
|
14439
16297
|
function filterOperatorsForKey(
|
|
14440
16298
|
key: string,
|
|
14441
16299
|
properties: DocumentProperty[],
|
|
@@ -14446,8 +16304,13 @@ function filterOperatorsForKey(
|
|
|
14446
16304
|
return ["is_checked", "is_unchecked"];
|
|
14447
16305
|
}
|
|
14448
16306
|
|
|
14449
|
-
if (
|
|
14450
|
-
|
|
16307
|
+
if (
|
|
16308
|
+
type === "select" ||
|
|
16309
|
+
type === "status" ||
|
|
16310
|
+
type === "multi_select" ||
|
|
16311
|
+
type === "person"
|
|
16312
|
+
) {
|
|
16313
|
+
return ["contains", "does_not_equal", "is_empty", "is_not_empty"];
|
|
14451
16314
|
}
|
|
14452
16315
|
|
|
14453
16316
|
if (type === "number") {
|
|
@@ -14471,6 +16334,7 @@ function filterOperatorsForKey(
|
|
|
14471
16334
|
"does_not_equal",
|
|
14472
16335
|
"before",
|
|
14473
16336
|
"after",
|
|
16337
|
+
"between",
|
|
14474
16338
|
"is_empty",
|
|
14475
16339
|
"is_not_empty",
|
|
14476
16340
|
];
|
|
@@ -14514,6 +16378,18 @@ function filterValuePlaceholder(key: string, properties: DocumentProperty[]) {
|
|
|
14514
16378
|
return "Value";
|
|
14515
16379
|
}
|
|
14516
16380
|
|
|
16381
|
+
function filterOptionSearchPlaceholder(
|
|
16382
|
+
key: string,
|
|
16383
|
+
properties: DocumentProperty[],
|
|
16384
|
+
) {
|
|
16385
|
+
const type = filterPropertyTypeForKey(key, properties);
|
|
16386
|
+
if (type === "person") return "Search for one or more people...";
|
|
16387
|
+
if (type === "select" || type === "status" || type === "multi_select") {
|
|
16388
|
+
return "Search for one or more options...";
|
|
16389
|
+
}
|
|
16390
|
+
return "Search for an option...";
|
|
16391
|
+
}
|
|
16392
|
+
|
|
14517
16393
|
function filterValueInputType(type: DocumentPropertyType) {
|
|
14518
16394
|
if (type === "number") return "number";
|
|
14519
16395
|
if (type === "date" || type === "created_time" || type === "last_edited_time")
|
|
@@ -14521,12 +16397,106 @@ function filterValueInputType(type: DocumentPropertyType) {
|
|
|
14521
16397
|
return "text";
|
|
14522
16398
|
}
|
|
14523
16399
|
|
|
16400
|
+
function DatabaseBetweenDateFilterValueControl({
|
|
16401
|
+
filter,
|
|
16402
|
+
onValueChange,
|
|
16403
|
+
}: {
|
|
16404
|
+
filter: DatabaseFilter;
|
|
16405
|
+
onValueChange: (value: string) => void;
|
|
16406
|
+
}) {
|
|
16407
|
+
const [start, end] = databaseFilterDateRangeValues(filter.value);
|
|
16408
|
+
const updateRange = (nextStart: string, nextEnd: string) => {
|
|
16409
|
+
onValueChange(databaseFilterEncodeDateRangeValues(nextStart, nextEnd));
|
|
16410
|
+
};
|
|
16411
|
+
|
|
16412
|
+
return (
|
|
16413
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2">
|
|
16414
|
+
<Input
|
|
16415
|
+
type="date"
|
|
16416
|
+
value={start}
|
|
16417
|
+
aria-label={`Start ${filter.label} filter date`}
|
|
16418
|
+
onChange={(event) => updateRange(event.target.value, end)}
|
|
16419
|
+
className="h-9 text-sm focus-visible:border-[#2383e2] focus-visible:ring-[#2383e2]/25"
|
|
16420
|
+
/>
|
|
16421
|
+
<span className="text-xs text-muted-foreground">to</span>
|
|
16422
|
+
<Input
|
|
16423
|
+
type="date"
|
|
16424
|
+
value={end}
|
|
16425
|
+
aria-label={`End ${filter.label} filter date`}
|
|
16426
|
+
onChange={(event) => updateRange(start, event.target.value)}
|
|
16427
|
+
className="h-9 text-sm focus-visible:border-[#2383e2] focus-visible:ring-[#2383e2]/25"
|
|
16428
|
+
/>
|
|
16429
|
+
</div>
|
|
16430
|
+
);
|
|
16431
|
+
}
|
|
16432
|
+
|
|
14524
16433
|
export function databaseFilterOptionChoices(
|
|
14525
16434
|
key: string,
|
|
14526
16435
|
properties: DocumentProperty[],
|
|
16436
|
+
items: ContentDatabaseItem[] = [],
|
|
16437
|
+
currentUserEmail = "",
|
|
16438
|
+
): DatabaseFilterChoice[] {
|
|
16439
|
+
const property = databaseFilterPropertyForKey(key, properties);
|
|
16440
|
+
if (property?.definition.type === "person") {
|
|
16441
|
+
return databaseFilterPersonChoices(
|
|
16442
|
+
property.definition.id,
|
|
16443
|
+
items,
|
|
16444
|
+
currentUserEmail,
|
|
16445
|
+
);
|
|
16446
|
+
}
|
|
16447
|
+
const optionProperty = databaseFilterOptionPropertyForKey(key, properties);
|
|
16448
|
+
return optionProperty?.definition.options.options ?? [];
|
|
16449
|
+
}
|
|
16450
|
+
|
|
16451
|
+
type DatabaseFilterChoice = DocumentPropertyOption & {
|
|
16452
|
+
filterValue?: string;
|
|
16453
|
+
};
|
|
16454
|
+
|
|
16455
|
+
function databaseFilterChoiceValue(option: DatabaseFilterChoice) {
|
|
16456
|
+
return option.filterValue ?? option.id;
|
|
16457
|
+
}
|
|
16458
|
+
|
|
16459
|
+
function databaseFilterPersonChoices(
|
|
16460
|
+
propertyId: string,
|
|
16461
|
+
items: ContentDatabaseItem[],
|
|
16462
|
+
currentUserEmail = "",
|
|
14527
16463
|
) {
|
|
14528
|
-
const
|
|
14529
|
-
|
|
16464
|
+
const people = new Map<string, string>();
|
|
16465
|
+
for (const item of items) {
|
|
16466
|
+
const property = item.properties.find(
|
|
16467
|
+
(candidate) => candidate.definition.id === propertyId,
|
|
16468
|
+
);
|
|
16469
|
+
for (const person of personItems(property?.value ?? null)) {
|
|
16470
|
+
const key = person.trim().toLowerCase();
|
|
16471
|
+
if (!people.has(key)) people.set(key, person);
|
|
16472
|
+
}
|
|
16473
|
+
}
|
|
16474
|
+
const choices: DatabaseFilterChoice[] = [];
|
|
16475
|
+
const currentUserLabel = currentUserEmail
|
|
16476
|
+
? personLabel(currentUserEmail)
|
|
16477
|
+
: "";
|
|
16478
|
+
if (currentUserEmail) {
|
|
16479
|
+
const matchingPerson =
|
|
16480
|
+
Array.from(people.values()).find(
|
|
16481
|
+
(person) =>
|
|
16482
|
+
person.toLowerCase() === currentUserEmail.toLowerCase() ||
|
|
16483
|
+
personLabel(person).toLowerCase() === currentUserLabel.toLowerCase(),
|
|
16484
|
+
) ?? currentUserEmail;
|
|
16485
|
+
choices.push({
|
|
16486
|
+
id: `__current_user__:${matchingPerson}`,
|
|
16487
|
+
name: "Me",
|
|
16488
|
+
color: "gray" as const,
|
|
16489
|
+
filterValue: matchingPerson,
|
|
16490
|
+
});
|
|
16491
|
+
}
|
|
16492
|
+
choices.push(
|
|
16493
|
+
...Array.from(people.values()).map((person) => ({
|
|
16494
|
+
id: person,
|
|
16495
|
+
name: personLabel(person),
|
|
16496
|
+
color: "gray" as const,
|
|
16497
|
+
})),
|
|
16498
|
+
);
|
|
16499
|
+
return choices;
|
|
14530
16500
|
}
|
|
14531
16501
|
|
|
14532
16502
|
export function databaseFilterOptionPropertyForKey(
|
|
@@ -14538,7 +16508,8 @@ export function databaseFilterOptionPropertyForKey(
|
|
|
14538
16508
|
if (
|
|
14539
16509
|
property.definition.type !== "select" &&
|
|
14540
16510
|
property.definition.type !== "status" &&
|
|
14541
|
-
property.definition.type !== "multi_select"
|
|
16511
|
+
property.definition.type !== "multi_select" &&
|
|
16512
|
+
property.definition.type !== "person"
|
|
14542
16513
|
) {
|
|
14543
16514
|
return null;
|
|
14544
16515
|
}
|
|
@@ -14549,18 +16520,105 @@ function databaseFilterValueLabel(
|
|
|
14549
16520
|
filter: DatabaseFilter,
|
|
14550
16521
|
properties: DocumentProperty[],
|
|
14551
16522
|
) {
|
|
14552
|
-
|
|
14553
|
-
(
|
|
14554
|
-
|
|
16523
|
+
if (filter.operator === "between") {
|
|
16524
|
+
const [start, end] = databaseFilterDateRangeValues(filter.value);
|
|
16525
|
+
if (start && end) return `${start} to ${end}`;
|
|
16526
|
+
return start || end || "Choose dates";
|
|
16527
|
+
}
|
|
16528
|
+
const values = databaseFilterSelectedValues(filter.value);
|
|
16529
|
+
const options = databaseFilterOptionChoices(filter.key, properties);
|
|
16530
|
+
if (options.length > 0) {
|
|
16531
|
+
const labels = values.map((value) => {
|
|
16532
|
+
const option = options.find(
|
|
16533
|
+
(candidate) =>
|
|
16534
|
+
candidate.id === value ||
|
|
16535
|
+
candidate.name.trim().toLowerCase() === value.trim().toLowerCase(),
|
|
16536
|
+
);
|
|
16537
|
+
return option?.name ?? value;
|
|
16538
|
+
});
|
|
16539
|
+
return labels.length > 0 ? labels.join(", ") : "Choose option";
|
|
16540
|
+
}
|
|
16541
|
+
return values[0] || "Choose option";
|
|
16542
|
+
}
|
|
16543
|
+
|
|
16544
|
+
function databaseFilterSelectedValues(value: string) {
|
|
16545
|
+
const trimmed = value.trim();
|
|
16546
|
+
if (!trimmed) return [];
|
|
16547
|
+
try {
|
|
16548
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
16549
|
+
if (!Array.isArray(parsed)) return [trimmed];
|
|
16550
|
+
return [
|
|
16551
|
+
...new Set(
|
|
16552
|
+
parsed
|
|
16553
|
+
.filter((item): item is string => typeof item === "string")
|
|
16554
|
+
.map((item) => item.trim())
|
|
16555
|
+
.filter(Boolean),
|
|
16556
|
+
),
|
|
16557
|
+
];
|
|
16558
|
+
} catch {
|
|
16559
|
+
return [trimmed];
|
|
16560
|
+
}
|
|
16561
|
+
}
|
|
16562
|
+
|
|
16563
|
+
function databaseFilterDateRangeValues(value: string): [string, string] {
|
|
16564
|
+
const trimmed = value.trim();
|
|
16565
|
+
if (!trimmed) return ["", ""];
|
|
16566
|
+
try {
|
|
16567
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
16568
|
+
if (!Array.isArray(parsed)) return [trimmed, ""];
|
|
16569
|
+
const start = typeof parsed[0] === "string" ? parsed[0].trim() : "";
|
|
16570
|
+
const end = typeof parsed[1] === "string" ? parsed[1].trim() : "";
|
|
16571
|
+
return [start, end];
|
|
16572
|
+
} catch {
|
|
16573
|
+
return [trimmed, ""];
|
|
16574
|
+
}
|
|
16575
|
+
}
|
|
16576
|
+
|
|
16577
|
+
function databaseFilterEncodeDateRangeValues(start: string, end: string) {
|
|
16578
|
+
const normalizedStart = start.trim();
|
|
16579
|
+
const normalizedEnd = end.trim();
|
|
16580
|
+
if (!normalizedStart && !normalizedEnd) return "";
|
|
16581
|
+
return JSON.stringify([normalizedStart, normalizedEnd]);
|
|
16582
|
+
}
|
|
16583
|
+
|
|
16584
|
+
function databaseFilterDateRangeValue(value: string): [number, number] | null {
|
|
16585
|
+
const [startValue, endValue] = databaseFilterDateRangeValues(value);
|
|
16586
|
+
const start = new Date(startValue).getTime();
|
|
16587
|
+
const end = new Date(endValue).getTime();
|
|
16588
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
16589
|
+
return start <= end ? [start, end] : [end, start];
|
|
16590
|
+
}
|
|
16591
|
+
|
|
16592
|
+
function databaseFilterEncodeSelectedValues(values: string[]) {
|
|
16593
|
+
const normalized = [
|
|
16594
|
+
...new Set(values.map((value) => value.trim()).filter(Boolean)),
|
|
16595
|
+
];
|
|
16596
|
+
if (normalized.length === 0) return "";
|
|
16597
|
+
if (normalized.length === 1) return normalized[0];
|
|
16598
|
+
return JSON.stringify(normalized);
|
|
16599
|
+
}
|
|
16600
|
+
|
|
16601
|
+
function databaseFilterOptionIsSelected(
|
|
16602
|
+
option: DatabaseFilterChoice,
|
|
16603
|
+
values: string[],
|
|
16604
|
+
) {
|
|
16605
|
+
const optionValue = databaseFilterChoiceValue(option);
|
|
16606
|
+
const optionName = option.name.trim().toLowerCase();
|
|
16607
|
+
return values.some(
|
|
16608
|
+
(value) =>
|
|
16609
|
+
value === optionValue || value.trim().toLowerCase() === optionName,
|
|
14555
16610
|
);
|
|
14556
|
-
return (option?.name ?? filter.value) || "Choose option";
|
|
14557
16611
|
}
|
|
14558
16612
|
|
|
14559
16613
|
function databaseFilterChipLabel(
|
|
14560
16614
|
filter: DatabaseFilter,
|
|
14561
16615
|
properties: DocumentProperty[],
|
|
14562
16616
|
) {
|
|
14563
|
-
const operator =
|
|
16617
|
+
const operator = databaseFilterOperatorLabel(
|
|
16618
|
+
filter.operator,
|
|
16619
|
+
filter.key,
|
|
16620
|
+
properties,
|
|
16621
|
+
);
|
|
14564
16622
|
if (!filterOperatorNeedsValue(filter.operator)) {
|
|
14565
16623
|
return `${filter.label} ${operator.toLowerCase()}`;
|
|
14566
16624
|
}
|
|
@@ -14570,6 +16628,23 @@ function databaseFilterChipLabel(
|
|
|
14570
16628
|
)}`;
|
|
14571
16629
|
}
|
|
14572
16630
|
|
|
16631
|
+
export function databaseInlineFilterLabel(
|
|
16632
|
+
filter: DatabaseFilter,
|
|
16633
|
+
properties: DocumentProperty[],
|
|
16634
|
+
) {
|
|
16635
|
+
if (!filterOperatorNeedsValue(filter.operator)) {
|
|
16636
|
+
return `${filter.label}: ${databaseFilterOperatorLabel(
|
|
16637
|
+
filter.operator,
|
|
16638
|
+
filter.key,
|
|
16639
|
+
properties,
|
|
16640
|
+
)}`;
|
|
16641
|
+
}
|
|
16642
|
+
if (databaseFilterSelectedValues(filter.value).length === 0) {
|
|
16643
|
+
return filter.label;
|
|
16644
|
+
}
|
|
16645
|
+
return `${filter.label}: ${databaseFilterValueLabel(filter, properties)}`;
|
|
16646
|
+
}
|
|
16647
|
+
|
|
14573
16648
|
function databaseFilterPropertyForKey(
|
|
14574
16649
|
key: string,
|
|
14575
16650
|
properties: DocumentProperty[],
|
|
@@ -14827,6 +16902,9 @@ function DatabaseTableRow({
|
|
|
14827
16902
|
item.properties.find(
|
|
14828
16903
|
(candidate) => candidate.definition.id === property.definition.id,
|
|
14829
16904
|
) ?? property;
|
|
16905
|
+
const bodyCellHydrationPending =
|
|
16906
|
+
itemProperty.definition.type === "blocks" &&
|
|
16907
|
+
databaseItemBodyHydrationIsPending(item);
|
|
14830
16908
|
|
|
14831
16909
|
const value = (
|
|
14832
16910
|
<div
|
|
@@ -14835,10 +16913,12 @@ function DatabaseTableRow({
|
|
|
14835
16913
|
wrapCells
|
|
14836
16914
|
? "whitespace-normal break-words"
|
|
14837
16915
|
: "truncate whitespace-nowrap",
|
|
14838
|
-
isEmptyPropertyValue(itemProperty.value) &&
|
|
16916
|
+
isEmptyPropertyValue(itemProperty.value) &&
|
|
16917
|
+
!bodyCellHydrationPending &&
|
|
16918
|
+
"text-transparent",
|
|
14839
16919
|
)}
|
|
14840
16920
|
>
|
|
14841
|
-
{databaseTableCellDisplayValue(itemProperty)}
|
|
16921
|
+
{databaseTableCellDisplayValue(itemProperty, item)}
|
|
14842
16922
|
</div>
|
|
14843
16923
|
);
|
|
14844
16924
|
const isEditableCheckbox =
|