@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.
Files changed (263) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/templates-meta.ts +12 -0
  5. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  6. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  7. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
  8. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  9. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  10. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
  11. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
  12. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
  13. package/corpus/templates/crm/.env.example +5 -0
  14. package/corpus/templates/crm/AGENTS.md +82 -0
  15. package/corpus/templates/crm/CHANGELOG.md +25 -0
  16. package/corpus/templates/crm/DEVELOPING.md +66 -0
  17. package/corpus/templates/crm/README.md +122 -0
  18. package/corpus/templates/crm/_gitignore +12 -0
  19. package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
  20. package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
  21. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  22. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  23. package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
  24. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  25. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
  26. package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
  27. package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
  28. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  29. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  30. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  31. package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
  32. package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
  33. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  34. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  35. package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
  36. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  37. package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
  38. package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
  39. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  40. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
  41. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  42. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  43. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  44. package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
  45. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  46. package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
  47. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  48. package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
  49. package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
  50. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  51. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  52. package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
  53. package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
  54. package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
  55. package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
  56. package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
  57. package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
  58. package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
  59. package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
  60. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  61. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  62. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  63. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  64. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  65. package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
  66. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  67. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  68. package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
  69. package/corpus/templates/crm/actions/navigate.ts +54 -0
  70. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  71. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  72. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  73. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  74. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  75. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
  76. package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
  77. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
  78. package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
  79. package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
  80. package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
  81. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
  82. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
  83. package/corpus/templates/crm/actions/run.ts +7 -0
  84. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  85. package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
  86. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  87. package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
  88. package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
  89. package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
  90. package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
  91. package/corpus/templates/crm/actions/view-screen.ts +324 -0
  92. package/corpus/templates/crm/agent-native.app-skill.json +69 -0
  93. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  94. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
  95. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
  96. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
  97. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
  98. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
  99. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
  100. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
  101. package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
  102. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  103. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
  104. package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
  105. package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
  106. package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
  107. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
  108. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
  109. package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
  110. package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
  111. package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
  112. package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
  113. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
  114. package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
  115. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
  116. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
  117. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
  118. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
  119. package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
  120. package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
  121. package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
  122. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
  123. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
  124. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
  125. package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
  126. package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
  127. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
  128. package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
  129. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
  130. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
  131. package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
  132. package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
  133. package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
  134. package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
  135. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
  136. package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
  137. package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
  138. package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
  139. package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
  140. package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
  141. package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
  142. package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
  143. package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
  144. package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
  145. package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
  146. package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
  147. package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
  148. package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
  149. package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
  150. package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
  151. package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
  152. package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
  153. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
  154. package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
  155. package/corpus/templates/crm/app/design-system.ts +3 -0
  156. package/corpus/templates/crm/app/entry.client.tsx +19 -0
  157. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  158. package/corpus/templates/crm/app/global.css +167 -0
  159. package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
  160. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
  161. package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
  162. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  163. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  164. package/corpus/templates/crm/app/lib/navigation.ts +17 -0
  165. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  166. package/corpus/templates/crm/app/lib/types.ts +172 -0
  167. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  168. package/corpus/templates/crm/app/root.tsx +93 -0
  169. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  170. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  171. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  172. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  173. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  174. package/corpus/templates/crm/app/routes/lists.tsx +228 -0
  175. package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
  176. package/corpus/templates/crm/app/routes/people.tsx +41 -0
  177. package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
  178. package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
  179. package/corpus/templates/crm/app/routes/records.tsx +64 -0
  180. package/corpus/templates/crm/app/routes/settings.tsx +118 -0
  181. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  182. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  183. package/corpus/templates/crm/app/routes/views.tsx +683 -0
  184. package/corpus/templates/crm/app/routes.ts +19 -0
  185. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  186. 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
  187. package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
  188. package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
  189. package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
  190. package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
  191. package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
  192. 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
  193. package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
  194. package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
  195. package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
  196. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
  197. package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
  198. package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
  199. package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
  200. package/corpus/templates/crm/components.json +20 -0
  201. package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
  202. package/corpus/templates/crm/docs/features/README.md +55 -0
  203. package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
  204. package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
  205. package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
  206. package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
  207. package/corpus/templates/crm/learnings.defaults.md +5 -0
  208. package/corpus/templates/crm/netlify.toml +11 -0
  209. package/corpus/templates/crm/package.json +61 -0
  210. package/corpus/templates/crm/public/favicon.svg +1 -0
  211. package/corpus/templates/crm/public/icon-180.svg +1 -0
  212. package/corpus/templates/crm/public/manifest.json +11 -0
  213. package/corpus/templates/crm/react-router.config.ts +7 -0
  214. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  215. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  216. package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
  217. package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
  218. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
  219. package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
  220. package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
  221. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  222. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  223. package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
  224. package/corpus/templates/crm/server/db/index.ts +191 -0
  225. package/corpus/templates/crm/server/db/schema.ts +647 -0
  226. package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
  227. package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
  228. package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
  229. package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
  230. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  231. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  232. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
  233. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
  234. package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
  235. package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
  236. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  237. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  238. package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
  239. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  240. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  241. package/corpus/templates/crm/server/plugins/db.ts +738 -0
  242. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  243. package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
  244. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  245. package/corpus/templates/crm/shared/crm-contract.ts +417 -0
  246. package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
  247. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  248. package/corpus/templates/crm/ssr-entry.ts +11 -0
  249. package/corpus/templates/crm/tsconfig.json +24 -0
  250. package/corpus/templates/crm/vite.config.ts +15 -0
  251. package/corpus/templates/crm/vitest.config.ts +5 -0
  252. package/corpus/templates/design/app/i18n-data.ts +12 -12
  253. package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
  254. package/dist/cli/templates-meta.d.ts.map +1 -1
  255. package/dist/cli/templates-meta.js +12 -0
  256. package/dist/cli/templates-meta.js.map +1 -1
  257. package/dist/collab/routes.d.ts +1 -1
  258. package/dist/observability/routes.d.ts +3 -3
  259. package/dist/resources/handlers.d.ts +1 -1
  260. package/dist/server/realtime-token.d.ts +1 -1
  261. package/dist/server/transcribe-voice.d.ts +1 -1
  262. package/package.json +1 -1
  263. package/src/cli/templates-meta.ts +12 -0
