@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,339 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useActionMutation,
|
|
3
|
+
useActionQuery,
|
|
4
|
+
} from "@agent-native/core/client/hooks";
|
|
5
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
6
|
+
import {
|
|
7
|
+
IconAlertTriangle,
|
|
8
|
+
IconArchive,
|
|
9
|
+
IconListDetails,
|
|
10
|
+
IconPlus,
|
|
11
|
+
} from "@tabler/icons-react";
|
|
12
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { Link } from "react-router";
|
|
15
|
+
import { toast } from "sonner";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
AlertDialog,
|
|
19
|
+
AlertDialogAction,
|
|
20
|
+
AlertDialogCancel,
|
|
21
|
+
AlertDialogContent,
|
|
22
|
+
AlertDialogDescription,
|
|
23
|
+
AlertDialogFooter,
|
|
24
|
+
AlertDialogHeader,
|
|
25
|
+
AlertDialogTitle,
|
|
26
|
+
AlertDialogTrigger,
|
|
27
|
+
} from "@/components/ui/alert-dialog";
|
|
28
|
+
import { Badge } from "@/components/ui/badge";
|
|
29
|
+
import { Button } from "@/components/ui/button";
|
|
30
|
+
import {
|
|
31
|
+
Dialog,
|
|
32
|
+
DialogContent,
|
|
33
|
+
DialogDescription,
|
|
34
|
+
DialogFooter,
|
|
35
|
+
DialogHeader,
|
|
36
|
+
DialogTitle,
|
|
37
|
+
DialogTrigger,
|
|
38
|
+
} from "@/components/ui/dialog";
|
|
39
|
+
import { Input } from "@/components/ui/input";
|
|
40
|
+
import { Label } from "@/components/ui/label";
|
|
41
|
+
import {
|
|
42
|
+
Select,
|
|
43
|
+
SelectContent,
|
|
44
|
+
SelectItem,
|
|
45
|
+
SelectTrigger,
|
|
46
|
+
SelectValue,
|
|
47
|
+
} from "@/components/ui/select";
|
|
48
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
49
|
+
|
|
50
|
+
interface CrmListRow {
|
|
51
|
+
id: string;
|
|
52
|
+
connectionId: string;
|
|
53
|
+
name: string;
|
|
54
|
+
apiSlug: string;
|
|
55
|
+
parentObjectType: string;
|
|
56
|
+
description: string | null;
|
|
57
|
+
archived: boolean;
|
|
58
|
+
entryCount?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface CrmConnectionSummary {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
objectTypes: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const LISTS_PARAMS = { limit: 100 } as const;
|
|
68
|
+
const LISTS_KEY = ["action", "list-crm-lists", LISTS_PARAMS];
|
|
69
|
+
|
|
70
|
+
export function ListsSettings() {
|
|
71
|
+
const t = useT();
|
|
72
|
+
const queryClient = useQueryClient();
|
|
73
|
+
const listsQuery = useActionQuery<{ lists: CrmListRow[] }>(
|
|
74
|
+
"list-crm-lists" as never,
|
|
75
|
+
LISTS_PARAMS as never,
|
|
76
|
+
);
|
|
77
|
+
const connectionsQuery = useActionQuery<{
|
|
78
|
+
connections: CrmConnectionSummary[];
|
|
79
|
+
}>("list-crm-connections" as never, {} as never);
|
|
80
|
+
const archive = useActionMutation<
|
|
81
|
+
unknown,
|
|
82
|
+
{ listId: string; archived: boolean }
|
|
83
|
+
>("update-crm-list" as never);
|
|
84
|
+
const lists = listsQuery.data?.lists ?? [];
|
|
85
|
+
|
|
86
|
+
async function archiveList(list: CrmListRow) {
|
|
87
|
+
void queryClient.cancelQueries({ queryKey: LISTS_KEY });
|
|
88
|
+
const previous = queryClient.getQueryData<{ lists: CrmListRow[] }>(
|
|
89
|
+
LISTS_KEY,
|
|
90
|
+
);
|
|
91
|
+
queryClient.setQueryData(LISTS_KEY, {
|
|
92
|
+
...previous,
|
|
93
|
+
lists: (previous?.lists ?? []).filter((row) => row.id !== list.id),
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await archive.mutateAsync({ listId: list.id, archived: true });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
queryClient.setQueryData(LISTS_KEY, previous);
|
|
99
|
+
toast.error(
|
|
100
|
+
error instanceof Error ? error.message : t("lists.archiveFailed"),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="mx-auto w-full max-w-2xl">
|
|
107
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
108
|
+
<div className="min-w-0">
|
|
109
|
+
<h1 className="text-xl font-semibold tracking-tight">
|
|
110
|
+
{t("lists.title")}
|
|
111
|
+
</h1>
|
|
112
|
+
<p className="mt-1 max-w-xl text-sm leading-6 text-muted-foreground">
|
|
113
|
+
{t("lists.description")}
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
<CreateListDialog
|
|
117
|
+
connections={connectionsQuery.data?.connections ?? []}
|
|
118
|
+
onCreated={() =>
|
|
119
|
+
void queryClient.invalidateQueries({
|
|
120
|
+
queryKey: ["action", "list-crm-lists"],
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{listsQuery.isError ? (
|
|
127
|
+
<div className="mt-6 flex items-center gap-3 rounded-lg border border-destructive/40 bg-destructive/5 px-4 py-3">
|
|
128
|
+
<IconAlertTriangle className="size-4 shrink-0 text-destructive" />
|
|
129
|
+
<p className="flex-1 text-sm">
|
|
130
|
+
{listsQuery.error instanceof Error
|
|
131
|
+
? listsQuery.error.message
|
|
132
|
+
: t("lists.loadFailed")}
|
|
133
|
+
</p>
|
|
134
|
+
<Button
|
|
135
|
+
variant="outline"
|
|
136
|
+
size="sm"
|
|
137
|
+
onClick={() => void listsQuery.refetch()}
|
|
138
|
+
>
|
|
139
|
+
{t("lists.retry")}
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
) : lists.length ? (
|
|
143
|
+
<div className="mt-6 divide-y divide-border/70 rounded-lg border border-border/70 bg-card">
|
|
144
|
+
{lists.map((list) => (
|
|
145
|
+
<section
|
|
146
|
+
key={list.id}
|
|
147
|
+
className="flex flex-wrap items-center gap-4 px-4 py-3.5"
|
|
148
|
+
>
|
|
149
|
+
<IconListDetails className="size-4 shrink-0 text-muted-foreground" />
|
|
150
|
+
<div className="min-w-0 flex-1">
|
|
151
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
152
|
+
<Link
|
|
153
|
+
to={`/lists/${encodeURIComponent(list.id)}`}
|
|
154
|
+
className="truncate text-sm font-medium hover:underline"
|
|
155
|
+
>
|
|
156
|
+
{list.name}
|
|
157
|
+
</Link>
|
|
158
|
+
<Badge variant="outline" className="font-normal">
|
|
159
|
+
{list.parentObjectType}
|
|
160
|
+
</Badge>
|
|
161
|
+
</div>
|
|
162
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
163
|
+
{t("lists.entryCount", { entries: list.entryCount ?? 0 })}
|
|
164
|
+
{" · "}
|
|
165
|
+
<code>{list.apiSlug}</code>
|
|
166
|
+
</p>
|
|
167
|
+
</div>
|
|
168
|
+
<AlertDialog>
|
|
169
|
+
<AlertDialogTrigger asChild>
|
|
170
|
+
<Button
|
|
171
|
+
variant="ghost"
|
|
172
|
+
size="icon"
|
|
173
|
+
aria-label={t("lists.archiveAria", { name: list.name })}
|
|
174
|
+
>
|
|
175
|
+
<IconArchive className="size-4" />
|
|
176
|
+
</Button>
|
|
177
|
+
</AlertDialogTrigger>
|
|
178
|
+
<AlertDialogContent>
|
|
179
|
+
<AlertDialogHeader>
|
|
180
|
+
<AlertDialogTitle>
|
|
181
|
+
{t("lists.archiveTitle", { name: list.name })}
|
|
182
|
+
</AlertDialogTitle>
|
|
183
|
+
<AlertDialogDescription>
|
|
184
|
+
{t("lists.archiveDescription")}
|
|
185
|
+
</AlertDialogDescription>
|
|
186
|
+
</AlertDialogHeader>
|
|
187
|
+
<AlertDialogFooter>
|
|
188
|
+
<AlertDialogCancel>{t("lists.cancel")}</AlertDialogCancel>
|
|
189
|
+
<AlertDialogAction onClick={() => void archiveList(list)}>
|
|
190
|
+
{t("lists.archive")}
|
|
191
|
+
</AlertDialogAction>
|
|
192
|
+
</AlertDialogFooter>
|
|
193
|
+
</AlertDialogContent>
|
|
194
|
+
</AlertDialog>
|
|
195
|
+
</section>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
) : (
|
|
199
|
+
<div className="mt-6 rounded-lg border border-dashed border-border px-4 py-10 text-center">
|
|
200
|
+
<p className="text-sm font-medium">
|
|
201
|
+
{listsQuery.isLoading ? t("lists.loading") : t("lists.emptyTitle")}
|
|
202
|
+
</p>
|
|
203
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
204
|
+
{t("lists.emptyDescription")}
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function CreateListDialog({
|
|
213
|
+
connections,
|
|
214
|
+
onCreated,
|
|
215
|
+
}: {
|
|
216
|
+
connections: CrmConnectionSummary[];
|
|
217
|
+
onCreated: () => void;
|
|
218
|
+
}) {
|
|
219
|
+
const t = useT();
|
|
220
|
+
const [open, setOpen] = useState(false);
|
|
221
|
+
const [name, setName] = useState("");
|
|
222
|
+
const [description, setDescription] = useState("");
|
|
223
|
+
const [parentObjectType, setParentObjectType] = useState("");
|
|
224
|
+
const create = useActionMutation<
|
|
225
|
+
unknown,
|
|
226
|
+
{
|
|
227
|
+
name: string;
|
|
228
|
+
parentObjectType: string;
|
|
229
|
+
description?: string;
|
|
230
|
+
connectionId?: string;
|
|
231
|
+
}
|
|
232
|
+
>("create-crm-list" as never);
|
|
233
|
+
|
|
234
|
+
const objectTypes = connections.flatMap((connection) =>
|
|
235
|
+
connection.objectTypes.map((objectType) => ({
|
|
236
|
+
connectionId: connection.id,
|
|
237
|
+
objectType,
|
|
238
|
+
label: `${connection.label} · ${objectType}`,
|
|
239
|
+
key: `${connection.id}:${objectType}`,
|
|
240
|
+
})),
|
|
241
|
+
);
|
|
242
|
+
const selected = objectTypes.find((entry) => entry.key === parentObjectType);
|
|
243
|
+
|
|
244
|
+
function openChange(next: boolean) {
|
|
245
|
+
if (!next) {
|
|
246
|
+
setName("");
|
|
247
|
+
setDescription("");
|
|
248
|
+
setParentObjectType("");
|
|
249
|
+
}
|
|
250
|
+
setOpen(next);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function submit() {
|
|
254
|
+
if (!selected) return;
|
|
255
|
+
try {
|
|
256
|
+
await create.mutateAsync({
|
|
257
|
+
name: name.trim(),
|
|
258
|
+
parentObjectType: selected.objectType,
|
|
259
|
+
connectionId: selected.connectionId,
|
|
260
|
+
...(description.trim() ? { description: description.trim() } : {}),
|
|
261
|
+
});
|
|
262
|
+
openChange(false);
|
|
263
|
+
onCreated();
|
|
264
|
+
toast.success(t("lists.created"));
|
|
265
|
+
} catch (error) {
|
|
266
|
+
toast.error(
|
|
267
|
+
error instanceof Error ? error.message : t("lists.createFailed"),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<Dialog open={open} onOpenChange={openChange}>
|
|
274
|
+
<DialogTrigger asChild>
|
|
275
|
+
<Button size="sm" className="gap-1.5">
|
|
276
|
+
<IconPlus className="size-4" /> {t("lists.newList")}
|
|
277
|
+
</Button>
|
|
278
|
+
</DialogTrigger>
|
|
279
|
+
<DialogContent className="sm:max-w-lg">
|
|
280
|
+
<DialogHeader>
|
|
281
|
+
<DialogTitle>{t("lists.createTitle")}</DialogTitle>
|
|
282
|
+
<DialogDescription>{t("lists.createDescription")}</DialogDescription>
|
|
283
|
+
</DialogHeader>
|
|
284
|
+
<div className="grid gap-4 py-2">
|
|
285
|
+
<div className="grid gap-2">
|
|
286
|
+
<Label htmlFor="new-list-name">{t("lists.name")}</Label>
|
|
287
|
+
<Input
|
|
288
|
+
id="new-list-name"
|
|
289
|
+
value={name}
|
|
290
|
+
maxLength={120}
|
|
291
|
+
onChange={(event) => setName(event.target.value)}
|
|
292
|
+
/>
|
|
293
|
+
</div>
|
|
294
|
+
<div className="grid gap-2">
|
|
295
|
+
<Label htmlFor="new-list-object">{t("lists.parentObject")}</Label>
|
|
296
|
+
<Select
|
|
297
|
+
value={parentObjectType}
|
|
298
|
+
onValueChange={setParentObjectType}
|
|
299
|
+
disabled={!objectTypes.length}
|
|
300
|
+
>
|
|
301
|
+
<SelectTrigger id="new-list-object">
|
|
302
|
+
<SelectValue placeholder={t("lists.parentObjectPlaceholder")} />
|
|
303
|
+
</SelectTrigger>
|
|
304
|
+
<SelectContent>
|
|
305
|
+
{objectTypes.map((entry) => (
|
|
306
|
+
<SelectItem key={entry.key} value={entry.key}>
|
|
307
|
+
{entry.label}
|
|
308
|
+
</SelectItem>
|
|
309
|
+
))}
|
|
310
|
+
</SelectContent>
|
|
311
|
+
</Select>
|
|
312
|
+
</div>
|
|
313
|
+
<div className="grid gap-2">
|
|
314
|
+
<Label htmlFor="new-list-description">
|
|
315
|
+
{t("lists.listDescription")}
|
|
316
|
+
</Label>
|
|
317
|
+
<Textarea
|
|
318
|
+
id="new-list-description"
|
|
319
|
+
value={description}
|
|
320
|
+
maxLength={500}
|
|
321
|
+
onChange={(event) => setDescription(event.target.value)}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
<p className="text-xs leading-5 text-muted-foreground">
|
|
325
|
+
{t("lists.stageSeedNote")}
|
|
326
|
+
</p>
|
|
327
|
+
</div>
|
|
328
|
+
<DialogFooter>
|
|
329
|
+
<Button
|
|
330
|
+
disabled={!name.trim() || !selected || create.isPending}
|
|
331
|
+
onClick={() => void submit()}
|
|
332
|
+
>
|
|
333
|
+
{t("lists.create")}
|
|
334
|
+
</Button>
|
|
335
|
+
</DialogFooter>
|
|
336
|
+
</DialogContent>
|
|
337
|
+
</Dialog>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision logic behind the CRM settings surfaces.
|
|
3
|
+
*
|
|
4
|
+
* It lives outside the components because `vitest.config.ts` collects
|
|
5
|
+
* `**\/*.test.ts` in a node environment: a `.tsx` component cannot be reached
|
|
6
|
+
* from a test at all, so anything worth proving has to be a plain module.
|
|
7
|
+
*
|
|
8
|
+
* Type behaviour is read from `ATTRIBUTE_TYPE_SPECS`, never re-derived here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ATTRIBUTE_TYPE_SPECS,
|
|
13
|
+
CRM_ATTRIBUTE_TYPES,
|
|
14
|
+
type CrmAttributeType,
|
|
15
|
+
} from "../../../../shared/crm-attributes";
|
|
16
|
+
import {
|
|
17
|
+
CRM_CONNECTION_MODES,
|
|
18
|
+
type CrmAttributeAuthority,
|
|
19
|
+
type CrmAttributeDefinition,
|
|
20
|
+
type CrmAttributeOption,
|
|
21
|
+
type CrmConnectionMode,
|
|
22
|
+
} from "../../../../shared/crm-contract";
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Attribute types
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** Types a person may author. `interaction` and `personal-name` are system-only. */
|
|
29
|
+
export const AUTHORED_ATTRIBUTE_TYPES: readonly CrmAttributeType[] =
|
|
30
|
+
CRM_ATTRIBUTE_TYPES.filter((type) => !ATTRIBUTE_TYPE_SPECS[type].systemOnly);
|
|
31
|
+
|
|
32
|
+
export interface AttributeTypeCapabilities {
|
|
33
|
+
supportsMulti: boolean;
|
|
34
|
+
usesOptions: boolean;
|
|
35
|
+
/** `targetDays` and `celebrate` describe a pipeline stage: status only. */
|
|
36
|
+
showsStageFields: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function attributeTypeCapabilities(
|
|
40
|
+
type: CrmAttributeType,
|
|
41
|
+
): AttributeTypeCapabilities {
|
|
42
|
+
const spec = ATTRIBUTE_TYPE_SPECS[type];
|
|
43
|
+
return {
|
|
44
|
+
supportsMulti: spec.supportsMulti,
|
|
45
|
+
usesOptions: spec.usesOptions,
|
|
46
|
+
showsStageFields: type === "status",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `api_slug` keys every stored value row and the type chose the column those
|
|
52
|
+
* values live in, so `update-crm-attribute` rejects a change to either.
|
|
53
|
+
* Rendering them as editable would offer a control the server always refuses.
|
|
54
|
+
*/
|
|
55
|
+
export const IMMUTABLE_ATTRIBUTE_FIELDS = ["apiSlug", "attributeType"] as const;
|
|
56
|
+
|
|
57
|
+
export type ImmutableAttributeField =
|
|
58
|
+
(typeof IMMUTABLE_ATTRIBUTE_FIELDS)[number];
|
|
59
|
+
|
|
60
|
+
export function isImmutableAttributeField(
|
|
61
|
+
field: string,
|
|
62
|
+
): field is ImmutableAttributeField {
|
|
63
|
+
return (IMMUTABLE_ATTRIBUTE_FIELDS as readonly string[]).includes(field);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Authority
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export interface AttributeAuthorityInfo {
|
|
71
|
+
labelKey: string;
|
|
72
|
+
descriptionKey: string;
|
|
73
|
+
/** A local edit cannot be written upstream; it is recorded as a proposal. */
|
|
74
|
+
editsBecomeProposals: boolean;
|
|
75
|
+
badge: "default" | "secondary" | "outline";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const ATTRIBUTE_AUTHORITY_INFO: Record<
|
|
79
|
+
CrmAttributeAuthority,
|
|
80
|
+
AttributeAuthorityInfo
|
|
81
|
+
> = {
|
|
82
|
+
provider: {
|
|
83
|
+
labelKey: "fields.authorityProvider",
|
|
84
|
+
descriptionKey: "fields.authorityProviderHelp",
|
|
85
|
+
editsBecomeProposals: true,
|
|
86
|
+
badge: "secondary",
|
|
87
|
+
},
|
|
88
|
+
"derived-local": {
|
|
89
|
+
labelKey: "fields.authorityDerived",
|
|
90
|
+
descriptionKey: "fields.authorityDerivedHelp",
|
|
91
|
+
editsBecomeProposals: false,
|
|
92
|
+
badge: "outline",
|
|
93
|
+
},
|
|
94
|
+
"local-authoritative": {
|
|
95
|
+
labelKey: "fields.authorityLocal",
|
|
96
|
+
descriptionKey: "fields.authorityLocalHelp",
|
|
97
|
+
editsBecomeProposals: false,
|
|
98
|
+
badge: "default",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Drafts and action payloads
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
export interface AttributeOptionDraft {
|
|
107
|
+
value: string;
|
|
108
|
+
title: string;
|
|
109
|
+
color: string | null;
|
|
110
|
+
targetDays: number | null;
|
|
111
|
+
celebrate: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface AttributeDraft {
|
|
115
|
+
title: string;
|
|
116
|
+
type: CrmAttributeType;
|
|
117
|
+
description: string;
|
|
118
|
+
multi: boolean;
|
|
119
|
+
required: boolean;
|
|
120
|
+
uniqueValue: boolean;
|
|
121
|
+
historyTracked: boolean;
|
|
122
|
+
options: AttributeOptionDraft[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function emptyAttributeDraft(): AttributeDraft {
|
|
126
|
+
return {
|
|
127
|
+
title: "",
|
|
128
|
+
type: "text",
|
|
129
|
+
description: "",
|
|
130
|
+
multi: false,
|
|
131
|
+
required: false,
|
|
132
|
+
uniqueValue: false,
|
|
133
|
+
historyTracked: true,
|
|
134
|
+
options: [],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function emptyAttributeOptionDraft(): AttributeOptionDraft {
|
|
139
|
+
return {
|
|
140
|
+
value: "",
|
|
141
|
+
title: "",
|
|
142
|
+
color: null,
|
|
143
|
+
targetDays: null,
|
|
144
|
+
celebrate: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface CrmAttributeTarget {
|
|
149
|
+
target: "object" | "list";
|
|
150
|
+
targetId: string;
|
|
151
|
+
connectionId?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface CreateAttributeInput extends CrmAttributeTarget {
|
|
155
|
+
title: string;
|
|
156
|
+
type: CrmAttributeType;
|
|
157
|
+
multi: boolean;
|
|
158
|
+
required: boolean;
|
|
159
|
+
uniqueValue: boolean;
|
|
160
|
+
historyTracked: boolean;
|
|
161
|
+
description?: string;
|
|
162
|
+
options?: Array<{
|
|
163
|
+
value: string;
|
|
164
|
+
title: string;
|
|
165
|
+
color?: string;
|
|
166
|
+
targetDays?: number | null;
|
|
167
|
+
celebrate?: boolean;
|
|
168
|
+
}>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The exact payload `create-crm-attribute` accepts for a draft. Options and
|
|
173
|
+
* stage fields are dropped for types that do not take them: the action rejects
|
|
174
|
+
* both with a 422, and the picker that produced the draft may have been showing
|
|
175
|
+
* them before the type changed.
|
|
176
|
+
*/
|
|
177
|
+
export function buildCreateAttributeInput(
|
|
178
|
+
draft: AttributeDraft,
|
|
179
|
+
target: CrmAttributeTarget,
|
|
180
|
+
): CreateAttributeInput {
|
|
181
|
+
const capabilities = attributeTypeCapabilities(draft.type);
|
|
182
|
+
const description = draft.description.trim();
|
|
183
|
+
const options = capabilities.usesOptions
|
|
184
|
+
? draft.options
|
|
185
|
+
.filter((option) => option.value.trim())
|
|
186
|
+
.map((option) => ({
|
|
187
|
+
value: option.value.trim(),
|
|
188
|
+
title: option.title.trim() || option.value.trim(),
|
|
189
|
+
...(option.color ? { color: option.color } : {}),
|
|
190
|
+
...(capabilities.showsStageFields
|
|
191
|
+
? { targetDays: option.targetDays, celebrate: option.celebrate }
|
|
192
|
+
: {}),
|
|
193
|
+
}))
|
|
194
|
+
: [];
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
target: target.target,
|
|
198
|
+
targetId: target.targetId,
|
|
199
|
+
...(target.connectionId ? { connectionId: target.connectionId } : {}),
|
|
200
|
+
title: draft.title.trim(),
|
|
201
|
+
type: draft.type,
|
|
202
|
+
multi: capabilities.supportsMulti && draft.multi,
|
|
203
|
+
required: draft.required,
|
|
204
|
+
uniqueValue: draft.uniqueValue,
|
|
205
|
+
historyTracked: draft.historyTracked,
|
|
206
|
+
...(description ? { description } : {}),
|
|
207
|
+
...(options.length ? { options } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface AttributeEditDraft {
|
|
212
|
+
title: string;
|
|
213
|
+
description: string;
|
|
214
|
+
required: boolean;
|
|
215
|
+
historyTracked: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface UpdateAttributeInput {
|
|
219
|
+
attributeId: string;
|
|
220
|
+
title?: string;
|
|
221
|
+
description?: string | null;
|
|
222
|
+
required?: boolean;
|
|
223
|
+
historyTracked?: boolean;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function attributeEditDraft(
|
|
227
|
+
attribute: CrmAttributeDefinition,
|
|
228
|
+
): AttributeEditDraft {
|
|
229
|
+
return {
|
|
230
|
+
title: attribute.label,
|
|
231
|
+
description: attribute.description ?? "",
|
|
232
|
+
required: attribute.required,
|
|
233
|
+
historyTracked: attribute.historyTracked,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Only the fields that actually changed, and never `apiSlug` or `type`: the
|
|
239
|
+
* action accepts those two solely to reject them with an explanation.
|
|
240
|
+
*/
|
|
241
|
+
export function buildUpdateAttributeInput(
|
|
242
|
+
attribute: CrmAttributeDefinition,
|
|
243
|
+
draft: AttributeEditDraft,
|
|
244
|
+
): UpdateAttributeInput {
|
|
245
|
+
const title = draft.title.trim();
|
|
246
|
+
const description = draft.description.trim();
|
|
247
|
+
const current = attributeEditDraft(attribute);
|
|
248
|
+
return {
|
|
249
|
+
attributeId: attribute.id,
|
|
250
|
+
...(title && title !== current.title ? { title } : {}),
|
|
251
|
+
...(description === current.description.trim()
|
|
252
|
+
? {}
|
|
253
|
+
: { description: description || null }),
|
|
254
|
+
...(draft.required === current.required
|
|
255
|
+
? {}
|
|
256
|
+
: { required: draft.required }),
|
|
257
|
+
...(draft.historyTracked === current.historyTracked
|
|
258
|
+
? {}
|
|
259
|
+
: { historyTracked: draft.historyTracked }),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function hasAttributeEdits(input: UpdateAttributeInput): boolean {
|
|
264
|
+
return Object.keys(input).length > 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Optimistic cache patching
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
export interface CrmAttributeListResult {
|
|
272
|
+
target: "object" | "list";
|
|
273
|
+
targetId: string;
|
|
274
|
+
attributes: CrmAttributeDefinition[];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function applyAttributePatch(
|
|
278
|
+
result: CrmAttributeListResult | undefined,
|
|
279
|
+
attributeId: string,
|
|
280
|
+
patch: Partial<CrmAttributeDefinition>,
|
|
281
|
+
): CrmAttributeListResult | undefined {
|
|
282
|
+
if (!result) return result;
|
|
283
|
+
return {
|
|
284
|
+
...result,
|
|
285
|
+
attributes: result.attributes.map((attribute) =>
|
|
286
|
+
attribute.id === attributeId ? { ...attribute, ...patch } : attribute,
|
|
287
|
+
),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Option ordering
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/** A copy with `from` moved to `to`. Out-of-range indices leave the order alone. */
|
|
296
|
+
export function moveItem<T>(
|
|
297
|
+
items: readonly T[],
|
|
298
|
+
from: number,
|
|
299
|
+
to: number,
|
|
300
|
+
): T[] {
|
|
301
|
+
const next = [...items];
|
|
302
|
+
if (
|
|
303
|
+
from < 0 ||
|
|
304
|
+
to < 0 ||
|
|
305
|
+
from >= next.length ||
|
|
306
|
+
to >= next.length ||
|
|
307
|
+
from === to
|
|
308
|
+
) {
|
|
309
|
+
return next;
|
|
310
|
+
}
|
|
311
|
+
const [moved] = next.splice(from, 1);
|
|
312
|
+
next.splice(to, 0, moved as T);
|
|
313
|
+
return next;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The `optionIds` payload `manage-crm-attribute-option` reorder expects. */
|
|
317
|
+
export function reorderedOptionIds(
|
|
318
|
+
options: readonly CrmAttributeOption[],
|
|
319
|
+
from: number,
|
|
320
|
+
to: number,
|
|
321
|
+
): string[] {
|
|
322
|
+
return moveItem(options, from, to).map((option) => option.id);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Connection modes
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
export interface CrmConnectionModeInfo {
|
|
330
|
+
mode: CrmConnectionMode;
|
|
331
|
+
labelKey: string;
|
|
332
|
+
descriptionKey: string;
|
|
333
|
+
/** Deprecated modes still render for existing rows; they are never offered. */
|
|
334
|
+
deprecated: boolean;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export const CRM_CONNECTION_MODE_INFO: Record<
|
|
338
|
+
CrmConnectionMode,
|
|
339
|
+
CrmConnectionModeInfo
|
|
340
|
+
> = {
|
|
341
|
+
native: {
|
|
342
|
+
mode: "native",
|
|
343
|
+
labelKey: "connection.modeNative",
|
|
344
|
+
descriptionKey: "connection.modeNativeHelp",
|
|
345
|
+
deprecated: false,
|
|
346
|
+
},
|
|
347
|
+
connected: {
|
|
348
|
+
mode: "connected",
|
|
349
|
+
labelKey: "connection.modeConnected",
|
|
350
|
+
descriptionKey: "connection.modeConnectedHelp",
|
|
351
|
+
deprecated: false,
|
|
352
|
+
},
|
|
353
|
+
hybrid: {
|
|
354
|
+
mode: "hybrid",
|
|
355
|
+
labelKey: "connection.modeHybrid",
|
|
356
|
+
descriptionKey: "connection.modeHybridHelp",
|
|
357
|
+
deprecated: true,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The only set a mode picker may offer. `hybrid` is derived out rather than
|
|
363
|
+
* hand-omitted so it cannot reappear by someone extending a literal list.
|
|
364
|
+
*/
|
|
365
|
+
export const SELECTABLE_CRM_CONNECTION_MODES: readonly CrmConnectionMode[] =
|
|
366
|
+
CRM_CONNECTION_MODES.filter(
|
|
367
|
+
(mode) => !CRM_CONNECTION_MODE_INFO[mode].deprecated,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Null for a mode this build does not know. The caller must render that as
|
|
372
|
+
* unrecognized — falling back to a default would print a confident wrong label
|
|
373
|
+
* for a row whose real mode nobody can read.
|
|
374
|
+
*/
|
|
375
|
+
export function connectionModeInfo(mode: string): CrmConnectionModeInfo | null {
|
|
376
|
+
return CRM_CONNECTION_MODE_INFO[mode as CrmConnectionMode] ?? null;
|
|
377
|
+
}
|