@aglyn/aglyn 1.0.0-beta.219 → 1.0.0-beta.221
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/package.json +12 -12
- package/src/lib/app-utils/actions.d.ts +1 -59
- package/src/lib/app-utils/actions.js +7 -72
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/consent-groups.d.ts +1 -1
- package/src/lib/app-utils/consent-groups.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +0 -36
- package/src/lib/app-utils/console-routes.js +2 -37
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +1 -1
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/container-membership.d.ts +93 -0
- package/src/lib/app-utils/container-membership.js +165 -0
- package/src/lib/app-utils/container-membership.js.map +1 -0
- package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
- package/src/lib/app-utils/crm-task-reminders.js +32 -11
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +24 -4
- package/src/lib/app-utils/crm.js +23 -0
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/csv-upload.d.ts +1 -1
- package/src/lib/app-utils/csv-upload.js +1 -1
- package/src/lib/app-utils/csv-upload.js.map +1 -1
- package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
- package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
- package/src/lib/app-utils/csv.js.map +1 -0
- package/src/lib/app-utils/datasets.d.ts +0 -19
- package/src/lib/app-utils/datasets.js +0 -13
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +4 -4
- package/src/lib/app-utils/docs-help.generated.js +5 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +71 -36
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js +3 -3
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/email-text.d.ts +80 -0
- package/src/lib/app-utils/email-text.js +189 -0
- package/src/lib/app-utils/email-text.js.map +1 -0
- package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
- package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
- package/src/lib/app-utils/enrollment-basis.js.map +1 -0
- package/src/lib/app-utils/expand-repeatables.js +4 -4
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/forms.js +2 -2
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +2 -3
- package/src/lib/app-utils/health-report.js +30 -6
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
- package/src/lib/app-utils/inbound-mail-domain.js +31 -0
- package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
- package/src/lib/app-utils/leaving-notice.d.ts +96 -0
- package/src/lib/app-utils/leaving-notice.js +157 -0
- package/src/lib/app-utils/leaving-notice.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +11 -0
- package/src/lib/app-utils/lockdown.js +33 -3
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
- package/src/lib/app-utils/member-email-aliases.js +29 -0
- package/src/lib/app-utils/member-email-aliases.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +69 -0
- package/src/lib/app-utils/notifications.js +119 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.d.ts +15 -1
- package/src/lib/app-utils/operator-alerts.js +64 -10
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +164 -178
- package/src/lib/app-utils/plan-entitlements.js +200 -224
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
- package/src/lib/app-utils/platform-marketing-consent.js +6 -5
- package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
- package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
- package/src/lib/app-utils/repeat-query.d.ts +56 -0
- package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
- package/src/lib/app-utils/repeat-query.js.map +1 -0
- package/src/lib/app-utils/screen-link-context-value.d.ts +7 -0
- package/src/lib/app-utils/screen-link-context-value.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +14 -2
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +3 -0
- package/src/lib/app-utils/screen-route.js +6 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +7 -9
- package/src/lib/app-utils/server.js +18 -26
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
- package/src/lib/app-utils/starter-template-nodes.js +129 -0
- package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
- package/src/lib/app-utils/starter-templates.d.ts +11 -34
- package/src/lib/app-utils/starter-templates.js +19 -230
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
- package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
- package/src/lib/app-utils/subscription-topics.js.map +1 -0
- package/src/lib/app-utils/usage-budget.d.ts +73 -188
- package/src/lib/app-utils/usage-budget.js +58 -194
- package/src/lib/app-utils/usage-budget.js.map +1 -1
- package/src/lib/app-utils/utm-attribution.d.ts +1 -1
- package/src/lib/app-utils/utm-attribution.js +1 -1
- package/src/lib/app-utils/utm-attribution.js.map +1 -1
- package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +21 -13
- package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +30 -22
- package/src/lib/app-utils/utm-touch.js.map +1 -0
- package/src/lib/app-utils/variables.d.ts +9 -9
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
- package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +2 -2
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +80 -79
- package/src/lib/plugin-manager/feature-plugins.js +22 -35
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +18 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +62 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/index.d.ts +0 -1
- package/src/lib/plugin-manager/index.js +0 -1
- package/src/lib/plugin-manager/index.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
- package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-containers.js +27 -0
- package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
- package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
- package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/plugin-server-steps.d.ts +170 -0
- package/src/lib/plugin-manager/plugin-server-steps.js +116 -0
- package/src/lib/plugin-manager/plugin-server-steps.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +212 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +82 -3
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
- package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
- package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
- package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
- package/src/lib/app-utils/assist-credits.d.ts +0 -482
- package/src/lib/app-utils/assist-credits.js +0 -510
- package/src/lib/app-utils/assist-credits.js.map +0 -1
- package/src/lib/app-utils/campaign-membership.d.ts +0 -121
- package/src/lib/app-utils/campaign-membership.js +0 -189
- package/src/lib/app-utils/campaign-membership.js.map +0 -1
- package/src/lib/app-utils/campaign-touch.js.map +0 -1
- package/src/lib/app-utils/console-record-links.d.ts +0 -84
- package/src/lib/app-utils/console-record-links.js +0 -76
- package/src/lib/app-utils/console-record-links.js.map +0 -1
- package/src/lib/app-utils/crm-booking.d.ts +0 -75
- package/src/lib/app-utils/crm-booking.js +0 -133
- package/src/lib/app-utils/crm-booking.js.map +0 -1
- package/src/lib/app-utils/crm-inbound.d.ts +0 -245
- package/src/lib/app-utils/crm-inbound.js +0 -407
- package/src/lib/app-utils/crm-inbound.js.map +0 -1
- package/src/lib/app-utils/crm-reports.d.ts +0 -479
- package/src/lib/app-utils/crm-reports.js +0 -680
- package/src/lib/app-utils/crm-reports.js.map +0 -1
- package/src/lib/app-utils/dataset-csv.js.map +0 -1
- package/src/lib/app-utils/dataset-query.d.ts +0 -57
- package/src/lib/app-utils/dataset-query.js.map +0 -1
- package/src/lib/app-utils/email-topics.js.map +0 -1
- package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
- package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
- package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
- package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
/**
|
|
18
18
|
* The most characters one uploaded CSV may carry, whoever reads it.
|
|
19
19
|
*
|
|
20
|
-
* A file a person uploads is parsed in one pass by `parseCsv` (`
|
|
20
|
+
* A file a person uploads is parsed in one pass by `parseCsv` (`csv.ts`), in the
|
|
21
21
|
* browser that picked it and again on the server that imports it. This bounds
|
|
22
22
|
* that pass: it is checked before the parse rather than after, because the
|
|
23
23
|
* parse is what the ceiling is protecting — a refusal answered on the byte
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/ /**
|
|
17
17
|
* The most characters one uploaded CSV may carry, whoever reads it.
|
|
18
18
|
*
|
|
19
|
-
* A file a person uploads is parsed in one pass by `parseCsv` (`
|
|
19
|
+
* A file a person uploads is parsed in one pass by `parseCsv` (`csv.ts`), in the
|
|
20
20
|
* browser that picked it and again on the server that imports it. This bounds
|
|
21
21
|
* that pass: it is checked before the parse rather than after, because the
|
|
22
22
|
* parse is what the ceiling is protecting — a refusal answered on the byte
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/csv-upload.ts"],"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/**\n * The most characters one uploaded CSV may carry, whoever reads it.\n *\n * A file a person uploads is parsed in one pass by `parseCsv` (`
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/csv-upload.ts"],"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/**\n * The most characters one uploaded CSV may carry, whoever reads it.\n *\n * A file a person uploads is parsed in one pass by `parseCsv` (`csv.ts`), in the\n * browser that picked it and again on the server that imports it. This bounds\n * that pass: it is checked before the parse rather than after, because the\n * parse is what the ceiling is protecting — a refusal answered on the byte\n * count costs nothing, where discovering the file was too big by\n * materializing all of its cells has already spent the memory the limit\n * exists to bound. Every importer that reads a CSV in one go shares it, so a\n * file one importer accepts is never one another refuses for its size.\n */\nexport const CSV_UPLOAD_MAX_CHARACTERS = 8_000_000\n"],"names":["CSV_UPLOAD_MAX_CHARACTERS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GACD,OAAO,MAAMA,4BAA4B,QAAS"}
|
|
@@ -15,49 +15,18 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
* `coerceDocumentValues`: ISO dates, `lat, lon` coordinates, comma-joined
|
|
20
|
-
* lists, JSON maps, reference ids — so an export re-imports losslessly
|
|
21
|
-
* through the same coercion.
|
|
18
|
+
* CSV, read and written the one way the platform does it (AGL-2335).
|
|
22
19
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `aglyn:addons` — apps reach plugins only through the generated loader
|
|
31
|
-
* manifests (AGL-417/419). The choice was a second copy of the escaper in
|
|
32
|
-
* the console or one copy somewhere both may reach. There are already five
|
|
33
|
-
* divergent hand-rolled CSV escapers in this repo; this is not becoming the
|
|
34
|
-
* sixth. `dataset-io.ts` re-exports every name below, so the plugin's own
|
|
35
|
-
* imports are unchanged.
|
|
36
|
-
*
|
|
37
|
-
* READING one is shared for the same reason. The email list importer parses
|
|
38
|
-
* a merchant's contact file, and it lives in `@aglyn/plugins-email`, which
|
|
39
|
-
* may not import `@aglyn/plugins-data` either. So {@link parseCsv} sits here
|
|
40
|
-
* beside the escaper that is its inverse, and `dataset-io.ts` re-exports it
|
|
41
|
-
* exactly as it re-exports the writing half.
|
|
42
|
-
*/
|
|
43
|
-
import type { DatasetFieldDefinition, DatasetModel } from './dataset-models';
|
|
44
|
-
/** Storage value → portable string (CSV cell / JSON value). */
|
|
45
|
-
export declare function serializeDatasetValue(field: DatasetFieldDefinition, value: unknown): string;
|
|
46
|
-
/**
|
|
47
|
-
* The header line — field ids, in model order (AGL-2335).
|
|
48
|
-
*
|
|
49
|
-
* Split out of {@link datasetRecordsToCsv} so a server export can emit the
|
|
50
|
-
* file a page at a time. The whole point of that export is that it is not
|
|
51
|
-
* bounded by what one process can hold in memory, and a helper that only
|
|
52
|
-
* takes a complete `rows` array cannot serve it. Everything below shares
|
|
53
|
-
* one escaper: there are already five divergent hand-rolled CSV escapers in
|
|
54
|
-
* this repo and this is not becoming the sixth.
|
|
20
|
+
* There were five divergent hand-rolled CSV escapers in this repo; this is
|
|
21
|
+
* the one escaper and the one parser every plugin and app reaches — the data
|
|
22
|
+
* plugin's dataset export and import, the email list import, the commerce
|
|
23
|
+
* catalog import, the CRM's contact import — and the check a download runs
|
|
24
|
+
* against the row count its server promised. Nothing here knows what the rows
|
|
25
|
+
* are; a plugin that writes its own records into a file formats each value
|
|
26
|
+
* itself and escapes it here.
|
|
55
27
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
export declare function datasetCsvRow(model: DatasetModel, row: Record<string, unknown>): string;
|
|
59
|
-
/** One record's values → the portable JSON object the JSON export emits. */
|
|
60
|
-
export declare function datasetRecordToJson(model: DatasetModel, row: Record<string, unknown>): Record<string, string>;
|
|
28
|
+
/** One cell, quoted when it has to be: a comma, a quote or a line break. */
|
|
29
|
+
export declare const escapeCsvCell: (cell: string) => string;
|
|
61
30
|
/**
|
|
62
31
|
* Data rows in a CSV document — the header excluded, quoted newlines not
|
|
63
32
|
* counted as row breaks (AGL-2335).
|
|
@@ -93,8 +62,6 @@ export declare function exportShortfall(promisedHeader: string | null | undefine
|
|
|
93
62
|
received: number;
|
|
94
63
|
short: boolean;
|
|
95
64
|
};
|
|
96
|
-
/** Rows (storage-form value maps) → CSV with a fieldId header row. */
|
|
97
|
-
export declare function datasetRecordsToCsv(model: DatasetModel, rows: Array<Record<string, unknown>>): string;
|
|
98
65
|
/**
|
|
99
66
|
* Minimal RFC-4180 CSV parser (quoted fields, escaped quotes, CRLF).
|
|
100
67
|
*
|
|
@@ -14,81 +14,16 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ /**
|
|
17
|
-
*
|
|
18
|
-
* `coerceDocumentValues`: ISO dates, `lat, lon` coordinates, comma-joined
|
|
19
|
-
* lists, JSON maps, reference ids — so an export re-imports losslessly
|
|
20
|
-
* through the same coercion.
|
|
17
|
+
* CSV, read and written the one way the platform does it (AGL-2335).
|
|
21
18
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
* manifests (AGL-417/419). The choice was a second copy of the escaper in
|
|
31
|
-
* the console or one copy somewhere both may reach. There are already five
|
|
32
|
-
* divergent hand-rolled CSV escapers in this repo; this is not becoming the
|
|
33
|
-
* sixth. `dataset-io.ts` re-exports every name below, so the plugin's own
|
|
34
|
-
* imports are unchanged.
|
|
35
|
-
*
|
|
36
|
-
* READING one is shared for the same reason. The email list importer parses
|
|
37
|
-
* a merchant's contact file, and it lives in `@aglyn/plugins-email`, which
|
|
38
|
-
* may not import `@aglyn/plugins-data` either. So {@link parseCsv} sits here
|
|
39
|
-
* beside the escaper that is its inverse, and `dataset-io.ts` re-exports it
|
|
40
|
-
* exactly as it re-exports the writing half.
|
|
41
|
-
*/ /** Storage value → portable string (CSV cell / JSON value). */ export function serializeDatasetValue(field, value) {
|
|
42
|
-
if (value == null) return '';
|
|
43
|
-
switch(field.type){
|
|
44
|
-
case 'timestamp':
|
|
45
|
-
return typeof value === 'number' && Number.isFinite(value) ? new Date(value).toISOString() : String(value);
|
|
46
|
-
case 'coordinates':
|
|
47
|
-
{
|
|
48
|
-
const coordinates = value;
|
|
49
|
-
return coordinates && typeof coordinates.latitude === 'number' && typeof coordinates.longitude === 'number' ? `${coordinates.latitude}, ${coordinates.longitude}` : String(value);
|
|
50
|
-
}
|
|
51
|
-
case 'sorted':
|
|
52
|
-
case 'reference':
|
|
53
|
-
return Array.isArray(value) ? value.join(', ') : String(value);
|
|
54
|
-
case 'map':
|
|
55
|
-
try {
|
|
56
|
-
return JSON.stringify(value);
|
|
57
|
-
} catch (unused) {
|
|
58
|
-
return String(value);
|
|
59
|
-
}
|
|
60
|
-
default:
|
|
61
|
-
return String(value);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
const csvEscape = (cell)=>/[",\r\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"` : cell;
|
|
65
|
-
/**
|
|
66
|
-
* The header line — field ids, in model order (AGL-2335).
|
|
67
|
-
*
|
|
68
|
-
* Split out of {@link datasetRecordsToCsv} so a server export can emit the
|
|
69
|
-
* file a page at a time. The whole point of that export is that it is not
|
|
70
|
-
* bounded by what one process can hold in memory, and a helper that only
|
|
71
|
-
* takes a complete `rows` array cannot serve it. Everything below shares
|
|
72
|
-
* one escaper: there are already five divergent hand-rolled CSV escapers in
|
|
73
|
-
* this repo and this is not becoming the sixth.
|
|
74
|
-
*/ export function datasetCsvHeader(model) {
|
|
75
|
-
return model.order.map(csvEscape).join(',');
|
|
76
|
-
}
|
|
77
|
-
/** One record's values (storage form) → one CSV line, no terminator. */ export function datasetCsvRow(model, row) {
|
|
78
|
-
return model.order.map((fieldId)=>{
|
|
79
|
-
const field = model.fields[fieldId];
|
|
80
|
-
return csvEscape(field ? serializeDatasetValue(field, row[fieldId]) : '');
|
|
81
|
-
}).join(',');
|
|
82
|
-
}
|
|
83
|
-
/** One record's values → the portable JSON object the JSON export emits. */ export function datasetRecordToJson(model, row) {
|
|
84
|
-
return Object.fromEntries(model.order.map((fieldId)=>{
|
|
85
|
-
const field = model.fields[fieldId];
|
|
86
|
-
return [
|
|
87
|
-
fieldId,
|
|
88
|
-
field ? serializeDatasetValue(field, row[fieldId]) : ''
|
|
89
|
-
];
|
|
90
|
-
}));
|
|
91
|
-
}
|
|
19
|
+
* There were five divergent hand-rolled CSV escapers in this repo; this is
|
|
20
|
+
* the one escaper and the one parser every plugin and app reaches — the data
|
|
21
|
+
* plugin's dataset export and import, the email list import, the commerce
|
|
22
|
+
* catalog import, the CRM's contact import — and the check a download runs
|
|
23
|
+
* against the row count its server promised. Nothing here knows what the rows
|
|
24
|
+
* are; a plugin that writes its own records into a file formats each value
|
|
25
|
+
* itself and escapes it here.
|
|
26
|
+
*/ /** One cell, quoted when it has to be: a comma, a quote or a line break. */ export const escapeCsvCell = (cell)=>/[",\r\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"` : cell;
|
|
92
27
|
/**
|
|
93
28
|
* Data rows in a CSV document — the header excluded, quoted newlines not
|
|
94
29
|
* counted as row breaks (AGL-2335).
|
|
@@ -173,12 +108,6 @@ const csvEscape = (cell)=>/[",\r\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"
|
|
|
173
108
|
short: Number.isFinite(promised) && promisedHeader != null && received < promised
|
|
174
109
|
};
|
|
175
110
|
}
|
|
176
|
-
/** Rows (storage-form value maps) → CSV with a fieldId header row. */ export function datasetRecordsToCsv(model, rows) {
|
|
177
|
-
return [
|
|
178
|
-
datasetCsvHeader(model),
|
|
179
|
-
...rows.map((row)=>datasetCsvRow(model, row))
|
|
180
|
-
].join('\n');
|
|
181
|
-
}
|
|
182
111
|
/**
|
|
183
112
|
* Minimal RFC-4180 CSV parser (quoted fields, escaped quotes, CRLF).
|
|
184
113
|
*
|
|
@@ -232,4 +161,4 @@ const csvEscape = (cell)=>/[",\r\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"
|
|
|
232
161
|
return rows.filter((cells)=>cells.some((value)=>value.trim() !== ''));
|
|
233
162
|
}
|
|
234
163
|
|
|
235
|
-
//# sourceMappingURL=
|
|
164
|
+
//# sourceMappingURL=csv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/csv.ts"],"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/**\n * CSV, read and written the one way the platform does it (AGL-2335).\n *\n * There were five divergent hand-rolled CSV escapers in this repo; this is\n * the one escaper and the one parser every plugin and app reaches — the data\n * plugin's dataset export and import, the email list import, the commerce\n * catalog import, the CRM's contact import — and the check a download runs\n * against the row count its server promised. Nothing here knows what the rows\n * are; a plugin that writes its own records into a file formats each value\n * itself and escapes it here.\n */\n\n/** One cell, quoted when it has to be: a comma, a quote or a line break. */\nexport const escapeCsvCell = (cell: string): string =>\n /[\",\\r\\n]/.test(cell) ? `\"${cell.replace(/\"/g, '\"\"')}\"` : cell\n\n/**\n * Data rows in a CSV document — the header excluded, quoted newlines not\n * counted as row breaks (AGL-2335).\n *\n * This exists so a download can be CHECKED against the row count the server\n * promised, rather than trusted. A truncated export is the defect this whole\n * area is about, and a stream that dies halfway produces a perfectly\n * well-formed shorter file: nothing about the bytes says they are short.\n *\n * Single pass, no array building — {@link parseCsv} answers the same\n * question but materializes every cell, which is the wrong trade when the\n * file may be hundreds of thousands of rows and the only thing wanted is\n * how many there are.\n */\nexport function countCsvDataRows(text: string): number {\n const source = String(text ?? '')\n if (!source) return 0\n let rows = 0\n let quoted = false\n let cellsSeen = false\n for (let index = 0; index < source.length; index += 1) {\n const char = source[index]\n if (quoted) {\n if (char === '\"') {\n if (source[index + 1] === '\"') index += 1\n else quoted = false\n }\n cellsSeen = true\n continue\n }\n if (char === '\"') {\n quoted = true\n cellsSeen = true\n continue\n }\n if (char === '\\n') {\n rows += 1\n cellsSeen = false\n continue\n }\n if (char !== '\\r') cellsSeen = true\n }\n // A file that does not end in a newline still has a final row.\n if (cellsSeen) rows += 1\n // The header is not data.\n return Math.max(0, rows - 1)\n}\n\n/**\n * Did a downloaded export arrive whole? (AGL-2335)\n *\n * The server streams and reports its `count()` aggregate in a header, so the\n * client can CHECK rather than trust. This is the check, kept here as a pure\n * function because a guard living inline in a component is a guard nobody\n * can force red — and this one exists precisely to catch a silent shortfall.\n *\n * `short` is only true when the count is both KNOWN and lower. A missing or\n * unparseable header is not evidence of truncation, and refusing a download\n * on the strength of a header that never arrived would fail the user for the\n * server's bookkeeping. More rows than promised is not short either — the\n * count is a snapshot taken before the first page, so a concurrent insert\n * can legitimately overtake it.\n */\nexport function exportShortfall(\n promisedHeader: string | null | undefined,\n body: string,\n format: 'csv' | 'json',\n): { promised: number; received: number; short: boolean } {\n const promised = Number(promisedHeader)\n let received: number\n if (format === 'csv') {\n received = countCsvDataRows(body)\n } else {\n try {\n const parsed = JSON.parse(body) as unknown\n received = Array.isArray(parsed) ? parsed.length : 0\n } catch {\n // A body that is not JSON at all did not arrive whole.\n return { promised, received: 0, short: true }\n }\n }\n return {\n promised,\n received,\n short: Number.isFinite(promised) && promisedHeader != null && received < promised,\n }\n}\n\n/**\n * Minimal RFC-4180 CSV parser (quoted fields, escaped quotes, CRLF).\n *\n * The inverse of the escaper above, and the one parser in the repo — see the\n * module note for why the email list importer reaches this rather than\n * writing its own.\n *\n * A wholly blank row is dropped. Every spreadsheet writes a trailing newline\n * and a row of empty cells is not a record; carrying it through would give\n * every caller the same off-by-one to remember.\n */\nexport function parseCsv(text: string): string[][] {\n const rows: string[][] = []\n let row: string[] = []\n let cell = ''\n let quoted = false\n const source = String(text ?? '')\n for (let index = 0; index < source.length; index += 1) {\n const char = source[index]\n if (quoted) {\n if (char === '\"') {\n if (source[index + 1] === '\"') {\n cell += '\"'\n index += 1\n } else {\n quoted = false\n }\n } else {\n cell += char\n }\n continue\n }\n if (char === '\"') {\n quoted = true\n } else if (char === ',') {\n row.push(cell)\n cell = ''\n } else if (char === '\\n' || char === '\\r') {\n if (char === '\\r' && source[index + 1] === '\\n') index += 1\n row.push(cell)\n cell = ''\n rows.push(row)\n row = []\n } else {\n cell += char\n }\n }\n if (cell !== '' || row.length) {\n row.push(cell)\n rows.push(row)\n }\n return rows.filter((cells) => cells.some((value) => value.trim() !== ''))\n}\n"],"names":["escapeCsvCell","cell","test","replace","countCsvDataRows","text","source","String","rows","quoted","cellsSeen","index","length","char","Math","max","exportShortfall","promisedHeader","body","format","promised","Number","received","parsed","JSON","parse","Array","isArray","short","isFinite","parseCsv","row","push","filter","cells","some","value","trim"],"mappings":"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"}
|
|
@@ -167,24 +167,5 @@ export declare function datasetDisplayName(dataset: {
|
|
|
167
167
|
displayName?: unknown;
|
|
168
168
|
name?: unknown;
|
|
169
169
|
} | null | undefined): string;
|
|
170
|
-
/** One entry in a dataset select: stored by id, shown by name. */
|
|
171
|
-
export interface DatasetPickerOption {
|
|
172
|
-
id: string;
|
|
173
|
-
name: string;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* The options a dataset select offers, sorted by the label shown.
|
|
177
|
-
*
|
|
178
|
-
* Deleted datasets are left out. Each is labeled by
|
|
179
|
-
* {@link datasetDisplayName}; one whose document carries no name at all is
|
|
180
|
-
* offered under its id, because a select that hides it leaves a dataset that
|
|
181
|
-
* cannot be picked.
|
|
182
|
-
*/
|
|
183
|
-
export declare function datasetPickerOptions(docs: ReadonlyArray<{
|
|
184
|
-
$id?: string;
|
|
185
|
-
deletedAt?: unknown;
|
|
186
|
-
displayName?: unknown;
|
|
187
|
-
name?: unknown;
|
|
188
|
-
}> | null | undefined): DatasetPickerOption[];
|
|
189
170
|
/** Records sorted by their editor order (then id for stability). */
|
|
190
171
|
export declare function sortDatasetRecords<T extends HostDatasetRecord>(records: T[]): T[];
|
|
@@ -215,19 +215,6 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
215
215
|
}
|
|
216
216
|
return '';
|
|
217
217
|
}
|
|
218
|
-
/**
|
|
219
|
-
* The options a dataset select offers, sorted by the label shown.
|
|
220
|
-
*
|
|
221
|
-
* Deleted datasets are left out. Each is labeled by
|
|
222
|
-
* {@link datasetDisplayName}; one whose document carries no name at all is
|
|
223
|
-
* offered under its id, because a select that hides it leaves a dataset that
|
|
224
|
-
* cannot be picked.
|
|
225
|
-
*/ export function datasetPickerOptions(docs) {
|
|
226
|
-
return (docs != null ? docs : []).filter((dataset)=>!dataset.deletedAt && dataset.$id).map((dataset)=>({
|
|
227
|
-
id: dataset.$id,
|
|
228
|
-
name: datasetDisplayName(dataset) || dataset.$id
|
|
229
|
-
})).sort((a, b)=>a.name.localeCompare(b.name));
|
|
230
|
-
}
|
|
231
218
|
/** Records sorted by their editor order (then id for stability). */ export function sortDatasetRecords(records) {
|
|
232
219
|
return [
|
|
233
220
|
...records
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/datasets.ts"],"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/**\n * Datasets (AGL-102): tabular data at `orgs/{orgId}/datasets/{id}` with a\n * `records` subcollection, edited in the console Data card and consumed by\n * repeatable components (AGL-103) via `{{item.field}}` bindings.\n */\n\nimport {\n coerceDocumentValues,\n type DatasetModel,\n effectiveDatasetModel,\n validateDocument,\n} from './dataset-models'\n\n/** Dataset field name: starts with a letter; letters/digits/underscores. */\nexport const DATASET_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/\n\nexport interface HostDataset {\n $id?: string\n displayName?: string\n /** Column names, in display order (v1; superseded by `model`). */\n fields?: string[]\n /** Typed model (AGL-177); when absent, derive from `fields`. */\n model?: import('./dataset-models').DatasetModel\n /** dod.ts Schema.Name shape (AGL-178). */\n names?: { singular?: string; plural?: string }\n /**\n * Which sites may see this dataset (AGL-1037). Absent = org-wide, until\n * the AGL-1040 backfill stamps it. See `app-utils/scope-tokens`.\n */\n visibleTo?: import('../foundation').ScopeToken[]\n}\n\nexport interface HostDatasetRecord {\n $id?: string\n /**\n * Field → value, in storage form per the field's type (see\n * `dataset-models`). Rows written by a form or an automation step before\n * AGL-2773 hold every value as text; readers accept both.\n */\n values?: Record<string, unknown>\n /** Row position in the editor and in repeated output. */\n order?: number\n}\n\n/**\n * A human field entry from the quick creator: the stable id plus the\n * display name shown in table headers and bindings pickers (AGL-558).\n */\nexport interface DatasetFieldEntry {\n id: string\n name: string\n}\n\n/**\n * Stable field id from a human name: \"Roast preference\" → \"roast_preference\".\n * Mirrors DATASET_FIELD_PATTERN; returns '' when nothing salvageable.\n */\nexport function slugifyDatasetFieldId(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .replace(/[\\s-]+/g, '_')\n .replace(/[^a-z0-9_]/g, '')\n .replace(/^[0-9_]+/, '')\n .replace(/_+$/, '')\n return DATASET_FIELD_PATTERN.test(slug) ? slug : ''\n}\n\n/**\n * Default reference id for a new field (AGL-578): the `slugifyDatasetFieldId`\n * slug, suffixed (`_2`, `_3`, …) to stay unique within the dataset. `taken`\n * holds the ids already in use (compared case-insensitively). Returns '' when\n * the name yields nothing valid. The user can override the result.\n */\nexport function defaultDatasetFieldId(\n name: string,\n taken: ReadonlySet<string>,\n): string {\n const base = slugifyDatasetFieldId(name)\n if (!base) return ''\n const used = new Set([...taken].map((id) => id.toLowerCase()))\n let candidate = base\n let suffix = 2\n while (used.has(candidate.toLowerCase())) candidate = `${base}_${suffix++}`\n return candidate\n}\n\n/**\n * Validate a user-entered reference id (AGL-578): non-empty, matches\n * `DATASET_FIELD_PATTERN`, and unique within the dataset (`taken` = ids already\n * used, compared case-insensitively). Returns an error message, or null when\n * the id is usable.\n */\nexport function validateDatasetFieldId(\n id: string,\n taken: ReadonlySet<string>,\n): string | null {\n const trimmed = id.trim()\n if (!trimmed) return 'A reference ID is required'\n if (!DATASET_FIELD_PATTERN.test(trimmed))\n return 'Start with a letter; use only letters, numbers, and underscores'\n const used = new Set([...taken].map((existing) => existing.toLowerCase()))\n if (used.has(trimmed.toLowerCase()))\n return 'Another field already uses this reference ID'\n return null\n}\n\n/** Display fallback for raw ids: \"roast_preference\" → \"Roast preference\". */\nexport function humanizeDatasetFieldId(id: string): string {\n const words = id.replace(/_/g, ' ').trim()\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : id\n}\n\n/**\n * Parses a comma/newline separated list of HUMAN field names into\n * {id, name} entries — \"Roast preference\" keeps its pretty name and gets\n * the stable id `roast_preference` (AGL-558). Plain snake_case keys\n * still work and pick up a humanized display name. Duplicate ids and\n * unsalvageable entries are dropped rather than failing the set.\n */\nexport function parseDatasetFieldEntries(input: string): DatasetFieldEntry[] {\n const seen = new Set<string>()\n const entries: DatasetFieldEntry[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const trimmed = raw.trim()\n if (!trimmed) continue\n const id = slugifyDatasetFieldId(trimmed)\n if (!id || seen.has(id)) continue\n seen.add(id)\n entries.push({\n id,\n name: DATASET_FIELD_PATTERN.test(trimmed)\n ? humanizeDatasetFieldId(trimmed)\n : trimmed,\n })\n }\n return entries\n}\n\n/**\n * Parses a comma/newline separated field list into valid unique names;\n * invalid entries are dropped rather than failing the whole set.\n */\nexport function parseDatasetFields(input: string): string[] {\n const seen = new Set<string>()\n const fields: string[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const name = raw.trim()\n if (!DATASET_FIELD_PATTERN.test(name)) continue\n const key = name.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n fields.push(name)\n }\n return fields\n}\n\n/** Record values restricted to the dataset's declared fields. */\nexport function sanitizeRecordValues(\n fields: string[],\n input: Record<string, unknown> | null | undefined,\n): Record<string, string> {\n const values: Record<string, string> = {}\n for (const field of fields) {\n const value = input?.[field]\n if (value == null) continue\n values[field] = String(value)\n }\n return values\n}\n\n/**\n * The submitted values a dataset write would store, picked by field id and\n * left as they arrived (AGL-556). With a `fieldMap` (submitted key → stable\n * model fieldId), mapped values land under the mapped fieldId — so renamed\n * schema fields keep receiving data — and entries whose fieldId isn't in the\n * model are dropped. Submitted keys without a mapping (and calls without a\n * map) fall back to the legacy name-intersection against the model's field\n * ids, matching `sanitizeRecordValues`.\n */\nexport function pickDatasetRecordInput(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n fieldMap?: Record<string, string> | null,\n): Record<string, unknown> {\n const model = effectiveDatasetModel(dataset)\n const map = fieldMap ?? {}\n const values: Record<string, unknown> = {}\n for (const fieldId of model.order) {\n if (!model.fields[fieldId]) continue\n // A submitted key with an explicit mapping never doubles as a\n // name-match — the mapping decides where it lands.\n if (fieldId in map) continue\n const value = input?.[fieldId]\n if (value == null) continue\n values[fieldId] = value\n }\n for (const [submittedKey, fieldId] of Object.entries(map)) {\n if (typeof fieldId !== 'string' || !model.fields[fieldId]) continue\n const value = input?.[submittedKey]\n if (value == null) continue\n values[fieldId] = value\n }\n return values\n}\n\n/** A dataset record write, checked against the dataset's model. */\nexport interface DatasetRecordWrite {\n /**\n * The model fields this write supplied a value for, before coercion. Empty\n * means nothing submitted matched a field of the dataset.\n */\n matched: string[]\n /**\n * The values to store, in storage form (see `dataset-models`): numbers,\n * booleans, epoch millis and so on per the field's type. For a merge, the\n * existing values with this write's laid over them.\n */\n values: Record<string, unknown>\n /** fieldId → the reason it was refused. Empty when the write may land. */\n errors: Record<string, string>\n}\n\n/**\n * A text field takes what an event payload carries as a number or a boolean\n * as the text it reads as, which is what these writes always stored.\n */\nfunction asTextInput(\n model: DatasetModel,\n picked: Record<string, unknown>,\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const [fieldId, value] of Object.entries(picked)) {\n const type = model.fields[fieldId]?.type\n values[fieldId] =\n (type === 'text' || type === 'reference') &&\n (typeof value === 'number' || typeof value === 'boolean')\n ? String(value)\n : value\n }\n return values\n}\n\n/**\n * A record write from a form submission or an automation step, coerced and\n * validated against the dataset's model (AGL-2773, option B).\n *\n * These writes used to store `String(value)` for every field and check\n * nothing, so a form could put `abc` in a number column and skip a required\n * one. They now run the same `coerceDocumentValues` + `validateDocument` pair\n * the console and `/v1` do: a value is stored in its field's type, and a\n * record with any refused field is not written at all. The caller decides\n * what a refusal costs — a form keeps the Inbox submission, a run records a\n * failed step.\n *\n * `existing` makes it a merge (update-or-append's update leg): this write's\n * values are laid over the stored ones, and only the fields THIS write\n * supplied are held to the model. A stored row written as text before this\n * change keeps its text values and is not refused for them — the reads have\n * always rendered them, and a merge that touched `email` must not fail over a\n * legacy `price` of `\"12\"` it never sent.\n *\n * Custom field types validate only once registered: call\n * `ensureDeclaredCustomFieldTypes(model)` before this on a server path.\n */\nexport function prepareDatasetRecordWrite(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n options: {\n fieldMap?: Record<string, string> | null\n existing?: Record<string, unknown> | null\n } = {},\n): DatasetRecordWrite {\n const model = effectiveDatasetModel(dataset)\n const picked = pickDatasetRecordInput(dataset, input, options.fieldMap)\n const matched = Object.keys(picked)\n const coerced = coerceDocumentValues(model, asTextInput(model, picked))\n if (!options.existing) {\n return {\n matched,\n values: coerced,\n errors: validateDocument(model, coerced),\n }\n }\n const values = { ...options.existing, ...coerced }\n const errors: Record<string, string> = {}\n for (const [fieldId, error] of Object.entries(\n validateDocument(model, values),\n )) {\n if (fieldId in coerced) errors[fieldId] = error\n }\n return { matched, values, errors }\n}\n\n/**\n * A refused write's errors as one line, in the model's field order:\n * `Stars must be a whole number; Email is required`.\n */\nexport function describeDatasetRecordErrors(\n errors: Record<string, string>,\n): string {\n return Object.values(errors).join('; ')\n}\n\n/**\n * The name a dataset is shown under: `displayName`, which is what every create\n * path writes (AGL-536), then the pre-migration `name`. Blank when the document\n * carries neither.\n */\nexport function datasetDisplayName(\n dataset: { displayName?: unknown; name?: unknown } | null | undefined,\n): string {\n for (const candidate of [dataset?.displayName, dataset?.name]) {\n if (typeof candidate === 'string' && candidate.trim()) {\n return candidate.trim()\n }\n }\n return ''\n}\n\n/** One entry in a dataset select: stored by id, shown by name. */\nexport interface DatasetPickerOption {\n id: string\n name: string\n}\n\n/**\n * The options a dataset select offers, sorted by the label shown.\n *\n * Deleted datasets are left out. Each is labeled by\n * {@link datasetDisplayName}; one whose document carries no name at all is\n * offered under its id, because a select that hides it leaves a dataset that\n * cannot be picked.\n */\nexport function datasetPickerOptions(\n docs:\n | ReadonlyArray<{\n $id?: string\n deletedAt?: unknown\n displayName?: unknown\n name?: unknown\n }>\n | null\n | undefined,\n): DatasetPickerOption[] {\n return (docs ?? [])\n .filter((dataset) => !dataset.deletedAt && dataset.$id)\n .map((dataset) => ({\n id: dataset.$id as string,\n name: datasetDisplayName(dataset) || (dataset.$id as string),\n }))\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/** Records sorted by their editor order (then id for stability). */\nexport function sortDatasetRecords<T extends HostDatasetRecord>(\n records: T[],\n): T[] {\n return [...records].sort(\n (a, b) =>\n (a.order ?? Number.MAX_SAFE_INTEGER) -\n (b.order ?? Number.MAX_SAFE_INTEGER) ||\n String(a.$id ?? '').localeCompare(String(b.$id ?? '')),\n )\n}\n"],"names":["coerceDocumentValues","effectiveDatasetModel","validateDocument","DATASET_FIELD_PATTERN","slugifyDatasetFieldId","name","slug","trim","toLowerCase","replace","test","defaultDatasetFieldId","taken","base","used","Set","map","id","candidate","suffix","has","validateDatasetFieldId","trimmed","existing","humanizeDatasetFieldId","words","charAt","toUpperCase","slice","parseDatasetFieldEntries","input","seen","entries","raw","split","add","push","parseDatasetFields","fields","key","sanitizeRecordValues","values","field","value","String","pickDatasetRecordInput","dataset","fieldMap","model","fieldId","order","submittedKey","Object","asTextInput","picked","type","prepareDatasetRecordWrite","options","matched","keys","coerced","errors","error","describeDatasetRecordErrors","join","datasetDisplayName","displayName","datasetPickerOptions","docs","filter","deletedAt","$id","sort","a","b","localeCompare","sortDatasetRecords","records","Number","MAX_SAFE_INTEGER"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/datasets.ts"],"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/**\n * Datasets (AGL-102): tabular data at `orgs/{orgId}/datasets/{id}` with a\n * `records` subcollection, edited in the console Data card and consumed by\n * repeatable components (AGL-103) via `{{item.field}}` bindings.\n */\n\nimport {\n coerceDocumentValues,\n type DatasetModel,\n effectiveDatasetModel,\n validateDocument,\n} from './dataset-models'\n\n/** Dataset field name: starts with a letter; letters/digits/underscores. */\nexport const DATASET_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/\n\nexport interface HostDataset {\n $id?: string\n displayName?: string\n /** Column names, in display order (v1; superseded by `model`). */\n fields?: string[]\n /** Typed model (AGL-177); when absent, derive from `fields`. */\n model?: import('./dataset-models').DatasetModel\n /** dod.ts Schema.Name shape (AGL-178). */\n names?: { singular?: string; plural?: string }\n /**\n * Which sites may see this dataset (AGL-1037). Absent = org-wide, until\n * the AGL-1040 backfill stamps it. See `app-utils/scope-tokens`.\n */\n visibleTo?: import('../foundation').ScopeToken[]\n}\n\nexport interface HostDatasetRecord {\n $id?: string\n /**\n * Field → value, in storage form per the field's type (see\n * `dataset-models`). Rows written by a form or an automation step before\n * AGL-2773 hold every value as text; readers accept both.\n */\n values?: Record<string, unknown>\n /** Row position in the editor and in repeated output. */\n order?: number\n}\n\n/**\n * A human field entry from the quick creator: the stable id plus the\n * display name shown in table headers and bindings pickers (AGL-558).\n */\nexport interface DatasetFieldEntry {\n id: string\n name: string\n}\n\n/**\n * Stable field id from a human name: \"Roast preference\" → \"roast_preference\".\n * Mirrors DATASET_FIELD_PATTERN; returns '' when nothing salvageable.\n */\nexport function slugifyDatasetFieldId(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .replace(/[\\s-]+/g, '_')\n .replace(/[^a-z0-9_]/g, '')\n .replace(/^[0-9_]+/, '')\n .replace(/_+$/, '')\n return DATASET_FIELD_PATTERN.test(slug) ? slug : ''\n}\n\n/**\n * Default reference id for a new field (AGL-578): the `slugifyDatasetFieldId`\n * slug, suffixed (`_2`, `_3`, …) to stay unique within the dataset. `taken`\n * holds the ids already in use (compared case-insensitively). Returns '' when\n * the name yields nothing valid. The user can override the result.\n */\nexport function defaultDatasetFieldId(\n name: string,\n taken: ReadonlySet<string>,\n): string {\n const base = slugifyDatasetFieldId(name)\n if (!base) return ''\n const used = new Set([...taken].map((id) => id.toLowerCase()))\n let candidate = base\n let suffix = 2\n while (used.has(candidate.toLowerCase())) candidate = `${base}_${suffix++}`\n return candidate\n}\n\n/**\n * Validate a user-entered reference id (AGL-578): non-empty, matches\n * `DATASET_FIELD_PATTERN`, and unique within the dataset (`taken` = ids already\n * used, compared case-insensitively). Returns an error message, or null when\n * the id is usable.\n */\nexport function validateDatasetFieldId(\n id: string,\n taken: ReadonlySet<string>,\n): string | null {\n const trimmed = id.trim()\n if (!trimmed) return 'A reference ID is required'\n if (!DATASET_FIELD_PATTERN.test(trimmed))\n return 'Start with a letter; use only letters, numbers, and underscores'\n const used = new Set([...taken].map((existing) => existing.toLowerCase()))\n if (used.has(trimmed.toLowerCase()))\n return 'Another field already uses this reference ID'\n return null\n}\n\n/** Display fallback for raw ids: \"roast_preference\" → \"Roast preference\". */\nexport function humanizeDatasetFieldId(id: string): string {\n const words = id.replace(/_/g, ' ').trim()\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : id\n}\n\n/**\n * Parses a comma/newline separated list of HUMAN field names into\n * {id, name} entries — \"Roast preference\" keeps its pretty name and gets\n * the stable id `roast_preference` (AGL-558). Plain snake_case keys\n * still work and pick up a humanized display name. Duplicate ids and\n * unsalvageable entries are dropped rather than failing the set.\n */\nexport function parseDatasetFieldEntries(input: string): DatasetFieldEntry[] {\n const seen = new Set<string>()\n const entries: DatasetFieldEntry[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const trimmed = raw.trim()\n if (!trimmed) continue\n const id = slugifyDatasetFieldId(trimmed)\n if (!id || seen.has(id)) continue\n seen.add(id)\n entries.push({\n id,\n name: DATASET_FIELD_PATTERN.test(trimmed)\n ? humanizeDatasetFieldId(trimmed)\n : trimmed,\n })\n }\n return entries\n}\n\n/**\n * Parses a comma/newline separated field list into valid unique names;\n * invalid entries are dropped rather than failing the whole set.\n */\nexport function parseDatasetFields(input: string): string[] {\n const seen = new Set<string>()\n const fields: string[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const name = raw.trim()\n if (!DATASET_FIELD_PATTERN.test(name)) continue\n const key = name.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n fields.push(name)\n }\n return fields\n}\n\n/** Record values restricted to the dataset's declared fields. */\nexport function sanitizeRecordValues(\n fields: string[],\n input: Record<string, unknown> | null | undefined,\n): Record<string, string> {\n const values: Record<string, string> = {}\n for (const field of fields) {\n const value = input?.[field]\n if (value == null) continue\n values[field] = String(value)\n }\n return values\n}\n\n/**\n * The submitted values a dataset write would store, picked by field id and\n * left as they arrived (AGL-556). With a `fieldMap` (submitted key → stable\n * model fieldId), mapped values land under the mapped fieldId — so renamed\n * schema fields keep receiving data — and entries whose fieldId isn't in the\n * model are dropped. Submitted keys without a mapping (and calls without a\n * map) fall back to the legacy name-intersection against the model's field\n * ids, matching `sanitizeRecordValues`.\n */\nexport function pickDatasetRecordInput(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n fieldMap?: Record<string, string> | null,\n): Record<string, unknown> {\n const model = effectiveDatasetModel(dataset)\n const map = fieldMap ?? {}\n const values: Record<string, unknown> = {}\n for (const fieldId of model.order) {\n if (!model.fields[fieldId]) continue\n // A submitted key with an explicit mapping never doubles as a\n // name-match — the mapping decides where it lands.\n if (fieldId in map) continue\n const value = input?.[fieldId]\n if (value == null) continue\n values[fieldId] = value\n }\n for (const [submittedKey, fieldId] of Object.entries(map)) {\n if (typeof fieldId !== 'string' || !model.fields[fieldId]) continue\n const value = input?.[submittedKey]\n if (value == null) continue\n values[fieldId] = value\n }\n return values\n}\n\n/** A dataset record write, checked against the dataset's model. */\nexport interface DatasetRecordWrite {\n /**\n * The model fields this write supplied a value for, before coercion. Empty\n * means nothing submitted matched a field of the dataset.\n */\n matched: string[]\n /**\n * The values to store, in storage form (see `dataset-models`): numbers,\n * booleans, epoch millis and so on per the field's type. For a merge, the\n * existing values with this write's laid over them.\n */\n values: Record<string, unknown>\n /** fieldId → the reason it was refused. Empty when the write may land. */\n errors: Record<string, string>\n}\n\n/**\n * A text field takes what an event payload carries as a number or a boolean\n * as the text it reads as, which is what these writes always stored.\n */\nfunction asTextInput(\n model: DatasetModel,\n picked: Record<string, unknown>,\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const [fieldId, value] of Object.entries(picked)) {\n const type = model.fields[fieldId]?.type\n values[fieldId] =\n (type === 'text' || type === 'reference') &&\n (typeof value === 'number' || typeof value === 'boolean')\n ? String(value)\n : value\n }\n return values\n}\n\n/**\n * A record write from a form submission or an automation step, coerced and\n * validated against the dataset's model (AGL-2773, option B).\n *\n * These writes used to store `String(value)` for every field and check\n * nothing, so a form could put `abc` in a number column and skip a required\n * one. They now run the same `coerceDocumentValues` + `validateDocument` pair\n * the console and `/v1` do: a value is stored in its field's type, and a\n * record with any refused field is not written at all. The caller decides\n * what a refusal costs — a form keeps the Inbox submission, a run records a\n * failed step.\n *\n * `existing` makes it a merge (update-or-append's update leg): this write's\n * values are laid over the stored ones, and only the fields THIS write\n * supplied are held to the model. A stored row written as text before this\n * change keeps its text values and is not refused for them — the reads have\n * always rendered them, and a merge that touched `email` must not fail over a\n * legacy `price` of `\"12\"` it never sent.\n *\n * Custom field types validate only once registered: call\n * `ensureDeclaredCustomFieldTypes(model)` before this on a server path.\n */\nexport function prepareDatasetRecordWrite(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n options: {\n fieldMap?: Record<string, string> | null\n existing?: Record<string, unknown> | null\n } = {},\n): DatasetRecordWrite {\n const model = effectiveDatasetModel(dataset)\n const picked = pickDatasetRecordInput(dataset, input, options.fieldMap)\n const matched = Object.keys(picked)\n const coerced = coerceDocumentValues(model, asTextInput(model, picked))\n if (!options.existing) {\n return {\n matched,\n values: coerced,\n errors: validateDocument(model, coerced),\n }\n }\n const values = { ...options.existing, ...coerced }\n const errors: Record<string, string> = {}\n for (const [fieldId, error] of Object.entries(\n validateDocument(model, values),\n )) {\n if (fieldId in coerced) errors[fieldId] = error\n }\n return { matched, values, errors }\n}\n\n/**\n * A refused write's errors as one line, in the model's field order:\n * `Stars must be a whole number; Email is required`.\n */\nexport function describeDatasetRecordErrors(\n errors: Record<string, string>,\n): string {\n return Object.values(errors).join('; ')\n}\n\n/**\n * The name a dataset is shown under: `displayName`, which is what every create\n * path writes (AGL-536), then the pre-migration `name`. Blank when the document\n * carries neither.\n */\nexport function datasetDisplayName(\n dataset: { displayName?: unknown; name?: unknown } | null | undefined,\n): string {\n for (const candidate of [dataset?.displayName, dataset?.name]) {\n if (typeof candidate === 'string' && candidate.trim()) {\n return candidate.trim()\n }\n }\n return ''\n}\n\n/** Records sorted by their editor order (then id for stability). */\nexport function sortDatasetRecords<T extends HostDatasetRecord>(\n records: T[],\n): T[] {\n return [...records].sort(\n (a, b) =>\n (a.order ?? Number.MAX_SAFE_INTEGER) -\n (b.order ?? Number.MAX_SAFE_INTEGER) ||\n String(a.$id ?? '').localeCompare(String(b.$id ?? '')),\n )\n}\n"],"names":["coerceDocumentValues","effectiveDatasetModel","validateDocument","DATASET_FIELD_PATTERN","slugifyDatasetFieldId","name","slug","trim","toLowerCase","replace","test","defaultDatasetFieldId","taken","base","used","Set","map","id","candidate","suffix","has","validateDatasetFieldId","trimmed","existing","humanizeDatasetFieldId","words","charAt","toUpperCase","slice","parseDatasetFieldEntries","input","seen","entries","raw","split","add","push","parseDatasetFields","fields","key","sanitizeRecordValues","values","field","value","String","pickDatasetRecordInput","dataset","fieldMap","model","fieldId","order","submittedKey","Object","asTextInput","picked","type","prepareDatasetRecordWrite","options","matched","keys","coerced","errors","error","describeDatasetRecordErrors","join","datasetDisplayName","displayName","sortDatasetRecords","records","sort","a","b","Number","MAX_SAFE_INTEGER","$id","localeCompare"],"mappings":"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"}
|
|
@@ -286,7 +286,7 @@ export declare const PLUGIN_DOCS: {
|
|
|
286
286
|
};
|
|
287
287
|
export type PluginDocsKey = keyof typeof PLUGIN_DOCS;
|
|
288
288
|
export declare const PLUGIN_DOCS_ANCHORS: {
|
|
289
|
-
readonly abuseReports: readonly ["#fraud-and-risk-alerts-by-email", "#where-reports-come-from", "#held-outbound-email", "#what-is-screened", "#tiers", "#deciding-a-held-row", "#names-and-domains", "#stripe-fraud-signals", "#seller-fraud-pattern", "#card-testing-velocity", "#marketplace", "#risk-notices", "#triage-by-severity", "#csam", "#which-lever", "#statuses", "#disclosure", "#dmca", "#counter-notices", "#counter-notice-clock", "#counter-notice-steps", "#repeat-infringers", "#repeat-infringer-threshold", "#known-gaps", "#related"];
|
|
289
|
+
readonly abuseReports: readonly ["#fraud-and-risk-alerts-by-email", "#where-reports-come-from", "#held-outbound-email", "#what-is-screened", "#tiers", "#web-risk", "#deciding-a-held-row", "#security-hold", "#names-and-domains", "#stripe-fraud-signals", "#seller-fraud-pattern", "#card-testing-velocity", "#marketplace", "#risk-notices", "#triage-by-severity", "#csam", "#which-lever", "#statuses", "#disclosure", "#dmca", "#counter-notices", "#counter-notice-clock", "#counter-notice-steps", "#repeat-infringers", "#repeat-infringer-threshold", "#known-gaps", "#related"];
|
|
290
290
|
readonly actionsBuilder: readonly ["#create-an-action", "#recipes", "#describe-it", "#triggers", "#crm-events", "#only-run-when-a-field-matches", "#chain-multiple-conditions-andor", "#steps", "#crm-steps", "#step-conditions", "#sequences", "#run-history", "#what-is-and-isnt-recorded", "#interactions-from-the-besigner", "#when-to-use-which", "#related"];
|
|
291
291
|
readonly aglynAssist: readonly ["#what-it-can-do", "#aglyn-ai", "#answers-for-beginners-and-developers", "#offers-to-open-a-page", "#edits-in-the-besigner", "#where-an-answer-came-from", "#answers-straight-from-the-documentation", "#message-limits", "#feedback", "#privacy"];
|
|
292
292
|
readonly aiAutomations: readonly ["#draft", "#explain", "#why-a-run-failed", "#what-is-sent", "#who-can-use-it", "#related"];
|
|
@@ -297,7 +297,7 @@ export declare const PLUGIN_DOCS_ANCHORS: {
|
|
|
297
297
|
readonly aiSeo: readonly ["#write-a-pages-listing", "#write-a-products-listing", "#fix-what-the-seo-check-finds", "#apply-all-as-drafts", "#structured-data-and-llmstxt", "#related"];
|
|
298
298
|
readonly aiThemes: readonly ["#change-what-you-describe-or-design-a-new-theme", "#match-your-brand", "#review-the-proposal", "#save-it-or-dont", "#who-can-use-it", "#related"];
|
|
299
299
|
readonly assistSignals: readonly ["#the-workflow-this-board-exists-for", "#fleet", "#the-cache-read-rate-and-what-a-bad-number-looks-like", "#where-the-money-goes", "#tokens-by-kind", "#docs-gaps", "#questions-the-docs-could-not-answer", "#what-people-actually-asked", "#what-assist-costs-by-workspace", "#reading-the-sample-honestly", "#related"];
|
|
300
|
-
readonly billing: readonly ["#tiers--entitlements", "#enterprise", "#single-sign-on-and-enforcement", "#usage-meters", "#who-is-generating-what", "#ai-allotments", "#storage-overage", "#if-you-would-rather-uploads-stopped", "#assist-overage", "#stop-ai-assist-at-the-included-band", "#ai-overage-ceiling", "#free-ai-credits", "#ai-credit-alerts", "#usage-budget", "#seats", "#crm-records", "#the-crm-suite", "#one-to-one-email", "#organization-data", "#api-access", "#payments", "#outstanding", "#plan-total", "#billing-email", "#payment-methods", "#billing-address", "#tax-ids", "#sales-tax", "#platform-fees", "#related"];
|
|
300
|
+
readonly billing: readonly ["#tiers--entitlements", "#leaving-notice", "#enterprise", "#single-sign-on-and-enforcement", "#usage-meters", "#who-is-generating-what", "#ai-allotments", "#storage-overage", "#if-you-would-rather-uploads-stopped", "#assist-overage", "#stop-ai-assist-at-the-included-band", "#ai-overage-ceiling", "#free-ai-credits", "#ai-credit-alerts", "#usage-budget", "#seats", "#crm-records", "#the-crm-suite", "#one-to-one-email", "#organization-data", "#api-access", "#payments", "#outstanding", "#plan-total", "#billing-email", "#payment-methods", "#billing-address", "#tax-ids", "#sales-tax", "#platform-fees", "#related"];
|
|
301
301
|
readonly bindings: readonly ["#binding-tokens", "#rename-safe-id-tokens", "#insert-a-variable", "#token-pills", "#in-the-canvas-text-editor", "#site-details", "#typed-variables", "#no-code-functions", "#parameters-a-visitor-can-answer", "#a-calculator-you-lay-out", "#where-used--safety", "#workflows", "#related"];
|
|
302
302
|
readonly bookings: readonly ["#set-up-bookings", "#taking-bookings", "#reminders", "#payments-and-fees", "#service-tax", "#manage", "#booking-from-the-crm", "#canceling-and-refunding", "#related"];
|
|
303
303
|
readonly buildAWorkflow: readonly ["#1-open-the-workflows-page", "#2-choose-a-trigger", "#3-add-steps", "#waiting", "#4-save-and-test", "#duplicate-a-workflow", "#tips", "#related"];
|
|
@@ -305,13 +305,13 @@ export declare const PLUGIN_DOCS_ANCHORS: {
|
|
|
305
305
|
readonly commerce: readonly ["#products-hub", "#inventory", "#reserved-stock", "#stock-movements", "#gift-cards", "#recovery-and-alerts", "#orders", "#orders-screen", "#order-statuses", "#order-money-tiles", "#a-lost-dispute", "#shipping--taxes", "#lodging-tax-on-reservations", "#storefront-sales-tax", "#destination-coverage", "#dropshipping", "#related"];
|
|
306
306
|
readonly commerceEndToEnd: readonly ["#1-connect-payments", "#2-create-products", "#3-design-the-storefront", "#catalog-search-filters-and-sort", "#category-pages", "#the-product-page-template", "#4-what-checkout-does", "#paying-without-leaving-your-site", "#5-run-orders-from-the-console", "#6-subscriptions--the-stripe-portal", "#related"];
|
|
307
307
|
readonly companies: readonly ["#the-companies-list", "#create-a-company", "#a-companys-page", "#contacts-at-a-company", "#linked-on-capture", "#import-from-csv", "#export-csv", "#deleting-a-company", "#who-can-see-a-company", "#files", "#related"];
|
|
308
|
-
readonly consoleTour: readonly ["#the-app-bar", "#in-context-help", "#filter-and-search", "#primary-navigation", "#editing-vs-managing", "#the-sites-list", "#the-status-pill", "#how-the-pill-is-decided", "#your-site-allowance", "#a-sites-dashboard", "#next", "#workspace-settings--notifications", "#the-notifications-feed", "#notification-settings", "#one-kind-at-a-time", "#workspace-and-site-overrides", "#daily-digests", "#alerts-on-this-device"];
|
|
308
|
+
readonly consoleTour: readonly ["#the-app-bar", "#in-context-help", "#filter-and-search", "#primary-navigation", "#editing-vs-managing", "#the-sites-list", "#the-status-pill", "#how-the-pill-is-decided", "#your-site-allowance", "#a-sites-dashboard", "#next", "#workspace-settings--notifications", "#the-notifications-feed", "#notification-levels", "#notification-settings", "#one-kind-at-a-time", "#workspace-and-site-overrides", "#daily-digests", "#alerts-on-this-device"];
|
|
309
309
|
readonly contactActivities: readonly ["#four-kinds-of-history", "#reading-the-timeline", "#campaign-email", "#logging-an-activity", "#meeting-from-a-booking", "#click-to-call", "#sending-an-email", "#delivery-states", "#captured-email", "#where-an-activity-is-visible", "#the-recent-activity-feed", "#related"];
|
|
310
310
|
readonly contactFields: readonly ["#define-a-field", "#fields-per-record", "#where-values-show", "#save-a-form-field", "#lead-source-values", "#over-the-api", "#retire-restore-delete", "#recompute-next-activity", "#related"];
|
|
311
311
|
readonly contactRecord: readonly ["#adding-a-contact-by-hand", "#the-record-page", "#deleting-and-erasing", "#what-each-site-keeps-to-itself", "#when-sites-join-or-leave-a-group", "#merging-two-records", "#likely-duplicates", "#owner", "#last-engaged", "#lifecycle-stages", "#where-the-persons-lead-is", "#finding-a-contact", "#files", "#related"];
|
|
312
312
|
readonly contacts: readonly ["#whats-in-the-crm-area", "#unified-ingestion", "#what-each-plan-includes", "#the-contacts-page", "#import-from-csv", "#segments", "#everywhere-the-crm-shows-up", "#capture-replies", "#at-the-organization-level", "#who-can-open-the-crm", "#one-sender-one-crm", "#related"];
|
|
313
313
|
readonly crmEmailTemplates: readonly ["#templates-and-snippets", "#merge-fields", "#saving", "#managing-templates", "#duplicate-a-template", "#shared-or-personal", "#over-the-rest-api", "#related"];
|
|
314
|
-
readonly crmLeads: readonly ["#what-makes-a-lead", "#what-a-lead-holds", "#adding-a-lead-by-hand", "#the-leads-list", "#filter-the-leads", "#working-a-lead-from-the-row", "#several-leads-at-once", "#export-csv", "#import-from-csv", "#who-owns-a-lead", "#a-leads-page", "#email-state", "#converting-a-lead", "#unqualifying-a-lead", "#erasing-the-person", "#who-can-do-this", "#related"];
|
|
314
|
+
readonly crmLeads: readonly ["#what-makes-a-lead", "#what-a-lead-holds", "#adding-a-lead-by-hand", "#the-leads-list", "#lead-statuses", "#filter-the-leads", "#working-a-lead-from-the-row", "#several-leads-at-once", "#export-csv", "#import-from-csv", "#who-owns-a-lead", "#a-leads-page", "#email-state", "#converting-a-lead", "#unqualifying-a-lead", "#erasing-the-person", "#who-can-do-this", "#related"];
|
|
315
315
|
readonly crmReports: readonly ["#choosing-a-period", "#contacts", "#sources-and-lifecycle", "#conversion-by-source", "#lead-funnel", "#pipeline", "#forecast-by-close-month", "#won-and-lost", "#won-and-lost-by-owner", "#activity-by-teammate", "#tasks", "#exporting-a-table", "#crm-at-a-glance", "#how-the-numbers-are-counted", "#related"];
|
|
316
316
|
readonly crmSettings: readonly ["#companies", "#create-companies-from-work-email-domains", "#default-owner", "#assignment-rules", "#round-robin", "#email-templates", "#email-capture", "#your-sending-addresses", "#recipes", "#related"];
|
|
317
317
|
readonly crmSharing: readonly ["#what-a-shared-record-looks-like", "#share-a-record-by-hand", "#several-records-at-once", "#sharing-rules", "#access-read-only-or-read-and-edit", "#sharing-is-not-consent", "#who-can-do-this", "#related"];
|
|
@@ -288,7 +288,9 @@ export const PLUGIN_DOCS_ANCHORS = {
|
|
|
288
288
|
'#held-outbound-email',
|
|
289
289
|
'#what-is-screened',
|
|
290
290
|
'#tiers',
|
|
291
|
+
'#web-risk',
|
|
291
292
|
'#deciding-a-held-row',
|
|
293
|
+
'#security-hold',
|
|
292
294
|
'#names-and-domains',
|
|
293
295
|
'#stripe-fraud-signals',
|
|
294
296
|
'#seller-fraud-pattern',
|
|
@@ -419,6 +421,7 @@ export const PLUGIN_DOCS_ANCHORS = {
|
|
|
419
421
|
],
|
|
420
422
|
billing: [
|
|
421
423
|
'#tiers--entitlements',
|
|
424
|
+
'#leaving-notice',
|
|
422
425
|
'#enterprise',
|
|
423
426
|
'#single-sign-on-and-enforcement',
|
|
424
427
|
'#usage-meters',
|
|
@@ -554,6 +557,7 @@ export const PLUGIN_DOCS_ANCHORS = {
|
|
|
554
557
|
'#next',
|
|
555
558
|
'#workspace-settings--notifications',
|
|
556
559
|
'#the-notifications-feed',
|
|
560
|
+
'#notification-levels',
|
|
557
561
|
'#notification-settings',
|
|
558
562
|
'#one-kind-at-a-time',
|
|
559
563
|
'#workspace-and-site-overrides',
|
|
@@ -630,6 +634,7 @@ export const PLUGIN_DOCS_ANCHORS = {
|
|
|
630
634
|
'#what-a-lead-holds',
|
|
631
635
|
'#adding-a-lead-by-hand',
|
|
632
636
|
'#the-leads-list',
|
|
637
|
+
'#lead-statuses',
|
|
633
638
|
'#filter-the-leads',
|
|
634
639
|
'#working-a-lead-from-the-row',
|
|
635
640
|
'#several-leads-at-once',
|