@@ -0,0 +1,235 @@
1
+ /**
2
+ * List memberships for one record.
3
+ *
4
+ * A record may hold more than one entry in the same list — two open renewals on
5
+ * one account — so every entry is its own card. Collapsing them into a
6
+ * membership flag would silently drop the second pipeline.
7
+ */
8
+
9
+ import {
10
+ useActionMutation,
11
+ useActionQuery,
12
+ } from "@agent-native/core/client/hooks";
13
+ import { useT } from "@agent-native/core/client/i18n";
14
+ import { IconPlus } from "@tabler/icons-react";
15
+ import { useState } from "react";
16
+ import { Link } from "react-router";
17
+ import { toast } from "sonner";
18
+
19
+ import { Button } from "@/components/ui/button";
20
+ import {
21
+ Select,
22
+ SelectContent,
23
+ SelectItem,
24
+ SelectTrigger,
25
+ SelectValue,
26
+ } from "@/components/ui/select";
27
+
28
+ import type { CrmValue } from "../../../../shared/crm-contract";
29
+ import { FieldEditor } from "./field-editors";
30
+ import { FieldHistoryButton } from "./FieldHistory";
31
+ import {
32
+ entryAttributeAsEditable,
33
+ type CrmRecordPage,
34
+ type CrmRecordPageList,
35
+ type CrmRecordPageListAttribute,
36
+ } from "./record-data";
37
+
38
+ interface ListsResponse {
39
+ lists?: Array<{
40
+ id: string;
41
+ name: string;
42
+ parentObjectType: string;
43
+ archived?: boolean;
44
+ }>;
45
+ }
46
+
47
+ export function RecordLists({
48
+ page,
49
+ onEntryCommit,
50
+ }: {
51
+ page: CrmRecordPage;
52
+ onEntryCommit: (
53
+ entryId: string,
54
+ attribute: CrmRecordPageListAttribute,
55
+ value: CrmValue,
56
+ ) => void;
57
+ }) {
58
+ const t = useT();
59
+ return (
60
+ <section className="grid gap-3" aria-labelledby="crm-record-lists">
61
+ <div className="flex items-center justify-between gap-2">
62
+ <h2
63
+ id="crm-record-lists"
64
+ className="text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground"
65
+ >
66
+ {t("record.lists")}
67
+ </h2>
68
+ <AddToListControl page={page} />
69
+ </div>
70
+ {page.lists.length ? (
71
+ page.lists.map((list) => (
72
+ <ListCard
73
+ key={list.id}
74
+ recordId={page.record.id}
75
+ list={list}
76
+ onEntryCommit={onEntryCommit}
77
+ />
78
+ ))
79
+ ) : (
80
+ <p className="text-sm text-muted-foreground">
81
+ {t("record.listsEmpty")}
82
+ </p>
83
+ )}
84
+ {page.listMembershipsTruncated ? (
85
+ <p className="text-xs text-muted-foreground">
86
+ {t("record.listsTruncated")}
87
+ </p>
88
+ ) : null}
89
+ </section>
90
+ );
91
+ }
92
+
93
+ function ListCard({
94
+ recordId,
95
+ list,
96
+ onEntryCommit,
97
+ }: {
98
+ recordId: string;
99
+ list: CrmRecordPageList;
100
+ onEntryCommit: (
101
+ entryId: string,
102
+ attribute: CrmRecordPageListAttribute,
103
+ value: CrmValue,
104
+ ) => void;
105
+ }) {
106
+ const t = useT();
107
+ return (
108
+ <div className="rounded-lg border border-border/70 bg-card">
109
+ <div className="flex items-center justify-between gap-2 border-b border-border/70 px-3 py-2">
110
+ <Link
111
+ to={`/lists/${encodeURIComponent(list.id)}`}
112
+ className="truncate text-sm font-medium hover:underline"
113
+ >
114
+ {list.name}
115
+ </Link>
116
+ <span className="shrink-0 text-xs text-muted-foreground">
117
+ {t("record.entryCount", { count: list.entries.length })}
118
+ </span>
119
+ </div>
120
+ <div className="divide-y divide-border/70">
121
+ {list.entries.map((entry, index) => (
122
+ <div key={entry.id} className="grid gap-1.5 px-3 py-2.5">
123
+ {list.entries.length > 1 ? (
124
+ <p className="text-[11px] uppercase tracking-[0.1em] text-muted-foreground">
125
+ {t("record.entryOrdinal", { index: index + 1 })}
126
+ </p>
127
+ ) : null}
128
+ {list.attributes.length ? (
129
+ list.attributes.map((attribute) => (
130
+ <div
131
+ key={attribute.id}
132
+ className="group grid grid-cols-[minmax(5rem,0.7fr)_minmax(0,1.3fr)] items-center gap-2"
133
+ >
134
+ <div className="flex min-w-0 items-start gap-1">
135
+ {/* An entry attribute is the list's own workflow value, not
136
+ the record's — an opportunity's `Stage` and a pipeline's
137
+ `Stage` are different fields, and moving one does not
138
+ move the other. This label still reads identically to
139
+ the object attribute in the left panel; qualifying it
140
+ needs a new i18n key, which is reported rather than
141
+ added here. */}
142
+ <span className="min-w-0 text-xs text-muted-foreground">
143
+ {attribute.label}
144
+ </span>
145
+ <FieldHistoryButton
146
+ recordId={recordId}
147
+ entryId={entry.id}
148
+ attribute={entryAttributeAsEditable(attribute)}
149
+ />
150
+ </div>
151
+ <FieldEditor
152
+ attribute={entryAttributeAsEditable(attribute)}
153
+ value={entry.values[attribute.apiSlug]}
154
+ onCommit={(value) =>
155
+ onEntryCommit(entry.id, attribute, value)
156
+ }
157
+ />
158
+ </div>
159
+ ))
160
+ ) : (
161
+ <p className="text-xs text-muted-foreground">
162
+ {t("record.listNoAttributes")}
163
+ </p>
164
+ )}
165
+ </div>
166
+ ))}
167
+ </div>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ function AddToListControl({ page }: { page: CrmRecordPage }) {
173
+ const t = useT();
174
+ const [selected, setSelected] = useState<string>("");
175
+ const listsQuery = useActionQuery<ListsResponse>(
176
+ "list-crm-lists" as never,
177
+ { limit: 100 } as never,
178
+ );
179
+ const add = useActionMutation<
180
+ { entryId: string },
181
+ { listId: string; recordId: string }
182
+ >("add-crm-record-to-list" as never);
183
+
184
+ const candidates = (listsQuery.data?.lists ?? []).filter(
185
+ (list) =>
186
+ !list.archived && list.parentObjectType === page.record.objectType,
187
+ );
188
+
189
+ if (listsQuery.error)
190
+ return (
191
+ <span className="text-xs text-destructive">
192
+ {t("record.listsLoadFailed")}
193
+ </span>
194
+ );
195
+ if (!candidates.length) return null;
196
+
197
+ async function submit(listId: string) {
198
+ setSelected("");
199
+ try {
200
+ await add.mutateAsync({ listId, recordId: page.record.id });
201
+ toast.success(t("record.addedToList"));
202
+ } catch (error) {
203
+ toast.error(
204
+ error instanceof Error ? error.message : t("record.addToListFailed"),
205
+ );
206
+ }
207
+ }
208
+
209
+ return (
210
+ <div className="flex items-center gap-1.5">
211
+ <Select
212
+ value={selected}
213
+ onValueChange={(value) => {
214
+ setSelected(value);
215
+ void submit(value);
216
+ }}
217
+ >
218
+ <SelectTrigger
219
+ className="h-7 w-auto gap-1 border-none px-2 text-xs shadow-none"
220
+ aria-label={t("record.addToList")}
221
+ >
222
+ <IconPlus className="size-3.5" />
223
+ <SelectValue placeholder={t("record.addToList")} />
224
+ </SelectTrigger>
225
+ <SelectContent>
226
+ {candidates.map((list) => (
227
+ <SelectItem key={list.id} value={list.id}>
228
+ {list.name}
229
+ </SelectItem>
230
+ ))}
231
+ </SelectContent>
232
+ </Select>
233
+ </div>
234
+ );
235
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * The centre pane: Activity, Notes, Tasks, Related.
3
+ *
4
+ * Two of these are deliberately honest about not existing yet rather than
5
+ * dressed up with a plausible-looking feed:
6
+ * - Activity reads `crm_interactions`, which nothing in this app writes today.
7
+ * - Notes has no table at all; inventing one here would be a schema change.
8
+ */
9
+
10
+ import { useT } from "@agent-native/core/client/i18n";
11
+ import {
12
+ IconChecklist,
13
+ IconHistory,
14
+ IconLink,
15
+ IconNotes,
16
+ } from "@tabler/icons-react";
17
+ import { Link } from "react-router";
18
+
19
+ import { Badge } from "@/components/ui/badge";
20
+ import { Button } from "@/components/ui/button";
21
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
22
+ import type { CrmRecordDetail } from "@/lib/types";
23
+ import { normalizeTasks } from "@/lib/types";
24
+
25
+ import { resolveActivityState, type RecordTab } from "./record-data";
26
+
27
+ export function RecordTabs({
28
+ record,
29
+ tab,
30
+ onTabChange,
31
+ onCompleteTask,
32
+ isCompletingTask,
33
+ }: {
34
+ record: CrmRecordDetail;
35
+ tab: RecordTab;
36
+ onTabChange: (tab: RecordTab) => void;
37
+ onCompleteTask: (taskId: string) => void;
38
+ isCompletingTask: boolean;
39
+ }) {
40
+ const t = useT();
41
+ const tasks = normalizeTasks(record.tasks);
42
+ const activity = resolveActivityState(record.activity);
43
+
44
+ return (
45
+ <Tabs
46
+ value={tab}
47
+ onValueChange={(next) => onTabChange(next as RecordTab)}
48
+ className="flex min-h-full flex-col p-5 sm:p-6"
49
+ >
50
+ <TabsList className="h-9 self-start bg-muted/70">
51
+ <TabsTrigger value="activity" className="gap-1.5">
52
+ <IconHistory className="size-3.5" />
53
+ {t("record.tabActivity")}
54
+ </TabsTrigger>
55
+ <TabsTrigger value="notes" className="gap-1.5">
56
+ <IconNotes className="size-3.5" />
57
+ {t("record.tabNotes")}
58
+ </TabsTrigger>
59
+ <TabsTrigger value="tasks" className="gap-1.5">
60
+ <IconChecklist className="size-3.5" />
61
+ {t("record.tabTasks")}
62
+ </TabsTrigger>
63
+ <TabsTrigger value="related" className="gap-1.5">
64
+ <IconLink className="size-3.5" />
65
+ {t("record.tabRelated")}
66
+ </TabsTrigger>
67
+ </TabsList>
68
+
69
+ <TabsContent value="activity" className="mt-4 max-w-2xl">
70
+ {activity.kind === "not-ingested" ? (
71
+ <ComingSoon
72
+ title={t("record.activityNotIngestedTitle")}
73
+ description={t("record.activityNotIngestedDescription")}
74
+ />
75
+ ) : (
76
+ <div className="divide-y divide-border rounded-lg border border-border/70 bg-card">
77
+ {activity.items.map((item) => (
78
+ <div key={item.id} className="px-4 py-3">
79
+ <p className="text-sm font-medium">{item.title}</p>
80
+ {item.summary ? (
81
+ <p className="mt-1 text-sm text-muted-foreground">
82
+ {item.summary}
83
+ </p>
84
+ ) : null}
85
+ <p className="mt-2 text-xs text-muted-foreground">
86
+ {[
87
+ item.actor,
88
+ item.occurredAt ? formatDate(item.occurredAt) : undefined,
89
+ ]
90
+ .filter(Boolean)
91
+ .join(" · ")}
92
+ </p>
93
+ </div>
94
+ ))}
95
+ </div>
96
+ )}
97
+ </TabsContent>
98
+
99
+ <TabsContent value="notes" className="mt-4 max-w-2xl">
100
+ <ComingSoon
101
+ title={t("record.notesComingSoonTitle")}
102
+ description={t("record.notesComingSoonDescription")}
103
+ />
104
+ </TabsContent>
105
+
106
+ <TabsContent value="tasks" className="mt-4 max-w-2xl">
107
+ <div className="divide-y divide-border rounded-lg border border-border/70 bg-card">
108
+ {tasks.length ? (
109
+ tasks.map((task) => (
110
+ <div key={task.id} className="flex items-center gap-3 px-4 py-3">
111
+ <div className="min-w-0 flex-1">
112
+ <p className="text-sm font-medium">{task.title}</p>
113
+ <p className="mt-0.5 text-xs text-muted-foreground">
114
+ {task.dueAt
115
+ ? t("record.taskDue", { when: formatDate(task.dueAt) })
116
+ : task.status}
117
+ </p>
118
+ </div>
119
+ {task.status === "done" ? (
120
+ <Badge variant="secondary">{t("record.taskDone")}</Badge>
121
+ ) : (
122
+ <Button
123
+ variant="outline"
124
+ size="sm"
125
+ className="cursor-pointer"
126
+ disabled={isCompletingTask}
127
+ onClick={() => onCompleteTask(task.id)}
128
+ >
129
+ {t("record.completeTask")}
130
+ </Button>
131
+ )}
132
+ </div>
133
+ ))
134
+ ) : (
135
+ <p className="px-4 py-8 text-center text-sm text-muted-foreground">
136
+ {t("record.tasksEmpty")}
137
+ </p>
138
+ )}
139
+ </div>
140
+ </TabsContent>
141
+
142
+ <TabsContent value="related" className="mt-4 max-w-2xl">
143
+ <div className="divide-y divide-border rounded-lg border border-border/70 bg-card">
144
+ {record.relatedRecords?.length ? (
145
+ record.relatedRecords.map((related) => (
146
+ <Link
147
+ key={`${related.id}:${related.relationshipType}`}
148
+ to={`/records/${encodeURIComponent(related.id)}`}
149
+ className="flex items-center gap-3 px-4 py-3 hover:bg-muted/50"
150
+ >
151
+ <div className="min-w-0 flex-1">
152
+ <p className="truncate text-sm font-medium">
153
+ {related.displayName}
154
+ </p>
155
+ <p className="mt-0.5 truncate text-xs text-muted-foreground">
156
+ {[
157
+ related.relationshipLabel ?? related.relationshipType,
158
+ related.subtitle,
159
+ ]
160
+ .filter(Boolean)
161
+ .join(" · ")}
162
+ </p>
163
+ </div>
164
+ <Badge variant="secondary" className="font-normal capitalize">
165
+ {related.kind}
166
+ </Badge>
167
+ </Link>
168
+ ))
169
+ ) : (
170
+ <p className="px-4 py-8 text-center text-sm text-muted-foreground">
171
+ {t("record.relatedEmpty")}
172
+ </p>
173
+ )}
174
+ </div>
175
+ </TabsContent>
176
+ </Tabs>
177
+ );
178
+ }
179
+
180
+ function ComingSoon({
181
+ title,
182
+ description,
183
+ }: {
184
+ title: string;
185
+ description: string;
186
+ }) {
187
+ return (
188
+ <div className="rounded-lg border border-dashed border-border/70 bg-card px-5 py-8 text-center">
189
+ <p className="text-sm font-medium">{title}</p>
190
+ <p className="mx-auto mt-1.5 max-w-md text-sm leading-6 text-muted-foreground">
191
+ {description}
192
+ </p>
193
+ </div>
194
+ );
195
+ }
196
+
197
+ function formatDate(value: string) {
198
+ const date = new Date(value);
199
+ return Number.isNaN(date.valueOf())
200
+ ? value
201
+ : date.toLocaleDateString(undefined, {
202
+ month: "short",
203
+ day: "numeric",
204
+ year: "numeric",
205
+ });
206
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * The record panel's display and inline editors.
3
+ *
4
+ * What a value says, which option it resolves to, and how typed input parses
5
+ * back all come from `../shared/attribute-value`, the one registry the
6
+ * spreadsheet grid reads too. This file owns only the panel's own affordances:
7
+ * a labelled full-width row, a shadcn `Select` instead of the grid's popover,
8
+ * and commit-on-blur instead of the grid's commit-and-advance.
9
+ */
10
+
11
+ import { useT } from "@agent-native/core/client/i18n";
12
+ import { IconAlertTriangle } from "@tabler/icons-react";
13
+ import { useEffect, useRef, useState } from "react";
14
+
15
+ import { Input } from "@/components/ui/input";
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from "@/components/ui/select";
23
+ import { Switch } from "@/components/ui/switch";
24
+
25
+ import type {
26
+ CrmAttributeDefinition,
27
+ CrmValue,
28
+ } from "../../../../shared/crm-contract";
29
+ import {
30
+ activeOptions,
31
+ editorDraftFor,
32
+ valueSpecFor,
33
+ } from "../shared/attribute-value";
34
+ import {
35
+ AttributeOptionChip,
36
+ AttributeRating,
37
+ } from "../shared/AttributeValueParts";
38
+ import {
39
+ fieldInputValue,
40
+ formatFieldValue,
41
+ parseFieldInput,
42
+ type FieldLockReason,
43
+ } from "./record-data";
44
+
45
+ /** Sentinel for "clear this option" — shadcn `SelectItem` rejects an empty value. */
46
+ const CLEAR_OPTION = "__crm_clear__";
47
+
48
+ type EditableAttribute = Pick<
49
+ CrmAttributeDefinition,
50
+ | "apiSlug"
51
+ | "attributeType"
52
+ | "multi"
53
+ | "options"
54
+ | "config"
55
+ | "archived"
56
+ | "storagePolicy"
57
+ | "updateable"
58
+ | "required"
59
+ >;
60
+
61
+ export function FieldValueDisplay({
62
+ attribute,
63
+ value,
64
+ }: {
65
+ attribute: EditableAttribute;
66
+ value: CrmValue | undefined;
67
+ }) {
68
+ const t = useT();
69
+ const display = formatFieldValue(attribute, value);
70
+ if (display.kind === "empty")
71
+ return (
72
+ <span className="text-muted-foreground">{t("record.fieldEmpty")}</span>
73
+ );
74
+ if (attribute.attributeType === "rating")
75
+ return <AttributeRating value={value ?? null} />;
76
+ if (display.kind === "boolean")
77
+ return <span>{display.value ? t("record.yes") : t("record.no")}</span>;
78
+ if (display.kind === "tokens")
79
+ return (
80
+ <span className="flex flex-wrap gap-1">
81
+ {display.tokens.map((token, index) => (
82
+ <AttributeOptionChip key={`${token.label}:${index}`} token={token} />
83
+ ))}
84
+ </span>
85
+ );
86
+ if (display.kind === "structured")
87
+ return (
88
+ <code className="break-all text-xs text-muted-foreground">
89
+ {display.text}
90
+ </code>
91
+ );
92
+ return <span className="break-words">{display.text}</span>;
93
+ }
94
+
95
+ const LOCK_KEYS: Record<FieldLockReason, string> = {
96
+ archived: "record.lockArchived",
97
+ "read-only": "record.lockReadOnly",
98
+ redacted: "record.lockRedacted",
99
+ "provider-owned": "record.lockProviderOwned",
100
+ derived: "record.lockDerived",
101
+ "unsupported-type": "record.lockUnsupportedType",
102
+ };
103
+
104
+ export function FieldLockNote({ reason }: { reason: FieldLockReason }) {
105
+ const t = useT();
106
+ return (
107
+ <span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
108
+ <IconAlertTriangle className="size-3.5" />
109
+ {t(LOCK_KEYS[reason])}
110
+ </span>
111
+ );
112
+ }
113
+
114
+ /**
115
+ * One editable value. Options and checkboxes commit on change; free text
116
+ * commits on blur or Enter and reverts on Escape. `onCommit` is expected to be
117
+ * optimistic — nothing here blocks on the write.
118
+ */
119
+ export function FieldEditor({
120
+ attribute,
121
+ value,
122
+ onCommit,
123
+ autoFocus = false,
124
+ onDone,
125
+ }: {
126
+ attribute: EditableAttribute;
127
+ value: CrmValue | undefined;
128
+ onCommit: (next: CrmValue) => void;
129
+ autoFocus?: boolean;
130
+ onDone?: () => void;
131
+ }) {
132
+ const t = useT();
133
+ const seed = fieldInputValue(attribute, value);
134
+ const [state, setState] = useState(() => editorDraftFor(undefined, seed));
135
+ const [error, setError] = useState<string | null>(null);
136
+ const inputRef = useRef<HTMLInputElement>(null);
137
+
138
+ // Re-seeded during render, not from an effect: an effect re-seed lands after
139
+ // the browser has already applied the keystroke it is about to overwrite.
140
+ const current = editorDraftFor(state, seed);
141
+ if (current !== state) setState(current);
142
+ const draft = current.draft;
143
+
144
+ useEffect(() => {
145
+ if (!autoFocus) return;
146
+ const input = inputRef.current;
147
+ if (!input) return;
148
+ input.focus();
149
+ // Select, not just focus — the grid does the same. A bare focus leaves the
150
+ // caret inside the existing text, so the first thing typed is inserted into
151
+ // the old value rather than replacing it.
152
+ input.select();
153
+ }, [autoFocus]);
154
+
155
+ function commit(raw: string | boolean) {
156
+ const parsed = parseFieldInput(attribute, raw);
157
+ if (!parsed.ok) {
158
+ setError(
159
+ parsed.code === "not-a-number"
160
+ ? t("record.invalidNumber")
161
+ : parsed.code === "not-a-date"
162
+ ? t("record.invalidDate")
163
+ : parsed.code === "unknown-option"
164
+ ? t("record.unknownOption")
165
+ : t("record.notEditable"),
166
+ );
167
+ return;
168
+ }
169
+ setError(null);
170
+ onCommit(parsed.value);
171
+ onDone?.();
172
+ }
173
+
174
+ const spec = valueSpecFor(attribute);
175
+
176
+ if (spec.control === "checkbox") {
177
+ return (
178
+ <Switch
179
+ checked={value === true}
180
+ aria-label={attribute.apiSlug}
181
+ onCheckedChange={(next) => commit(next)}
182
+ />
183
+ );
184
+ }
185
+
186
+ const options = activeOptions(attribute);
187
+ if (!attribute.multi && spec.control === "options") {
188
+ return (
189
+ <Select
190
+ value={typeof value === "string" && value ? value : undefined}
191
+ onValueChange={(next) => commit(next === CLEAR_OPTION ? "" : next)}
192
+ >
193
+ <SelectTrigger className="h-8" aria-label={attribute.apiSlug}>
194
+ <SelectValue placeholder={t("record.fieldEmpty")} />
195
+ </SelectTrigger>
196
+ <SelectContent>
197
+ {attribute.required ? null : (
198
+ <SelectItem value={CLEAR_OPTION}>
199
+ {t("record.clearValue")}
200
+ </SelectItem>
201
+ )}
202
+ {options.map((option) => (
203
+ <SelectItem key={option.id} value={option.value}>
204
+ {option.title}
205
+ </SelectItem>
206
+ ))}
207
+ </SelectContent>
208
+ </Select>
209
+ );
210
+ }
211
+
212
+ // A multi value is a comma-separated list, which no typed input accepts.
213
+ const inputType = attribute.multi ? "text" : spec.inputType;
214
+
215
+ return (
216
+ <div className="grid gap-1">
217
+ <Input
218
+ ref={inputRef}
219
+ className="h-8"
220
+ type={inputType}
221
+ value={draft}
222
+ aria-label={attribute.apiSlug}
223
+ placeholder={attribute.multi ? t("record.multiValueHint") : undefined}
224
+ maxLength={4_000}
225
+ onChange={(event) =>
226
+ setState({ ...current, draft: event.target.value })
227
+ }
228
+ onBlur={() => commit(draft)}
229
+ onKeyDown={(event) => {
230
+ if (event.key === "Enter") {
231
+ event.preventDefault();
232
+ commit(draft);
233
+ }
234
+ if (event.key === "Escape") {
235
+ event.preventDefault();
236
+ setState(editorDraftFor(undefined, seed));
237
+ setError(null);
238
+ onDone?.();
239
+ }
240
+ }}
241
+ />
242
+ {error ? <p className="text-xs text-destructive">{error}</p> : null}
243
+ </div>
244
+ );
245
+ }