@aglyn/plugins-crm 1.0.0 → 1.1.0

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 (154) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/activity-link-fields.d.ts +34 -0
  3. package/src/lib/components/activity-link-fields.js +141 -0
  4. package/src/lib/components/activity-link-fields.js.map +1 -0
  5. package/src/lib/components/activity-list.d.ts +8 -0
  6. package/src/lib/components/activity-list.js +4 -2
  7. package/src/lib/components/activity-list.js.map +1 -1
  8. package/src/lib/components/activity-queries.d.ts +7 -2
  9. package/src/lib/components/activity-queries.js +7 -3
  10. package/src/lib/components/activity-queries.js.map +1 -1
  11. package/src/lib/components/company-contact-roles-card.d.ts +42 -0
  12. package/src/lib/components/company-contact-roles-card.js +135 -0
  13. package/src/lib/components/company-contact-roles-card.js.map +1 -0
  14. package/src/lib/components/company-detail-page.js +12 -1
  15. package/src/lib/components/company-detail-page.js.map +1 -1
  16. package/src/lib/components/company-edit-drawer.js +9 -6
  17. package/src/lib/components/company-edit-drawer.js.map +1 -1
  18. package/src/lib/components/company-picker.js +4 -2
  19. package/src/lib/components/company-picker.js.map +1 -1
  20. package/src/lib/components/contact-associations-card.js +15 -1
  21. package/src/lib/components/contact-associations-card.js.map +1 -1
  22. package/src/lib/components/contact-detail-page.js +13 -14
  23. package/src/lib/components/contact-detail-page.js.map +1 -1
  24. package/src/lib/components/contact-list-columns.js +5 -4
  25. package/src/lib/components/contact-list-columns.js.map +1 -1
  26. package/src/lib/components/contact-properties-card.js +41 -20
  27. package/src/lib/components/contact-properties-card.js.map +1 -1
  28. package/src/lib/components/contact-roles-editor.d.ts +54 -0
  29. package/src/lib/components/contact-roles-editor.js +270 -0
  30. package/src/lib/components/contact-roles-editor.js.map +1 -0
  31. package/src/lib/components/contacts-section.js +6 -4
  32. package/src/lib/components/contacts-section.js.map +1 -1
  33. package/src/lib/components/crm-console-sections.js +1 -1
  34. package/src/lib/components/crm-console-sections.js.map +1 -1
  35. package/src/lib/components/crm-record-picker.d.ts +19 -0
  36. package/src/lib/components/crm-record-picker.js +40 -11
  37. package/src/lib/components/crm-record-picker.js.map +1 -1
  38. package/src/lib/components/crm-related-to-picker.d.ts +33 -0
  39. package/src/lib/components/crm-related-to-picker.js +135 -0
  40. package/src/lib/components/crm-related-to-picker.js.map +1 -0
  41. package/src/lib/components/crm-site-picker.d.ts +13 -1
  42. package/src/lib/components/crm-site-picker.js +15 -6
  43. package/src/lib/components/crm-site-picker.js.map +1 -1
  44. package/src/lib/components/crm-transfer-buttons.d.ts +5 -0
  45. package/src/lib/components/crm-transfer-buttons.js +9 -6
  46. package/src/lib/components/crm-transfer-buttons.js.map +1 -1
  47. package/src/lib/components/deal-contact-roles-card.js +21 -239
  48. package/src/lib/components/deal-contact-roles-card.js.map +1 -1
  49. package/src/lib/components/deal-detail-page.js +2 -1
  50. package/src/lib/components/deal-detail-page.js.map +1 -1
  51. package/src/lib/components/deal-edit-drawer.js +11 -7
  52. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  53. package/src/lib/components/deals-section.js +5 -4
  54. package/src/lib/components/deals-section.js.map +1 -1
  55. package/src/lib/components/log-activity-dialog.d.ts +27 -9
  56. package/src/lib/components/log-activity-dialog.js +96 -32
  57. package/src/lib/components/log-activity-dialog.js.map +1 -1
  58. package/src/lib/components/new-contact-drawer.d.ts +7 -3
  59. package/src/lib/components/new-contact-drawer.js +27 -15
  60. package/src/lib/components/new-contact-drawer.js.map +1 -1
  61. package/src/lib/components/pipeline-stages-dialog.js +5 -1
  62. package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
  63. package/src/lib/components/record-activity-card.d.ts +11 -2
  64. package/src/lib/components/record-activity-card.js +90 -5
  65. package/src/lib/components/record-activity-card.js.map +1 -1
  66. package/src/lib/components/settings-section.d.ts +4 -2
  67. package/src/lib/components/settings-section.js +7 -5
  68. package/src/lib/components/settings-section.js.map +1 -1
  69. package/src/lib/hooks/crm-activity-client-reads.d.ts +8 -0
  70. package/src/lib/hooks/crm-activity-client-reads.js +50 -0
  71. package/src/lib/hooks/crm-activity-client-reads.js.map +1 -0
  72. package/src/lib/hooks/use-campaign-filing-log.d.ts +2 -1
  73. package/src/lib/hooks/use-campaign-filing-log.js +10 -3
  74. package/src/lib/hooks/use-campaign-filing-log.js.map +1 -1
  75. package/src/lib/hooks/use-crm-org-mount.d.ts +8 -6
  76. package/src/lib/hooks/use-crm-org-mount.js +2 -1
  77. package/src/lib/hooks/use-crm-org-mount.js.map +1 -1
  78. package/src/lib/hooks/use-crm-scope.d.ts +17 -5
  79. package/src/lib/hooks/use-crm-scope.js +12 -5
  80. package/src/lib/hooks/use-crm-scope.js.map +1 -1
  81. package/src/lib/model/contact-holder.d.ts +12 -0
  82. package/src/lib/model/contact-holder.js +20 -1
  83. package/src/lib/model/contact-holder.js.map +1 -1
  84. package/src/lib/model/contact-record.d.ts +5 -0
  85. package/src/lib/model/contact-record.js +7 -1
  86. package/src/lib/model/contact-record.js.map +1 -1
  87. package/src/lib/model/contact-update.d.ts +6 -0
  88. package/src/lib/model/contact-update.js +1 -0
  89. package/src/lib/model/contact-update.js.map +1 -1
  90. package/src/lib/model/crm-activity-link.d.ts +117 -0
  91. package/src/lib/model/crm-activity-link.js +113 -0
  92. package/src/lib/model/crm-activity-link.js.map +1 -0
  93. package/src/lib/model/crm-api.d.ts +4 -1
  94. package/src/lib/model/crm-api.js +9 -5
  95. package/src/lib/model/crm-api.js.map +1 -1
  96. package/src/lib/model/deal-board-model.js +5 -3
  97. package/src/lib/model/deal-board-model.js.map +1 -1
  98. package/src/lib/model/deal-contact-roles.d.ts +15 -1
  99. package/src/lib/model/deal-contact-roles.js +17 -3
  100. package/src/lib/model/deal-contact-roles.js.map +1 -1
  101. package/src/lib/model/settle-stage-names.d.ts +25 -0
  102. package/src/lib/model/settle-stage-names.js +29 -0
  103. package/src/lib/model/settle-stage-names.js.map +1 -0
  104. package/src/lib/native-nav.js +1 -1
  105. package/src/lib/native-nav.js.map +1 -1
  106. package/src/lib/server/add-contact-by-hand.d.ts +84 -0
  107. package/src/lib/server/add-contact-by-hand.js +212 -0
  108. package/src/lib/server/add-contact-by-hand.js.map +1 -0
  109. package/src/lib/server/api-v1/contacts.js +61 -23
  110. package/src/lib/server/api-v1/contacts.js.map +1 -1
  111. package/src/lib/server/api-v1/crm-activities.js +13 -3
  112. package/src/lib/server/api-v1/crm-activities.js.map +1 -1
  113. package/src/lib/server/api-v1/crm-pipelines.d.ts +1 -1
  114. package/src/lib/server/api-v1/crm-pipelines.js +8 -2
  115. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
  116. package/src/lib/server/api-v1/crm-shared.d.ts +16 -9
  117. package/src/lib/server/api-v1/crm-shared.js +23 -11
  118. package/src/lib/server/api-v1/crm-shared.js.map +1 -1
  119. package/src/lib/server/api-v1/openapi.js +3 -3
  120. package/src/lib/server/api-v1/openapi.js.map +1 -1
  121. package/src/lib/server/automation-steps.js +11 -9
  122. package/src/lib/server/automation-steps.js.map +1 -1
  123. package/src/lib/server/campaign-filing-activity.js +7 -2
  124. package/src/lib/server/campaign-filing-activity.js.map +1 -1
  125. package/src/lib/server/contact-update.d.ts +4 -0
  126. package/src/lib/server/contact-update.js +71 -10
  127. package/src/lib/server/contact-update.js.map +1 -1
  128. package/src/lib/server/convert-host-lead.js +13 -2
  129. package/src/lib/server/convert-host-lead.js.map +1 -1
  130. package/src/lib/server/convert-open-lead.js +8 -1
  131. package/src/lib/server/convert-open-lead.js.map +1 -1
  132. package/src/lib/server/crm-activity-company.d.ts +25 -0
  133. package/src/lib/server/crm-activity-company.js +34 -0
  134. package/src/lib/server/crm-activity-company.js.map +1 -0
  135. package/src/lib/server/crm-inbound-email.js +21 -15
  136. package/src/lib/server/crm-inbound-email.js.map +1 -1
  137. package/src/lib/server/crm-sharing.js +9 -2
  138. package/src/lib/server/crm-sharing.js.map +1 -1
  139. package/src/lib/server/email-send.js +10 -8
  140. package/src/lib/server/email-send.js.map +1 -1
  141. package/src/lib/server/merge-contacts.js +6 -2
  142. package/src/lib/server/merge-contacts.js.map +1 -1
  143. package/src/lib/server/org-caller.d.ts +8 -0
  144. package/src/lib/server/org-caller.js +10 -0
  145. package/src/lib/server/org-caller.js.map +1 -1
  146. package/src/lib/server/record-timeline.d.ts +3 -1
  147. package/src/lib/server/record-timeline.js +11 -3
  148. package/src/lib/server/record-timeline.js.map +1 -1
  149. package/src/lib/server.js +152 -43
  150. package/src/lib/server.js.map +1 -1
  151. package/src/lib/transfer/contacts.js +83 -48
  152. package/src/lib/transfer/contacts.js.map +1 -1
  153. package/src/lib/transfer/fields.js +1 -2
  154. package/src/lib/transfer/fields.js.map +1 -1
