@agent-native/core 0.123.0 → 0.123.1
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 +16 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/templates-meta.ts +12 -0
- 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/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 +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/templates-meta.ts +12 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic behind the record page: what an attribute value looks like, who is
|
|
3
|
+
* allowed to edit it inline, how typed input is parsed, and how bitemporal rows
|
|
4
|
+
* become "changed from X to Y".
|
|
5
|
+
*
|
|
6
|
+
* It lives apart from the components because the template's vitest config is
|
|
7
|
+
* node-only (no DOM, `.test.ts` only), so this is the layer that can be tested.
|
|
8
|
+
* Anything here that returns a value must keep "absent" and "unreadable"
|
|
9
|
+
* distinguishable — a field the mirror never carried is not an empty field.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
ATTRIBUTE_TYPE_SPECS,
|
|
14
|
+
type CrmAttributeType,
|
|
15
|
+
} from "../../../../shared/crm-attributes";
|
|
16
|
+
import type {
|
|
17
|
+
CrmAttributeDefinition,
|
|
18
|
+
CrmAttributeOption,
|
|
19
|
+
CrmValue,
|
|
20
|
+
} from "../../../../shared/crm-contract";
|
|
21
|
+
import {
|
|
22
|
+
ATTRIBUTE_VALUE_SPECS,
|
|
23
|
+
formatAttributeValue,
|
|
24
|
+
parseAttributeValue,
|
|
25
|
+
valueTokens,
|
|
26
|
+
type CrmAttributeControl,
|
|
27
|
+
} from "../shared/attribute-value";
|
|
28
|
+
|
|
29
|
+
export { attributeInputValue as fieldInputValue } from "../shared/attribute-value";
|
|
30
|
+
|
|
31
|
+
/** Up to six pinned attributes sit above the rest of the panel. */
|
|
32
|
+
export const MAX_HIGHLIGHTS = 6;
|
|
33
|
+
|
|
34
|
+
export interface CrmRecordPageEntry {
|
|
35
|
+
id: string;
|
|
36
|
+
listId: string;
|
|
37
|
+
recordId: string;
|
|
38
|
+
position: number;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
createdByActorType: string;
|
|
41
|
+
createdByActorId: string | null;
|
|
42
|
+
values: Record<string, CrmValue>;
|
|
43
|
+
valuesSince: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CrmRecordPageListAttribute {
|
|
47
|
+
id: string;
|
|
48
|
+
apiSlug: string;
|
|
49
|
+
label: string;
|
|
50
|
+
description: string | null;
|
|
51
|
+
attributeType: CrmAttributeType;
|
|
52
|
+
multi: boolean;
|
|
53
|
+
required: boolean;
|
|
54
|
+
position: number;
|
|
55
|
+
usesOptions: boolean;
|
|
56
|
+
options: CrmAttributeOption[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CrmRecordPageList {
|
|
60
|
+
id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
apiSlug: string;
|
|
63
|
+
parentObjectType: string;
|
|
64
|
+
attributes: CrmRecordPageListAttribute[];
|
|
65
|
+
entries: CrmRecordPageEntry[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CrmRecordPageValueMeta {
|
|
69
|
+
since: string;
|
|
70
|
+
actorType: string;
|
|
71
|
+
actorId: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CrmRecordPage {
|
|
75
|
+
record: {
|
|
76
|
+
id: string;
|
|
77
|
+
connectionId: string;
|
|
78
|
+
provider: string;
|
|
79
|
+
objectType: string;
|
|
80
|
+
kind: string;
|
|
81
|
+
displayName: string;
|
|
82
|
+
remoteRevision: string | null;
|
|
83
|
+
updatedAt: string;
|
|
84
|
+
};
|
|
85
|
+
attributes: CrmAttributeDefinition[];
|
|
86
|
+
values: Record<string, CrmValue>;
|
|
87
|
+
valueMeta: Record<string, CrmRecordPageValueMeta>;
|
|
88
|
+
lists: CrmRecordPageList[];
|
|
89
|
+
listMembershipsTruncated: boolean;
|
|
90
|
+
recordUrl: string | null;
|
|
91
|
+
recordUrlUnavailableReason: string | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Editability
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Controls this panel implements. The registry says a `record-reference` is
|
|
100
|
+
* edited with a record picker; a labelled panel row has no picker yet, so the
|
|
101
|
+
* field is honestly locked here instead of offering a text input that would
|
|
102
|
+
* store whatever display name the user typed.
|
|
103
|
+
*/
|
|
104
|
+
const PANEL_CONTROLS: ReadonlySet<CrmAttributeControl> = new Set([
|
|
105
|
+
"text",
|
|
106
|
+
"number",
|
|
107
|
+
"checkbox",
|
|
108
|
+
"date",
|
|
109
|
+
"datetime",
|
|
110
|
+
"options",
|
|
111
|
+
"rating",
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
export type FieldLockReason =
|
|
115
|
+
| "archived"
|
|
116
|
+
| "read-only"
|
|
117
|
+
| "redacted"
|
|
118
|
+
| "provider-owned"
|
|
119
|
+
| "derived"
|
|
120
|
+
| "unsupported-type";
|
|
121
|
+
|
|
122
|
+
export type FieldEditability =
|
|
123
|
+
| { editable: true }
|
|
124
|
+
| { editable: false; reason: FieldLockReason };
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Mirrors what `update-crm-record` will actually accept for `target: "local"`.
|
|
128
|
+
* Offering an editor the action would reject is the same class of lie as a
|
|
129
|
+
* success-shaped failure.
|
|
130
|
+
*/
|
|
131
|
+
export function fieldEditability(
|
|
132
|
+
attribute: Pick<
|
|
133
|
+
CrmAttributeDefinition,
|
|
134
|
+
"attributeType" | "archived" | "storagePolicy" | "updateable"
|
|
135
|
+
>,
|
|
136
|
+
): FieldEditability {
|
|
137
|
+
if (attribute.archived) return { editable: false, reason: "archived" };
|
|
138
|
+
if (attribute.storagePolicy === "redacted")
|
|
139
|
+
return { editable: false, reason: "redacted" };
|
|
140
|
+
if (!attribute.updateable) return { editable: false, reason: "read-only" };
|
|
141
|
+
if (attribute.storagePolicy === "derived-local")
|
|
142
|
+
return { editable: false, reason: "derived" };
|
|
143
|
+
if (attribute.storagePolicy !== "local-authoritative")
|
|
144
|
+
return { editable: false, reason: "provider-owned" };
|
|
145
|
+
if (
|
|
146
|
+
!PANEL_CONTROLS.has(ATTRIBUTE_VALUE_SPECS[attribute.attributeType].control)
|
|
147
|
+
)
|
|
148
|
+
return { editable: false, reason: "unsupported-type" };
|
|
149
|
+
return { editable: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Display
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
export type FieldDisplay =
|
|
157
|
+
| { kind: "empty" }
|
|
158
|
+
| { kind: "boolean"; value: boolean }
|
|
159
|
+
| { kind: "text"; text: string }
|
|
160
|
+
| { kind: "tokens"; tokens: Array<{ label: string; color?: string }> }
|
|
161
|
+
| { kind: "structured"; text: string };
|
|
162
|
+
|
|
163
|
+
const STRUCTURED_PREVIEW_LIMIT = 200;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The panel's own display union. Which chips a value produces, how a currency
|
|
167
|
+
* or a timestamp reads, and which option a value resolves to all come from the
|
|
168
|
+
* shared registry — only the union the panel renders against lives here.
|
|
169
|
+
*/
|
|
170
|
+
export function formatFieldValue(
|
|
171
|
+
attribute: Pick<
|
|
172
|
+
CrmAttributeDefinition,
|
|
173
|
+
"attributeType" | "multi" | "options" | "config"
|
|
174
|
+
>,
|
|
175
|
+
value: CrmValue | undefined,
|
|
176
|
+
): FieldDisplay {
|
|
177
|
+
if (value === undefined || value === null) return { kind: "empty" };
|
|
178
|
+
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
const tokens = valueTokens(attribute, value);
|
|
181
|
+
return tokens.length ? { kind: "tokens", tokens } : { kind: "empty" };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (typeof value === "boolean") return { kind: "boolean", value };
|
|
185
|
+
|
|
186
|
+
if (typeof value === "object") {
|
|
187
|
+
const text = formatAttributeValue(attribute, value);
|
|
188
|
+
return {
|
|
189
|
+
kind: "structured",
|
|
190
|
+
text:
|
|
191
|
+
text.length > STRUCTURED_PREVIEW_LIMIT
|
|
192
|
+
? `${text.slice(0, STRUCTURED_PREVIEW_LIMIT)}…`
|
|
193
|
+
: text,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (value === "") return { kind: "empty" };
|
|
198
|
+
if (ATTRIBUTE_TYPE_SPECS[attribute.attributeType].usesOptions)
|
|
199
|
+
return { kind: "tokens", tokens: valueTokens(attribute, value) };
|
|
200
|
+
return { kind: "text", text: formatAttributeValue(attribute, value) };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Input parsing
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
export type FieldParseResult =
|
|
208
|
+
| { ok: true; value: CrmValue }
|
|
209
|
+
| {
|
|
210
|
+
ok: false;
|
|
211
|
+
code: "not-a-number" | "not-a-date" | "unknown-option" | "not-editable";
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Parse a raw editor value into the typed `CrmValue` the action expects.
|
|
216
|
+
* `multi` attributes take a comma-separated list.
|
|
217
|
+
*
|
|
218
|
+
* The editability gate runs first, so this panel never parses a field
|
|
219
|
+
* `update-crm-record` would refuse; everything past it is the shared parser.
|
|
220
|
+
*
|
|
221
|
+
* ponytail: comma-separated text for multi values instead of a token input —
|
|
222
|
+
* upgrade to a real token editor when a customer keeps commas inside a value.
|
|
223
|
+
*/
|
|
224
|
+
export function parseFieldInput(
|
|
225
|
+
attribute: Pick<
|
|
226
|
+
CrmAttributeDefinition,
|
|
227
|
+
| "attributeType"
|
|
228
|
+
| "multi"
|
|
229
|
+
| "options"
|
|
230
|
+
| "archived"
|
|
231
|
+
| "storagePolicy"
|
|
232
|
+
| "updateable"
|
|
233
|
+
>,
|
|
234
|
+
raw: string | boolean,
|
|
235
|
+
): FieldParseResult {
|
|
236
|
+
if (!fieldEditability(attribute).editable)
|
|
237
|
+
return { ok: false, code: "not-editable" };
|
|
238
|
+
if (typeof raw === "boolean") return { ok: true, value: raw };
|
|
239
|
+
|
|
240
|
+
const parsed = parseAttributeValue(attribute, raw);
|
|
241
|
+
if (parsed.ok) return { ok: true, value: parsed.value };
|
|
242
|
+
// `read-only` cannot come back here — the gate above already refused those —
|
|
243
|
+
// but mapping it keeps a future registry change from turning into a wrong
|
|
244
|
+
// "invalid number" message.
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
code: parsed.reason === "read-only" ? "not-editable" : parsed.reason,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* A list attribute in the shape the field editors take. List attributes are
|
|
253
|
+
* local-authoritative on every backend by construction — `loadCrmListAttributes`
|
|
254
|
+
* refuses to return one that is not — so they carry no provider authority to
|
|
255
|
+
* check, and the summary the action returns simply omits those columns.
|
|
256
|
+
*/
|
|
257
|
+
export function entryAttributeAsEditable(
|
|
258
|
+
attribute: CrmRecordPageListAttribute,
|
|
259
|
+
): Pick<
|
|
260
|
+
CrmAttributeDefinition,
|
|
261
|
+
| "apiSlug"
|
|
262
|
+
| "label"
|
|
263
|
+
| "attributeType"
|
|
264
|
+
| "multi"
|
|
265
|
+
| "options"
|
|
266
|
+
| "config"
|
|
267
|
+
| "archived"
|
|
268
|
+
| "storagePolicy"
|
|
269
|
+
| "updateable"
|
|
270
|
+
| "required"
|
|
271
|
+
> {
|
|
272
|
+
return {
|
|
273
|
+
apiSlug: attribute.apiSlug,
|
|
274
|
+
label: attribute.label,
|
|
275
|
+
attributeType: attribute.attributeType,
|
|
276
|
+
multi: attribute.multi,
|
|
277
|
+
options: attribute.options,
|
|
278
|
+
config: {},
|
|
279
|
+
archived: false,
|
|
280
|
+
storagePolicy: "local-authoritative",
|
|
281
|
+
updateable: true,
|
|
282
|
+
required: attribute.required,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Highlights
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Curated highlight order per object `kind`. An attribute this app's schema
|
|
292
|
+
* does not declare for that kind (e.g. `people` has no `name`) is simply
|
|
293
|
+
* skipped by `splitHighlights`, which is how "name, or firstName+lastName
|
|
294
|
+
* when there is no name" falls out of one flat list instead of a branch.
|
|
295
|
+
*/
|
|
296
|
+
const HIGHLIGHT_ORDER: Record<string, readonly string[]> = {
|
|
297
|
+
account: ["name", "domain", "industry", "ownerName", "nextContactAt"],
|
|
298
|
+
person: [
|
|
299
|
+
"name",
|
|
300
|
+
"firstName",
|
|
301
|
+
"lastName",
|
|
302
|
+
"email",
|
|
303
|
+
"title",
|
|
304
|
+
"accountId",
|
|
305
|
+
"ownerName",
|
|
306
|
+
],
|
|
307
|
+
opportunity: ["name", "amount", "stage", "closeDate", "ownerName"],
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function hasHighlightValue(value: CrmValue | undefined): boolean {
|
|
311
|
+
if (value === undefined || value === null || value === "") return false;
|
|
312
|
+
return !Array.isArray(value) || value.length > 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Highlights are a curated order for `account`/`person`/`opportunity`, and the
|
|
317
|
+
* plain position order for `custom` or any other kind. There is no pinned
|
|
318
|
+
* column on `crm_field_policies`, and inventing one here would be a schema
|
|
319
|
+
* change in a table another slice owns — reordering the attribute is how a
|
|
320
|
+
* user pins one today.
|
|
321
|
+
*
|
|
322
|
+
* A curated slot the schema does not declare is skipped, and leftover slots
|
|
323
|
+
* backfill from position order, preferring an attribute that already carries
|
|
324
|
+
* a value — otherwise a schema with a short curated list would fill its
|
|
325
|
+
* remaining slots with guaranteed-empty attributes, reproducing the "mostly
|
|
326
|
+
* Empty" bug this replaces.
|
|
327
|
+
*/
|
|
328
|
+
export function splitHighlights<
|
|
329
|
+
T extends { apiSlug: string; position: number },
|
|
330
|
+
>(
|
|
331
|
+
attributes: T[],
|
|
332
|
+
options: {
|
|
333
|
+
kind?: string;
|
|
334
|
+
values?: Record<string, CrmValue>;
|
|
335
|
+
max?: number;
|
|
336
|
+
} = {},
|
|
337
|
+
): { highlights: T[]; rest: T[] } {
|
|
338
|
+
const { kind = "custom", values = {}, max = MAX_HIGHLIGHTS } = options;
|
|
339
|
+
const ordered = [...attributes].sort((a, b) => a.position - b.position);
|
|
340
|
+
const curated = HIGHLIGHT_ORDER[kind];
|
|
341
|
+
if (!curated) {
|
|
342
|
+
return { highlights: ordered.slice(0, max), rest: ordered.slice(max) };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const byApiSlug = new Map(
|
|
346
|
+
ordered.map((attribute) => [attribute.apiSlug, attribute]),
|
|
347
|
+
);
|
|
348
|
+
const picked: T[] = [];
|
|
349
|
+
const pickedSlugs = new Set<string>();
|
|
350
|
+
for (const slug of curated) {
|
|
351
|
+
if (picked.length >= max) break;
|
|
352
|
+
const attribute = byApiSlug.get(slug);
|
|
353
|
+
if (!attribute) continue;
|
|
354
|
+
picked.push(attribute);
|
|
355
|
+
pickedSlugs.add(slug);
|
|
356
|
+
}
|
|
357
|
+
if (picked.length < max) {
|
|
358
|
+
const filler = ordered
|
|
359
|
+
.filter((attribute) => !pickedSlugs.has(attribute.apiSlug))
|
|
360
|
+
.sort((a, b) => {
|
|
361
|
+
const byValue =
|
|
362
|
+
Number(hasHighlightValue(values[b.apiSlug])) -
|
|
363
|
+
Number(hasHighlightValue(values[a.apiSlug]));
|
|
364
|
+
return byValue !== 0 ? byValue : a.position - b.position;
|
|
365
|
+
});
|
|
366
|
+
for (const attribute of filler) {
|
|
367
|
+
if (picked.length >= max) break;
|
|
368
|
+
picked.push(attribute);
|
|
369
|
+
pickedSlugs.add(attribute.apiSlug);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
highlights: picked,
|
|
374
|
+
rest: ordered.filter((attribute) => !pickedSlugs.has(attribute.apiSlug)),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// Duplicate attributes
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
function sameCrmValue(a: CrmValue, b: CrmValue | undefined): boolean {
|
|
383
|
+
if (a === b) return true;
|
|
384
|
+
if (a === null || b === null || b === undefined) return false;
|
|
385
|
+
if (typeof a === "object" || typeof b === "object") {
|
|
386
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* `displayName` duplicates `name` when the native adapter minted both from
|
|
393
|
+
* the same write (`server/crm/native-adapter.ts` has stopped minting
|
|
394
|
+
* `displayName` going forward, but existing rows still carry both). Suppress
|
|
395
|
+
* only when `name` has a real value equal to `displayName` — an absent or
|
|
396
|
+
* cleared `name` must not read the same as a suppressed duplicate, so
|
|
397
|
+
* `displayName` keeps showing.
|
|
398
|
+
*/
|
|
399
|
+
export function isSuppressedDuplicateAttribute(
|
|
400
|
+
apiSlug: string,
|
|
401
|
+
values: Record<string, CrmValue>,
|
|
402
|
+
): boolean {
|
|
403
|
+
if (apiSlug !== "displayName") return false;
|
|
404
|
+
const name = values.name;
|
|
405
|
+
if (!name) return false;
|
|
406
|
+
return sameCrmValue(name, values.displayName);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Drop attributes the panel should not render twice — see above. */
|
|
410
|
+
export function withoutSuppressedDuplicates<T extends { apiSlug: string }>(
|
|
411
|
+
attributes: T[],
|
|
412
|
+
values: Record<string, CrmValue>,
|
|
413
|
+
): T[] {
|
|
414
|
+
return attributes.filter(
|
|
415
|
+
(attribute) => !isSuppressedDuplicateAttribute(attribute.apiSlug, values),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// History
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
export interface FieldHistoryChange {
|
|
424
|
+
id: string;
|
|
425
|
+
value: CrmValue;
|
|
426
|
+
activeFrom: string;
|
|
427
|
+
activeUntil: string | null;
|
|
428
|
+
current: boolean;
|
|
429
|
+
actorType: string;
|
|
430
|
+
actorId: string | null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface FieldHistoryResponse {
|
|
434
|
+
recordId: string;
|
|
435
|
+
entryId: string | null;
|
|
436
|
+
apiSlug: string;
|
|
437
|
+
label: string;
|
|
438
|
+
attributeType: CrmAttributeType;
|
|
439
|
+
multi: boolean;
|
|
440
|
+
historyTracked: boolean;
|
|
441
|
+
changes: FieldHistoryChange[];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export interface FieldHistoryTransition {
|
|
445
|
+
id: string;
|
|
446
|
+
from: CrmValue | undefined;
|
|
447
|
+
to: CrmValue;
|
|
448
|
+
at: string;
|
|
449
|
+
actorType: string;
|
|
450
|
+
actorId: string | null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Turn the newest-first rows into "from → to" transitions. The oldest row has
|
|
455
|
+
* no predecessor, so its `from` is `undefined` — deliberately not `null`, which
|
|
456
|
+
* is a real stored value meaning the field was cleared.
|
|
457
|
+
*/
|
|
458
|
+
export function historyTransitions(
|
|
459
|
+
changes: FieldHistoryChange[],
|
|
460
|
+
): FieldHistoryTransition[] {
|
|
461
|
+
return changes.map((change, index) => {
|
|
462
|
+
const previous = changes[index + 1];
|
|
463
|
+
return {
|
|
464
|
+
id: change.id,
|
|
465
|
+
from: previous ? previous.value : undefined,
|
|
466
|
+
to: change.value,
|
|
467
|
+
at: change.activeFrom,
|
|
468
|
+
actorType: change.actorType,
|
|
469
|
+
actorId: change.actorId,
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// Activity
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
export interface CrmActivityItem {
|
|
479
|
+
id: string;
|
|
480
|
+
title: string;
|
|
481
|
+
summary?: string;
|
|
482
|
+
occurredAt?: string;
|
|
483
|
+
actor?: string;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Nothing in this app writes `crm_interactions` yet, so an empty Activity tab
|
|
488
|
+
* means "we are not ingesting" and never "nothing happened". The UI must print
|
|
489
|
+
* that difference instead of an ambiguous "No activity".
|
|
490
|
+
*/
|
|
491
|
+
export type ActivityState =
|
|
492
|
+
| { kind: "not-ingested" }
|
|
493
|
+
| { kind: "items"; items: CrmActivityItem[] };
|
|
494
|
+
|
|
495
|
+
export function resolveActivityState(
|
|
496
|
+
items: CrmActivityItem[] | undefined,
|
|
497
|
+
): ActivityState {
|
|
498
|
+
return items && items.length
|
|
499
|
+
? { kind: "items", items }
|
|
500
|
+
: { kind: "not-ingested" };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// Optimistic value edits
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
|
|
507
|
+
export interface OptimisticFieldEdit {
|
|
508
|
+
apiSlug: string;
|
|
509
|
+
previousValue: CrmValue | undefined;
|
|
510
|
+
previousMeta: CrmRecordPageValueMeta | undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Apply an edit to a cached page and hand back what is needed to undo it. The
|
|
515
|
+
* caller restores with `rollbackFieldValue` when the action rejects, so a
|
|
516
|
+
* failed write never leaves the panel showing a value the server does not have.
|
|
517
|
+
*/
|
|
518
|
+
export function applyFieldValue(
|
|
519
|
+
page: CrmRecordPage,
|
|
520
|
+
apiSlug: string,
|
|
521
|
+
value: CrmValue,
|
|
522
|
+
meta: CrmRecordPageValueMeta,
|
|
523
|
+
): { page: CrmRecordPage; edit: OptimisticFieldEdit } {
|
|
524
|
+
return {
|
|
525
|
+
page: {
|
|
526
|
+
...page,
|
|
527
|
+
values: { ...page.values, [apiSlug]: value },
|
|
528
|
+
valueMeta: { ...page.valueMeta, [apiSlug]: meta },
|
|
529
|
+
},
|
|
530
|
+
edit: {
|
|
531
|
+
apiSlug,
|
|
532
|
+
previousValue: page.values[apiSlug],
|
|
533
|
+
previousMeta: page.valueMeta[apiSlug],
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function rollbackFieldValue(
|
|
539
|
+
page: CrmRecordPage,
|
|
540
|
+
edit: OptimisticFieldEdit,
|
|
541
|
+
): CrmRecordPage {
|
|
542
|
+
const values = { ...page.values };
|
|
543
|
+
const valueMeta = { ...page.valueMeta };
|
|
544
|
+
if (edit.previousValue === undefined) delete values[edit.apiSlug];
|
|
545
|
+
else values[edit.apiSlug] = edit.previousValue;
|
|
546
|
+
if (edit.previousMeta === undefined) delete valueMeta[edit.apiSlug];
|
|
547
|
+
else valueMeta[edit.apiSlug] = edit.previousMeta;
|
|
548
|
+
return { ...page, values, valueMeta };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function applyEntryValue(
|
|
552
|
+
page: CrmRecordPage,
|
|
553
|
+
entryId: string,
|
|
554
|
+
apiSlug: string,
|
|
555
|
+
value: CrmValue,
|
|
556
|
+
): { page: CrmRecordPage; previousValue: CrmValue | undefined } {
|
|
557
|
+
let previousValue: CrmValue | undefined;
|
|
558
|
+
const lists = page.lists.map((list) => ({
|
|
559
|
+
...list,
|
|
560
|
+
entries: list.entries.map((entry) => {
|
|
561
|
+
if (entry.id !== entryId) return entry;
|
|
562
|
+
previousValue = entry.values[apiSlug];
|
|
563
|
+
return { ...entry, values: { ...entry.values, [apiSlug]: value } };
|
|
564
|
+
}),
|
|
565
|
+
}));
|
|
566
|
+
return { page: { ...page, lists }, previousValue };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function rollbackEntryValue(
|
|
570
|
+
page: CrmRecordPage,
|
|
571
|
+
entryId: string,
|
|
572
|
+
apiSlug: string,
|
|
573
|
+
previousValue: CrmValue | undefined,
|
|
574
|
+
): CrmRecordPage {
|
|
575
|
+
return {
|
|
576
|
+
...page,
|
|
577
|
+
lists: page.lists.map((list) => ({
|
|
578
|
+
...list,
|
|
579
|
+
entries: list.entries.map((entry) => {
|
|
580
|
+
if (entry.id !== entryId) return entry;
|
|
581
|
+
const values = { ...entry.values };
|
|
582
|
+
if (previousValue === undefined) delete values[apiSlug];
|
|
583
|
+
else values[apiSlug] = previousValue;
|
|
584
|
+
return { ...entry, values };
|
|
585
|
+
}),
|
|
586
|
+
})),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// Tabs
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
export const RECORD_TABS = ["activity", "notes", "tasks", "related"] as const;
|
|
595
|
+
|
|
596
|
+
export type RecordTab = (typeof RECORD_TABS)[number];
|
|
597
|
+
|
|
598
|
+
export function isRecordTab(value: unknown): value is RecordTab {
|
|
599
|
+
return (
|
|
600
|
+
typeof value === "string" &&
|
|
601
|
+
(RECORD_TABS as readonly string[]).includes(value)
|
|
602
|
+
);
|
|
603
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { IconDatabaseCog, IconRefresh } from "@tabler/icons-react";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The irreversible-looking corner of CRM settings. It is deliberately thin:
|
|
9
|
+
* CRM has no delete path. Archiving an attribute, an option, or a list keeps
|
|
10
|
+
* every stored value, which is the one thing worth stating here.
|
|
11
|
+
*/
|
|
12
|
+
export function AdvancedSettings() {
|
|
13
|
+
const t = useT();
|
|
14
|
+
return (
|
|
15
|
+
<div className="mx-auto w-full max-w-2xl">
|
|
16
|
+
<h1 className="text-xl font-semibold tracking-tight">
|
|
17
|
+
{t("advanced.title")}
|
|
18
|
+
</h1>
|
|
19
|
+
<p className="mt-1 text-sm leading-6 text-muted-foreground">
|
|
20
|
+
{t("advanced.description")}
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<div className="mt-6 grid gap-3">
|
|
24
|
+
<section className="flex flex-wrap items-center gap-4 rounded-lg border border-border/70 bg-card px-4 py-3.5">
|
|
25
|
+
<IconRefresh className="size-4 shrink-0 text-muted-foreground" />
|
|
26
|
+
<div className="min-w-0 flex-1">
|
|
27
|
+
<p className="text-sm font-medium">{t("advanced.reconfigure")}</p>
|
|
28
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
29
|
+
{t("advanced.reconfigureHelp")}
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
<Button asChild variant="outline" size="sm">
|
|
33
|
+
<Link to="/setup">{t("advanced.openSetup")}</Link>
|
|
34
|
+
</Button>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
<section className="flex flex-wrap items-center gap-4 rounded-lg border border-border/70 bg-card px-4 py-3.5">
|
|
38
|
+
<IconDatabaseCog className="size-4 shrink-0 text-muted-foreground" />
|
|
39
|
+
<div className="min-w-0 flex-1">
|
|
40
|
+
<p className="text-sm font-medium">{t("advanced.retention")}</p>
|
|
41
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
42
|
+
{t("advanced.retentionHelp")}
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</section>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|