@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,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid state → server request, and the optimistic cache patch that goes with a
|
|
3
|
+
* cell write. Kept pure: the grid must never narrow a page it already has, and
|
|
4
|
+
* that is only provable if the request builder is testable on its own.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CrmCellValue, CrmGridAttribute } from "./model";
|
|
8
|
+
|
|
9
|
+
export interface CrmGridSortEntry {
|
|
10
|
+
attributeId?: string;
|
|
11
|
+
field?: string;
|
|
12
|
+
direction: "asc" | "desc";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CrmGridFilterLeaf {
|
|
16
|
+
attributeId?: string;
|
|
17
|
+
field?: string;
|
|
18
|
+
condition: string;
|
|
19
|
+
value?: string | number | boolean | null | Array<string | number | boolean>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CrmGridFilter {
|
|
23
|
+
op: "and" | "or";
|
|
24
|
+
conditions: CrmGridFilterLeaf[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CrmGridQueryState {
|
|
28
|
+
kind?: "account" | "person" | "opportunity";
|
|
29
|
+
connectionId?: string;
|
|
30
|
+
viewId?: string;
|
|
31
|
+
/** Display-name search. Sent to the server; never applied to a loaded page. */
|
|
32
|
+
search?: string;
|
|
33
|
+
filter?: CrmGridFilter;
|
|
34
|
+
sort?: CrmGridSortEntry[];
|
|
35
|
+
pageSize?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const CRM_GRID_PAGE_SIZE = 50;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parameters for `list-crm-records`. Every narrowing input is on the wire —
|
|
42
|
+
* the grid has no local filter path to fall back to, by construction.
|
|
43
|
+
*/
|
|
44
|
+
export function listRecordsParams(
|
|
45
|
+
state: CrmGridQueryState,
|
|
46
|
+
cursor?: string,
|
|
47
|
+
): Record<string, unknown> {
|
|
48
|
+
const search = state.search?.trim();
|
|
49
|
+
return {
|
|
50
|
+
...(state.kind ? { kind: state.kind } : {}),
|
|
51
|
+
...(state.connectionId ? { connectionId: state.connectionId } : {}),
|
|
52
|
+
...(state.viewId ? { viewId: state.viewId } : {}),
|
|
53
|
+
...(search ? { query: search } : {}),
|
|
54
|
+
// A saved view owns its filter; sending both is a 422 at the action.
|
|
55
|
+
...(state.filter && state.filter.conditions.length && !state.viewId
|
|
56
|
+
? { filter: state.filter }
|
|
57
|
+
: {}),
|
|
58
|
+
...(state.sort && state.sort.length ? { sort: state.sort } : {}),
|
|
59
|
+
limit: state.pageSize ?? CRM_GRID_PAGE_SIZE,
|
|
60
|
+
...(cursor ? { cursor } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Column configuration — the saved view's `columns_json`
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export interface CrmGridColumn {
|
|
69
|
+
attributeId: string;
|
|
70
|
+
width?: number;
|
|
71
|
+
hidden?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const MIN_COLUMN_WIDTH = 80;
|
|
75
|
+
const MAX_COLUMN_WIDTH = 720;
|
|
76
|
+
|
|
77
|
+
/** Accepts both the legacy `string[]` and the typed `columns_json` shape. */
|
|
78
|
+
export function normalizeGridColumns(raw: unknown): CrmGridColumn[] {
|
|
79
|
+
if (!Array.isArray(raw)) return [];
|
|
80
|
+
const columns: CrmGridColumn[] = [];
|
|
81
|
+
for (const entry of raw) {
|
|
82
|
+
if (typeof entry === "string" && entry) {
|
|
83
|
+
columns.push({ attributeId: entry });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!entry || typeof entry !== "object") continue;
|
|
87
|
+
const record = entry as Record<string, unknown>;
|
|
88
|
+
const attributeId =
|
|
89
|
+
typeof record.attributeId === "string" ? record.attributeId : null;
|
|
90
|
+
if (!attributeId) continue;
|
|
91
|
+
const width =
|
|
92
|
+
typeof record.width === "number" && Number.isFinite(record.width)
|
|
93
|
+
? Math.min(Math.max(record.width, MIN_COLUMN_WIDTH), MAX_COLUMN_WIDTH)
|
|
94
|
+
: undefined;
|
|
95
|
+
columns.push({
|
|
96
|
+
attributeId,
|
|
97
|
+
...(width === undefined ? {} : { width }),
|
|
98
|
+
...(record.hidden === true ? { hidden: true } : {}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return columns;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The grid's column order: saved columns first, in their saved order, then any
|
|
106
|
+
* attribute the saved view has never seen. A saved column whose attribute no
|
|
107
|
+
* longer exists is dropped rather than rendered as a blank column.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveGridColumns(
|
|
110
|
+
saved: CrmGridColumn[],
|
|
111
|
+
attributes: CrmGridAttribute[],
|
|
112
|
+
): CrmGridColumn[] {
|
|
113
|
+
const known = new Map(
|
|
114
|
+
attributes.map((attribute) => [attribute.apiSlug, attribute]),
|
|
115
|
+
);
|
|
116
|
+
const seen = new Set<string>();
|
|
117
|
+
const ordered: CrmGridColumn[] = [];
|
|
118
|
+
for (const column of saved) {
|
|
119
|
+
if (!known.has(column.attributeId) || seen.has(column.attributeId))
|
|
120
|
+
continue;
|
|
121
|
+
seen.add(column.attributeId);
|
|
122
|
+
ordered.push(column);
|
|
123
|
+
}
|
|
124
|
+
for (const attribute of attributes) {
|
|
125
|
+
if (seen.has(attribute.apiSlug)) continue;
|
|
126
|
+
ordered.push({ attributeId: attribute.apiSlug });
|
|
127
|
+
}
|
|
128
|
+
return ordered;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function setGridColumnWidth(
|
|
132
|
+
columns: CrmGridColumn[],
|
|
133
|
+
attributeId: string,
|
|
134
|
+
width: number,
|
|
135
|
+
): CrmGridColumn[] {
|
|
136
|
+
const clamped = Math.min(
|
|
137
|
+
Math.max(Math.round(width), MIN_COLUMN_WIDTH),
|
|
138
|
+
MAX_COLUMN_WIDTH,
|
|
139
|
+
);
|
|
140
|
+
return columns.map((column) =>
|
|
141
|
+
column.attributeId === attributeId ? { ...column, width: clamped } : column,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function setGridColumnHidden(
|
|
146
|
+
columns: CrmGridColumn[],
|
|
147
|
+
attributeId: string,
|
|
148
|
+
hidden: boolean,
|
|
149
|
+
): CrmGridColumn[] {
|
|
150
|
+
return columns.map((column) =>
|
|
151
|
+
column.attributeId === attributeId
|
|
152
|
+
? hidden
|
|
153
|
+
? { ...column, hidden: true }
|
|
154
|
+
: {
|
|
155
|
+
attributeId: column.attributeId,
|
|
156
|
+
...(column.width ? { width: column.width } : {}),
|
|
157
|
+
}
|
|
158
|
+
: column,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function moveGridColumn(
|
|
163
|
+
columns: CrmGridColumn[],
|
|
164
|
+
attributeId: string,
|
|
165
|
+
toIndex: number,
|
|
166
|
+
): CrmGridColumn[] {
|
|
167
|
+
const from = columns.findIndex(
|
|
168
|
+
(column) => column.attributeId === attributeId,
|
|
169
|
+
);
|
|
170
|
+
if (from < 0) return columns;
|
|
171
|
+
const next = [...columns];
|
|
172
|
+
const [moved] = next.splice(from, 1);
|
|
173
|
+
if (!moved) return columns;
|
|
174
|
+
next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, moved);
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Optimistic cell writes
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
export interface CrmRecordValuesEntry {
|
|
183
|
+
recordId: string;
|
|
184
|
+
remoteRevision?: string | null;
|
|
185
|
+
values: Record<string, CrmCellValue>;
|
|
186
|
+
valuesSince?: Record<string, string>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface CrmRecordValuesPayload {
|
|
190
|
+
records: CrmRecordValuesEntry[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Immutably set one cell in a `list-crm-record-values` payload.
|
|
195
|
+
*
|
|
196
|
+
* Returns the SAME object when no record matched, so a caller can tell "wrote
|
|
197
|
+
* optimistically" from "the row is not in this cache" instead of assuming the
|
|
198
|
+
* patch landed. Rollback is restoring the previous payload.
|
|
199
|
+
*/
|
|
200
|
+
export function patchRecordValues(
|
|
201
|
+
payload: CrmRecordValuesPayload | undefined,
|
|
202
|
+
recordId: string,
|
|
203
|
+
attributeSlug: string,
|
|
204
|
+
value: CrmCellValue,
|
|
205
|
+
): CrmRecordValuesPayload | undefined {
|
|
206
|
+
if (!payload) return payload;
|
|
207
|
+
let matched = false;
|
|
208
|
+
const records = payload.records.map((entry) => {
|
|
209
|
+
if (entry.recordId !== recordId) return entry;
|
|
210
|
+
matched = true;
|
|
211
|
+
return { ...entry, values: { ...entry.values, [attributeSlug]: value } };
|
|
212
|
+
});
|
|
213
|
+
return matched ? { ...payload, records } : payload;
|
|
214
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The left pane: highlights, then the rest of the object's typed attributes,
|
|
3
|
+
* each inline-editable when `update-crm-record` would actually accept it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ExtensionSlot } from "@agent-native/core/client/extensions";
|
|
7
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
8
|
+
import { IconChevronRight } from "@tabler/icons-react";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
CrmAttributeDefinition,
|
|
13
|
+
CrmValue,
|
|
14
|
+
} from "../../../../shared/crm-contract";
|
|
15
|
+
import { FieldEditor, FieldLockNote, FieldValueDisplay } from "./field-editors";
|
|
16
|
+
import { FieldHistoryButton } from "./FieldHistory";
|
|
17
|
+
import {
|
|
18
|
+
fieldEditability,
|
|
19
|
+
splitHighlights,
|
|
20
|
+
withoutSuppressedDuplicates,
|
|
21
|
+
type CrmRecordPage,
|
|
22
|
+
type CrmRecordPageValueMeta,
|
|
23
|
+
} from "./record-data";
|
|
24
|
+
|
|
25
|
+
export function AttributePanel({
|
|
26
|
+
page,
|
|
27
|
+
onCommit,
|
|
28
|
+
}: {
|
|
29
|
+
page: CrmRecordPage;
|
|
30
|
+
onCommit: (attribute: CrmAttributeDefinition, value: CrmValue) => void;
|
|
31
|
+
}) {
|
|
32
|
+
const t = useT();
|
|
33
|
+
const [editingSlug, setEditingSlug] = useState<string | null>(null);
|
|
34
|
+
const attributes = withoutSuppressedDuplicates(page.attributes, page.values);
|
|
35
|
+
const { highlights, rest } = splitHighlights(attributes, {
|
|
36
|
+
kind: page.record.kind,
|
|
37
|
+
values: page.values,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<aside className="flex min-h-full w-full flex-col gap-6 border-border/70 p-5 lg:w-[22rem] lg:shrink-0 lg:border-r">
|
|
42
|
+
<section aria-labelledby="crm-record-highlights">
|
|
43
|
+
<h2
|
|
44
|
+
id="crm-record-highlights"
|
|
45
|
+
className="text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground"
|
|
46
|
+
>
|
|
47
|
+
{t("record.highlights")}
|
|
48
|
+
</h2>
|
|
49
|
+
<div className="mt-3 grid gap-0.5">
|
|
50
|
+
{highlights.length ? (
|
|
51
|
+
highlights.map((attribute) => (
|
|
52
|
+
<AttributeRow
|
|
53
|
+
key={attribute.id}
|
|
54
|
+
recordId={page.record.id}
|
|
55
|
+
attribute={attribute}
|
|
56
|
+
value={page.values[attribute.apiSlug]}
|
|
57
|
+
meta={page.valueMeta[attribute.apiSlug]}
|
|
58
|
+
isEditing={editingSlug === attribute.apiSlug}
|
|
59
|
+
onStartEdit={() => setEditingSlug(attribute.apiSlug)}
|
|
60
|
+
onDone={() => setEditingSlug(null)}
|
|
61
|
+
onCommit={(value) => onCommit(attribute, value)}
|
|
62
|
+
/>
|
|
63
|
+
))
|
|
64
|
+
) : (
|
|
65
|
+
<p className="py-3 text-sm text-muted-foreground">
|
|
66
|
+
{t("record.noAttributes")}
|
|
67
|
+
</p>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
{rest.length ? (
|
|
73
|
+
<details open className="group/details">
|
|
74
|
+
<summary className="flex cursor-pointer list-none items-center gap-1.5 text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground">
|
|
75
|
+
<IconChevronRight className="size-3.5 transition-transform group-open/details:rotate-90" />
|
|
76
|
+
{t("record.allAttributes")}
|
|
77
|
+
</summary>
|
|
78
|
+
<div className="mt-3 grid gap-0.5">
|
|
79
|
+
{rest.map((attribute) => (
|
|
80
|
+
<AttributeRow
|
|
81
|
+
key={attribute.id}
|
|
82
|
+
recordId={page.record.id}
|
|
83
|
+
attribute={attribute}
|
|
84
|
+
value={page.values[attribute.apiSlug]}
|
|
85
|
+
meta={page.valueMeta[attribute.apiSlug]}
|
|
86
|
+
isEditing={editingSlug === attribute.apiSlug}
|
|
87
|
+
onStartEdit={() => setEditingSlug(attribute.apiSlug)}
|
|
88
|
+
onDone={() => setEditingSlug(null)}
|
|
89
|
+
onCommit={(value) => onCommit(attribute, value)}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</details>
|
|
94
|
+
) : null}
|
|
95
|
+
|
|
96
|
+
{/* Stable extension contract: changing this slot id or context is a migration. */}
|
|
97
|
+
<ExtensionSlot
|
|
98
|
+
id="crm.record-sidebar.bottom"
|
|
99
|
+
context={{
|
|
100
|
+
recordId: page.record.id,
|
|
101
|
+
objectType: page.record.objectType,
|
|
102
|
+
kind: page.record.kind,
|
|
103
|
+
displayName: page.record.displayName,
|
|
104
|
+
provider: page.record.provider,
|
|
105
|
+
values: page.values,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</aside>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function AttributeRow({
|
|
113
|
+
recordId,
|
|
114
|
+
attribute,
|
|
115
|
+
value,
|
|
116
|
+
meta,
|
|
117
|
+
isEditing,
|
|
118
|
+
onStartEdit,
|
|
119
|
+
onDone,
|
|
120
|
+
onCommit,
|
|
121
|
+
}: {
|
|
122
|
+
recordId: string;
|
|
123
|
+
attribute: CrmAttributeDefinition;
|
|
124
|
+
value: CrmValue | undefined;
|
|
125
|
+
meta: CrmRecordPageValueMeta | undefined;
|
|
126
|
+
isEditing: boolean;
|
|
127
|
+
onStartEdit: () => void;
|
|
128
|
+
onDone: () => void;
|
|
129
|
+
onCommit: (value: CrmValue) => void;
|
|
130
|
+
}) {
|
|
131
|
+
const t = useT();
|
|
132
|
+
const editability = fieldEditability(attribute);
|
|
133
|
+
// Options and checkboxes are always live controls; typing controls swap in
|
|
134
|
+
// only once the row is activated, so the panel stays readable at rest.
|
|
135
|
+
const alwaysLive =
|
|
136
|
+
editability.editable &&
|
|
137
|
+
(attribute.attributeType === "checkbox" ||
|
|
138
|
+
(!attribute.multi &&
|
|
139
|
+
(attribute.attributeType === "status" ||
|
|
140
|
+
attribute.attributeType === "select")));
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="group grid grid-cols-[minmax(6.5rem,0.8fr)_minmax(0,1.2fr)] items-start gap-3 rounded-md px-2 py-1.5 hover:bg-muted/50">
|
|
144
|
+
<div className="flex min-w-0 items-start gap-1 pt-1">
|
|
145
|
+
{/* Wrapped, not truncated: a label column this narrow truncates long
|
|
146
|
+
attribute names (e.g. "Desired Cadence Days") past readability, and
|
|
147
|
+
a hover-only tooltip would hide the same text at rest. Wrapping
|
|
148
|
+
keeps the two-column grid and just grows the row. */}
|
|
149
|
+
<span
|
|
150
|
+
className="text-sm text-muted-foreground"
|
|
151
|
+
title={attribute.description ?? attribute.label}
|
|
152
|
+
>
|
|
153
|
+
{attribute.label}
|
|
154
|
+
</span>
|
|
155
|
+
{attribute.historyTracked ? (
|
|
156
|
+
<FieldHistoryButton recordId={recordId} attribute={attribute} />
|
|
157
|
+
) : null}
|
|
158
|
+
</div>
|
|
159
|
+
<div className="min-w-0 text-sm">
|
|
160
|
+
{alwaysLive || isEditing ? (
|
|
161
|
+
<FieldEditor
|
|
162
|
+
attribute={attribute}
|
|
163
|
+
value={value}
|
|
164
|
+
autoFocus={isEditing}
|
|
165
|
+
onCommit={onCommit}
|
|
166
|
+
onDone={onDone}
|
|
167
|
+
/>
|
|
168
|
+
) : editability.editable ? (
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
className="w-full cursor-pointer rounded px-1 py-1 text-left hover:bg-background"
|
|
172
|
+
onClick={onStartEdit}
|
|
173
|
+
>
|
|
174
|
+
<FieldValueDisplay attribute={attribute} value={value} />
|
|
175
|
+
</button>
|
|
176
|
+
) : (
|
|
177
|
+
<div className="grid gap-1 px-1 py-1">
|
|
178
|
+
<FieldValueDisplay attribute={attribute} value={value} />
|
|
179
|
+
<FieldLockNote reason={editability.reason} />
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
{meta && !isEditing ? (
|
|
183
|
+
<p className="mt-0.5 px-1 text-[11px] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100">
|
|
184
|
+
{t("record.lastSetBy", {
|
|
185
|
+
actor: meta.actorId ?? meta.actorType,
|
|
186
|
+
})}
|
|
187
|
+
</p>
|
|
188
|
+
) : null}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "changed from X to Y" affordance next to a history-tracked attribute.
|
|
3
|
+
*
|
|
4
|
+
* A dialog rather than a hover card: the template ships no popover primitive,
|
|
5
|
+
* and the house rule forbids hand-rolling one. History is fetched only when the
|
|
6
|
+
* dialog opens — the panel would otherwise issue one query per attribute.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
10
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
11
|
+
import { IconHistory } from "@tabler/icons-react";
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import {
|
|
16
|
+
Dialog,
|
|
17
|
+
DialogContent,
|
|
18
|
+
DialogDescription,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
DialogTrigger,
|
|
22
|
+
} from "@/components/ui/dialog";
|
|
23
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
24
|
+
|
|
25
|
+
import type { CrmAttributeDefinition } from "../../../../shared/crm-contract";
|
|
26
|
+
import { FieldValueDisplay } from "./field-editors";
|
|
27
|
+
import { historyTransitions, type FieldHistoryResponse } from "./record-data";
|
|
28
|
+
|
|
29
|
+
export function FieldHistoryButton({
|
|
30
|
+
recordId,
|
|
31
|
+
entryId,
|
|
32
|
+
attribute,
|
|
33
|
+
}: {
|
|
34
|
+
recordId: string;
|
|
35
|
+
entryId?: string;
|
|
36
|
+
attribute: Pick<
|
|
37
|
+
CrmAttributeDefinition,
|
|
38
|
+
| "apiSlug"
|
|
39
|
+
| "label"
|
|
40
|
+
| "attributeType"
|
|
41
|
+
| "multi"
|
|
42
|
+
| "options"
|
|
43
|
+
| "config"
|
|
44
|
+
| "archived"
|
|
45
|
+
| "storagePolicy"
|
|
46
|
+
| "updateable"
|
|
47
|
+
| "required"
|
|
48
|
+
>;
|
|
49
|
+
}) {
|
|
50
|
+
const t = useT();
|
|
51
|
+
const [open, setOpen] = useState(false);
|
|
52
|
+
const query = useActionQuery<FieldHistoryResponse>(
|
|
53
|
+
"list-crm-record-field-history" as never,
|
|
54
|
+
{
|
|
55
|
+
recordId,
|
|
56
|
+
apiSlug: attribute.apiSlug,
|
|
57
|
+
...(entryId ? { entryId } : {}),
|
|
58
|
+
} as never,
|
|
59
|
+
{ enabled: open },
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
64
|
+
<DialogTrigger asChild>
|
|
65
|
+
<Button
|
|
66
|
+
variant="ghost"
|
|
67
|
+
size="icon"
|
|
68
|
+
className="size-6 shrink-0 cursor-pointer text-muted-foreground opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
|
|
69
|
+
aria-label={t("record.viewHistory", { field: attribute.label })}
|
|
70
|
+
>
|
|
71
|
+
<IconHistory className="size-3.5" />
|
|
72
|
+
</Button>
|
|
73
|
+
</DialogTrigger>
|
|
74
|
+
<DialogContent className="sm:max-w-lg">
|
|
75
|
+
<DialogHeader>
|
|
76
|
+
<DialogTitle>
|
|
77
|
+
{t("record.historyTitle", { field: attribute.label })}
|
|
78
|
+
</DialogTitle>
|
|
79
|
+
<DialogDescription>
|
|
80
|
+
{t("record.historyDescription")}
|
|
81
|
+
</DialogDescription>
|
|
82
|
+
</DialogHeader>
|
|
83
|
+
<FieldHistoryBody attribute={attribute} query={query} />
|
|
84
|
+
</DialogContent>
|
|
85
|
+
</Dialog>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function FieldHistoryBody({
|
|
90
|
+
attribute,
|
|
91
|
+
query,
|
|
92
|
+
}: {
|
|
93
|
+
attribute: React.ComponentProps<typeof FieldValueDisplay>["attribute"] & {
|
|
94
|
+
label: string;
|
|
95
|
+
};
|
|
96
|
+
query: {
|
|
97
|
+
data?: FieldHistoryResponse;
|
|
98
|
+
isLoading: boolean;
|
|
99
|
+
error: unknown;
|
|
100
|
+
};
|
|
101
|
+
}) {
|
|
102
|
+
const t = useT();
|
|
103
|
+
if (query.isLoading) return <Skeleton className="h-24 w-full" />;
|
|
104
|
+
if (query.error)
|
|
105
|
+
return (
|
|
106
|
+
<p className="text-sm text-destructive">
|
|
107
|
+
{query.error instanceof Error
|
|
108
|
+
? query.error.message
|
|
109
|
+
: t("record.historyLoadFailed")}
|
|
110
|
+
</p>
|
|
111
|
+
);
|
|
112
|
+
const data = query.data;
|
|
113
|
+
if (!data) return null;
|
|
114
|
+
// Not tracked and no changes are different answers: one says history is off
|
|
115
|
+
// for this attribute, the other says it is on and empty.
|
|
116
|
+
if (!data.historyTracked)
|
|
117
|
+
return (
|
|
118
|
+
<p className="text-sm text-muted-foreground">
|
|
119
|
+
{t("record.historyNotTracked")}
|
|
120
|
+
</p>
|
|
121
|
+
);
|
|
122
|
+
const transitions = historyTransitions(data.changes);
|
|
123
|
+
if (!transitions.length)
|
|
124
|
+
return (
|
|
125
|
+
<p className="text-sm text-muted-foreground">
|
|
126
|
+
{t("record.historyEmpty")}
|
|
127
|
+
</p>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<ol className="divide-y divide-border/70 overflow-y-auto rounded-md border border-border/70">
|
|
132
|
+
{transitions.map((transition) => (
|
|
133
|
+
<li key={transition.id} className="grid gap-1 px-3 py-2.5 text-sm">
|
|
134
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
135
|
+
{transition.from === undefined ? (
|
|
136
|
+
<span className="text-muted-foreground">
|
|
137
|
+
{t("record.historySet")}
|
|
138
|
+
</span>
|
|
139
|
+
) : (
|
|
140
|
+
<>
|
|
141
|
+
<span className="text-muted-foreground">
|
|
142
|
+
<FieldValueDisplay
|
|
143
|
+
attribute={attribute}
|
|
144
|
+
value={transition.from}
|
|
145
|
+
/>
|
|
146
|
+
</span>
|
|
147
|
+
<span aria-hidden className="text-muted-foreground">
|
|
148
|
+
→
|
|
149
|
+
</span>
|
|
150
|
+
</>
|
|
151
|
+
)}
|
|
152
|
+
<span className="font-medium">
|
|
153
|
+
<FieldValueDisplay attribute={attribute} value={transition.to} />
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
<p className="text-xs text-muted-foreground">
|
|
157
|
+
{t("record.historyBy", {
|
|
158
|
+
actor: transition.actorId ?? transition.actorType,
|
|
159
|
+
when: formatWhen(transition.at),
|
|
160
|
+
})}
|
|
161
|
+
</p>
|
|
162
|
+
</li>
|
|
163
|
+
))}
|
|
164
|
+
</ol>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function formatWhen(value: string): string {
|
|
169
|
+
const date = new Date(value);
|
|
170
|
+
return Number.isNaN(date.valueOf())
|
|
171
|
+
? value
|
|
172
|
+
: date.toLocaleString(undefined, {
|
|
173
|
+
month: "short",
|
|
174
|
+
day: "numeric",
|
|
175
|
+
year: "numeric",
|
|
176
|
+
hour: "numeric",
|
|
177
|
+
minute: "2-digit",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record header: identity, provenance, the upstream deep link, quick actions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
6
|
+
import { IconExternalLink } from "@tabler/icons-react";
|
|
7
|
+
|
|
8
|
+
import { Badge } from "@/components/ui/badge";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import type { CrmRecordDetail } from "@/lib/types";
|
|
11
|
+
|
|
12
|
+
const PROVIDER_LABELS: Record<string, string> = {
|
|
13
|
+
hubspot: "HubSpot",
|
|
14
|
+
salesforce: "Salesforce",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function RecordHeader({
|
|
18
|
+
record,
|
|
19
|
+
recordUrl,
|
|
20
|
+
recordUrlUnavailableReason,
|
|
21
|
+
actions,
|
|
22
|
+
}: {
|
|
23
|
+
record: CrmRecordDetail;
|
|
24
|
+
recordUrl: string | null;
|
|
25
|
+
recordUrlUnavailableReason: string | null;
|
|
26
|
+
actions?: React.ReactNode;
|
|
27
|
+
}) {
|
|
28
|
+
const t = useT();
|
|
29
|
+
const providerLabel = record.provider
|
|
30
|
+
? PROVIDER_LABELS[record.provider]
|
|
31
|
+
: undefined;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<header className="border-b border-border/70 px-5 py-4 sm:px-6">
|
|
35
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
36
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
37
|
+
<RecordAvatar name={record.displayName} kind={record.kind} />
|
|
38
|
+
<div className="min-w-0">
|
|
39
|
+
<h1 className="truncate text-xl font-semibold tracking-tight">
|
|
40
|
+
{record.displayName}
|
|
41
|
+
</h1>
|
|
42
|
+
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
|
|
43
|
+
<span className="capitalize">{record.kind}</span>
|
|
44
|
+
{record.stage ? (
|
|
45
|
+
<Badge variant="secondary" className="font-normal">
|
|
46
|
+
{record.stage}
|
|
47
|
+
</Badge>
|
|
48
|
+
) : null}
|
|
49
|
+
{providerLabel ? (
|
|
50
|
+
<Badge variant="outline" className="font-normal">
|
|
51
|
+
{t("record.mirroredFrom", { provider: providerLabel })}
|
|
52
|
+
</Badge>
|
|
53
|
+
) : null}
|
|
54
|
+
{record.owner ? (
|
|
55
|
+
<span>{t("record.ownedBy", { owner: record.owner })}</span>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
61
|
+
{/* A native record has no upstream record at all, so it shows no link
|
|
62
|
+
and no "unavailable" note — absent and unavailable are different. */}
|
|
63
|
+
{providerLabel && recordUrl ? (
|
|
64
|
+
<Button asChild variant="outline" size="sm" className="gap-1.5">
|
|
65
|
+
<a href={recordUrl} target="_blank" rel="noopener noreferrer">
|
|
66
|
+
{t("record.openUpstream", { provider: providerLabel })}
|
|
67
|
+
<IconExternalLink className="size-4" />
|
|
68
|
+
</a>
|
|
69
|
+
</Button>
|
|
70
|
+
) : null}
|
|
71
|
+
{providerLabel && !recordUrl && recordUrlUnavailableReason ? (
|
|
72
|
+
<span className="text-xs text-muted-foreground">
|
|
73
|
+
{t("record.upstreamLinkUnavailable", {
|
|
74
|
+
provider: providerLabel,
|
|
75
|
+
reason: recordUrlUnavailableReason,
|
|
76
|
+
})}
|
|
77
|
+
</span>
|
|
78
|
+
) : null}
|
|
79
|
+
{actions}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</header>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function RecordAvatar({ name, kind }: { name: string; kind: string }) {
|
|
87
|
+
const initials = name
|
|
88
|
+
.split(/\s+/)
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.slice(0, 2)
|
|
91
|
+
.map((part) => part[0]?.toUpperCase() ?? "")
|
|
92
|
+
.join("");
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
aria-hidden
|
|
96
|
+
className={`grid size-10 shrink-0 place-items-center text-sm font-semibold text-muted-foreground ${
|
|
97
|
+
kind === "person" ? "rounded-full" : "rounded-lg"
|
|
98
|
+
} bg-muted`}
|
|
99
|
+
>
|
|
100
|
+
{initials || "?"}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|