@@ -0,0 +1,33 @@
1
+ import type { CrmActivityRelated } from '../model/crm-activity-link';
2
+ export interface CrmRelatedToPickerProps {
3
+ /** The org root, or `null` until it is known. */
4
+ scope: readonly [string, string] | null;
5
+ orgId: string | null;
6
+ /** The reader's tokens, or `null` at the organization level. */
7
+ readTokens: readonly string[] | null;
8
+ /** The contact the entry is with, whose open deals lead the list. */
9
+ contactId: string | null;
10
+ /** The company the entry is about, whose open deals lead the list. */
11
+ companyId: string | null;
12
+ value: CrmActivityRelated | null;
13
+ /** The current value's name, shown while the lists do not hold the record. */
14
+ valueLabel?: string;
15
+ onChange: (next: CrmActivityRelated | null) => void;
16
+ disabled?: boolean;
17
+ }
18
+ /**
19
+ * An activity's **Related To** (AGL-4123), Salesforce's What: one company or
20
+ * one deal.
21
+ *
22
+ * The open deals of the person and the company on the entry lead the list,
23
+ * because logging a call with someone who has a deal in flight is the case
24
+ * this field exists for: naming that deal puts the entry on it too. Below
25
+ * them sit the windows every record picker offers, so an older record stays
26
+ * one search away. Nothing listens until the menu is first opened, because
27
+ * most entries are saved with the field as it opened.
28
+ */
29
+ export declare function CrmRelatedToPicker(props: CrmRelatedToPickerProps): import("react").JSX.Element;
30
+ export declare namespace CrmRelatedToPicker {
31
+ var displayName: string;
32
+ }
33
+ export default CrmRelatedToPicker;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx } from "react/jsx-runtime";
19
+ import { Autocomplete, TextField } from "@mui/material";
20
+ import { useMemo, useState } from "react";
21
+ import { useCrmFoldsScope } from "../hooks/use-crm-list-query.js";
22
+ import { useContactRoleDeals, useLinkedDeals } from "../hooks/use-deals.js";
23
+ import { CRM_RECORD_PICKER_LIMIT, useCrmRecordWindow } from "./crm-record-picker.js";
24
+ const OPEN_DEALS_GROUP = 'Open deals';
25
+ const sameRecord = (a, b)=>a.kind === b.kind && a.id === b.id;
26
+ const byLabel = (a, b)=>a.label.localeCompare(b.label);
27
+ /**
28
+ * An activity's **Related To** (AGL-4123), Salesforce's What: one company or
29
+ * one deal.
30
+ *
31
+ * The open deals of the person and the company on the entry lead the list,
32
+ * because logging a call with someone who has a deal in flight is the case
33
+ * this field exists for: naming that deal puts the entry on it too. Below
34
+ * them sit the windows every record picker offers, so an older record stays
35
+ * one search away. Nothing listens until the menu is first opened, because
36
+ * most entries are saved with the field as it opened.
37
+ */ export function CrmRelatedToPicker(props) {
38
+ var _ref, _ref1;
39
+ var _companies_data, _deals_data;
40
+ const { scope, orgId, readTokens, contactId, companyId, value, valueLabel, onChange, disabled } = props;
41
+ const [opened, setOpened] = useState(false);
42
+ const companies = useCrmRecordWindow('company', scope, readTokens, opened);
43
+ const deals = useCrmRecordWindow('deal', scope, readTokens, opened);
44
+ const foldsScope = useCrmFoldsScope(orgId, readTokens);
45
+ const contactDeals = useLinkedDeals(orgId, readTokens, {
46
+ contactId: opened ? contactId != null ? contactId : '' : ''
47
+ });
48
+ const roleDeals = useContactRoleDeals(orgId, readTokens, foldsScope, opened ? contactId : null);
49
+ const companyDeals = useLinkedDeals(orgId, readTokens, {
50
+ companyId: opened ? companyId != null ? companyId : '' : ''
51
+ });
52
+ const options = useMemo(()=>{
53
+ var _contactDeals_data, _roleDeals_data, _companyDeals_data, _deals_data, _companies_data;
54
+ const open = new Map();
55
+ for (const deal of [
56
+ ...(_contactDeals_data = contactDeals.data) != null ? _contactDeals_data : [],
57
+ ...(_roleDeals_data = roleDeals.data) != null ? _roleDeals_data : [],
58
+ ...(_companyDeals_data = companyDeals.data) != null ? _companyDeals_data : []
59
+ ]){
60
+ if (deal.status !== 'open') continue;
61
+ open.set(deal.$id, {
62
+ kind: 'deal',
63
+ id: deal.$id,
64
+ label: deal.title || 'Untitled deal',
65
+ group: OPEN_DEALS_GROUP
66
+ });
67
+ }
68
+ const named = (row, field)=>{
69
+ var _row_field;
70
+ return String((_row_field = row[field]) != null ? _row_field : '') || row.$id;
71
+ };
72
+ const otherDeals = ((_deals_data = deals.data) != null ? _deals_data : []).filter((row)=>!open.has(row.$id)).map((row)=>({
73
+ kind: 'deal',
74
+ id: row.$id,
75
+ label: named(row, 'title'),
76
+ group: 'Deals'
77
+ }));
78
+ const listed = ((_companies_data = companies.data) != null ? _companies_data : []).map((row)=>({
79
+ kind: 'company',
80
+ id: row.$id,
81
+ label: named(row, 'name'),
82
+ group: 'Companies'
83
+ }));
84
+ return [
85
+ ...[
86
+ ...open.values()
87
+ ].sort(byLabel),
88
+ ...listed.sort(byLabel),
89
+ ...otherDeals.sort(byLabel)
90
+ ];
91
+ }, [
92
+ contactDeals.data,
93
+ roleDeals.data,
94
+ companyDeals.data,
95
+ deals.data,
96
+ companies.data
97
+ ]);
98
+ const windowFull = ((_ref = (_companies_data = companies.data) == null ? void 0 : _companies_data.length) != null ? _ref : 0) >= CRM_RECORD_PICKER_LIMIT || ((_ref1 = (_deals_data = deals.data) == null ? void 0 : _deals_data.length) != null ? _ref1 : 0) >= CRM_RECORD_PICKER_LIMIT;
99
+ const selected = useMemo(()=>{
100
+ var _options_find;
101
+ if (!value) return null;
102
+ return (_options_find = options.find((option)=>sameRecord(option, value))) != null ? _options_find : _extends({}, value, {
103
+ label: valueLabel || (value.kind === 'deal' ? 'Deal' : 'Company'),
104
+ group: ''
105
+ });
106
+ }, [
107
+ options,
108
+ value,
109
+ valueLabel
110
+ ]);
111
+ return /*#__PURE__*/ _jsx(Autocomplete, {
112
+ options: options,
113
+ value: selected,
114
+ onChange: (_event, next)=>onChange(next ? {
115
+ kind: next.kind,
116
+ id: next.id
117
+ } : null),
118
+ onOpen: ()=>setOpened(true),
119
+ getOptionLabel: (option)=>option.label,
120
+ getOptionKey: (option)=>`${option.kind}:${option.id}`,
121
+ isOptionEqualToValue: (option, current)=>sameRecord(option, current),
122
+ groupBy: (option)=>option.group,
123
+ loading: opened && (companies.status === 'loading' || deals.status === 'loading'),
124
+ disabled: disabled,
125
+ size: "small",
126
+ renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
127
+ label: "Related To",
128
+ helperText: windowFull ? `The ${CRM_RECORD_PICKER_LIMIT} most recently updated — type to narrow.` : undefined
129
+ }))
130
+ });
131
+ }
132
+ CrmRelatedToPicker.displayName = 'CrmRelatedToPicker';
133
+ export default CrmRelatedToPicker;
134
+
135
+ //# sourceMappingURL=crm-related-to-picker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-related-to-picker.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { Autocomplete, TextField } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { useCrmFoldsScope } from '../hooks/use-crm-list-query'\nimport { useContactRoleDeals, useLinkedDeals } from '../hooks/use-deals'\nimport type { CrmActivityRelated } from '../model/crm-activity-link'\nimport { CRM_RECORD_PICKER_LIMIT, useCrmRecordWindow } from './crm-record-picker'\n\nexport interface CrmRelatedToPickerProps {\n /** The org root, or `null` until it is known. */\n scope: readonly [string, string] | null\n orgId: string | null\n /** The reader's tokens, or `null` at the organization level. */\n readTokens: readonly string[] | null\n /** The contact the entry is with, whose open deals lead the list. */\n contactId: string | null\n /** The company the entry is about, whose open deals lead the list. */\n companyId: string | null\n value: CrmActivityRelated | null\n /** The current value's name, shown while the lists do not hold the record. */\n valueLabel?: string\n onChange: (next: CrmActivityRelated | null) => void\n disabled?: boolean\n}\n\ninterface RelatedOption extends CrmActivityRelated {\n label: string\n group: string\n}\n\nconst OPEN_DEALS_GROUP = 'Open deals'\n\nconst sameRecord = (a: CrmActivityRelated, b: CrmActivityRelated) =>\n a.kind === b.kind && a.id === b.id\n\nconst byLabel = (a: RelatedOption, b: RelatedOption) => a.label.localeCompare(b.label)\n\n/**\n * An activity's **Related To** (AGL-4123), Salesforce's What: one company or\n * one deal.\n *\n * The open deals of the person and the company on the entry lead the list,\n * because logging a call with someone who has a deal in flight is the case\n * this field exists for: naming that deal puts the entry on it too. Below\n * them sit the windows every record picker offers, so an older record stays\n * one search away. Nothing listens until the menu is first opened, because\n * most entries are saved with the field as it opened.\n */\nexport function CrmRelatedToPicker(props: CrmRelatedToPickerProps) {\n const { scope, orgId, readTokens, contactId, companyId, value, valueLabel, onChange, disabled } =\n props\n const [opened, setOpened] = useState(false)\n const companies = useCrmRecordWindow('company', scope, readTokens, opened)\n const deals = useCrmRecordWindow('deal', scope, readTokens, opened)\n const foldsScope = useCrmFoldsScope(orgId, readTokens)\n const contactDeals = useLinkedDeals(orgId, readTokens, {\n contactId: opened ? (contactId ?? '') : '',\n })\n const roleDeals = useContactRoleDeals(\n orgId,\n readTokens,\n foldsScope,\n opened ? contactId : null,\n )\n const companyDeals = useLinkedDeals(orgId, readTokens, {\n companyId: opened ? (companyId ?? '') : '',\n })\n\n const options = useMemo<RelatedOption[]>(() => {\n const open = new Map<string, RelatedOption>()\n for (const deal of [\n ...(contactDeals.data ?? []),\n ...(roleDeals.data ?? []),\n ...(companyDeals.data ?? []),\n ]) {\n if (deal.status !== 'open') continue\n open.set(deal.$id, {\n kind: 'deal',\n id: deal.$id,\n label: deal.title || 'Untitled deal',\n group: OPEN_DEALS_GROUP,\n })\n }\n const named = (row: Record<string, unknown> & { $id: string }, field: string) =>\n String(row[field] ?? '') || row.$id\n const otherDeals = (deals.data ?? [])\n .filter((row) => !open.has(row.$id))\n .map<RelatedOption>((row) => ({\n kind: 'deal',\n id: row.$id,\n label: named(row, 'title'),\n group: 'Deals',\n }))\n const listed = (companies.data ?? []).map<RelatedOption>((row) => ({\n kind: 'company',\n id: row.$id,\n label: named(row, 'name'),\n group: 'Companies',\n }))\n return [\n ...[...open.values()].sort(byLabel),\n ...listed.sort(byLabel),\n ...otherDeals.sort(byLabel),\n ]\n }, [contactDeals.data, roleDeals.data, companyDeals.data, deals.data, companies.data])\n\n const windowFull =\n (companies.data?.length ?? 0) >= CRM_RECORD_PICKER_LIMIT ||\n (deals.data?.length ?? 0) >= CRM_RECORD_PICKER_LIMIT\n\n const selected = useMemo<RelatedOption | null>(() => {\n if (!value) return null\n return (\n options.find((option) => sameRecord(option, value)) ?? {\n ...value,\n label: valueLabel || (value.kind === 'deal' ? 'Deal' : 'Company'),\n group: '',\n }\n )\n }, [options, value, valueLabel])\n\n return (\n <Autocomplete\n options={options}\n value={selected}\n onChange={(_event, next) =>\n onChange(next ? { kind: next.kind, id: next.id } : null)\n }\n onOpen={() => setOpened(true)}\n getOptionLabel={(option) => option.label}\n getOptionKey={(option) => `${option.kind}:${option.id}`}\n isOptionEqualToValue={(option, current) => sameRecord(option, current)}\n groupBy={(option) => option.group}\n loading={opened && (companies.status === 'loading' || deals.status === 'loading')}\n disabled={disabled}\n size=\"small\"\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Related To\"\n helperText={\n windowFull ? `The ${CRM_RECORD_PICKER_LIMIT} most recently updated — type to narrow.` : undefined\n }\n />\n )}\n />\n )\n}\nCrmRelatedToPicker.displayName = 'CrmRelatedToPicker'\n\nexport default CrmRelatedToPicker\n"],"names":["Autocomplete","TextField","useMemo","useState","useCrmFoldsScope","useContactRoleDeals","useLinkedDeals","CRM_RECORD_PICKER_LIMIT","useCrmRecordWindow","OPEN_DEALS_GROUP","sameRecord","a","b","kind","id","byLabel","label","localeCompare","CrmRelatedToPicker","props","companies","deals","scope","orgId","readTokens","contactId","companyId","value","valueLabel","onChange","disabled","opened","setOpened","foldsScope","contactDeals","roleDeals","companyDeals","options","open","Map","deal","data","status","set","$id","title","group","named","row","field","String","otherDeals","filter","has","map","listed","values","sort","windowFull","length","selected","find","option","_event","next","onOpen","getOptionLabel","getOptionKey","isOptionEqualToValue","current","groupBy","loading","size","renderInput","params","helperText","undefined","displayName"],"mappings":"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"}
@@ -10,6 +10,14 @@ export interface CrmSitePickerProps {
10
10
  /** What the pick decides, said under the field. */
11
11
  helperText?: string;
12
12
  disabled?: boolean;
13
+ /**
14
+ * The org's records belong to every site, so the site is only the SOURCE a
15
+ * record came from (`useCrmScope`'s `createNeedsSite` is false): the field
16
+ * reads "Source site", is not required, and offers **None** first. In an
17
+ * org that keeps each site's records apart the site decides who may see
18
+ * the record and the field stays required.
19
+ */
20
+ optional?: boolean;
13
21
  /**
14
22
  * A choice that is no site at all — the organization itself (AGL-2637).
15
23
  * Offered under the sites when given, and then the field renders even
@@ -45,7 +53,11 @@ export interface CrmSitePickerProps {
45
53
  * that never met them, which is exactly the disclosure the per-site model
46
54
  * exists to prevent.
47
55
  *
48
- * Rendered as a required select rather than an autocomplete: an org has at
56
+ * In an org whose records belong to every site ({@link CrmSitePickerProps.optional})
57
+ * the field is the record's SOURCE, not its scope: optional, **None** first,
58
+ * and a record from no site is stamped for the whole org.
59
+ *
60
+ * Rendered as a select rather than an autocomplete: an org has at
49
61
  * most thirty sites in a consent group and rarely more than a handful at
50
62
  * all, and a required field with a fixed list should look like one.
51
63
  *
@@ -41,7 +41,11 @@ import { useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
41
41
  * that never met them, which is exactly the disclosure the per-site model
42
42
  * exists to prevent.
43
43
  *
44
- * Rendered as a required select rather than an autocomplete: an org has at
44
+ * In an org whose records belong to every site ({@link CrmSitePickerProps.optional})
45
+ * the field is the record's SOURCE, not its scope: optional, **None** first,
46
+ * and a record from no site is stamped for the whole org.
47
+ *
48
+ * Rendered as a select rather than an autocomplete: an org has at
45
49
  * most thirty sites in a consent group and rarely more than a handful at
46
50
  * all, and a required field with a fixed list should look like one.
47
51
  *
@@ -50,8 +54,9 @@ import { useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
50
54
  * no-site pick is the FORM's, not the session's: the remembered site stays
51
55
  * what the next contact or deal defaults to.
52
56
  */ export function CrmSitePicker(props) {
53
- var _mount_createHostId, _noSite_helperText;
54
- const { hostId, label = 'Site', helperText, disabled, noSite } = props;
57
+ var _props_label, _mount_createHostId, _noSite_helperText;
58
+ const { hostId, optional = false, helperText, disabled, noSite } = props;
59
+ const label = (_props_label = props.label) != null ? _props_label : optional ? 'Source site' : 'Site';
55
60
  const mount = useCrmOrgMount();
56
61
  // Under a site, or with one site and nothing else to choose, there is no
57
62
  // question.
@@ -61,7 +66,7 @@ import { useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
61
66
  const picked = (noSite == null ? void 0 : noSite.picked) ? NO_SITE_VALUE : (_mount_createHostId = mount.createHostId) != null ? _mount_createHostId : '';
62
67
  return /*#__PURE__*/ _jsxs(TextField, {
63
68
  select: true,
64
- required: true,
69
+ required: !optional,
65
70
  size: "small",
66
71
  label: label,
67
72
  value: picked,
@@ -72,16 +77,20 @@ import { useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
72
77
  return;
73
78
  }
74
79
  noSite == null ? void 0 : noSite.onPick(false);
75
- mount.setCreateHostId(next);
80
+ mount.setCreateHostId(next || null);
76
81
  },
77
82
  disabled: disabled || !mount.hostsReady,
78
- helperText: !mount.hostsReady ? 'Loading your sites…' : (noSite == null ? void 0 : noSite.picked) ? (_noSite_helperText = noSite.helperText) != null ? _noSite_helperText : helperText : mount.hosts.length === 0 && !noSite ? 'This organization has no sites yet, so nothing can be filed.' : helperText != null ? helperText : 'The site this record belongs to — it decides which of your ' + 'sites may see it.',
83
+ helperText: !mount.hostsReady ? 'Loading your sites…' : (noSite == null ? void 0 : noSite.picked) ? (_noSite_helperText = noSite.helperText) != null ? _noSite_helperText : helperText : optional ? helperText != null ? helperText : 'Where it came from, if anywhere. Every site in the organization ' + 'can see it either way.' : mount.hosts.length === 0 && !noSite ? 'This organization has no sites yet, so nothing can be filed.' : helperText != null ? helperText : 'The site this record belongs to — it decides which of your ' + 'sites may see it.',
79
84
  slotProps: {
80
85
  inputLabel: {
81
86
  shrink: true
82
87
  }
83
88
  },
84
89
  children: [
90
+ optional && !noSite ? /*#__PURE__*/ _jsx(MenuItem, {
91
+ value: "",
92
+ children: 'None'
93
+ }) : null,
85
94
  mount.hosts.map((host)=>/*#__PURE__*/ _jsx(MenuItem, {
86
95
  value: host.id,
87
96
  children: host.name
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-site-picker.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { MenuItem, TextField } from '@mui/material'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\n\nexport interface CrmSitePickerProps {\n /**\n * The site the surface is mounted under. Under a site the picker renders\n * NOTHING — the site is known — so a drawer mounts this unconditionally\n * and only the organization-level reader ever sees a field.\n */\n hostId: string | null\n /** The field's label; \"Site\" unless the form has a reason to say more. */\n label?: string\n /** What the pick decides, said under the field. */\n helperText?: string\n disabled?: boolean\n /**\n * A choice that is no site at all — the organization itself (AGL-2637).\n * Offered under the sites when given, and then the field renders even\n * for an org with one site, because there is a choice to make. `picked`\n * says the form holds that choice; picking a site clears it. Only a task\n * offers this: every other CRM record is a fact about a person some site\n * met, and is captured BY that site.\n */\n noSite?: {\n label: string\n /** What the choice decides, said under the field while it is picked. */\n helperText?: string\n picked: boolean\n onPick: (picked: boolean) => void\n }\n}\n\n/**\n * The select's value for the no-site choice. Never a site's id, which is a\n * Firestore document id and cannot begin with an underscore-and-word the\n * console minted itself.\n */\nconst NO_SITE_VALUE = '__organization'\n\n/**\n * THE SITE A CREATE IS CAPTURED BY, at the organization level (AGL-2630).\n *\n * Every CRM record names the site that created it: its `hostId`, the\n * `visibleTo` that decides who may read it, and — for a contact — the\n * consent group whose facet holds the profile and whose name the consent\n * checkbox records a basis for. Under a site all three follow from the\n * URL. At `/[orgSlug]/crm` nothing does, so every create and import drawer\n * mounted there asks, with this field, and holds its submit until it is\n * answered. The REST API asks the same question as `consentSiteId`; this is\n * that parameter with a list attached.\n *\n * The pick is REMEMBERED for the session (`useCrmOrgMount`), so a reader\n * filing ten records for one brand picks once; an org with exactly one\n * site never sees the field, because there is nothing to choose. Nothing\n * else is picked silently — a wrong guess files a person under a brand\n * that never met them, which is exactly the disclosure the per-site model\n * exists to prevent.\n *\n * Rendered as a required select rather than an autocomplete: an org has at\n * most thirty sites in a consent group and rarely more than a handful at\n * all, and a required field with a fixed list should look like one.\n *\n * With a {@link CrmSitePickerProps.noSite} choice the field is a question\n * even for an org with one site — the site, or the organization — and the\n * no-site pick is the FORM's, not the session's: the remembered site stays\n * what the next contact or deal defaults to.\n */\nexport function CrmSitePicker(props: CrmSitePickerProps) {\n const { hostId, label = 'Site', helperText, disabled, noSite } = props\n const mount = useCrmOrgMount()\n // Under a site, or with one site and nothing else to choose, there is no\n // question.\n if (\n hostId ||\n !mount ||\n (!noSite && mount.hostsReady && mount.hosts.length === 1)\n ) {\n return null\n }\n const picked = noSite?.picked ? NO_SITE_VALUE : (mount.createHostId ?? '')\n return (\n <TextField\n select\n required\n size=\"small\"\n label={label}\n value={picked}\n onChange={(event) => {\n const next = String(event.target.value)\n if (next === NO_SITE_VALUE) {\n noSite?.onPick(true)\n return\n }\n noSite?.onPick(false)\n mount.setCreateHostId(next)\n }}\n disabled={disabled || !mount.hostsReady}\n helperText={\n !mount.hostsReady\n ? 'Loading your sites…'\n : noSite?.picked\n ? (noSite.helperText ?? helperText)\n : mount.hosts.length === 0 && !noSite\n ? 'This organization has no sites yet, so nothing can be filed.'\n : (helperText ??\n 'The site this record belongs to — it decides which of your ' +\n 'sites may see it.')\n }\n slotProps={{ inputLabel: { shrink: true } }}\n >\n {mount.hosts.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.name}\n </MenuItem>\n ))}\n {noSite ? <MenuItem value={NO_SITE_VALUE}>{noSite.label}</MenuItem> : null}\n </TextField>\n )\n}\nCrmSitePicker.displayName = 'CrmSitePicker'\n\nexport default CrmSitePicker\n"],"names":["MenuItem","TextField","useCrmOrgMount","NO_SITE_VALUE","CrmSitePicker","props","mount","noSite","hostId","label","helperText","disabled","hostsReady","hosts","length","picked","createHostId","select","required","size","value","onChange","event","next","String","target","onPick","setCreateHostId","slotProps","inputLabel","shrink","map","host","id","name","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,QAAQ,EAAEC,SAAS,QAAQ,gBAAe;AACnD,SAASC,cAAc,QAAQ,gCAA4B;AA+B3D;;;;CAIC,GACD,MAAMC,gBAAgB;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,SAASC,cAAcC,KAAyB;QAYJC,qBAsBpCC;IAjCb,MAAM,EAAEC,MAAM,EAAEC,QAAQ,MAAM,EAAEC,UAAU,EAAEC,QAAQ,EAAEJ,MAAM,EAAE,GAAGF;IACjE,MAAMC,QAAQJ;IACd,yEAAyE;IACzE,YAAY;IACZ,IACEM,UACA,CAACF,SACA,CAACC,UAAUD,MAAMM,UAAU,IAAIN,MAAMO,KAAK,CAACC,MAAM,KAAK,GACvD;QACA,OAAO;IACT;IACA,MAAMC,SAASR,CAAAA,0BAAAA,OAAQQ,MAAM,IAAGZ,iBAAiBG,sBAAAA,MAAMU,YAAY,YAAlBV,sBAAsB;IACvE,qBACE,MAACL;QACCgB,MAAM;QACNC,QAAQ;QACRC,MAAK;QACLV,OAAOA;QACPW,OAAOL;QACPM,UAAU,CAACC;YACT,MAAMC,OAAOC,OAAOF,MAAMG,MAAM,CAACL,KAAK;YACtC,IAAIG,SAASpB,eAAe;gBAC1BI,0BAAAA,OAAQmB,MAAM,CAAC;gBACf;YACF;YACAnB,0BAAAA,OAAQmB,MAAM,CAAC;YACfpB,MAAMqB,eAAe,CAACJ;QACxB;QACAZ,UAAUA,YAAY,CAACL,MAAMM,UAAU;QACvCF,YACE,CAACJ,MAAMM,UAAU,GACb,wBACAL,CAAAA,0BAAAA,OAAQQ,MAAM,KACXR,qBAAAA,OAAOG,UAAU,YAAjBH,qBAAqBG,aACtBJ,MAAMO,KAAK,CAACC,MAAM,KAAK,KAAK,CAACP,SAC3B,iEACCG,qBAAAA,aACD,gEACE;QAEZkB,WAAW;YAAEC,YAAY;gBAAEC,QAAQ;YAAK;QAAE;;YAEzCxB,MAAMO,KAAK,CAACkB,GAAG,CAAC,CAACC,qBAChB,KAAChC;oBAAuBoB,OAAOY,KAAKC,EAAE;8BACnCD,KAAKE,IAAI;mBADGF,KAAKC,EAAE;YAIvB1B,uBAAS,KAACP;gBAASoB,OAAOjB;0BAAgBI,OAAOE,KAAK;iBAAe;;;AAG5E;AACAL,cAAc+B,WAAW,GAAG;AAE5B,eAAe/B,cAAa"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-site-picker.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { MenuItem, TextField } from '@mui/material'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\n\nexport interface CrmSitePickerProps {\n /**\n * The site the surface is mounted under. Under a site the picker renders\n * NOTHING — the site is known — so a drawer mounts this unconditionally\n * and only the organization-level reader ever sees a field.\n */\n hostId: string | null\n /** The field's label; \"Site\" unless the form has a reason to say more. */\n label?: string\n /** What the pick decides, said under the field. */\n helperText?: string\n disabled?: boolean\n /**\n * The org's records belong to every site, so the site is only the SOURCE a\n * record came from (`useCrmScope`'s `createNeedsSite` is false): the field\n * reads \"Source site\", is not required, and offers **None** first. In an\n * org that keeps each site's records apart the site decides who may see\n * the record and the field stays required.\n */\n optional?: boolean\n /**\n * A choice that is no site at all — the organization itself (AGL-2637).\n * Offered under the sites when given, and then the field renders even\n * for an org with one site, because there is a choice to make. `picked`\n * says the form holds that choice; picking a site clears it. Only a task\n * offers this: every other CRM record is a fact about a person some site\n * met, and is captured BY that site.\n */\n noSite?: {\n label: string\n /** What the choice decides, said under the field while it is picked. */\n helperText?: string\n picked: boolean\n onPick: (picked: boolean) => void\n }\n}\n\n/**\n * The select's value for the no-site choice. Never a site's id, which is a\n * Firestore document id and cannot begin with an underscore-and-word the\n * console minted itself.\n */\nconst NO_SITE_VALUE = '__organization'\n\n/**\n * THE SITE A CREATE IS CAPTURED BY, at the organization level (AGL-2630).\n *\n * Every CRM record names the site that created it: its `hostId`, the\n * `visibleTo` that decides who may read it, and — for a contact — the\n * consent group whose facet holds the profile and whose name the consent\n * checkbox records a basis for. Under a site all three follow from the\n * URL. At `/[orgSlug]/crm` nothing does, so every create and import drawer\n * mounted there asks, with this field, and holds its submit until it is\n * answered. The REST API asks the same question as `consentSiteId`; this is\n * that parameter with a list attached.\n *\n * The pick is REMEMBERED for the session (`useCrmOrgMount`), so a reader\n * filing ten records for one brand picks once; an org with exactly one\n * site never sees the field, because there is nothing to choose. Nothing\n * else is picked silently — a wrong guess files a person under a brand\n * that never met them, which is exactly the disclosure the per-site model\n * exists to prevent.\n *\n * In an org whose records belong to every site ({@link CrmSitePickerProps.optional})\n * the field is the record's SOURCE, not its scope: optional, **None** first,\n * and a record from no site is stamped for the whole org.\n *\n * Rendered as a select rather than an autocomplete: an org has at\n * most thirty sites in a consent group and rarely more than a handful at\n * all, and a required field with a fixed list should look like one.\n *\n * With a {@link CrmSitePickerProps.noSite} choice the field is a question\n * even for an org with one site — the site, or the organization — and the\n * no-site pick is the FORM's, not the session's: the remembered site stays\n * what the next contact or deal defaults to.\n */\nexport function CrmSitePicker(props: CrmSitePickerProps) {\n const { hostId, optional = false, helperText, disabled, noSite } = props\n const label = props.label ?? (optional ? 'Source site' : 'Site')\n const mount = useCrmOrgMount()\n // Under a site, or with one site and nothing else to choose, there is no\n // question.\n if (\n hostId ||\n !mount ||\n (!noSite && mount.hostsReady && mount.hosts.length === 1)\n ) {\n return null\n }\n const picked = noSite?.picked ? NO_SITE_VALUE : (mount.createHostId ?? '')\n return (\n <TextField\n select\n required={!optional}\n size=\"small\"\n label={label}\n value={picked}\n onChange={(event) => {\n const next = String(event.target.value)\n if (next === NO_SITE_VALUE) {\n noSite?.onPick(true)\n return\n }\n noSite?.onPick(false)\n mount.setCreateHostId(next || null)\n }}\n disabled={disabled || !mount.hostsReady}\n helperText={\n !mount.hostsReady\n ? 'Loading your sites…'\n : noSite?.picked\n ? (noSite.helperText ?? helperText)\n : optional\n ? (helperText ??\n 'Where it came from, if anywhere. Every site in the organization ' +\n 'can see it either way.')\n : mount.hosts.length === 0 && !noSite\n ? 'This organization has no sites yet, so nothing can be filed.'\n : (helperText ??\n 'The site this record belongs to — it decides which of your ' +\n 'sites may see it.')\n }\n slotProps={{ inputLabel: { shrink: true } }}\n >\n {optional && !noSite ? <MenuItem value=\"\">{'None'}</MenuItem> : null}\n {mount.hosts.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.name}\n </MenuItem>\n ))}\n {noSite ? <MenuItem value={NO_SITE_VALUE}>{noSite.label}</MenuItem> : null}\n </TextField>\n )\n}\nCrmSitePicker.displayName = 'CrmSitePicker'\n\nexport default CrmSitePicker\n"],"names":["MenuItem","TextField","useCrmOrgMount","NO_SITE_VALUE","CrmSitePicker","props","mount","noSite","hostId","optional","helperText","disabled","label","hostsReady","hosts","length","picked","createHostId","select","required","size","value","onChange","event","next","String","target","onPick","setCreateHostId","slotProps","inputLabel","shrink","map","host","id","name","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,QAAQ,EAAEC,SAAS,QAAQ,gBAAe;AACnD,SAASC,cAAc,QAAQ,gCAA4B;AAuC3D;;;;CAIC,GACD,MAAMC,gBAAgB;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BC,GACD,OAAO,SAASC,cAAcC,KAAyB;QAEvCA,cAWmCC,qBAsBpCC;IAlCb,MAAM,EAAEC,MAAM,EAAEC,WAAW,KAAK,EAAEC,UAAU,EAAEC,QAAQ,EAAEJ,MAAM,EAAE,GAAGF;IACnE,MAAMO,SAAQP,eAAAA,MAAMO,KAAK,YAAXP,eAAgBI,WAAW,gBAAgB;IACzD,MAAMH,QAAQJ;IACd,yEAAyE;IACzE,YAAY;IACZ,IACEM,UACA,CAACF,SACA,CAACC,UAAUD,MAAMO,UAAU,IAAIP,MAAMQ,KAAK,CAACC,MAAM,KAAK,GACvD;QACA,OAAO;IACT;IACA,MAAMC,SAAST,CAAAA,0BAAAA,OAAQS,MAAM,IAAGb,iBAAiBG,sBAAAA,MAAMW,YAAY,YAAlBX,sBAAsB;IACvE,qBACE,MAACL;QACCiB,MAAM;QACNC,UAAU,CAACV;QACXW,MAAK;QACLR,OAAOA;QACPS,OAAOL;QACPM,UAAU,CAACC;YACT,MAAMC,OAAOC,OAAOF,MAAMG,MAAM,CAACL,KAAK;YACtC,IAAIG,SAASrB,eAAe;gBAC1BI,0BAAAA,OAAQoB,MAAM,CAAC;gBACf;YACF;YACApB,0BAAAA,OAAQoB,MAAM,CAAC;YACfrB,MAAMsB,eAAe,CAACJ,QAAQ;QAChC;QACAb,UAAUA,YAAY,CAACL,MAAMO,UAAU;QACvCH,YACE,CAACJ,MAAMO,UAAU,GACb,wBACAN,CAAAA,0BAAAA,OAAQS,MAAM,KACXT,qBAAAA,OAAOG,UAAU,YAAjBH,qBAAqBG,aACtBD,WACGC,qBAAAA,aACD,qEACE,2BACFJ,MAAMQ,KAAK,CAACC,MAAM,KAAK,KAAK,CAACR,SAC3B,iEACCG,qBAAAA,aACD,gEACE;QAEdmB,WAAW;YAAEC,YAAY;gBAAEC,QAAQ;YAAK;QAAE;;YAEzCtB,YAAY,CAACF,uBAAS,KAACP;gBAASqB,OAAM;0BAAI;iBAAqB;YAC/Df,MAAMQ,KAAK,CAACkB,GAAG,CAAC,CAACC,qBAChB,KAACjC;oBAAuBqB,OAAOY,KAAKC,EAAE;8BACnCD,KAAKE,IAAI;mBADGF,KAAKC,EAAE;YAIvB3B,uBAAS,KAACP;gBAASqB,OAAOlB;0BAAgBI,OAAOK,KAAK;iBAAe;;;AAG5E;AACAR,cAAcgC,WAAW,GAAG;AAE5B,eAAehC,cAAa"}
@@ -27,6 +27,11 @@ export interface CrmImportButtonProps {
27
27
  /** What the `importMapping` zone's widgets know the file as (`contacts`). */
28
28
  mappingZone?: string;
29
29
  label?: string;
30
+ /**
31
+ * The file needs no site: the org's records belong to every site, so a site
32
+ * is only where these came from, and the reader may name none (AGL-4122).
33
+ */
34
+ siteOptional?: boolean;
30
35
  }
31
36
  /** Opens the import wizard on a CRM resource, under the list's site or one the reader picks. */
32
37
  export declare function CrmImportButton(props: CrmImportButtonProps): import("react").JSX.Element;
@@ -53,7 +53,7 @@ import { CrmSitePicker } from "./crm-site-picker.js";
53
53
  }
54
54
  /** Opens the import wizard on a CRM resource, under the list's site or one the reader picks. */ export function CrmImportButton(props) {
55
55
  var _ref;
56
- const { resource, noun, hostId, mappingZone, label = 'Import' } = props;
56
+ const { resource, noun, hostId, mappingZone, label = 'Import', siteOptional = false } = props;
57
57
  const transfer = useTransferLauncher();
58
58
  const mount = useCrmOrgMount();
59
59
  const [asking, setAsking] = useState(false);
@@ -105,12 +105,14 @@ import { CrmSitePicker } from "./crm-site-picker.js";
105
105
  sx: {
106
106
  mb: 2
107
107
  },
108
- children: `Every new record is filed under a site — it decides which of your sites may see the ${noun}.`
108
+ children: siteOptional ? `Every site in the organization can see these ${noun}. Name the site they came from, if any.` : `Every new record is filed under a site — it decides which of your sites may see the ${noun}.`
109
109
  }),
110
- /*#__PURE__*/ _jsx(CrmSitePicker, {
110
+ /*#__PURE__*/ _jsx(CrmSitePicker, _extends({
111
111
  hostId: null,
112
+ optional: siteOptional
113
+ }, siteOptional ? {} : {
112
114
  helperText: `The site these ${noun} are imported into.`
113
- })
115
+ }))
114
116
  ]
115
117
  }),
116
118
  /*#__PURE__*/ _jsxs(DialogActions, {
@@ -121,10 +123,11 @@ import { CrmSitePicker } from "./crm-site-picker.js";
121
123
  }),
122
124
  /*#__PURE__*/ _jsx(Button, {
123
125
  variant: "contained",
124
- disabled: !(mount == null ? void 0 : mount.createHostId),
126
+ disabled: !siteOptional && !(mount == null ? void 0 : mount.createHostId),
125
127
  onClick: ()=>{
128
+ var _ref;
126
129
  setAsking(false);
127
- if (mount == null ? void 0 : mount.createHostId) open(mount.createHostId);
130
+ open((_ref = mount == null ? void 0 : mount.createHostId) != null ? _ref : null);
128
131
  },
129
132
  children: 'Continue'
130
133
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-transfer-buttons.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\n/*==========================================\n * IMPORT AND EXPORT, FROM ANY CRM LIST (AGL-3527).\n *\n * The CRM never draws the wizard or the dialog itself: the console does,\n * and hands the plugin a launcher (`useTransferLauncher`). These buttons are\n * all a list needs — Import opens the wizard on the list's resource, Export\n * the field-picking dialog on its selection, its filter or everything.\n * Outside the console shell there is no launcher, and no button.\n *\n * At the organization level an import is filed under a site, as every CRM\n * create is (AGL-2630): with one site it is that one, with several the\n * button asks first, remembering the pick for the session like every other\n * create.\n *=========================================*/\n\nimport { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport { useTransferLauncher } from '@aglyn/aglyn/app-utils/transfer-launcher-context'\nimport type { ListQueryPlan } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Typography,\n} from '@mui/material'\nimport { useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { CrmSitePicker } from './crm-site-picker'\n\n/** A list's served query as an export's filter: its predicates and its order, dates kept as dates. */\nexport function crmExportFilterOf(plan: Pick<ListQueryPlan, 'filters' | 'orderBy'>): Record<string, unknown> {\n const value = (raw: unknown): unknown =>\n raw instanceof Date ? { $date: raw.toISOString() } : Array.isArray(raw) ? raw.map(value) : raw\n return {\n filters: plan.filters.map((filter) => ({ path: filter.path, op: filter.op, value: value(filter.value) })),\n orderBy: { path: plan.orderBy.path, direction: plan.orderBy.direction },\n }\n}\n\nexport interface CrmImportButtonProps {\n /** The resource key the CRM declared (`crm.contacts`). */\n resource: string\n /** What one batch is called in a sentence (\"contacts\"). */\n noun: string\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** What the `importMapping` zone's widgets know the file as (`contacts`). */\n mappingZone?: string\n label?: string\n}\n\n/** Opens the import wizard on a CRM resource, under the list's site or one the reader picks. */\nexport function CrmImportButton(props: CrmImportButtonProps) {\n const { resource, noun, hostId, mappingZone, label = 'Import' } = props\n const transfer = useTransferLauncher()\n const mount = useCrmOrgMount()\n const [asking, setAsking] = useState(false)\n // Importing needs \"Manage data\" (AGL-3546); a reader sees no Import.\n if (!transfer?.can('import', { resource, scope: 'org', hostId })) return null\n\n const open = (site: string) =>\n transfer.openImport({ resource, scope: 'org', hostId: site, ...(mappingZone ? { mappingZone } : {}) })\n\n const site = hostId ?? mount?.createHostId ?? null\n return (\n <>\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport target={{ resource, scope: 'org', hostId }} />\n <Button\n size=\"small\"\n onClick={() => {\n if (site && (hostId || (mount?.hostsReady && mount.hosts.length === 1))) open(site)\n else setAsking(true)\n }}\n >\n {label}\n </Button>\n <Dialog open={asking} onClose={() => setAsking(false)} aria-labelledby={`${resource}-import-site`}>\n <DialogTitle id={`${resource}-import-site`}>{`Import ${noun}`}</DialogTitle>\n <DialogContent>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ mb: 2 }}>\n {`Every new record is filed under a site — it decides which of your sites may see the ${noun}.`}\n </Typography>\n <CrmSitePicker hostId={null} helperText={`The site these ${noun} are imported into.`} />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setAsking(false)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n disabled={!mount?.createHostId}\n onClick={() => {\n setAsking(false)\n if (mount?.createHostId) open(mount.createHostId)\n }}\n >\n {'Continue'}\n </Button>\n </DialogActions>\n </Dialog>\n </>\n )\n}\nCrmImportButton.displayName = 'CrmImportButton'\n\nexport interface CrmExportButtonProps {\n resource: string\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** The rows the reader ticked; the dialog offers exporting just these. */\n selection?: readonly string[]\n /** The list's filter, when one is applied: a name for it and its served query. */\n filter?: { label: string; plan: Pick<ListQueryPlan, 'filters' | 'orderBy'> } | null\n label?: string\n disabled?: boolean\n}\n\n/** Opens the export dialog on a CRM resource: the selection, the list's filter, or every record. */\nexport function CrmExportButton(props: CrmExportButtonProps) {\n const { resource, hostId, selection, filter, label = 'Export…', disabled } = props\n const transfer = useTransferLauncher()\n if (!transfer?.can('export', { resource, scope: 'org', hostId })) return null\n return (\n <Button\n size=\"small\"\n disabled={disabled}\n onClick={() =>\n transfer.openExport({\n resource,\n scope: 'org',\n hostId,\n ...(selection?.length ? { selection } : {}),\n ...(filter ? { filter: { label: filter.label, value: crmExportFilterOf(filter.plan) } } : {}),\n })\n }\n >\n {label}\n </Button>\n )\n}\nCrmExportButton.displayName = 'CrmExportButton'\n"],"names":["TransferResumeImport","useTransferLauncher","Button","Dialog","DialogActions","DialogContent","DialogTitle","Typography","useState","useCrmOrgMount","CrmSitePicker","crmExportFilterOf","plan","value","raw","Date","$date","toISOString","Array","isArray","map","filters","filter","path","op","orderBy","direction","CrmImportButton","props","hostId","resource","noun","mappingZone","label","transfer","mount","asking","setAsking","can","scope","open","site","openImport","createHostId","target","size","onClick","hostsReady","hosts","length","onClose","aria-labelledby","id","variant","color","sx","mb","helperText","disabled","displayName","CrmExportButton","selection","openExport"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;AAEA;;;;;;;;;;;;;2CAa2C,GAE3C,SAASA,oBAAoB,QAAQ,gDAA+C;AACpF,SAASC,mBAAmB,QAAQ,mDAAkD;AAEtF,SACEC,MAAM,EACNC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,UAAU,QACL,gBAAe;AACtB,SAASC,QAAQ,QAAQ,QAAO;AAChC,SAASC,cAAc,QAAQ,gCAA4B;AAC3D,SAASC,aAAa,QAAQ,uBAAmB;AAEjD,oGAAoG,GACpG,OAAO,SAASC,kBAAkBC,IAAgD;IAChF,MAAMC,QAAQ,CAACC,MACbA,eAAeC,OAAO;YAAEC,OAAOF,IAAIG,WAAW;QAAG,IAAIC,MAAMC,OAAO,CAACL,OAAOA,IAAIM,GAAG,CAACP,SAASC;IAC7F,OAAO;QACLO,SAAST,KAAKS,OAAO,CAACD,GAAG,CAAC,CAACE,SAAY,CAAA;gBAAEC,MAAMD,OAAOC,IAAI;gBAAEC,IAAIF,OAAOE,EAAE;gBAAEX,OAAOA,MAAMS,OAAOT,KAAK;YAAE,CAAA;QACtGY,SAAS;YAAEF,MAAMX,KAAKa,OAAO,CAACF,IAAI;YAAEG,WAAWd,KAAKa,OAAO,CAACC,SAAS;QAAC;IACxE;AACF;AAcA,8FAA8F,GAC9F,OAAO,SAASC,gBAAgBC,KAA2B;QAW5CC;IAVb,MAAM,EAAEC,QAAQ,EAAEC,IAAI,EAAEF,MAAM,EAAEG,WAAW,EAAEC,QAAQ,QAAQ,EAAE,GAAGL;IAClE,MAAMM,WAAWjC;IACjB,MAAMkC,QAAQ1B;IACd,MAAM,CAAC2B,QAAQC,UAAU,GAAG7B,SAAS;IACrC,qEAAqE;IACrE,IAAI,EAAC0B,4BAAAA,SAAUI,GAAG,CAAC,UAAU;QAAER;QAAUS,OAAO;QAAOV;IAAO,KAAI,OAAO;IAEzE,MAAMW,OAAO,CAACC,OACZP,SAASQ,UAAU,CAAC;YAAEZ;YAAUS,OAAO;YAAOV,QAAQY;WAAUT,cAAc;YAAEA;QAAY,IAAI,CAAC;IAEnG,MAAMS,QAAOZ,OAAAA,iBAAAA,SAAUM,yBAAAA,MAAOQ,YAAY,YAA7Bd,OAAiC;IAC9C,qBACE;;0BAEE,KAAC7B;gBAAqB4C,QAAQ;oBAAEd;oBAAUS,OAAO;oBAAOV;gBAAO;;0BAC/D,KAAC3B;gBACC2C,MAAK;gBACLC,SAAS;oBACP,IAAIL,QAASZ,CAAAA,UAAWM,CAAAA,yBAAAA,MAAOY,UAAU,KAAIZ,MAAMa,KAAK,CAACC,MAAM,KAAK,CAAC,GAAIT,KAAKC;yBACzEJ,UAAU;gBACjB;0BAECJ;;0BAEH,MAAC9B;gBAAOqC,MAAMJ;gBAAQc,SAAS,IAAMb,UAAU;gBAAQc,mBAAiB,GAAGrB,SAAS,YAAY,CAAC;;kCAC/F,KAACxB;wBAAY8C,IAAI,GAAGtB,SAAS,YAAY,CAAC;kCAAG,CAAC,OAAO,EAAEC,MAAM;;kCAC7D,MAAC1B;;0CACC,KAACE;gCAAW8C,SAAQ;gCAAQC,OAAM;gCAAiBC,IAAI;oCAAEC,IAAI;gCAAE;0CAC5D,CAAC,oFAAoF,EAAEzB,KAAK,CAAC,CAAC;;0CAEjG,KAACrB;gCAAcmB,QAAQ;gCAAM4B,YAAY,CAAC,eAAe,EAAE1B,KAAK,mBAAmB,CAAC;;;;kCAEtF,MAAC3B;;0CACC,KAACF;gCAAO4C,SAAS,IAAMT,UAAU;0CAAS;;0CAC1C,KAACnC;gCACCmD,SAAQ;gCACRK,UAAU,EAACvB,yBAAAA,MAAOQ,YAAY;gCAC9BG,SAAS;oCACPT,UAAU;oCACV,IAAIF,yBAAAA,MAAOQ,YAAY,EAAEH,KAAKL,MAAMQ,YAAY;gCAClD;0CAEC;;;;;;;;AAMb;AACAhB,gBAAgBgC,WAAW,GAAG;AAc9B,kGAAkG,GAClG,OAAO,SAASC,gBAAgBhC,KAA2B;IACzD,MAAM,EAAEE,QAAQ,EAAED,MAAM,EAAEgC,SAAS,EAAEvC,MAAM,EAAEW,QAAQ,SAAS,EAAEyB,QAAQ,EAAE,GAAG9B;IAC7E,MAAMM,WAAWjC;IACjB,IAAI,EAACiC,4BAAAA,SAAUI,GAAG,CAAC,UAAU;QAAER;QAAUS,OAAO;QAAOV;IAAO,KAAI,OAAO;IACzE,qBACE,KAAC3B;QACC2C,MAAK;QACLa,UAAUA;QACVZ,SAAS,IACPZ,SAAS4B,UAAU,CAAC;gBAClBhC;gBACAS,OAAO;gBACPV;eACIgC,CAAAA,6BAAAA,UAAWZ,MAAM,IAAG;gBAAEY;YAAU,IAAI,CAAC,GACrCvC,SAAS;gBAAEA,QAAQ;oBAAEW,OAAOX,OAAOW,KAAK;oBAAEpB,OAAOF,kBAAkBW,OAAOV,IAAI;gBAAE;YAAE,IAAI,CAAC;kBAI9FqB;;AAGP;AACA2B,gBAAgBD,WAAW,GAAG"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-transfer-buttons.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\n/*==========================================\n * IMPORT AND EXPORT, FROM ANY CRM LIST (AGL-3527).\n *\n * The CRM never draws the wizard or the dialog itself: the console does,\n * and hands the plugin a launcher (`useTransferLauncher`). These buttons are\n * all a list needs — Import opens the wizard on the list's resource, Export\n * the field-picking dialog on its selection, its filter or everything.\n * Outside the console shell there is no launcher, and no button.\n *\n * At the organization level an import is filed under a site, as every CRM\n * create is (AGL-2630): with one site it is that one, with several the\n * button asks first, remembering the pick for the session like every other\n * create.\n *=========================================*/\n\nimport { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport { useTransferLauncher } from '@aglyn/aglyn/app-utils/transfer-launcher-context'\nimport type { ListQueryPlan } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Typography,\n} from '@mui/material'\nimport { useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { CrmSitePicker } from './crm-site-picker'\n\n/** A list's served query as an export's filter: its predicates and its order, dates kept as dates. */\nexport function crmExportFilterOf(plan: Pick<ListQueryPlan, 'filters' | 'orderBy'>): Record<string, unknown> {\n const value = (raw: unknown): unknown =>\n raw instanceof Date ? { $date: raw.toISOString() } : Array.isArray(raw) ? raw.map(value) : raw\n return {\n filters: plan.filters.map((filter) => ({ path: filter.path, op: filter.op, value: value(filter.value) })),\n orderBy: { path: plan.orderBy.path, direction: plan.orderBy.direction },\n }\n}\n\nexport interface CrmImportButtonProps {\n /** The resource key the CRM declared (`crm.contacts`). */\n resource: string\n /** What one batch is called in a sentence (\"contacts\"). */\n noun: string\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** What the `importMapping` zone's widgets know the file as (`contacts`). */\n mappingZone?: string\n label?: string\n /**\n * The file needs no site: the org's records belong to every site, so a site\n * is only where these came from, and the reader may name none (AGL-4122).\n */\n siteOptional?: boolean\n}\n\n/** Opens the import wizard on a CRM resource, under the list's site or one the reader picks. */\nexport function CrmImportButton(props: CrmImportButtonProps) {\n const { resource, noun, hostId, mappingZone, label = 'Import', siteOptional = false } = props\n const transfer = useTransferLauncher()\n const mount = useCrmOrgMount()\n const [asking, setAsking] = useState(false)\n // Importing needs \"Manage data\" (AGL-3546); a reader sees no Import.\n if (!transfer?.can('import', { resource, scope: 'org', hostId })) return null\n\n const open = (site: string | null) =>\n transfer.openImport({ resource, scope: 'org', hostId: site, ...(mappingZone ? { mappingZone } : {}) })\n\n const site = hostId ?? mount?.createHostId ?? null\n return (\n <>\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport target={{ resource, scope: 'org', hostId }} />\n <Button\n size=\"small\"\n onClick={() => {\n if (site && (hostId || (mount?.hostsReady && mount.hosts.length === 1))) open(site)\n else setAsking(true)\n }}\n >\n {label}\n </Button>\n <Dialog open={asking} onClose={() => setAsking(false)} aria-labelledby={`${resource}-import-site`}>\n <DialogTitle id={`${resource}-import-site`}>{`Import ${noun}`}</DialogTitle>\n <DialogContent>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ mb: 2 }}>\n {siteOptional\n ? `Every site in the organization can see these ${noun}. Name the site they came from, if any.`\n : `Every new record is filed under a site — it decides which of your sites may see the ${noun}.`}\n </Typography>\n <CrmSitePicker\n hostId={null}\n optional={siteOptional}\n {...(siteOptional ? {} : { helperText: `The site these ${noun} are imported into.` })}\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setAsking(false)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n disabled={!siteOptional && !mount?.createHostId}\n onClick={() => {\n setAsking(false)\n open(mount?.createHostId ?? null)\n }}\n >\n {'Continue'}\n </Button>\n </DialogActions>\n </Dialog>\n </>\n )\n}\nCrmImportButton.displayName = 'CrmImportButton'\n\nexport interface CrmExportButtonProps {\n resource: string\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** The rows the reader ticked; the dialog offers exporting just these. */\n selection?: readonly string[]\n /** The list's filter, when one is applied: a name for it and its served query. */\n filter?: { label: string; plan: Pick<ListQueryPlan, 'filters' | 'orderBy'> } | null\n label?: string\n disabled?: boolean\n}\n\n/** Opens the export dialog on a CRM resource: the selection, the list's filter, or every record. */\nexport function CrmExportButton(props: CrmExportButtonProps) {\n const { resource, hostId, selection, filter, label = 'Export…', disabled } = props\n const transfer = useTransferLauncher()\n if (!transfer?.can('export', { resource, scope: 'org', hostId })) return null\n return (\n <Button\n size=\"small\"\n disabled={disabled}\n onClick={() =>\n transfer.openExport({\n resource,\n scope: 'org',\n hostId,\n ...(selection?.length ? { selection } : {}),\n ...(filter ? { filter: { label: filter.label, value: crmExportFilterOf(filter.plan) } } : {}),\n })\n }\n >\n {label}\n </Button>\n )\n}\nCrmExportButton.displayName = 'CrmExportButton'\n"],"names":["TransferResumeImport","useTransferLauncher","Button","Dialog","DialogActions","DialogContent","DialogTitle","Typography","useState","useCrmOrgMount","CrmSitePicker","crmExportFilterOf","plan","value","raw","Date","$date","toISOString","Array","isArray","map","filters","filter","path","op","orderBy","direction","CrmImportButton","props","hostId","resource","noun","mappingZone","label","siteOptional","transfer","mount","asking","setAsking","can","scope","open","site","openImport","createHostId","target","size","onClick","hostsReady","hosts","length","onClose","aria-labelledby","id","variant","color","sx","mb","optional","helperText","disabled","displayName","CrmExportButton","selection","openExport"],"mappings":"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"}