@aglyn/plugins-crm 1.0.0-beta.246 → 1.0.0-beta.247
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/README.md +1 -1
- package/package.json +14 -14
- package/src/lib/components/contact-custom-columns.d.ts +15 -6
- package/src/lib/components/contact-custom-columns.js +27 -7
- package/src/lib/components/contact-custom-columns.js.map +1 -1
- package/src/lib/components/contact-field-drawer.d.ts +2 -0
- package/src/lib/components/contact-field-drawer.js +25 -1
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-custom-field-control.d.ts +4 -2
- package/src/lib/components/crm-custom-field-control.js +53 -1
- package/src/lib/components/crm-custom-field-control.js.map +1 -1
- package/src/lib/components/fields-section.js +4 -2
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/form-contact-fields-card.js +2 -0
- package/src/lib/components/form-contact-fields-card.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +7 -0
- package/src/lib/constants/contact-filters.js +8 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +2 -0
- package/src/lib/declarations.console-server.js +5 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/model/crm-picklist-setup-content.d.ts +121 -0
- package/src/lib/model/crm-picklist-setup-content.js +262 -0
- package/src/lib/model/crm-picklist-setup-content.js.map +1 -0
- package/src/lib/model/crm-record-routes.js +6 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-setup-content.d.ts +4 -1
- package/src/lib/model/crm-setup-content.js +15 -4
- package/src/lib/model/crm-setup-content.js.map +1 -1
- package/src/lib/server/crm-setup-ai-capabilities.d.ts +13 -0
- package/src/lib/server/crm-setup-ai-capabilities.js +94 -2
- package/src/lib/server/crm-setup-ai-capabilities.js.map +1 -1
- package/src/lib/server/crm-setup-writers.server.d.ts +28 -0
- package/src/lib/server/crm-setup-writers.server.js +181 -4
- package/src/lib/server/crm-setup-writers.server.js.map +1 -1
- package/src/lib/server/picklist-regroup.d.ts +30 -0
- package/src/lib/server/picklist-regroup.js +61 -0
- package/src/lib/server/picklist-regroup.js.map +1 -0
- package/src/lib/server/picklist-values.d.ts +2 -14
- package/src/lib/server/picklist-values.js +3 -40
- package/src/lib/server/picklist-values.js.map +1 -1
- package/src/lib/transfer/common.d.ts +4 -2
- package/src/lib/transfer/common.js +14 -3
- package/src/lib/transfer/common.js.map +1 -1
- package/src/lib/transfer/fields.js +10 -1
- package/src/lib/transfer/fields.js.map +1 -1
- package/src/lib/transfer/pipelines.js +4 -2
- package/src/lib/transfer/pipelines.js.map +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ The CRM is console-only. Its records live in Firestore and it has no canvas comp
|
|
|
40
40
|
|
|
41
41
|
`registerCrmConsoleApi()` from `@aglyn/plugins-crm/server` (the `consoleApi` surface) registers routes under the `crm` prefix for contact stage and field updates, contact creation and merge, lead conversion, deal stages, tasks, one-to-one email and a contact's email history, erasing a person, organization-level activity, recipes, and the workspace's inbound email capture address. Contacts, companies, leads, deals and tasks are imported and exported through the platform's transfer framework, and activities, pipelines and custom fields exported: `registerCrmConsoleServerDeclarations()` registers each resource's server half (`src/lib/transfer/`) and the console registrar its name in the import wizard and export dialog. It also registers the CRM on two core seams, so another plugin can read a record's facts or file an item on a record's timeline without importing this package.
|
|
42
42
|
|
|
43
|
-
At the console's boot it also registers the CRM's setup writers on the `plugin-resource-drafts` seam (`crm-pipeline`, `crm-field`) and the build operations Aglyn AI plans with them (`plugin-ai-capabilities`): a pipeline with its stages and a
|
|
43
|
+
At the console's boot it also registers the CRM's setup writers on the `plugin-resource-drafts` seam (`crm-pipeline`, `crm-field`, `crm-picklist-values`) and the build operations Aglyn AI plans with them (`plugin-ai-capabilities`): a pipeline with its stages, a custom field, and values added to a picklist or a Choice field (`src/lib/model/crm-picklist-setup-content.ts`), saved live under the console's rules (`src/lib/model/crm-setup-content.ts`, `src/lib/server/crm-setup-writers.server.ts`).
|
|
44
44
|
|
|
45
45
|
At the console's boot (`registerCrmConsoleServerDeclarations`) it registers a membership detacher (`plugin-membership-detach`): when the plugin that keeps a container — a campaign — removes it, the container comes off every lead and every contact facet that names it, and the records stay.
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-crm",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.247",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.247",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.247",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.247",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.247",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.247",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.247",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.247",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.247",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.247",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.247",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.247",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.247",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.247",
|
|
41
41
|
"@dnd-kit/core": "^6.3.1",
|
|
42
42
|
"@dnd-kit/utilities": "^3.2.2",
|
|
43
43
|
"@swc/helpers": "0.5.23",
|
|
@@ -12,11 +12,18 @@ export declare const customFieldColumnId: (key: string) => string;
|
|
|
12
12
|
*
|
|
13
13
|
* A checkbox reads as Yes or an em dash rather than `true`/`false`; a date
|
|
14
14
|
* as the local day rather than the ISO stamp it is stored as; a link as an
|
|
15
|
-
* anchor
|
|
15
|
+
* anchor; an amount as money in the field's currency (AGL-3756). Everything
|
|
16
|
+
* else is the value itself, and nothing is `null` on
|
|
16
17
|
* screen — a cleared value and an absent one look the same, because to the
|
|
17
18
|
* reader of a list they are.
|
|
18
19
|
*/
|
|
19
|
-
export declare function formatContactCustomValue(definition: Pick<ContactFieldDefinition, 'type'>, value: ContactCustomValue | undefined): string;
|
|
20
|
+
export declare function formatContactCustomValue(definition: Pick<ContactFieldDefinition, 'type' | 'currency'>, value: ContactCustomValue | undefined): string;
|
|
21
|
+
/**
|
|
22
|
+
* How each active currency field's filter reads a typed amount, by column
|
|
23
|
+
* (AGL-3756) — `ContactQueryReader.amounts`: major units in, the minor
|
|
24
|
+
* units its facet key holds out.
|
|
25
|
+
*/
|
|
26
|
+
export declare function contactCustomAmountReaders(definitions: readonly Pick<ContactFieldDefinition, 'key' | 'type' | 'currency' | 'retiredAt'>[]): Record<string, (typed: string) => number | null>;
|
|
20
27
|
/**
|
|
21
28
|
* A CRM list's columns for the org's custom fields (AGL-2601), one per
|
|
22
29
|
* ACTIVE definition in `order` — the contacts list's, and since AGL-2661
|
|
@@ -26,10 +33,12 @@ export declare function formatContactCustomValue(definition: Pick<ContactFieldDe
|
|
|
26
33
|
* The value is read off `row.custom`: on a contact the list projects it
|
|
27
34
|
* from THIS holder's facet — the same projection that puts `tags` and
|
|
28
35
|
* `notes` on the row — so a column can never show another holder's value;
|
|
29
|
-
* on a company or a deal it is the document's own map.
|
|
30
|
-
* filtering are off: the value lives under a map no index
|
|
31
|
-
*
|
|
32
|
-
*
|
|
36
|
+
* on a company or a deal it is the document's own map. The grid's own
|
|
37
|
+
* sorting and filtering are off: the value lives under a map no index
|
|
38
|
+
* covers, so a list's column sort sorts the PAGE through `valueGetter` and
|
|
39
|
+
* says so (AGL-3680), and its filters ask the query (`contact-filters.ts`).
|
|
40
|
+
* A currency column's `valueGetter` is the stored amount, so the page sorts
|
|
41
|
+
* by money rather than by the formatted text.
|
|
33
42
|
*
|
|
34
43
|
* Retired definitions get no column. Their values are still on the rows,
|
|
35
44
|
* and an export can still read them; a column for a field the merchant
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
-
import { crmContactCustomColumn } from "@aglyn/aglyn";
|
|
18
|
+
import { customFieldCurrency, crmContactCustomColumn, formatCurrencyFieldValue, parseCurrencyInput } from "@aglyn/aglyn";
|
|
19
19
|
import { Link, Typography } from "@mui/material";
|
|
20
20
|
/**
|
|
21
21
|
* The column `field` a definition's key is listed under — namespaced so a
|
|
@@ -28,7 +28,8 @@ import { Link, Typography } from "@mui/material";
|
|
|
28
28
|
*
|
|
29
29
|
* A checkbox reads as Yes or an em dash rather than `true`/`false`; a date
|
|
30
30
|
* as the local day rather than the ISO stamp it is stored as; a link as an
|
|
31
|
-
* anchor
|
|
31
|
+
* anchor; an amount as money in the field's currency (AGL-3756). Everything
|
|
32
|
+
* else is the value itself, and nothing is `null` on
|
|
32
33
|
* screen — a cleared value and an absent one look the same, because to the
|
|
33
34
|
* reader of a list they are.
|
|
34
35
|
*/ export function formatContactCustomValue(definition, value) {
|
|
@@ -43,10 +44,25 @@ import { Link, Typography } from "@mui/material";
|
|
|
43
44
|
}
|
|
44
45
|
case 'number':
|
|
45
46
|
return typeof value === 'number' ? value.toLocaleString() : String(value);
|
|
47
|
+
case 'currency':
|
|
48
|
+
return typeof value === 'number' ? formatCurrencyFieldValue(value, customFieldCurrency(definition)) : String(value);
|
|
46
49
|
default:
|
|
47
50
|
return String(value);
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* How each active currency field's filter reads a typed amount, by column
|
|
55
|
+
* (AGL-3756) — `ContactQueryReader.amounts`: major units in, the minor
|
|
56
|
+
* units its facet key holds out.
|
|
57
|
+
*/ export function contactCustomAmountReaders(definitions) {
|
|
58
|
+
return Object.fromEntries(definitions.filter((definition)=>definition.type === 'currency' && !definition.retiredAt).map((definition)=>{
|
|
59
|
+
const code = customFieldCurrency(definition);
|
|
60
|
+
return [
|
|
61
|
+
customFieldColumnId(definition.key),
|
|
62
|
+
(typed)=>parseCurrencyInput(typed, code)
|
|
63
|
+
];
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
50
66
|
/**
|
|
51
67
|
* A CRM list's columns for the org's custom fields (AGL-2601), one per
|
|
52
68
|
* ACTIVE definition in `order` — the contacts list's, and since AGL-2661
|
|
@@ -56,10 +72,12 @@ import { Link, Typography } from "@mui/material";
|
|
|
56
72
|
* The value is read off `row.custom`: on a contact the list projects it
|
|
57
73
|
* from THIS holder's facet — the same projection that puts `tags` and
|
|
58
74
|
* `notes` on the row — so a column can never show another holder's value;
|
|
59
|
-
* on a company or a deal it is the document's own map.
|
|
60
|
-
* filtering are off: the value lives under a map no index
|
|
61
|
-
*
|
|
62
|
-
*
|
|
75
|
+
* on a company or a deal it is the document's own map. The grid's own
|
|
76
|
+
* sorting and filtering are off: the value lives under a map no index
|
|
77
|
+
* covers, so a list's column sort sorts the PAGE through `valueGetter` and
|
|
78
|
+
* says so (AGL-3680), and its filters ask the query (`contact-filters.ts`).
|
|
79
|
+
* A currency column's `valueGetter` is the stored amount, so the page sorts
|
|
80
|
+
* by money rather than by the formatted text.
|
|
63
81
|
*
|
|
64
82
|
* Retired definitions get no column. Their values are still on the rows,
|
|
65
83
|
* and an export can still read them; a column for a field the merchant
|
|
@@ -74,7 +92,9 @@ import { Link, Typography } from "@mui/material";
|
|
|
74
92
|
filterable: false,
|
|
75
93
|
valueGetter: (_value, row)=>{
|
|
76
94
|
var _row_custom;
|
|
77
|
-
|
|
95
|
+
const raw = (_row_custom = row.custom) == null ? void 0 : _row_custom[definition.key];
|
|
96
|
+
if (definition.type === 'currency') return typeof raw === 'number' ? raw : null;
|
|
97
|
+
return formatContactCustomValue(definition, raw);
|
|
78
98
|
},
|
|
79
99
|
renderCell: ({ row })=>{
|
|
80
100
|
var _row_custom;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-custom-columns.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 {\n type ContactCustomValue,\n type ContactFieldDefinition,\n crmContactCustomColumn,\n} from '@aglyn/aglyn'\nimport { Link, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\n\n/**\n * The column `field` a definition's key is listed under — namespaced so a\n * key can never collide with a built-in column, and the shared contract\n * since AGL-2617 so a saved view's filter clause and its column list name\n * the field the same way.\n */\nexport const customFieldColumnId = crmContactCustomColumn\n\n/**\n * One stored value, as the list shows it.\n *\n * A checkbox reads as Yes or an em dash rather than `true`/`false`; a date\n * as the local day rather than the ISO stamp it is stored as; a link as an\n * anchor. Everything else is the value itself, and nothing is `null` on\n * screen — a cleared value and an absent one look the same, because to the\n * reader of a list they are.\n */\nexport function formatContactCustomValue(\n definition: Pick<ContactFieldDefinition, 'type'>,\n value: ContactCustomValue | undefined,\n): string {\n if (value === undefined || value === null || value === '') return ''\n switch (definition.type) {\n case 'checkbox':\n return value === true ? 'Yes' : value === false ? 'No' : String(value)\n case 'date': {\n const ms = typeof value === 'string' ? Date.parse(value) : Number(value)\n return Number.isFinite(ms) ? new Date(ms).toLocaleDateString() : String(value)\n }\n case 'number':\n return typeof value === 'number' ? value.toLocaleString() : String(value)\n default:\n return String(value)\n }\n}\n\n/**\n * A CRM list's columns for the org's custom fields (AGL-2601), one per\n * ACTIVE definition in `order` — the contacts list's, and since AGL-2661\n * the companies list's and the deals table's, each handed the definitions\n * of its own object.\n *\n * The value is read off `row.custom`: on a contact the list projects it\n * from THIS holder's facet — the same projection that puts `tags` and\n * `notes` on the row — so a column can never show another holder's value;\n * on a company or a deal it is the document's own map.
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-custom-columns.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 {\n type ContactCustomValue,\n type ContactFieldDefinition,\n customFieldCurrency,\n crmContactCustomColumn,\n formatCurrencyFieldValue,\n parseCurrencyInput,\n} from '@aglyn/aglyn'\nimport { Link, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\n\n/**\n * The column `field` a definition's key is listed under — namespaced so a\n * key can never collide with a built-in column, and the shared contract\n * since AGL-2617 so a saved view's filter clause and its column list name\n * the field the same way.\n */\nexport const customFieldColumnId = crmContactCustomColumn\n\n/**\n * One stored value, as the list shows it.\n *\n * A checkbox reads as Yes or an em dash rather than `true`/`false`; a date\n * as the local day rather than the ISO stamp it is stored as; a link as an\n * anchor; an amount as money in the field's currency (AGL-3756). Everything\n * else is the value itself, and nothing is `null` on\n * screen — a cleared value and an absent one look the same, because to the\n * reader of a list they are.\n */\nexport function formatContactCustomValue(\n definition: Pick<ContactFieldDefinition, 'type' | 'currency'>,\n value: ContactCustomValue | undefined,\n): string {\n if (value === undefined || value === null || value === '') return ''\n switch (definition.type) {\n case 'checkbox':\n return value === true ? 'Yes' : value === false ? 'No' : String(value)\n case 'date': {\n const ms = typeof value === 'string' ? Date.parse(value) : Number(value)\n return Number.isFinite(ms) ? new Date(ms).toLocaleDateString() : String(value)\n }\n case 'number':\n return typeof value === 'number' ? value.toLocaleString() : String(value)\n case 'currency':\n return typeof value === 'number'\n ? formatCurrencyFieldValue(value, customFieldCurrency(definition))\n : String(value)\n default:\n return String(value)\n }\n}\n\n/**\n * How each active currency field's filter reads a typed amount, by column\n * (AGL-3756) — `ContactQueryReader.amounts`: major units in, the minor\n * units its facet key holds out.\n */\nexport function contactCustomAmountReaders(\n definitions: readonly Pick<ContactFieldDefinition, 'key' | 'type' | 'currency' | 'retiredAt'>[],\n): Record<string, (typed: string) => number | null> {\n return Object.fromEntries(\n definitions\n .filter((definition) => definition.type === 'currency' && !definition.retiredAt)\n .map((definition) => {\n const code = customFieldCurrency(definition)\n return [customFieldColumnId(definition.key), (typed: string) => parseCurrencyInput(typed, code)]\n }),\n )\n}\n\n/**\n * A CRM list's columns for the org's custom fields (AGL-2601), one per\n * ACTIVE definition in `order` — the contacts list's, and since AGL-2661\n * the companies list's and the deals table's, each handed the definitions\n * of its own object.\n *\n * The value is read off `row.custom`: on a contact the list projects it\n * from THIS holder's facet — the same projection that puts `tags` and\n * `notes` on the row — so a column can never show another holder's value;\n * on a company or a deal it is the document's own map. The grid's own\n * sorting and filtering are off: the value lives under a map no index\n * covers, so a list's column sort sorts the PAGE through `valueGetter` and\n * says so (AGL-3680), and its filters ask the query (`contact-filters.ts`).\n * A currency column's `valueGetter` is the stored amount, so the page sorts\n * by money rather than by the formatted text.\n *\n * Retired definitions get no column. Their values are still on the rows,\n * and an export can still read them; a column for a field the merchant\n * retired would be the field refusing to go away.\n */\nexport function customFieldColumns(\n definitions: readonly ContactFieldDefinition[],\n): GridColDef[] {\n return definitions\n .filter((definition) => !definition.retiredAt)\n .map((definition) => ({\n field: customFieldColumnId(definition.key),\n headerName: definition.label,\n flex: 0.8,\n minWidth: 140,\n sortable: false,\n filterable: false,\n valueGetter: (_value: unknown, row: { custom?: Record<string, ContactCustomValue> }) => {\n const raw = row.custom?.[definition.key]\n if (definition.type === 'currency') return typeof raw === 'number' ? raw : null\n return formatContactCustomValue(definition, raw)\n },\n renderCell: ({ row }: { row: { custom?: Record<string, ContactCustomValue> } }) => {\n const raw = row.custom?.[definition.key]\n const text = formatContactCustomValue(definition, raw)\n if (!text) return null\n if (definition.type === 'url') {\n return (\n <Link\n href={text}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n variant=\"body2\"\n onClick={(event) => event.stopPropagation()}\n >\n {text}\n </Link>\n )\n }\n return <Typography variant=\"body2\">{text}</Typography>\n },\n }))\n}\n"],"names":["customFieldCurrency","crmContactCustomColumn","formatCurrencyFieldValue","parseCurrencyInput","Link","Typography","customFieldColumnId","formatContactCustomValue","definition","value","undefined","type","String","ms","Date","parse","Number","isFinite","toLocaleDateString","toLocaleString","contactCustomAmountReaders","definitions","Object","fromEntries","filter","retiredAt","map","code","key","typed","customFieldColumns","field","headerName","label","flex","minWidth","sortable","filterable","valueGetter","_value","row","raw","custom","renderCell","text","href","target","rel","variant","onClick","event","stopPropagation"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAGEA,mBAAmB,EACnBC,sBAAsB,EACtBC,wBAAwB,EACxBC,kBAAkB,QACb,eAAc;AACrB,SAASC,IAAI,EAAEC,UAAU,QAAQ,gBAAe;AAGhD;;;;;CAKC,GACD,OAAO,MAAMC,sBAAsBL,uBAAsB;AAEzD;;;;;;;;;CASC,GACD,OAAO,SAASM,yBACdC,UAA6D,EAC7DC,KAAqC;IAErC,IAAIA,UAAUC,aAAaD,UAAU,QAAQA,UAAU,IAAI,OAAO;IAClE,OAAQD,WAAWG,IAAI;QACrB,KAAK;YACH,OAAOF,UAAU,OAAO,QAAQA,UAAU,QAAQ,OAAOG,OAAOH;QAClE,KAAK;YAAQ;gBACX,MAAMI,KAAK,OAAOJ,UAAU,WAAWK,KAAKC,KAAK,CAACN,SAASO,OAAOP;gBAClE,OAAOO,OAAOC,QAAQ,CAACJ,MAAM,IAAIC,KAAKD,IAAIK,kBAAkB,KAAKN,OAAOH;YAC1E;QACA,KAAK;YACH,OAAO,OAAOA,UAAU,WAAWA,MAAMU,cAAc,KAAKP,OAAOH;QACrE,KAAK;YACH,OAAO,OAAOA,UAAU,WACpBP,yBAAyBO,OAAOT,oBAAoBQ,eACpDI,OAAOH;QACb;YACE,OAAOG,OAAOH;IAClB;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASW,2BACdC,WAA+F;IAE/F,OAAOC,OAAOC,WAAW,CACvBF,YACGG,MAAM,CAAC,CAAChB,aAAeA,WAAWG,IAAI,KAAK,cAAc,CAACH,WAAWiB,SAAS,EAC9EC,GAAG,CAAC,CAAClB;QACJ,MAAMmB,OAAO3B,oBAAoBQ;QACjC,OAAO;YAACF,oBAAoBE,WAAWoB,GAAG;YAAG,CAACC,QAAkB1B,mBAAmB0B,OAAOF;SAAM;IAClG;AAEN;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASG,mBACdT,WAA8C;IAE9C,OAAOA,YACJG,MAAM,CAAC,CAAChB,aAAe,CAACA,WAAWiB,SAAS,EAC5CC,GAAG,CAAC,CAAClB,aAAgB,CAAA;YACpBuB,OAAOzB,oBAAoBE,WAAWoB,GAAG;YACzCI,YAAYxB,WAAWyB,KAAK;YAC5BC,MAAM;YACNC,UAAU;YACVC,UAAU;YACVC,YAAY;YACZC,aAAa,CAACC,QAAiBC;oBACjBA;gBAAZ,MAAMC,OAAMD,cAAAA,IAAIE,MAAM,qBAAVF,WAAY,CAAChC,WAAWoB,GAAG,CAAC;gBACxC,IAAIpB,WAAWG,IAAI,KAAK,YAAY,OAAO,OAAO8B,QAAQ,WAAWA,MAAM;gBAC3E,OAAOlC,yBAAyBC,YAAYiC;YAC9C;YACAE,YAAY,CAAC,EAAEH,GAAG,EAA4D;oBAChEA;gBAAZ,MAAMC,OAAMD,cAAAA,IAAIE,MAAM,qBAAVF,WAAY,CAAChC,WAAWoB,GAAG,CAAC;gBACxC,MAAMgB,OAAOrC,yBAAyBC,YAAYiC;gBAClD,IAAI,CAACG,MAAM,OAAO;gBAClB,IAAIpC,WAAWG,IAAI,KAAK,OAAO;oBAC7B,qBACE,KAACP;wBACCyC,MAAMD;wBACNE,QAAO;wBACPC,KAAI;wBACJC,SAAQ;wBACRC,SAAS,CAACC,QAAUA,MAAMC,eAAe;kCAExCP;;gBAGP;gBACA,qBAAO,KAACvC;oBAAW2C,SAAQ;8BAASJ;;YACtC;QACF,CAAA;AACJ"}
|
|
@@ -7,6 +7,8 @@ export interface ContactFieldDraft {
|
|
|
7
7
|
type: ContactFieldType;
|
|
8
8
|
/** The choices, for `select`; empty for every other type. */
|
|
9
9
|
options: string[];
|
|
10
|
+
/** For `currency` (AGL-3756): the lowercase ISO 4217 code; `null` for every other type. */
|
|
11
|
+
currency: string | null;
|
|
10
12
|
required: boolean;
|
|
11
13
|
}
|
|
12
14
|
export interface ContactFieldDrawerProps {
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
-
import { CONTACT_FIELD_TYPE_LABELS, CONTACT_FIELD_TYPES, isContactFieldType, normalizeContactFieldKey } from "@aglyn/aglyn";
|
|
18
|
+
import { CONTACT_FIELD_TYPE_LABELS, CONTACT_FIELD_TYPES, customFieldCurrency, isContactFieldType, normalizeContactFieldKey } from "@aglyn/aglyn";
|
|
19
19
|
import { Button, Drawer, FormControlLabel, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
|
|
20
20
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
21
21
|
import { CRM_FIELD_LABEL_MAX_LENGTH, crmFieldOptions } from "../model/crm-setup-content.js";
|
|
22
|
+
import { DEAL_CURRENCIES } from "../model/deal-board-model.js";
|
|
22
23
|
/** How the object reads in the drawer's own sentences — singular, lowercase. */ const OBJECT_NOUN = {
|
|
23
24
|
contact: 'contact',
|
|
24
25
|
company: 'company',
|
|
@@ -80,6 +81,7 @@ import { CRM_FIELD_LABEL_MAX_LENGTH, crmFieldOptions } from "../model/crm-setup-
|
|
|
80
81
|
const [keyTouched, setKeyTouched] = useState(false);
|
|
81
82
|
const [type, setType] = useState('text');
|
|
82
83
|
const [optionsText, setOptionsText] = useState('');
|
|
84
|
+
const [currency, setCurrency] = useState(customFieldCurrency(null));
|
|
83
85
|
const [required, setRequired] = useState(false);
|
|
84
86
|
const [busy, setBusy] = useState(false);
|
|
85
87
|
const [error, setError] = useState(null);
|
|
@@ -93,6 +95,7 @@ import { CRM_FIELD_LABEL_MAX_LENGTH, crmFieldOptions } from "../model/crm-setup-
|
|
|
93
95
|
setKeyTouched(Boolean(definition));
|
|
94
96
|
setType((_ref2 = definition == null ? void 0 : definition.type) != null ? _ref2 : 'text');
|
|
95
97
|
setOptionsText(((_ref3 = definition == null ? void 0 : definition.options) != null ? _ref3 : []).join('\n'));
|
|
98
|
+
setCurrency(customFieldCurrency(definition));
|
|
96
99
|
setRequired((definition == null ? void 0 : definition.required) === true);
|
|
97
100
|
setBusy(false);
|
|
98
101
|
setError(null);
|
|
@@ -122,6 +125,7 @@ import { CRM_FIELD_LABEL_MAX_LENGTH, crmFieldOptions } from "../model/crm-setup-
|
|
|
122
125
|
key: derivedKey,
|
|
123
126
|
type,
|
|
124
127
|
options: type === 'select' ? options : [],
|
|
128
|
+
currency: type === 'currency' ? currency : null,
|
|
125
129
|
required
|
|
126
130
|
});
|
|
127
131
|
} catch (caught) {
|
|
@@ -137,6 +141,7 @@ import { CRM_FIELD_LABEL_MAX_LENGTH, crmFieldOptions } from "../model/crm-setup-
|
|
|
137
141
|
label,
|
|
138
142
|
type,
|
|
139
143
|
options,
|
|
144
|
+
currency,
|
|
140
145
|
required
|
|
141
146
|
]);
|
|
142
147
|
return /*#__PURE__*/ _jsx(Drawer, {
|
|
@@ -219,6 +224,25 @@ import { CRM_FIELD_LABEL_MAX_LENGTH, crmFieldOptions } from "../model/crm-setup-
|
|
|
219
224
|
minRows: 3,
|
|
220
225
|
fullWidth: true
|
|
221
226
|
}) : null,
|
|
227
|
+
type === 'currency' ? /*#__PURE__*/ _jsx(TextField, {
|
|
228
|
+
select: true,
|
|
229
|
+
size: "small",
|
|
230
|
+
label: "Currency",
|
|
231
|
+
value: currency,
|
|
232
|
+
onChange: (event)=>setCurrency(event.target.value),
|
|
233
|
+
disabled: editing,
|
|
234
|
+
helperText: editing ? 'Amounts are stored in this currency, so it is fixed once the field exists.' : 'Every amount in this field is in this currency.',
|
|
235
|
+
fullWidth: true,
|
|
236
|
+
children: [
|
|
237
|
+
...new Set([
|
|
238
|
+
...DEAL_CURRENCIES,
|
|
239
|
+
currency
|
|
240
|
+
])
|
|
241
|
+
].map((code)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
242
|
+
value: code,
|
|
243
|
+
children: code.toUpperCase()
|
|
244
|
+
}, code))
|
|
245
|
+
}) : null,
|
|
222
246
|
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
223
247
|
control: /*#__PURE__*/ _jsx(Switch, {
|
|
224
248
|
checked: required,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-field-drawer.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 {\n CONTACT_FIELD_TYPE_LABELS,\n CONTACT_FIELD_TYPES,\n type ContactFieldType,\n type CrmFieldObject,\n isContactFieldType,\n normalizeContactFieldKey,\n} from '@aglyn/aglyn'\nimport {\n Button,\n Drawer,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ContactFieldDefinitionDoc } from '../hooks/use-contact-field-definitions'\nimport { CRM_FIELD_LABEL_MAX_LENGTH, crmFieldOptions } from '../model/crm-setup-content'\n\n/** What the drawer hands back — the definition's editable half. */\nexport interface ContactFieldDraft {\n label: string\n key: string\n type: ContactFieldType\n /** The choices, for `select`; empty for every other type. */\n options: string[]\n required: boolean\n}\n\nexport interface ContactFieldDrawerProps {\n open: boolean\n onClose: () => void\n /** The definition being edited, or `null` to create one. */\n definition: ContactFieldDefinitionDoc | null\n /**\n * Every key the org has already used, retired ones included. A key is\n * the map key values are stored under, so a retired field's key is still\n * taken: a new field reusing it would read the old values back as its own.\n */\n takenKeys: readonly string[]\n /** Receives the draft; closing on success is the caller's job. */\n onSubmit: (draft: ContactFieldDraft) => Promise<void> | void\n /**\n * The record the field describes (AGL-2661) — the Fields section's\n * current tab. Contacts when unsaid. The drawer's copy follows it, and it\n * is the one thing here a form-field mapping cares about: a website form\n * saves into a CONTACT field only, because a submission is a fact about\n * a person, so the other objects' drawers say so instead of offering it.\n */\n object?: CrmFieldObject\n}\n\n/** How the object reads in the drawer's own sentences — singular, lowercase. */\nconst OBJECT_NOUN: Record<CrmFieldObject, string> = {\n contact: 'contact',\n company: 'company',\n deal: 'deal',\n lead: 'lead',\n}\n\n/**\n * Which objects a CSV import can fill a custom field on — see\n * `CUSTOM_FIELD_OBJECT` in `record-facts.ts`, which decides what the\n * mapping menu offers. The deal and lead files carry the standard columns\n * only, so their drawers must not promise an import that would silently\n * drop the column.\n */\nconst IMPORTABLE: Record<CrmFieldObject, boolean> = {\n contact: true,\n company: true,\n deal: false,\n lead: false,\n}\n\n/**\n * What the drawer says about website forms, per object — see\n * {@link ContactFieldDrawerProps.object}.\n */\nexport function fieldDrawerMappingNote(object: CrmFieldObject): string {\n if (object === 'contact') {\n return 'A website form field can save its answer into this field.'\n }\n const filled = IMPORTABLE[object]\n ? 'is filled on the record, by CSV import or over the API.'\n : 'is filled on the record or over the API.'\n return (\n `A website form field saves into contact fields only; a ${OBJECT_NOUN[object]} ` +\n `field ${filled}`\n )\n}\n\n/** How long a label may be: the one bound an AI build's field is held to too (AGL-3746). */\nconst LABEL_MAX_LENGTH = CRM_FIELD_LABEL_MAX_LENGTH\n\n/** One choice per line, trimmed, blanks dropped, duplicates collapsed, under the shared bounds. */\nfunction parseOptions(text: string): string[] {\n return crmFieldOptions(text.split('\\n'))\n}\n\n/**\n * The one form a custom contact field is created and edited through\n * (AGL-2601).\n *\n * ## The key follows the label until the author takes it over\n *\n * A merchant types \"Annual revenue\" and should not have to know that the\n * value is stored under `annual_revenue`. So the key is derived from the\n * label as they type — `normalizeContactFieldKey` — and shown beside it, and\n * only becomes the author's own the moment they edit it. That is the whole\n * of the grammar they are asked to learn, and the field is still there for\n * the author who wants `arr` instead.\n *\n * ## Two things do not change after creation\n *\n * The KEY is the map key every value is stored under, so changing it would\n * orphan every value already written; the TYPE is what every reader coerces\n * those values by, so changing it would make them unreadable. Both are\n * disabled on an existing field and say why. A rename is a new field and a\n * retire, which the section's list offers.\n */\nexport function ContactFieldDrawer(props: ContactFieldDrawerProps) {\n const { open, onClose, definition, takenKeys, onSubmit, object = 'contact' } = props\n const editing = Boolean(definition)\n const noun = OBJECT_NOUN[object]\n\n const [label, setLabel] = useState('')\n const [key, setKey] = useState('')\n const [keyTouched, setKeyTouched] = useState(false)\n const [type, setType] = useState<ContactFieldType>('text')\n const [optionsText, setOptionsText] = useState('')\n const [required, setRequired] = useState(false)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n // Seeded on every open, from the definition when there is one: a drawer\n // that kept the previous field's draft would offer it as the next one.\n useEffect(() => {\n if (!open) return\n setLabel(definition?.label ?? '')\n setKey(definition?.key ?? '')\n setKeyTouched(Boolean(definition))\n setType(definition?.type ?? 'text')\n setOptionsText((definition?.options ?? []).join('\\n'))\n setRequired(definition?.required === true)\n setBusy(false)\n setError(null)\n }, [open, definition])\n\n const derivedKey = useMemo(\n () => (keyTouched ? normalizeContactFieldKey(key) : normalizeContactFieldKey(label)),\n [keyTouched, key, label],\n )\n const shownKey = keyTouched ? key : (derivedKey ?? '')\n const keyTaken =\n !editing && derivedKey != null && takenKeys.includes(derivedKey)\n const options = useMemo(() => parseOptions(optionsText), [optionsText])\n\n const keyHelp = editing\n ? 'Values are stored under this key, so it cannot change once the field exists.'\n : keyTaken\n ? 'That key is already in use — a retired field keeps its key.'\n : shownKey && !derivedKey\n ? 'A key starts with a letter and uses letters, digits and underscores.'\n : 'Derived from the label; edit it to choose your own.'\n\n const canSubmit =\n !busy &&\n label.trim().length > 0 &&\n derivedKey != null &&\n !keyTaken &&\n (type !== 'select' || options.length > 0)\n\n const handleSubmit = useCallback(async () => {\n if (!canSubmit || derivedKey == null) return\n setBusy(true)\n setError(null)\n try {\n await onSubmit({\n label: label.trim().slice(0, LABEL_MAX_LENGTH),\n key: derivedKey,\n type,\n options: type === 'select' ? options : [],\n required,\n })\n } catch (caught) {\n console.error(caught)\n setError('Could not save the field. Try again.')\n } finally {\n setBusy(false)\n }\n }, [canSubmit, derivedKey, onSubmit, label, type, options, required])\n\n return (\n <Drawer anchor=\"right\" open={open} onClose={onClose}>\n <Stack spacing={2} sx={{ width: 360, maxWidth: '100vw', p: 3 }}>\n <Typography variant=\"h6\">\n {editing ? `Edit ${noun} field` : `New ${noun} field`}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {fieldDrawerMappingNote(object)}\n </Typography>\n <TextField\n size=\"small\"\n label=\"Label\"\n value={label}\n onChange={(event) => setLabel(event.target.value)}\n slotProps={{ htmlInput: { maxLength: LABEL_MAX_LENGTH } }}\n autoFocus={!editing}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Key\"\n value={shownKey}\n onChange={(event) => {\n setKeyTouched(true)\n setKey(event.target.value)\n }}\n disabled={editing}\n error={!editing && (keyTaken || (shownKey.length > 0 && !derivedKey))}\n helperText={keyHelp}\n slotProps={{ htmlInput: { maxLength: 40, spellCheck: false } }}\n fullWidth\n />\n <TextField\n select\n size=\"small\"\n label=\"Type\"\n value={type}\n onChange={(event) => {\n const next = event.target.value\n if (isContactFieldType(next)) setType(next)\n }}\n disabled={editing}\n helperText={\n editing\n ? 'Values are read by type, so it is fixed once the field exists.'\n : undefined\n }\n fullWidth\n >\n {CONTACT_FIELD_TYPES.map((option) => (\n <MenuItem key={option} value={option}>\n {CONTACT_FIELD_TYPE_LABELS[option]}\n </MenuItem>\n ))}\n </TextField>\n {type === 'select' ? (\n <TextField\n size=\"small\"\n label=\"Choices\"\n value={optionsText}\n onChange={(event) => setOptionsText(event.target.value)}\n helperText=\"One per line. A stored value has to be one of these.\"\n multiline\n minRows={3}\n fullWidth\n />\n ) : null}\n <FormControlLabel\n control={\n <Switch\n checked={required}\n onChange={(event) => setRequired(event.target.checked)}\n />\n }\n label={`Required on the ${noun} form`}\n />\n {error ? (\n <Typography variant=\"body2\" color=\"error\">\n {error}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!canSubmit}\n onClick={handleSubmit}\n >\n {editing ? 'Save' : 'Create field'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </Drawer>\n )\n}\nContactFieldDrawer.displayName = 'ContactFieldDrawer'\n\nexport default ContactFieldDrawer\n"],"names":["CONTACT_FIELD_TYPE_LABELS","CONTACT_FIELD_TYPES","isContactFieldType","normalizeContactFieldKey","Button","Drawer","FormControlLabel","MenuItem","Stack","Switch","TextField","Typography","useCallback","useEffect","useMemo","useState","CRM_FIELD_LABEL_MAX_LENGTH","crmFieldOptions","OBJECT_NOUN","contact","company","deal","lead","IMPORTABLE","fieldDrawerMappingNote","object","filled","LABEL_MAX_LENGTH","parseOptions","text","split","ContactFieldDrawer","props","open","onClose","definition","takenKeys","onSubmit","editing","Boolean","noun","label","setLabel","key","setKey","keyTouched","setKeyTouched","type","setType","optionsText","setOptionsText","required","setRequired","busy","setBusy","error","setError","options","join","derivedKey","shownKey","keyTaken","includes","keyHelp","canSubmit","trim","length","handleSubmit","slice","caught","console","anchor","spacing","sx","width","maxWidth","p","variant","color","size","value","onChange","event","target","slotProps","htmlInput","maxLength","autoFocus","fullWidth","disabled","helperText","spellCheck","select","next","undefined","map","option","multiline","minRows","control","checked","direction","onClick","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-field-drawer.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 {\n CONTACT_FIELD_TYPE_LABELS,\n CONTACT_FIELD_TYPES,\n customFieldCurrency,\n type ContactFieldType,\n type CrmFieldObject,\n isContactFieldType,\n normalizeContactFieldKey,\n} from '@aglyn/aglyn'\nimport {\n Button,\n Drawer,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ContactFieldDefinitionDoc } from '../hooks/use-contact-field-definitions'\nimport { CRM_FIELD_LABEL_MAX_LENGTH, crmFieldOptions } from '../model/crm-setup-content'\nimport { DEAL_CURRENCIES } from '../model/deal-board-model'\n\n/** What the drawer hands back — the definition's editable half. */\nexport interface ContactFieldDraft {\n label: string\n key: string\n type: ContactFieldType\n /** The choices, for `select`; empty for every other type. */\n options: string[]\n /** For `currency` (AGL-3756): the lowercase ISO 4217 code; `null` for every other type. */\n currency: string | null\n required: boolean\n}\n\nexport interface ContactFieldDrawerProps {\n open: boolean\n onClose: () => void\n /** The definition being edited, or `null` to create one. */\n definition: ContactFieldDefinitionDoc | null\n /**\n * Every key the org has already used, retired ones included. A key is\n * the map key values are stored under, so a retired field's key is still\n * taken: a new field reusing it would read the old values back as its own.\n */\n takenKeys: readonly string[]\n /** Receives the draft; closing on success is the caller's job. */\n onSubmit: (draft: ContactFieldDraft) => Promise<void> | void\n /**\n * The record the field describes (AGL-2661) — the Fields section's\n * current tab. Contacts when unsaid. The drawer's copy follows it, and it\n * is the one thing here a form-field mapping cares about: a website form\n * saves into a CONTACT field only, because a submission is a fact about\n * a person, so the other objects' drawers say so instead of offering it.\n */\n object?: CrmFieldObject\n}\n\n/** How the object reads in the drawer's own sentences — singular, lowercase. */\nconst OBJECT_NOUN: Record<CrmFieldObject, string> = {\n contact: 'contact',\n company: 'company',\n deal: 'deal',\n lead: 'lead',\n}\n\n/**\n * Which objects a CSV import can fill a custom field on — see\n * `CUSTOM_FIELD_OBJECT` in `record-facts.ts`, which decides what the\n * mapping menu offers. The deal and lead files carry the standard columns\n * only, so their drawers must not promise an import that would silently\n * drop the column.\n */\nconst IMPORTABLE: Record<CrmFieldObject, boolean> = {\n contact: true,\n company: true,\n deal: false,\n lead: false,\n}\n\n/**\n * What the drawer says about website forms, per object — see\n * {@link ContactFieldDrawerProps.object}.\n */\nexport function fieldDrawerMappingNote(object: CrmFieldObject): string {\n if (object === 'contact') {\n return 'A website form field can save its answer into this field.'\n }\n const filled = IMPORTABLE[object]\n ? 'is filled on the record, by CSV import or over the API.'\n : 'is filled on the record or over the API.'\n return (\n `A website form field saves into contact fields only; a ${OBJECT_NOUN[object]} ` +\n `field ${filled}`\n )\n}\n\n/** How long a label may be: the one bound an AI build's field is held to too (AGL-3746). */\nconst LABEL_MAX_LENGTH = CRM_FIELD_LABEL_MAX_LENGTH\n\n/** One choice per line, trimmed, blanks dropped, duplicates collapsed, under the shared bounds. */\nfunction parseOptions(text: string): string[] {\n return crmFieldOptions(text.split('\\n'))\n}\n\n/**\n * The one form a custom contact field is created and edited through\n * (AGL-2601).\n *\n * ## The key follows the label until the author takes it over\n *\n * A merchant types \"Annual revenue\" and should not have to know that the\n * value is stored under `annual_revenue`. So the key is derived from the\n * label as they type — `normalizeContactFieldKey` — and shown beside it, and\n * only becomes the author's own the moment they edit it. That is the whole\n * of the grammar they are asked to learn, and the field is still there for\n * the author who wants `arr` instead.\n *\n * ## Two things do not change after creation\n *\n * The KEY is the map key every value is stored under, so changing it would\n * orphan every value already written; the TYPE is what every reader coerces\n * those values by, so changing it would make them unreadable. Both are\n * disabled on an existing field and say why. A rename is a new field and a\n * retire, which the section's list offers.\n */\nexport function ContactFieldDrawer(props: ContactFieldDrawerProps) {\n const { open, onClose, definition, takenKeys, onSubmit, object = 'contact' } = props\n const editing = Boolean(definition)\n const noun = OBJECT_NOUN[object]\n\n const [label, setLabel] = useState('')\n const [key, setKey] = useState('')\n const [keyTouched, setKeyTouched] = useState(false)\n const [type, setType] = useState<ContactFieldType>('text')\n const [optionsText, setOptionsText] = useState('')\n const [currency, setCurrency] = useState(customFieldCurrency(null))\n const [required, setRequired] = useState(false)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n // Seeded on every open, from the definition when there is one: a drawer\n // that kept the previous field's draft would offer it as the next one.\n useEffect(() => {\n if (!open) return\n setLabel(definition?.label ?? '')\n setKey(definition?.key ?? '')\n setKeyTouched(Boolean(definition))\n setType(definition?.type ?? 'text')\n setOptionsText((definition?.options ?? []).join('\\n'))\n setCurrency(customFieldCurrency(definition))\n setRequired(definition?.required === true)\n setBusy(false)\n setError(null)\n }, [open, definition])\n\n const derivedKey = useMemo(\n () => (keyTouched ? normalizeContactFieldKey(key) : normalizeContactFieldKey(label)),\n [keyTouched, key, label],\n )\n const shownKey = keyTouched ? key : (derivedKey ?? '')\n const keyTaken =\n !editing && derivedKey != null && takenKeys.includes(derivedKey)\n const options = useMemo(() => parseOptions(optionsText), [optionsText])\n\n const keyHelp = editing\n ? 'Values are stored under this key, so it cannot change once the field exists.'\n : keyTaken\n ? 'That key is already in use — a retired field keeps its key.'\n : shownKey && !derivedKey\n ? 'A key starts with a letter and uses letters, digits and underscores.'\n : 'Derived from the label; edit it to choose your own.'\n\n const canSubmit =\n !busy &&\n label.trim().length > 0 &&\n derivedKey != null &&\n !keyTaken &&\n (type !== 'select' || options.length > 0)\n\n const handleSubmit = useCallback(async () => {\n if (!canSubmit || derivedKey == null) return\n setBusy(true)\n setError(null)\n try {\n await onSubmit({\n label: label.trim().slice(0, LABEL_MAX_LENGTH),\n key: derivedKey,\n type,\n options: type === 'select' ? options : [],\n currency: type === 'currency' ? currency : null,\n required,\n })\n } catch (caught) {\n console.error(caught)\n setError('Could not save the field. Try again.')\n } finally {\n setBusy(false)\n }\n }, [canSubmit, derivedKey, onSubmit, label, type, options, currency, required])\n\n return (\n <Drawer anchor=\"right\" open={open} onClose={onClose}>\n <Stack spacing={2} sx={{ width: 360, maxWidth: '100vw', p: 3 }}>\n <Typography variant=\"h6\">\n {editing ? `Edit ${noun} field` : `New ${noun} field`}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {fieldDrawerMappingNote(object)}\n </Typography>\n <TextField\n size=\"small\"\n label=\"Label\"\n value={label}\n onChange={(event) => setLabel(event.target.value)}\n slotProps={{ htmlInput: { maxLength: LABEL_MAX_LENGTH } }}\n autoFocus={!editing}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Key\"\n value={shownKey}\n onChange={(event) => {\n setKeyTouched(true)\n setKey(event.target.value)\n }}\n disabled={editing}\n error={!editing && (keyTaken || (shownKey.length > 0 && !derivedKey))}\n helperText={keyHelp}\n slotProps={{ htmlInput: { maxLength: 40, spellCheck: false } }}\n fullWidth\n />\n <TextField\n select\n size=\"small\"\n label=\"Type\"\n value={type}\n onChange={(event) => {\n const next = event.target.value\n if (isContactFieldType(next)) setType(next)\n }}\n disabled={editing}\n helperText={\n editing\n ? 'Values are read by type, so it is fixed once the field exists.'\n : undefined\n }\n fullWidth\n >\n {CONTACT_FIELD_TYPES.map((option) => (\n <MenuItem key={option} value={option}>\n {CONTACT_FIELD_TYPE_LABELS[option]}\n </MenuItem>\n ))}\n </TextField>\n {type === 'select' ? (\n <TextField\n size=\"small\"\n label=\"Choices\"\n value={optionsText}\n onChange={(event) => setOptionsText(event.target.value)}\n helperText=\"One per line. A stored value has to be one of these.\"\n multiline\n minRows={3}\n fullWidth\n />\n ) : null}\n {type === 'currency' ? (\n <TextField\n select\n size=\"small\"\n label=\"Currency\"\n value={currency}\n onChange={(event) => setCurrency(event.target.value)}\n disabled={editing}\n helperText={\n editing\n ? 'Amounts are stored in this currency, so it is fixed once the field exists.'\n : 'Every amount in this field is in this currency.'\n }\n fullWidth\n >\n {[...new Set<string>([...DEAL_CURRENCIES, currency])].map((code) => (\n <MenuItem key={code} value={code}>\n {code.toUpperCase()}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <FormControlLabel\n control={\n <Switch\n checked={required}\n onChange={(event) => setRequired(event.target.checked)}\n />\n }\n label={`Required on the ${noun} form`}\n />\n {error ? (\n <Typography variant=\"body2\" color=\"error\">\n {error}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!canSubmit}\n onClick={handleSubmit}\n >\n {editing ? 'Save' : 'Create field'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </Drawer>\n )\n}\nContactFieldDrawer.displayName = 'ContactFieldDrawer'\n\nexport default ContactFieldDrawer\n"],"names":["CONTACT_FIELD_TYPE_LABELS","CONTACT_FIELD_TYPES","customFieldCurrency","isContactFieldType","normalizeContactFieldKey","Button","Drawer","FormControlLabel","MenuItem","Stack","Switch","TextField","Typography","useCallback","useEffect","useMemo","useState","CRM_FIELD_LABEL_MAX_LENGTH","crmFieldOptions","DEAL_CURRENCIES","OBJECT_NOUN","contact","company","deal","lead","IMPORTABLE","fieldDrawerMappingNote","object","filled","LABEL_MAX_LENGTH","parseOptions","text","split","ContactFieldDrawer","props","open","onClose","definition","takenKeys","onSubmit","editing","Boolean","noun","label","setLabel","key","setKey","keyTouched","setKeyTouched","type","setType","optionsText","setOptionsText","currency","setCurrency","required","setRequired","busy","setBusy","error","setError","options","join","derivedKey","shownKey","keyTaken","includes","keyHelp","canSubmit","trim","length","handleSubmit","slice","caught","console","anchor","spacing","sx","width","maxWidth","p","variant","color","size","value","onChange","event","target","slotProps","htmlInput","maxLength","autoFocus","fullWidth","disabled","helperText","spellCheck","select","next","undefined","map","option","multiline","minRows","Set","code","toUpperCase","control","checked","direction","onClick","displayName"],"mappings":"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"}
|
|
@@ -53,7 +53,7 @@ import { useCrmApi } from "./use-crm-api.js";
|
|
|
53
53
|
import { useOrgMembers } from "./use-org-members.js";
|
|
54
54
|
import { useContactFieldDefinitions } from "../hooks/use-contact-field-definitions.js";
|
|
55
55
|
import { useLeadSourcePicklist } from "../hooks/use-lead-source-picklist.js";
|
|
56
|
-
import { customFieldColumns } from "./contact-custom-columns.js";
|
|
56
|
+
import { contactCustomAmountReaders, customFieldColumns } from "./contact-custom-columns.js";
|
|
57
57
|
import { useCrmSavedView } from "../hooks/use-crm-saved-view.js";
|
|
58
58
|
import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
|
|
59
59
|
import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
|
|
@@ -272,13 +272,19 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
272
272
|
* particular sites.
|
|
273
273
|
*/ const foldsScope = useCrmFoldsScope(orgId, visibleToTokens);
|
|
274
274
|
const groupId = (_ref3 = consentGroup == null ? void 0 : consentGroup.groupId) != null ? _ref3 : null;
|
|
275
|
+
// A currency field's `=` is typed in major units (AGL-3756).
|
|
276
|
+
const amounts = useMemo(()=>contactCustomAmountReaders(customFields.active), [
|
|
277
|
+
customFields.active
|
|
278
|
+
]);
|
|
275
279
|
const asked = useMemo(()=>crmAskClauses(views.state.filters, (clause)=>contactQueryClause(clause, {
|
|
276
280
|
groupId,
|
|
277
|
-
foldsScope
|
|
281
|
+
foldsScope,
|
|
282
|
+
amounts
|
|
278
283
|
})), [
|
|
279
284
|
views.state.filters,
|
|
280
285
|
groupId,
|
|
281
|
-
foldsScope
|
|
286
|
+
foldsScope,
|
|
287
|
+
amounts
|
|
282
288
|
]);
|
|
283
289
|
const impliesScope = useMemo(()=>contactClauseImpliesScope(groupId), [
|
|
284
290
|
groupId
|