@aglyn/aglyn 1.0.0-beta.178 → 1.0.0-beta.179
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 +11 -11
- package/src/lib/app-utils/contacts.d.ts +6 -0
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +103 -1
- package/src/lib/app-utils/crm.js +225 -6
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +1 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +10 -3
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
package/src/lib/app-utils/crm.js
CHANGED
|
@@ -66,7 +66,8 @@ export * from "./crm-kinds.js";
|
|
|
66
66
|
activities: 'crmActivities',
|
|
67
67
|
contactFields: 'contactFields',
|
|
68
68
|
views: 'crmViews',
|
|
69
|
-
emailTemplates: 'crmEmailTemplates'
|
|
69
|
+
emailTemplates: 'crmEmailTemplates',
|
|
70
|
+
/** An org's value sets for its standard picklist fields (AGL-3298). */ picklists: 'crmPicklists'
|
|
70
71
|
};
|
|
71
72
|
/**
|
|
72
73
|
* The three collections the CRM RECORDS band counts (AGL-2611), in the order
|
|
@@ -1497,11 +1498,11 @@ export function isCrmLeadStatus(value) {
|
|
|
1497
1498
|
*
|
|
1498
1499
|
* `sources` is the capture door's record of which SURFACES met the person
|
|
1499
1500
|
* — `signup`, `booking`, `form:{id}`, `import` — and no file may rewrite
|
|
1500
|
-
* it. `leadSource` is what Salesforce calls Lead Source:
|
|
1501
|
-
*
|
|
1502
|
-
* and filtered by, and never derived. Two
|
|
1503
|
-
* questions, and a file that could write
|
|
1504
|
-
* site's own history.
|
|
1501
|
+
* it. `leadSource` is what Salesforce calls Lead Source: one of the org's
|
|
1502
|
+
* own picklist values ("Trade show", "Referral" — see LEAD SOURCE IS A
|
|
1503
|
+
* PICKLIST below), reported on and filtered by, and never derived. Two
|
|
1504
|
+
* fields because they answer two questions, and a file that could write
|
|
1505
|
+
* the first would be rewriting the site's own history.
|
|
1505
1506
|
*=========================================*/ /** The longest text one profile field holds — the contact's own cap. */ export const CRM_LEAD_TEXT_MAX = 120;
|
|
1506
1507
|
/** The most tags one lead keeps — the contact's own cap. */ export const CRM_LEAD_TAGS_MAX = 20;
|
|
1507
1508
|
/** The longest note a lead keeps — the lead page's own cap. */ export const CRM_LEAD_NOTES_MAX = 4000;
|
|
@@ -1582,6 +1583,224 @@ export function isCrmLeadStatus(value) {
|
|
|
1582
1583
|
errors
|
|
1583
1584
|
};
|
|
1584
1585
|
}
|
|
1586
|
+
/*==========================================
|
|
1587
|
+
* LEAD SOURCE IS A PICKLIST (AGL-3298).
|
|
1588
|
+
*
|
|
1589
|
+
* Salesforce's Lead Source is a restricted picklist: an admin keeps the
|
|
1590
|
+
* list of values, a rep picks one, and an import or an API write naming
|
|
1591
|
+
* anything else is refused. Here the list is one org document,
|
|
1592
|
+
* `orgs/{orgId}/crmPicklists/leadSource`, holding the values in order as
|
|
1593
|
+
* `{ id, label, active }` and an optional default for new records.
|
|
1594
|
+
*
|
|
1595
|
+
* ## Records store the LABEL
|
|
1596
|
+
*
|
|
1597
|
+
* A lead carries `leadSource: "Trade show"`, not an id. Every reader that
|
|
1598
|
+
* existed before the picklist — the CSV export, the REST resource, the
|
|
1599
|
+
* record facts, a saved view's filter, a report grouped by the field —
|
|
1600
|
+
* reads the text and keeps working, and a file exported from here
|
|
1601
|
+
* re-imports as is. What an id would have bought is a free rename; here a
|
|
1602
|
+
* rename (and a delete) REPLACES the old label on every record in the same
|
|
1603
|
+
* operation, the way Salesforce's own Replace does, so a report grouped
|
|
1604
|
+
* by the field follows the rename rather than splitting in two. The value's
|
|
1605
|
+
* `id` is what the list itself is keyed by — the default, a reorder, the
|
|
1606
|
+
* manage page's rows — and it never changes.
|
|
1607
|
+
*
|
|
1608
|
+
* ## Inactive, not gone
|
|
1609
|
+
*
|
|
1610
|
+
* A deactivated value leaves every picker but stays on the records that
|
|
1611
|
+
* hold it, shown with an "(inactive)" hint, and a write that keeps a
|
|
1612
|
+
* record's current value is never refused for it. A value is removed only
|
|
1613
|
+
* by Delete, which names the value its records move to.
|
|
1614
|
+
*
|
|
1615
|
+
* ## An org that never edited the list
|
|
1616
|
+
*
|
|
1617
|
+
* Has no document, and reads {@link CRM_LEAD_SOURCE_STARTER_LABELS} — a
|
|
1618
|
+
* small Salesforce-like set. The first edit on the manage page writes that
|
|
1619
|
+
* set down as the org's own, so a new org starts with a usable list and no
|
|
1620
|
+
* org-creation step has to seed it.
|
|
1621
|
+
*=========================================*/ /** The picklists an org keeps, by document id — one per standard field. */ export const CRM_PICKLIST_IDS = [
|
|
1622
|
+
'leadSource'
|
|
1623
|
+
];
|
|
1624
|
+
/** The lead source value set's document id. */ export const CRM_LEAD_SOURCE_PICKLIST = 'leadSource';
|
|
1625
|
+
/** The most values one picklist holds — a menu, not a table. */ export const CRM_PICKLIST_VALUES_MAX = 200;
|
|
1626
|
+
/**
|
|
1627
|
+
* The values an org that has never edited its list reads — see the block
|
|
1628
|
+
* header. Salesforce's own standard set, trimmed to what a small team
|
|
1629
|
+
* recognizes.
|
|
1630
|
+
*/ export const CRM_LEAD_SOURCE_STARTER_LABELS = [
|
|
1631
|
+
'Web',
|
|
1632
|
+
'Phone inquiry',
|
|
1633
|
+
'Referral',
|
|
1634
|
+
'Partner',
|
|
1635
|
+
'Purchased list',
|
|
1636
|
+
'Trade show',
|
|
1637
|
+
'Other'
|
|
1638
|
+
];
|
|
1639
|
+
/** A label as a picklist stores it: trimmed, single-spaced, capped. */ export function normalizeCrmPicklistLabel(value) {
|
|
1640
|
+
return String(value != null ? value : '').trim().replace(/\s+/g, ' ').slice(0, CRM_LEAD_TEXT_MAX);
|
|
1641
|
+
}
|
|
1642
|
+
/** The key two labels are compared by: case and spacing do not make a new value. */ function picklistKey(label) {
|
|
1643
|
+
return normalizeCrmPicklistLabel(label).toLowerCase();
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* A new value's id: the label as a slug, suffixed until it is not one of
|
|
1647
|
+
* `taken`. Derived rather than random so a seeded list reads the same in
|
|
1648
|
+
* every org, and never reused, because a default names a value by it.
|
|
1649
|
+
*/ export function crmPicklistValueId(label, taken) {
|
|
1650
|
+
const used = new Set(taken);
|
|
1651
|
+
const base = normalizeCrmPicklistLabel(label).toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'value';
|
|
1652
|
+
let id = base;
|
|
1653
|
+
for(let n = 2; used.has(id); n += 1)id = `${base}-${n}`;
|
|
1654
|
+
return id;
|
|
1655
|
+
}
|
|
1656
|
+
/** A picklist of active values from labels, in order, with no default. */ export function crmPicklistFromLabels(labels) {
|
|
1657
|
+
const values = [];
|
|
1658
|
+
const seen = new Set();
|
|
1659
|
+
for (const raw of labels){
|
|
1660
|
+
const label = normalizeCrmPicklistLabel(raw);
|
|
1661
|
+
if (!label || seen.has(picklistKey(label))) continue;
|
|
1662
|
+
seen.add(picklistKey(label));
|
|
1663
|
+
values.push({
|
|
1664
|
+
id: crmPicklistValueId(label, values.map((value)=>value.id)),
|
|
1665
|
+
label,
|
|
1666
|
+
active: true
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
return {
|
|
1670
|
+
values: values.slice(0, CRM_PICKLIST_VALUES_MAX),
|
|
1671
|
+
defaultValueId: null
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* A stored document as a picklist, or `null` for one that holds none.
|
|
1676
|
+
*
|
|
1677
|
+
* Tolerant, because the document is client-written: a value with no label
|
|
1678
|
+
* is dropped, a second value with the same label (in any case) or the same
|
|
1679
|
+
* id is dropped, `active` is true unless it is `false`, and a default that
|
|
1680
|
+
* names no value is no default.
|
|
1681
|
+
*/ export function normalizeCrmPicklist(raw) {
|
|
1682
|
+
var _stored_defaultValueId;
|
|
1683
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
1684
|
+
const stored = raw;
|
|
1685
|
+
if (!Array.isArray(stored['values'])) return null;
|
|
1686
|
+
const values = [];
|
|
1687
|
+
const labels = new Set();
|
|
1688
|
+
const ids = new Set();
|
|
1689
|
+
for (const entry of stored['values']){
|
|
1690
|
+
var _value_id;
|
|
1691
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
1692
|
+
const value = entry;
|
|
1693
|
+
const label = normalizeCrmPicklistLabel(value['label']);
|
|
1694
|
+
if (!label || labels.has(picklistKey(label))) continue;
|
|
1695
|
+
let id = String((_value_id = value['id']) != null ? _value_id : '').trim().slice(0, 64);
|
|
1696
|
+
if (!id || id.includes('/') || ids.has(id)) id = crmPicklistValueId(label, ids);
|
|
1697
|
+
labels.add(picklistKey(label));
|
|
1698
|
+
ids.add(id);
|
|
1699
|
+
values.push({
|
|
1700
|
+
id,
|
|
1701
|
+
label,
|
|
1702
|
+
active: value['active'] !== false
|
|
1703
|
+
});
|
|
1704
|
+
if (values.length >= CRM_PICKLIST_VALUES_MAX) break;
|
|
1705
|
+
}
|
|
1706
|
+
const defaultId = String((_stored_defaultValueId = stored['defaultValueId']) != null ? _stored_defaultValueId : '');
|
|
1707
|
+
return {
|
|
1708
|
+
values,
|
|
1709
|
+
defaultValueId: ids.has(defaultId) ? defaultId : null
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
/** The org's lead source list as every reader should take it — see the block header. */ export function effectiveCrmLeadSourcePicklist(raw) {
|
|
1713
|
+
var _normalizeCrmPicklist;
|
|
1714
|
+
return (_normalizeCrmPicklist = normalizeCrmPicklist(raw)) != null ? _normalizeCrmPicklist : crmPicklistFromLabels(CRM_LEAD_SOURCE_STARTER_LABELS);
|
|
1715
|
+
}
|
|
1716
|
+
/** The value a label names, in any case or spacing — `null` for none. */ export function crmPicklistValueByLabel(picklist, label) {
|
|
1717
|
+
var _picklist_values_find;
|
|
1718
|
+
const key = picklistKey(String(label != null ? label : ''));
|
|
1719
|
+
if (!key) return null;
|
|
1720
|
+
return (_picklist_values_find = picklist.values.find((value)=>picklistKey(value.label) === key)) != null ? _picklist_values_find : null;
|
|
1721
|
+
}
|
|
1722
|
+
/** The values a picker offers. */ export function crmPicklistActiveValues(picklist) {
|
|
1723
|
+
return picklist.values.filter((value)=>value.active);
|
|
1724
|
+
}
|
|
1725
|
+
/** The label a new record starts with — only while the default value is active. */ export function crmPicklistDefaultLabel(picklist) {
|
|
1726
|
+
const value = picklist.values.find((entry)=>entry.id === picklist.defaultValueId);
|
|
1727
|
+
return (value == null ? void 0 : value.active) ? value.label : null;
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* The sentence a lead source outside the list is refused with, naming what
|
|
1731
|
+
* the list allows. A long list is cut at twenty names so the sentence stays
|
|
1732
|
+
* one a person can read.
|
|
1733
|
+
*/ export function crmLeadSourceRefusal(picklist) {
|
|
1734
|
+
const active = crmPicklistActiveValues(picklist).map((value)=>value.label);
|
|
1735
|
+
if (!active.length) {
|
|
1736
|
+
return 'This organization has no active lead sources. Add one under CRM › Fields › Leads.';
|
|
1737
|
+
}
|
|
1738
|
+
const shown = active.slice(0, 20).join(', ');
|
|
1739
|
+
const more = active.length > 20 ? `, and ${active.length - 20} more` : '';
|
|
1740
|
+
return `Lead source must be one of: ${shown}${more}.`;
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Whether a write may store `value` as a record's lead source, and the
|
|
1744
|
+
* label it stores.
|
|
1745
|
+
*
|
|
1746
|
+
* - Blank or `null` clears the field — lead source is never required.
|
|
1747
|
+
* - An ACTIVE value, in any case or spacing, stores that value's label.
|
|
1748
|
+
* - The record's `current` value is kept even when it is inactive or no
|
|
1749
|
+
* longer listed: re-saving a record must never be refused for a value
|
|
1750
|
+
* somebody else deactivated.
|
|
1751
|
+
* - Anything else is refused with {@link crmLeadSourceRefusal}.
|
|
1752
|
+
*/ export function judgeCrmLeadSource(picklist, value, current) {
|
|
1753
|
+
const label = normalizeCrmPicklistLabel(value);
|
|
1754
|
+
if (!label) return {
|
|
1755
|
+
ok: true,
|
|
1756
|
+
value: null
|
|
1757
|
+
};
|
|
1758
|
+
const held = normalizeCrmPicklistLabel(current);
|
|
1759
|
+
if (held && picklistKey(held) === picklistKey(label)) return {
|
|
1760
|
+
ok: true,
|
|
1761
|
+
value: held
|
|
1762
|
+
};
|
|
1763
|
+
const match = crmPicklistValueByLabel(picklist, label);
|
|
1764
|
+
if (match == null ? void 0 : match.active) return {
|
|
1765
|
+
ok: true,
|
|
1766
|
+
value: match.label
|
|
1767
|
+
};
|
|
1768
|
+
return {
|
|
1769
|
+
ok: false,
|
|
1770
|
+
error: crmLeadSourceRefusal(picklist)
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* What a select offers for a record holding `current`: every active value
|
|
1775
|
+
* in order, then the record's own value when the list would not otherwise
|
|
1776
|
+
* show it, so the select keeps what the record holds until it is changed.
|
|
1777
|
+
*/ export function crmPicklistOptions(picklist, current) {
|
|
1778
|
+
const options = crmPicklistActiveValues(picklist).map((value)=>({
|
|
1779
|
+
label: value.label,
|
|
1780
|
+
inactive: false,
|
|
1781
|
+
unlisted: false
|
|
1782
|
+
}));
|
|
1783
|
+
const held = normalizeCrmPicklistLabel(current);
|
|
1784
|
+
if (held && !options.some((option)=>picklistKey(option.label) === picklistKey(held))) {
|
|
1785
|
+
const listed = crmPicklistValueByLabel(picklist, held);
|
|
1786
|
+
options.push({
|
|
1787
|
+
label: held,
|
|
1788
|
+
inactive: Boolean(listed),
|
|
1789
|
+
unlisted: !listed
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
return options;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Where a label sorts: its value's position in the list, unlisted values
|
|
1796
|
+
* after every listed one, and a record with none last. The order the admin
|
|
1797
|
+
* chose is the order a sort by the field reads in, as Salesforce's does.
|
|
1798
|
+
*/ export function crmPicklistRank(picklist, label) {
|
|
1799
|
+
const text = normalizeCrmPicklistLabel(label);
|
|
1800
|
+
if (!text) return Number.MAX_SAFE_INTEGER;
|
|
1801
|
+
const at = picklist.values.findIndex((value)=>picklistKey(value.label) === picklistKey(text));
|
|
1802
|
+
return at < 0 ? CRM_PICKLIST_VALUES_MAX : at;
|
|
1803
|
+
}
|
|
1585
1804
|
/** The name a lead is listed under: the name it carries, else its address. */ export function crmLeadDisplayName(lead) {
|
|
1586
1805
|
var _ref, _ref1;
|
|
1587
1806
|
const name = String((_ref = lead == null ? void 0 : lead['name']) != null ? _ref : '').trim();
|