@agent-native/core 0.123.0 → 0.123.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +24 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/templates-meta.ts +12 -0
- package/corpus/core/src/deploy/build.ts +8 -0
- package/corpus/core/src/server/builder-browser.ts +76 -0
- package/corpus/core/src/server/core-routes-plugin.ts +15 -5
- package/corpus/templates/assets/app/global.css +0 -4
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
- package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
- package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
- package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
- package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
- package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
- package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
- package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
- package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
- package/corpus/templates/content/app/root.tsx +1 -6
- package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/content/shared/builder-mdx.ts +73 -16
- package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
- package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
- package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
- package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
- package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
- package/corpus/templates/crm/.env.example +5 -0
- package/corpus/templates/crm/AGENTS.md +82 -0
- package/corpus/templates/crm/CHANGELOG.md +25 -0
- package/corpus/templates/crm/DEVELOPING.md +66 -0
- package/corpus/templates/crm/README.md +122 -0
- package/corpus/templates/crm/_gitignore +12 -0
- package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
- package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
- package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
- package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
- package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
- package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
- package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
- package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
- package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
- package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
- package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
- package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
- package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
- package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
- package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
- package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
- package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
- package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
- package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
- package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
- package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
- package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
- package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
- package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
- package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
- package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
- package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
- package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
- package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
- package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
- package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
- package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
- package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
- package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
- package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
- package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
- package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
- package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
- package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
- package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
- package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
- package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
- package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
- package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
- package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
- package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
- package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
- package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
- package/corpus/templates/crm/actions/navigate.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
- package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
- package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
- package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
- package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
- package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
- package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
- package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
- package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
- package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
- package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
- package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
- package/corpus/templates/crm/actions/run.ts +7 -0
- package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
- package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
- package/corpus/templates/crm/actions/sync-crm.ts +146 -0
- package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
- package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
- package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
- package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
- package/corpus/templates/crm/actions/view-screen.ts +324 -0
- package/corpus/templates/crm/agent-native.app-skill.json +69 -0
- package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
- package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
- package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
- package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
- package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
- package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
- package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
- package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
- package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
- package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
- package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
- package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
- package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
- package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
- package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
- package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
- package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
- package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
- package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
- package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
- package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
- package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
- package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
- package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
- package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
- package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
- package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
- package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
- package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
- package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
- package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
- package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
- package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
- package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
- package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
- package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
- package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
- package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
- package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
- package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
- package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
- package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
- package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
- package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/crm/app/design-system.ts +3 -0
- package/corpus/templates/crm/app/entry.client.tsx +19 -0
- package/corpus/templates/crm/app/entry.server.tsx +10 -0
- package/corpus/templates/crm/app/global.css +167 -0
- package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
- package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
- package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
- package/corpus/templates/crm/app/i18n/index.ts +34 -0
- package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
- package/corpus/templates/crm/app/lib/navigation.ts +17 -0
- package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
- package/corpus/templates/crm/app/lib/types.ts +172 -0
- package/corpus/templates/crm/app/lib/utils.ts +1 -0
- package/corpus/templates/crm/app/root.tsx +93 -0
- package/corpus/templates/crm/app/routes/_index.tsx +26 -0
- package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
- package/corpus/templates/crm/app/routes/agent.tsx +5 -0
- package/corpus/templates/crm/app/routes/ask.tsx +53 -0
- package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
- package/corpus/templates/crm/app/routes/lists.tsx +228 -0
- package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
- package/corpus/templates/crm/app/routes/people.tsx +41 -0
- package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
- package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
- package/corpus/templates/crm/app/routes/records.tsx +64 -0
- package/corpus/templates/crm/app/routes/settings.tsx +118 -0
- package/corpus/templates/crm/app/routes/setup.tsx +308 -0
- package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
- package/corpus/templates/crm/app/routes/views.tsx +683 -0
- package/corpus/templates/crm/app/routes.ts +19 -0
- package/corpus/templates/crm/app/vite-env.d.ts +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
- package/corpus/templates/crm/components.json +20 -0
- package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
- package/corpus/templates/crm/docs/features/README.md +55 -0
- package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
- package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
- package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
- package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
- package/corpus/templates/crm/learnings.defaults.md +5 -0
- package/corpus/templates/crm/netlify.toml +11 -0
- package/corpus/templates/crm/package.json +61 -0
- package/corpus/templates/crm/public/favicon.svg +1 -0
- package/corpus/templates/crm/public/icon-180.svg +1 -0
- package/corpus/templates/crm/public/manifest.json +11 -0
- package/corpus/templates/crm/react-router.config.ts +7 -0
- package/corpus/templates/crm/server/crm/adapter.ts +32 -0
- package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
- package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
- package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
- package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
- package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
- package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
- package/corpus/templates/crm/server/crm/read-through.ts +207 -0
- package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
- package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
- package/corpus/templates/crm/server/db/index.ts +191 -0
- package/corpus/templates/crm/server/db/schema.ts +647 -0
- package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
- package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
- package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
- package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
- package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
- package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
- package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
- package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
- package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
- package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
- package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
- package/corpus/templates/crm/server/middleware/auth.ts +4 -0
- package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
- package/corpus/templates/crm/server/plugins/auth.ts +14 -0
- package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
- package/corpus/templates/crm/server/plugins/db.ts +738 -0
- package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
- package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
- package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
- package/corpus/templates/crm/shared/crm-contract.ts +417 -0
- package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
- package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
- package/corpus/templates/crm/ssr-entry.ts +11 -0
- package/corpus/templates/crm/tsconfig.json +24 -0
- package/corpus/templates/crm/vite.config.ts +15 -0
- package/corpus/templates/crm/vitest.config.ts +5 -0
- package/corpus/templates/design/app/i18n-data.ts +12 -12
- package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +12 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/deploy/build.js +6 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/builder-browser.d.ts +13 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +59 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +10 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/src/cli/templates-meta.ts +12 -0
- package/src/deploy/build.ts +8 -0
- package/src/server/builder-browser.ts +76 -0
- package/src/server/core-routes-plugin.ts +15 -5
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure board logic: grouping, per-column totals, stage SLA, and the optimistic
|
|
3
|
+
* move with its rollback. No React, no icons, no i18n.
|
|
4
|
+
*
|
|
5
|
+
* The bitemporal `crm_record_fields` table IS the stage history. The current
|
|
6
|
+
* status row's `active_from` — surfaced per entry as `valuesSince[apiSlug]` by
|
|
7
|
+
* `list-crm-list-entries`, and stored alongside the option's `target_days` —
|
|
8
|
+
* is the whole input to time-in-stage. There is no separate stage-history
|
|
9
|
+
* table and there must not be one; an action can compute the same overrun from
|
|
10
|
+
* the same two fields.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Column key for cards with no value for the grouping attribute. */
|
|
14
|
+
export const BOARD_UNGROUPED = "__ungrouped__";
|
|
15
|
+
|
|
16
|
+
/** A card shows at most this many configured attributes as plain rows. */
|
|
17
|
+
export const CARD_ATTRIBUTE_LIMIT = 3;
|
|
18
|
+
|
|
19
|
+
/** `crm_record_fields.actor_type`, restated locally so this module stays leaf. */
|
|
20
|
+
export type BoardActorType =
|
|
21
|
+
| "user"
|
|
22
|
+
| "agent"
|
|
23
|
+
| "automation"
|
|
24
|
+
| "provider"
|
|
25
|
+
| "system";
|
|
26
|
+
|
|
27
|
+
export interface BoardOption {
|
|
28
|
+
id: string;
|
|
29
|
+
value: string;
|
|
30
|
+
title: string;
|
|
31
|
+
color?: string | undefined;
|
|
32
|
+
position: number;
|
|
33
|
+
archived: boolean;
|
|
34
|
+
/** Stage SLA in days. Null means this stage is not time-boxed. */
|
|
35
|
+
targetDays: number | null;
|
|
36
|
+
celebrate: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BoardCardAttribute {
|
|
40
|
+
slug: string;
|
|
41
|
+
label: string;
|
|
42
|
+
attributeType: string;
|
|
43
|
+
multi: boolean;
|
|
44
|
+
options?: BoardOption[];
|
|
45
|
+
config?: Record<string, unknown>;
|
|
46
|
+
/** Raw decoded value; the renderer localizes it through the shared registry. */
|
|
47
|
+
value: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface BoardCard {
|
|
51
|
+
/** Entry id on a list board, record id on an object board. */
|
|
52
|
+
id: string;
|
|
53
|
+
recordId: string;
|
|
54
|
+
title: string;
|
|
55
|
+
subtitle: string | null;
|
|
56
|
+
owner: string | null;
|
|
57
|
+
/** Option value, or `BOARD_UNGROUPED` when the attribute has no value. */
|
|
58
|
+
groupValue: string;
|
|
59
|
+
/**
|
|
60
|
+
* `active_from` of the current group value. `null` means no start is
|
|
61
|
+
* recorded for it — which is NOT "started just now". The SLA reports
|
|
62
|
+
* `unknown` for such a card rather than inventing an age.
|
|
63
|
+
*/
|
|
64
|
+
groupSince: string | null;
|
|
65
|
+
/**
|
|
66
|
+
* The record's current revision, for an object board's record write. `null`
|
|
67
|
+
* means this board never learned one — which is NOT "no concurrent change";
|
|
68
|
+
* see `objectBoardMoveArgs`. A list board leaves it null: an entry write is
|
|
69
|
+
* keyed by entry id and carries no record revision.
|
|
70
|
+
*/
|
|
71
|
+
remoteRevision: string | null;
|
|
72
|
+
amount: number | null;
|
|
73
|
+
currencyCode: string | null;
|
|
74
|
+
attributes: BoardCardAttribute[];
|
|
75
|
+
/** Provenance of the card itself; drives the agent/automation affordance. */
|
|
76
|
+
actorType: BoardActorType | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type BoardColumnKind = "option" | "unset" | "archived" | "unknown";
|
|
80
|
+
|
|
81
|
+
export interface BoardColumn {
|
|
82
|
+
/** Option value, or `BOARD_UNGROUPED`. Also the drop target identity. */
|
|
83
|
+
key: string;
|
|
84
|
+
option: BoardOption | null;
|
|
85
|
+
kind: BoardColumnKind;
|
|
86
|
+
cards: BoardCard[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One column per live option, then any archived or unrecognized value that
|
|
91
|
+
* still holds cards, then the unset column last.
|
|
92
|
+
*
|
|
93
|
+
* A value with no live option gets its own column instead of falling into the
|
|
94
|
+
* unset bucket: "this stage was retired" and "this card has no stage" are
|
|
95
|
+
* different facts, and merging them hides data behind a plausible-looking
|
|
96
|
+
* board.
|
|
97
|
+
*/
|
|
98
|
+
export function boardColumns(
|
|
99
|
+
cards: readonly BoardCard[],
|
|
100
|
+
options: readonly BoardOption[],
|
|
101
|
+
): BoardColumn[] {
|
|
102
|
+
const live = [...options]
|
|
103
|
+
.filter((option) => !option.archived)
|
|
104
|
+
.sort((a, b) => a.position - b.position || a.value.localeCompare(b.value));
|
|
105
|
+
const byValue = new Map(options.map((option) => [option.value, option]));
|
|
106
|
+
|
|
107
|
+
const columns = new Map<string, BoardColumn>();
|
|
108
|
+
for (const option of live) {
|
|
109
|
+
columns.set(option.value, {
|
|
110
|
+
key: option.value,
|
|
111
|
+
option,
|
|
112
|
+
kind: "option",
|
|
113
|
+
cards: [],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const extras: BoardColumn[] = [];
|
|
117
|
+
const unset: BoardColumn = {
|
|
118
|
+
key: BOARD_UNGROUPED,
|
|
119
|
+
option: null,
|
|
120
|
+
kind: "unset",
|
|
121
|
+
cards: [],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
for (const card of cards) {
|
|
125
|
+
if (card.groupValue === BOARD_UNGROUPED || card.groupValue === "") {
|
|
126
|
+
unset.cards.push(card);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const existing = columns.get(card.groupValue);
|
|
130
|
+
if (existing) {
|
|
131
|
+
existing.cards.push(card);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const option = byValue.get(card.groupValue) ?? null;
|
|
135
|
+
const column: BoardColumn = {
|
|
136
|
+
key: card.groupValue,
|
|
137
|
+
option,
|
|
138
|
+
kind: option ? "archived" : "unknown",
|
|
139
|
+
cards: [card],
|
|
140
|
+
};
|
|
141
|
+
columns.set(card.groupValue, column);
|
|
142
|
+
extras.push(column);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
...live.flatMap((option) => {
|
|
147
|
+
const column = columns.get(option.value);
|
|
148
|
+
return column ? [column] : [];
|
|
149
|
+
}),
|
|
150
|
+
...extras,
|
|
151
|
+
unset,
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface BoardColumnTotals {
|
|
156
|
+
count: number;
|
|
157
|
+
/** Null when nothing summable is present, or when currencies are mixed. */
|
|
158
|
+
sum: number | null;
|
|
159
|
+
currencyCode: string | null;
|
|
160
|
+
mixedCurrency: boolean;
|
|
161
|
+
/** Cards with no amount — the sum does not cover them. */
|
|
162
|
+
withoutAmount: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function boardColumnTotals(
|
|
166
|
+
cards: readonly BoardCard[],
|
|
167
|
+
): BoardColumnTotals {
|
|
168
|
+
let sum = 0;
|
|
169
|
+
let seen = 0;
|
|
170
|
+
let withoutAmount = 0;
|
|
171
|
+
let currencyCode: string | null = null;
|
|
172
|
+
let mixedCurrency = false;
|
|
173
|
+
|
|
174
|
+
for (const card of cards) {
|
|
175
|
+
if (card.amount === null || !Number.isFinite(card.amount)) {
|
|
176
|
+
withoutAmount += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
sum += card.amount;
|
|
180
|
+
seen += 1;
|
|
181
|
+
if (card.currencyCode === null) continue;
|
|
182
|
+
if (currencyCode === null) currencyCode = card.currencyCode;
|
|
183
|
+
else if (currencyCode !== card.currencyCode) mixedCurrency = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
count: cards.length,
|
|
188
|
+
// Adding EUR to USD produces a confident wrong number, so a mixed column
|
|
189
|
+
// reports no total instead of a meaningless one.
|
|
190
|
+
sum: seen === 0 || mixedCurrency ? null : sum,
|
|
191
|
+
currencyCode: mixedCurrency ? null : currencyCode,
|
|
192
|
+
mixedCurrency,
|
|
193
|
+
withoutAmount,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type BoardSla =
|
|
198
|
+
| { status: "not-tracked" }
|
|
199
|
+
| { status: "unknown"; reason: "no-start-recorded" | "unreadable-start" }
|
|
200
|
+
| { status: "within"; days: number; targetDays: number }
|
|
201
|
+
| { status: "overrun"; days: number; targetDays: number; overBy: number };
|
|
202
|
+
|
|
203
|
+
const DAY_MS = 86_400_000;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Time-in-stage against the option's `target_days`, measured from the current
|
|
207
|
+
* status row's `active_from`.
|
|
208
|
+
*/
|
|
209
|
+
export function boardCardSla(
|
|
210
|
+
card: Pick<BoardCard, "groupSince">,
|
|
211
|
+
option: BoardOption | null,
|
|
212
|
+
now: Date,
|
|
213
|
+
): BoardSla {
|
|
214
|
+
const targetDays = option?.targetDays ?? null;
|
|
215
|
+
if (targetDays === null || targetDays <= 0) return { status: "not-tracked" };
|
|
216
|
+
if (!card.groupSince) {
|
|
217
|
+
return { status: "unknown", reason: "no-start-recorded" };
|
|
218
|
+
}
|
|
219
|
+
const since = Date.parse(card.groupSince);
|
|
220
|
+
if (!Number.isFinite(since)) {
|
|
221
|
+
return { status: "unknown", reason: "unreadable-start" };
|
|
222
|
+
}
|
|
223
|
+
const days = Math.floor((now.getTime() - since) / DAY_MS);
|
|
224
|
+
if (days > targetDays) {
|
|
225
|
+
return { status: "overrun", days, targetDays, overBy: days - targetDays };
|
|
226
|
+
}
|
|
227
|
+
return { status: "within", days, targetDays };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Cards a board should surface first: every stage-SLA overrun, worst first. */
|
|
231
|
+
export function boardOverruns(
|
|
232
|
+
columns: readonly BoardColumn[],
|
|
233
|
+
now: Date,
|
|
234
|
+
): Array<{ card: BoardCard; sla: Extract<BoardSla, { status: "overrun" }> }> {
|
|
235
|
+
return columns
|
|
236
|
+
.flatMap((column) =>
|
|
237
|
+
column.cards.flatMap((card) => {
|
|
238
|
+
const sla = boardCardSla(card, column.option, now);
|
|
239
|
+
return sla.status === "overrun" ? [{ card, sla }] : [];
|
|
240
|
+
}),
|
|
241
|
+
)
|
|
242
|
+
.sort((a, b) => b.sla.overBy - a.sla.overBy);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The card moved into `toValue`, as a new array. Throws on an unknown id
|
|
247
|
+
* rather than returning the untouched list, which a caller cannot tell from a
|
|
248
|
+
* successful no-op move.
|
|
249
|
+
*/
|
|
250
|
+
export function applyBoardMove(
|
|
251
|
+
cards: readonly BoardCard[],
|
|
252
|
+
cardId: string,
|
|
253
|
+
toValue: string,
|
|
254
|
+
now: string,
|
|
255
|
+
): BoardCard[] {
|
|
256
|
+
if (!cards.some((card) => card.id === cardId)) {
|
|
257
|
+
throw new Error(`Board card "${cardId}" is not on this board.`);
|
|
258
|
+
}
|
|
259
|
+
return cards.map((card) =>
|
|
260
|
+
card.id === cardId
|
|
261
|
+
? { ...card, groupValue: toValue, groupSince: now }
|
|
262
|
+
: card,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface BoardMoveResult {
|
|
267
|
+
moved: boolean;
|
|
268
|
+
/** Present only when the commit failed and the board was rolled back. */
|
|
269
|
+
error?: unknown;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Optimistic stage move: paint it, then commit. A failed commit re-applies the
|
|
274
|
+
* pre-move snapshot, so the card visibly returns to the column it came from.
|
|
275
|
+
*/
|
|
276
|
+
export async function moveBoardCard(input: {
|
|
277
|
+
cards: readonly BoardCard[];
|
|
278
|
+
cardId: string;
|
|
279
|
+
toValue: string;
|
|
280
|
+
now: string;
|
|
281
|
+
apply: (next: BoardCard[]) => void;
|
|
282
|
+
commit: (move: { card: BoardCard; toValue: string }) => Promise<unknown>;
|
|
283
|
+
}): Promise<BoardMoveResult> {
|
|
284
|
+
const card = input.cards.find((entry) => entry.id === input.cardId);
|
|
285
|
+
if (!card)
|
|
286
|
+
throw new Error(`Board card "${input.cardId}" is not on this board.`);
|
|
287
|
+
if (card.groupValue === input.toValue) return { moved: false };
|
|
288
|
+
|
|
289
|
+
const snapshot = [...input.cards];
|
|
290
|
+
input.apply(
|
|
291
|
+
applyBoardMove(input.cards, input.cardId, input.toValue, input.now),
|
|
292
|
+
);
|
|
293
|
+
try {
|
|
294
|
+
await input.commit({ card, toValue: input.toValue });
|
|
295
|
+
return { moved: true };
|
|
296
|
+
} catch (error) {
|
|
297
|
+
input.apply(snapshot);
|
|
298
|
+
return { moved: false, error };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The value an action should be sent for a column: unset clears the field. */
|
|
303
|
+
export function moveValueForColumn(columnKey: string): string | null {
|
|
304
|
+
return columnKey === BOARD_UNGROUPED ? null : columnKey;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* `update-crm-record` args for an object-board move.
|
|
309
|
+
*
|
|
310
|
+
* A native local write is refused without the record's current revision, so
|
|
311
|
+
* the card's revision is carried through here. When the card has none the key
|
|
312
|
+
* is omitted rather than sent as `""` or `null`: an invented revision would
|
|
313
|
+
* pass as a real one and turn a concurrent edit into a silent overwrite, and
|
|
314
|
+
* the action's own "refresh the record" error is the honest outcome. It stays
|
|
315
|
+
* distinguishable from a stale revision, which the native adapter reports as a
|
|
316
|
+
* conflict instead.
|
|
317
|
+
*/
|
|
318
|
+
export function objectBoardMoveArgs(
|
|
319
|
+
card: Pick<BoardCard, "recordId" | "remoteRevision">,
|
|
320
|
+
groupSlug: string,
|
|
321
|
+
toValue: string,
|
|
322
|
+
) {
|
|
323
|
+
return {
|
|
324
|
+
recordId: card.recordId,
|
|
325
|
+
target: "local" as const,
|
|
326
|
+
fields: { [groupSlug]: moveValueForColumn(toValue) },
|
|
327
|
+
...(card.remoteRevision
|
|
328
|
+
? { expectedRemoteRevision: card.remoteRevision }
|
|
329
|
+
: {}),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The currency attribute a column header sums. Prefers one named by the view's
|
|
335
|
+
* columns so a board follows what the view already shows.
|
|
336
|
+
*/
|
|
337
|
+
export function pickCurrencyAttribute<
|
|
338
|
+
T extends { id: string; apiSlug: string; attributeType: string },
|
|
339
|
+
>(attributes: readonly T[], preferredIds: readonly string[] = []): T | null {
|
|
340
|
+
const currency = attributes.filter(
|
|
341
|
+
(attribute) => attribute.attributeType === "currency",
|
|
342
|
+
);
|
|
343
|
+
const preferred = currency.find(
|
|
344
|
+
(attribute) =>
|
|
345
|
+
preferredIds.includes(attribute.id) ||
|
|
346
|
+
preferredIds.includes(attribute.apiSlug),
|
|
347
|
+
);
|
|
348
|
+
return preferred ?? currency[0] ?? null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Up to `CARD_ATTRIBUTE_LIMIT` attributes a card shows as plain rows. The
|
|
353
|
+
* grouping and currency attributes get their own dedicated place on the card,
|
|
354
|
+
* so excluding them here keeps a value from appearing twice. Prefers the
|
|
355
|
+
* view's own column order; whatever is left is filled in by `position`.
|
|
356
|
+
*/
|
|
357
|
+
export function pickCardAttributes<
|
|
358
|
+
T extends {
|
|
359
|
+
id: string;
|
|
360
|
+
apiSlug: string;
|
|
361
|
+
attributeType: string;
|
|
362
|
+
position: number;
|
|
363
|
+
},
|
|
364
|
+
>(
|
|
365
|
+
attributes: readonly T[],
|
|
366
|
+
excludeIds: ReadonlySet<string>,
|
|
367
|
+
preferredIds: readonly string[] = [],
|
|
368
|
+
): T[] {
|
|
369
|
+
const eligible = attributes.filter(
|
|
370
|
+
(attribute) =>
|
|
371
|
+
!excludeIds.has(attribute.id) &&
|
|
372
|
+
attribute.attributeType !== "interaction",
|
|
373
|
+
);
|
|
374
|
+
const preferred = preferredIds.flatMap((id) => {
|
|
375
|
+
const match = eligible.find(
|
|
376
|
+
(attribute) => attribute.id === id || attribute.apiSlug === id,
|
|
377
|
+
);
|
|
378
|
+
return match ? [match] : [];
|
|
379
|
+
});
|
|
380
|
+
const rest = eligible
|
|
381
|
+
.filter((attribute) => !preferred.includes(attribute))
|
|
382
|
+
.sort((a, b) => a.position - b.position);
|
|
383
|
+
return [...preferred, ...rest].slice(0, CARD_ATTRIBUTE_LIMIT);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* A card's summable amount, read only from the view's currency attribute.
|
|
388
|
+
* `null` covers "this view has no currency attribute" and "the value under it
|
|
389
|
+
* is missing or unreadable" alike — a column total must not fold either into
|
|
390
|
+
* a silent zero, and this must never fall back to an untyped/legacy field.
|
|
391
|
+
*/
|
|
392
|
+
export function cardAmountFor(
|
|
393
|
+
currencyAttribute: { apiSlug: string } | null,
|
|
394
|
+
values: Readonly<Record<string, unknown>>,
|
|
395
|
+
): number | null {
|
|
396
|
+
if (!currencyAttribute) return null;
|
|
397
|
+
const value = values[currencyAttribute.apiSlug];
|
|
398
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
399
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates a saved view's filter tree into the vocabulary
|
|
3
|
+
* `list-crm-list-entries` speaks.
|
|
4
|
+
*
|
|
5
|
+
* The two surfaces were built against different dialects: records are filtered
|
|
6
|
+
* with `{op, conditions:[{attributeId|field, condition, value}]}`, entries with
|
|
7
|
+
* a flat `[{attribute, operator, value}]`. Anything this translator cannot
|
|
8
|
+
* express is thrown, never dropped — a list board that quietly ignored half a
|
|
9
|
+
* saved filter would render a full pipeline that looks exactly like a correct
|
|
10
|
+
* filtered one.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class BoardFilterError extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "BoardFilterError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type EntryFilterOperator =
|
|
21
|
+
| "eq"
|
|
22
|
+
| "neq"
|
|
23
|
+
| "contains"
|
|
24
|
+
| "gt"
|
|
25
|
+
| "gte"
|
|
26
|
+
| "lt"
|
|
27
|
+
| "lte"
|
|
28
|
+
| "in"
|
|
29
|
+
| "is-empty"
|
|
30
|
+
| "is-not-empty";
|
|
31
|
+
|
|
32
|
+
export interface EntryFilter {
|
|
33
|
+
attribute: string;
|
|
34
|
+
operator: EntryFilterOperator;
|
|
35
|
+
value?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CONDITION_OPERATORS: Record<string, EntryFilterOperator> = {
|
|
39
|
+
is: "eq",
|
|
40
|
+
"is-not": "neq",
|
|
41
|
+
contains: "contains",
|
|
42
|
+
"is-empty": "is-empty",
|
|
43
|
+
"is-not-empty": "is-not-empty",
|
|
44
|
+
"=": "eq",
|
|
45
|
+
"!=": "neq",
|
|
46
|
+
">": "gt",
|
|
47
|
+
">=": "gte",
|
|
48
|
+
"<": "lt",
|
|
49
|
+
"<=": "lte",
|
|
50
|
+
"is-any-of": "in",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** `crm_records` columns `list-crm-list-entries` exposes as `record.*`. */
|
|
54
|
+
const RECORD_FIELDS = new Set([
|
|
55
|
+
"displayName",
|
|
56
|
+
"kind",
|
|
57
|
+
"stage",
|
|
58
|
+
"ownerName",
|
|
59
|
+
"domain",
|
|
60
|
+
"primaryEmail",
|
|
61
|
+
"amount",
|
|
62
|
+
"closeDate",
|
|
63
|
+
"updatedAt",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
interface AttributeRef {
|
|
67
|
+
id: string;
|
|
68
|
+
apiSlug: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param attributes the list's own attributes, for resolving an `attributeId`
|
|
73
|
+
* (which may be an id or an api_slug) to the slug entry filters use.
|
|
74
|
+
*/
|
|
75
|
+
export function toEntryFilters(
|
|
76
|
+
filter: unknown,
|
|
77
|
+
attributes: readonly AttributeRef[],
|
|
78
|
+
): EntryFilter[] {
|
|
79
|
+
if (filter === undefined || filter === null) return [];
|
|
80
|
+
if (typeof filter !== "object") {
|
|
81
|
+
throw new BoardFilterError("This view's stored filter is not readable.");
|
|
82
|
+
}
|
|
83
|
+
const node = filter as { op?: unknown; conditions?: unknown };
|
|
84
|
+
if (node.conditions === undefined) return [];
|
|
85
|
+
if (!Array.isArray(node.conditions)) {
|
|
86
|
+
throw new BoardFilterError("This view's stored filter is not readable.");
|
|
87
|
+
}
|
|
88
|
+
if (node.op === "or" && node.conditions.length > 1) {
|
|
89
|
+
throw new BoardFilterError(
|
|
90
|
+
"A list board cannot yet apply an OR filter. Edit the view on the table.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return node.conditions.map((condition) =>
|
|
94
|
+
toEntryFilter(condition, attributes),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toEntryFilter(
|
|
99
|
+
condition: unknown,
|
|
100
|
+
attributes: readonly AttributeRef[],
|
|
101
|
+
): EntryFilter {
|
|
102
|
+
if (!condition || typeof condition !== "object") {
|
|
103
|
+
throw new BoardFilterError(
|
|
104
|
+
"This view holds an unreadable filter condition.",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const leaf = condition as Record<string, unknown>;
|
|
108
|
+
if (leaf.conditions !== undefined) {
|
|
109
|
+
throw new BoardFilterError(
|
|
110
|
+
"A list board cannot yet apply a nested filter group. Edit the view on the table.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const operator = CONDITION_OPERATORS[String(leaf.condition)];
|
|
114
|
+
if (!operator) {
|
|
115
|
+
throw new BoardFilterError(
|
|
116
|
+
`A list board cannot yet apply the "${String(leaf.condition)}" condition.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let attribute: string;
|
|
121
|
+
if (typeof leaf.field === "string" && leaf.field) {
|
|
122
|
+
if (!RECORD_FIELDS.has(leaf.field)) {
|
|
123
|
+
throw new BoardFilterError(
|
|
124
|
+
`A list board cannot filter on the record field "${leaf.field}".`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
attribute = `record.${leaf.field}`;
|
|
128
|
+
} else if (typeof leaf.attributeId === "string" && leaf.attributeId) {
|
|
129
|
+
const match = attributes.find(
|
|
130
|
+
(entry) =>
|
|
131
|
+
entry.id === leaf.attributeId || entry.apiSlug === leaf.attributeId,
|
|
132
|
+
);
|
|
133
|
+
if (!match) {
|
|
134
|
+
throw new BoardFilterError(
|
|
135
|
+
`This view filters on "${leaf.attributeId}", which is not an attribute of this list.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
attribute = match.apiSlug;
|
|
139
|
+
} else {
|
|
140
|
+
throw new BoardFilterError("A filter condition names no attribute.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
attribute,
|
|
145
|
+
operator,
|
|
146
|
+
...(operator === "is-empty" || operator === "is-not-empty"
|
|
147
|
+
? {}
|
|
148
|
+
: { value: leaf.value }),
|
|
149
|
+
};
|
|
150
|
+
}
|