@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.201
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 +13 -13
- package/src/lib/components/activity-list.d.ts +36 -5
- package/src/lib/components/activity-list.js +159 -137
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/activity-timeline.d.ts +84 -0
- package/src/lib/components/activity-timeline.js +224 -0
- package/src/lib/components/activity-timeline.js.map +1 -0
- package/src/lib/components/companies-section.d.ts +6 -6
- package/src/lib/components/companies-section.js +54 -84
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +17 -4
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-picker.js +6 -2
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/company-properties-card.js +53 -36
- package/src/lib/components/company-properties-card.js.map +1 -1
- package/src/lib/components/contact-associations-card.d.ts +3 -2
- package/src/lib/components/contact-associations-card.js +12 -12
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -3
- package/src/lib/components/contact-detail-page.js +18 -12
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -5
- package/src/lib/components/contact-list-columns.js +6 -8
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +21 -4
- package/src/lib/components/contact-properties-card.js +129 -29
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.d.ts +12 -6
- package/src/lib/components/contact-timeline-card.js +132 -174
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.js +10 -1
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +282 -331
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-email-state-chip.js +4 -1
- package/src/lib/components/crm-email-state-chip.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +4 -2
- package/src/lib/components/crm-next-activity-column.js +6 -3
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +8 -4
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deal-products-card.js +15 -13
- package/src/lib/components/deal-products-card.js.map +1 -1
- package/src/lib/components/deal-properties-card.js +7 -0
- package/src/lib/components/deal-properties-card.js.map +1 -1
- package/src/lib/components/deal-stage-card.js +31 -29
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +2 -2
- package/src/lib/components/deals-section.js +67 -97
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.d.ts +4 -0
- package/src/lib/components/fields-section.js +368 -154
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/form-contact-fields-card.js +24 -20
- package/src/lib/components/form-contact-fields-card.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +1 -1
- package/src/lib/components/lead-campaigns-card.js +43 -38
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +14 -6
- package/src/lib/components/lead-properties-card.js +323 -311
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +13 -2
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +16 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.d.ts +2 -1
- package/src/lib/components/leads-section.js +110 -148
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/linked-deals-card.js +12 -10
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/recent-activity-feed.d.ts +12 -8
- package/src/lib/components/recent-activity-feed.js +54 -29
- package/src/lib/components/recent-activity-feed.js.map +1 -1
- package/src/lib/components/record-activity-card.d.ts +4 -2
- package/src/lib/components/record-activity-card.js +29 -11
- package/src/lib/components/record-activity-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +18 -12
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +47 -37
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +22 -16
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-export.d.ts +4 -11
- package/src/lib/components/reports/report-export.js +16 -33
- package/src/lib/components/reports/report-export.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +18 -12
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +35 -25
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/tasks-section.js +151 -144
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +13 -0
- package/src/lib/constants/company-filters.js +47 -17
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +61 -10
- package/src/lib/constants/contact-filters.js +373 -141
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +44 -0
- package/src/lib/constants/deal-filters.js +78 -0
- package/src/lib/constants/deal-filters.js.map +1 -0
- package/src/lib/constants/field-list-query.d.ts +23 -0
- package/src/lib/constants/field-list-query.js +80 -0
- package/src/lib/constants/field-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
- package/src/lib/hooks/use-crm-list-query.js +182 -0
- package/src/lib/hooks/use-crm-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
- package/src/lib/hooks/use-crm-tasks.js +44 -42
- package/src/lib/hooks/use-crm-tasks.js.map +1 -1
- package/src/lib/hooks/use-deals.d.ts +2 -34
- package/src/lib/hooks/use-deals.js +3 -62
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +91 -0
- package/src/lib/model/crm-list-query.js +182 -0
- package/src/lib/model/crm-list-query.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +50 -44
- package/src/lib/model/lead-filters.js +194 -88
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/list-fields-api.d.ts +26 -0
- package/src/lib/model/list-fields-api.js +47 -0
- package/src/lib/model/list-fields-api.js.map +1 -0
- package/src/lib/model/list-fields.d.ts +47 -0
- package/src/lib/model/list-fields.js +39 -0
- package/src/lib/model/list-fields.js.map +1 -0
- package/src/lib/model/task-views.d.ts +15 -10
- package/src/lib/model/task-views.js +68 -9
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/capture-contact.js +7 -5
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/companies-import.js +3 -3
- package/src/lib/server/companies-import.js.map +1 -1
- package/src/lib/server/company-delete.js +3 -1
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/consent-group-participant.js +4 -0
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-update.js +14 -8
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.js +4 -2
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/deals-import.js +4 -2
- package/src/lib/server/deals-import.js.map +1 -1
- package/src/lib/server/erase-person.js +2 -1
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/lead-create.js +4 -1
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-values.js +9 -5
- package/src/lib/server/lead-source-values.js.map +1 -1
- package/src/lib/server/leads-import.js +4 -1
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/list-fields-routes.d.ts +29 -0
- package/src/lib/server/list-fields-routes.js +85 -0
- package/src/lib/server/list-fields-routes.js.map +1 -0
- package/src/lib/server/record-email-state.js +5 -2
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-timeline.js +3 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-routes.js +6 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server/tasks-import.js +5 -2
- package/src/lib/server/tasks-import.js.map +1 -1
- package/src/lib/server.js +10 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
- package/src/lib/components/contact-custom-fields-card.js +0 -204
- package/src/lib/components/contact-custom-fields-card.js.map +0 -1
- package/src/lib/hooks/use-org-leads.d.ts +0 -50
- package/src/lib/hooks/use-org-leads.js +0 -104
- package/src/lib/hooks/use-org-leads.js.map +0 -1
|
@@ -14,8 +14,10 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS,
|
|
18
|
-
import {
|
|
17
|
+
*/ import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS, CRM_LEAD_SCOPED_CAMPAIGNS_FIELD, CRM_LEAD_STATUSES, crmLeadSourceKey } from "@aglyn/aglyn";
|
|
18
|
+
import { SCOPED_SEARCH_JOIN } from "@aglyn/aglyn/app-utils/name-search";
|
|
19
|
+
import { listSelectCodec } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
20
|
+
import { CRM_LIST_SEARCH, crmAnyOf, crmClauseValues, crmSelectField } from "./crm-list-query.js";
|
|
19
21
|
export const LEAD_FILTERS = [
|
|
20
22
|
'open',
|
|
21
23
|
'new',
|
|
@@ -32,11 +34,6 @@ export const LEAD_FILTER_LABELS = {
|
|
|
32
34
|
qualified: CRM_LEAD_STATUS_LABELS.qualified,
|
|
33
35
|
unqualified: CRM_LEAD_STATUS_LABELS.unqualified
|
|
34
36
|
};
|
|
35
|
-
/** Whether a lead belongs in a filter's view. An absent status is `new`. */ export function leadMatchesFilter(lead, filter) {
|
|
36
|
-
if (filter === 'all') return true;
|
|
37
|
-
if (filter === 'open') return isCrmLeadOpen(lead);
|
|
38
|
-
return crmLeadStatus(lead) === filter;
|
|
39
|
-
}
|
|
40
37
|
export const LEAD_EMAIL_FILTERS = [
|
|
41
38
|
'any',
|
|
42
39
|
'problem',
|
|
@@ -48,66 +45,11 @@ export const LEAD_EMAIL_FILTER_LABELS = _extends({
|
|
|
48
45
|
problem: 'Cannot be emailed',
|
|
49
46
|
none: 'Nothing known'
|
|
50
47
|
}, EMAIL_STATE_LABELS);
|
|
51
|
-
/** Whether a lead belongs in an email filter's view. */ export function leadMatchesEmailFilter(lead, filter) {
|
|
52
|
-
if (filter === 'any') return true;
|
|
53
|
-
const state = readEmailState(lead);
|
|
54
|
-
if (filter === 'none') return state === null;
|
|
55
|
-
if (filter === 'problem') return emailStateForbidsEmail(state);
|
|
56
|
-
return (state == null ? void 0 : state.status) === filter;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* The Leads section's `Campaign` control (AGL-3254): every lead, or the
|
|
60
|
-
* ones filed under one campaign — read off the lead's own `campaignIds`,
|
|
61
|
-
* the field every campaign member carries. `''` is every lead, which is
|
|
62
|
-
* what the control opens on; the ids are the site's containers', and the
|
|
63
|
-
* section resolves their names for the menu.
|
|
64
|
-
*/ export function leadMatchesCampaignFilter(lead, campaignId) {
|
|
65
|
-
if (!campaignId) return true;
|
|
66
|
-
return readCampaignIds(lead).includes(campaignId);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* The fields the Leads section's search box reads (AGL-3246): who the lead
|
|
70
|
-
* is and where they work — the words a person types to find one — plus the
|
|
71
|
-
* tags, which are how an import names its batch (`sal-15`). `name` and
|
|
72
|
-
* `email` are the capture door's fields rather than the CRM's, so the row is
|
|
73
|
-
* read as a record rather than through `CrmLeadFields`.
|
|
74
|
-
*/ export const LEAD_SEARCH_FIELDS = [
|
|
75
|
-
'name',
|
|
76
|
-
'email',
|
|
77
|
-
'company',
|
|
78
|
-
'jobTitle',
|
|
79
|
-
'tags'
|
|
80
|
-
];
|
|
81
|
-
/**
|
|
82
|
-
* Whether a lead answers a search term.
|
|
83
|
-
*
|
|
84
|
-
* Case-insensitive, and every whitespace-separated word of the term must
|
|
85
|
-
* appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`
|
|
86
|
-
* both find Morgan Lamphere, `morgan smith` does not. A blank term matches
|
|
87
|
-
* every lead, so the box emptied is the list unfiltered.
|
|
88
|
-
*/ export function leadMatchesSearch(lead, term) {
|
|
89
|
-
// Tags are a list; a stray string there is no tag of the lead's.
|
|
90
|
-
const tags = Array.isArray(lead['tags']) ? lead['tags'] : undefined;
|
|
91
|
-
return listRowMatchesSearch(_extends({}, lead, {
|
|
92
|
-
tags
|
|
93
|
-
}), LEAD_SEARCH_FIELDS, term.trim().split(/\s+/));
|
|
94
|
-
}
|
|
95
48
|
/**
|
|
96
49
|
* The `Lead source` control's "no lead source" choice (AGL-3298). Held in
|
|
97
50
|
* the saved view as an `isEmpty` clause, never as this string; it is only
|
|
98
51
|
* the select's own value for the choice.
|
|
99
52
|
*/ export const LEAD_SOURCE_FILTER_NONE = '\u0000none';
|
|
100
|
-
/**
|
|
101
|
-
* The Leads section's `Lead source` control (AGL-3298): every lead (`''`),
|
|
102
|
-
* the ones holding one value — compared the way the picklist compares
|
|
103
|
-
* labels, so a value typed in another case before the list existed is
|
|
104
|
-
* still found — or the ones holding none.
|
|
105
|
-
*/ export function leadMatchesLeadSourceFilter(lead, filter) {
|
|
106
|
-
if (!filter) return true;
|
|
107
|
-
const held = normalizeCrmPicklistLabel(lead['leadSource']).toLowerCase();
|
|
108
|
-
if (filter === LEAD_SOURCE_FILTER_NONE) return held === '';
|
|
109
|
-
return held === normalizeCrmPicklistLabel(filter).toLowerCase();
|
|
110
|
-
}
|
|
111
53
|
/*==========================================
|
|
112
54
|
* THE LEADS GRID'S FILTERS (AGL-3313).
|
|
113
55
|
*
|
|
@@ -120,8 +62,8 @@ export const LEAD_EMAIL_FILTER_LABELS = _extends({
|
|
|
120
62
|
* campaignIds `contains <campaign id>`
|
|
121
63
|
* leadSource `equals <label>`, or `isEmpty` for the leads holding none
|
|
122
64
|
*
|
|
123
|
-
* Every one
|
|
124
|
-
*
|
|
65
|
+
* Every one is asked of the Leads query (AGL-3321) through the field a
|
|
66
|
+
* writer stores for it — see {@link leadQueryClause}.
|
|
125
67
|
*=========================================*/ /** The fields the grid's panel offers on Leads. */ export const LEAD_LIST_FILTER_FIELDS = [
|
|
126
68
|
{
|
|
127
69
|
column: 'status',
|
|
@@ -224,30 +166,6 @@ const STATUS_ALL = {
|
|
|
224
166
|
...shown
|
|
225
167
|
];
|
|
226
168
|
}
|
|
227
|
-
const listed = (clause)=>clause.op === 'isAnyOf' ? clause.value.split(',').map((entry)=>entry.trim()).filter(Boolean) : [
|
|
228
|
-
clause.value
|
|
229
|
-
];
|
|
230
|
-
/** Whether a lead answers every clause the grid shows. */ export function leadMatchesClauses(lead, clauses) {
|
|
231
|
-
return clauses.every((clause)=>{
|
|
232
|
-
switch(clause.field){
|
|
233
|
-
case 'status':
|
|
234
|
-
return listed(clause).some((value)=>LEAD_FILTERS.includes(value) ? leadMatchesFilter(lead, value) : true);
|
|
235
|
-
case 'emailState':
|
|
236
|
-
return listed(clause).some((value)=>LEAD_EMAIL_FILTERS.includes(value) ? leadMatchesEmailFilter(lead, value) : true);
|
|
237
|
-
case 'campaignIds':
|
|
238
|
-
return listed(clause).some((value)=>leadMatchesCampaignFilter(lead, value));
|
|
239
|
-
case 'leadSource':
|
|
240
|
-
return clause.op === 'isEmpty' ? leadMatchesLeadSourceFilter(lead, LEAD_SOURCE_FILTER_NONE) : listed(clause).some((value)=>leadMatchesLeadSourceFilter(lead, value));
|
|
241
|
-
case 'ownerUid':
|
|
242
|
-
var _lead_ownerUid;
|
|
243
|
-
return listed(clause).includes(String((_lead_ownerUid = lead['ownerUid']) != null ? _lead_ownerUid : ''));
|
|
244
|
-
default:
|
|
245
|
-
// A clause on a field this list does not answer narrows nothing,
|
|
246
|
-
// rather than emptying the list.
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
169
|
/** Campaign clauses were stored as `contains`; the panel's select says `is`. */ export const LEAD_CAMPAIGN_CODEC = {
|
|
252
170
|
toItem: (clause)=>clause.op === 'contains' || clause.op === 'equals' ? {
|
|
253
171
|
operator: 'is',
|
|
@@ -281,5 +199,193 @@ const listed = (clause)=>clause.op === 'isAnyOf' ? clause.value.split(',').map((
|
|
|
281
199
|
campaignIds: LEAD_CAMPAIGN_CODEC,
|
|
282
200
|
leadSource: LEAD_SOURCE_CODEC
|
|
283
201
|
};
|
|
202
|
+
/*==========================================
|
|
203
|
+
* THE LEADS QUERY (AGL-3321).
|
|
204
|
+
*
|
|
205
|
+
* `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the
|
|
206
|
+
* `visibleTo` scope clause. Each grid clause is asked through a field every
|
|
207
|
+
* lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):
|
|
208
|
+
*
|
|
209
|
+
* status `status`, stored `new` on a lead nobody has touched, so
|
|
210
|
+
* Open is `status in [new, working]`
|
|
211
|
+
* emailState `emailStatus`, the verdict's status or `none`; "Cannot be
|
|
212
|
+
* emailed" is every verdict but `ok`
|
|
213
|
+
* leadSource `leadSourceKey`, the label as the picklist compares it, or
|
|
214
|
+
* `null` for none
|
|
215
|
+
* ownerUid as stored
|
|
216
|
+
* campaignIds `array-contains` on the lead's own campaigns; under a site,
|
|
217
|
+
* `scopedCampaignIds` — the campaign behind each of the
|
|
218
|
+
* site's scope tokens — which stands in the scope clause's
|
|
219
|
+
* place, so the one array clause answers both
|
|
220
|
+
*
|
|
221
|
+
* and the search box reads `searchTokens` — word prefixes of the name,
|
|
222
|
+
* address, company, title and tags. Every equality rides one
|
|
223
|
+
* `(field, lastSeenAtMs DESC)` composite; the campaign and the search are
|
|
224
|
+
* array clauses, so the two do not stand together (nor under a site, where
|
|
225
|
+
* the scope clause is the query's one array clause, save the search, which
|
|
226
|
+
* folds into it for a reader who may drop it).
|
|
227
|
+
*=========================================*/ /** The one order the Leads list takes: newest seen first. */ export const LEAD_LIST_SORTS = [
|
|
228
|
+
{
|
|
229
|
+
path: 'lastSeenAtMs',
|
|
230
|
+
direction: 'desc'
|
|
231
|
+
}
|
|
232
|
+
];
|
|
233
|
+
/** The fields the Leads query asks — each a stored field, as named. */ export const LEAD_QUERY_FIELDS = [
|
|
234
|
+
crmSelectField('status'),
|
|
235
|
+
crmSelectField('emailStatus'),
|
|
236
|
+
crmSelectField('ownerUid'),
|
|
237
|
+
{
|
|
238
|
+
column: 'leadSourceKey',
|
|
239
|
+
kind: 'exact',
|
|
240
|
+
path: 'leadSourceKey',
|
|
241
|
+
presence: 'nullable',
|
|
242
|
+
operators: [
|
|
243
|
+
'equals',
|
|
244
|
+
'isAnyOf',
|
|
245
|
+
'isEmpty'
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
column: 'campaignIds',
|
|
250
|
+
kind: 'exact',
|
|
251
|
+
path: 'campaignIds',
|
|
252
|
+
tokensPath: 'campaignIds',
|
|
253
|
+
operators: [
|
|
254
|
+
'contains'
|
|
255
|
+
]
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
// A campaign under a site: the lead's campaigns behind its scope.
|
|
259
|
+
column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
|
|
260
|
+
kind: 'exact',
|
|
261
|
+
path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
|
|
262
|
+
tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
|
|
263
|
+
operators: [
|
|
264
|
+
'isAnyOf'
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
];
|
|
268
|
+
export const LEAD_LIST_DECLARATION = {
|
|
269
|
+
fields: LEAD_QUERY_FIELDS,
|
|
270
|
+
sorts: LEAD_LIST_SORTS,
|
|
271
|
+
search: CRM_LIST_SEARCH
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the
|
|
275
|
+
* start of the address, beside the scope clause — the `(visibleTo, email)`
|
|
276
|
+
* composite the Inbox's leads list reads too.
|
|
277
|
+
*/ export const LEAD_PREFIX_SEARCH = {
|
|
278
|
+
field: {
|
|
279
|
+
column: 'email',
|
|
280
|
+
kind: 'text',
|
|
281
|
+
path: 'email',
|
|
282
|
+
lowerPath: 'email',
|
|
283
|
+
presence: 'always',
|
|
284
|
+
operators: [
|
|
285
|
+
'startsWith'
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
notice: 'Search matches the start of a lead’s address for access limited to specific sites.'
|
|
289
|
+
};
|
|
290
|
+
/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */ export const leadClauseImpliesScope = (clause)=>clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD;
|
|
291
|
+
/** The verdicts a member must not email to: every one but `ok`. */ const EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status)=>status !== 'ok');
|
|
292
|
+
const ORG_READER = {
|
|
293
|
+
scopeTokens: null,
|
|
294
|
+
foldsScope: true
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* One grid clause as the clause the Leads query asks — see the block above.
|
|
298
|
+
* A value the list does not know is refused by name rather than dropped, so
|
|
299
|
+
* a stored view that names a retired choice says so.
|
|
300
|
+
*/ export function leadQueryClause(clause, reader = ORG_READER) {
|
|
301
|
+
const values = crmClauseValues(clause);
|
|
302
|
+
switch(clause.field){
|
|
303
|
+
case 'status':
|
|
304
|
+
{
|
|
305
|
+
const statuses = [];
|
|
306
|
+
for (const value of values){
|
|
307
|
+
if (value === 'open') statuses.push('new', 'working');
|
|
308
|
+
else if (CRM_LEAD_STATUSES.includes(value)) statuses.push(value);
|
|
309
|
+
else return {
|
|
310
|
+
refused: `${value} is not a lead status`
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return crmAnyOf('status', statuses);
|
|
314
|
+
}
|
|
315
|
+
case 'emailState':
|
|
316
|
+
{
|
|
317
|
+
const keys = [];
|
|
318
|
+
for (const value of values){
|
|
319
|
+
if (value === 'problem') keys.push(...EMAIL_PROBLEMS);
|
|
320
|
+
else if (value === 'none') keys.push('none');
|
|
321
|
+
else if (EMAIL_STATE_STATUSES.includes(value)) keys.push(value);
|
|
322
|
+
else return {
|
|
323
|
+
refused: `${value} is not an email verdict`
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return crmAnyOf('emailStatus', keys);
|
|
327
|
+
}
|
|
328
|
+
case 'leadSource':
|
|
329
|
+
{
|
|
330
|
+
if (clause.op === 'isEmpty' || values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE) {
|
|
331
|
+
return {
|
|
332
|
+
field: 'leadSourceKey',
|
|
333
|
+
op: 'isEmpty',
|
|
334
|
+
value: ''
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (values.includes(LEAD_SOURCE_FILTER_NONE)) {
|
|
338
|
+
return {
|
|
339
|
+
refused: '"No lead source" cannot be picked beside a value'
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const keys = values.map((value)=>crmLeadSourceKey(value)).filter((key)=>Boolean(key));
|
|
343
|
+
return crmAnyOf('leadSourceKey', keys);
|
|
344
|
+
}
|
|
345
|
+
case 'ownerUid':
|
|
346
|
+
return crmAnyOf('ownerUid', values);
|
|
347
|
+
case 'campaignIds':
|
|
348
|
+
{
|
|
349
|
+
if (values.length !== 1) return {
|
|
350
|
+
refused: 'pick one campaign'
|
|
351
|
+
};
|
|
352
|
+
if (!reader.scopeTokens) return {
|
|
353
|
+
field: 'campaignIds',
|
|
354
|
+
op: 'contains',
|
|
355
|
+
value: values[0]
|
|
356
|
+
};
|
|
357
|
+
if (!reader.foldsScope) {
|
|
358
|
+
return {
|
|
359
|
+
refused: 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it'
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
|
|
364
|
+
op: 'isAnyOf',
|
|
365
|
+
value: reader.scopeTokens.map((token)=>`${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(',')
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
default:
|
|
369
|
+
return {
|
|
370
|
+
refused: 'this list does not filter by that'
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The query shape the Leads list can send beyond its own declaration, for
|
|
376
|
+
* the index pin (AGL-3321): a collaborator's address prefix beside the
|
|
377
|
+
* equalities they may set.
|
|
378
|
+
*/ export function leadIndexShapes() {
|
|
379
|
+
const equalities = LEAD_QUERY_FIELDS.filter((field)=>!field.tokensPath);
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
fields: [
|
|
383
|
+
LEAD_PREFIX_SEARCH.field,
|
|
384
|
+
...equalities
|
|
385
|
+
],
|
|
386
|
+
sorts: []
|
|
387
|
+
}
|
|
388
|
+
];
|
|
389
|
+
}
|
|
284
390
|
|
|
285
391
|
//# sourceMappingURL=lead-filters.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.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\nimport {\n EMAIL_STATE_LABELS,\n EMAIL_STATE_STATUSES,\n CRM_LEAD_STATUS_LABELS,\n type EmailStateStatus,\n type CrmLeadFields,\n type CrmLeadStatus,\n emailStateForbidsEmail,\n crmLeadStatus,\n isCrmLeadOpen,\n normalizeCrmPicklistLabel,\n readCampaignIds,\n readEmailState,\n} from '@aglyn/aglyn'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec, listRowMatchesSearch } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — and it is the one view a Firestore query cannot\n * express, because a lead nobody has touched carries no status field at all;\n * see the section for why the filter runs over a loaded window.\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/** Whether a lead belongs in a filter's view. An absent status is `new`. */\nexport function leadMatchesFilter(\n lead: Pick<CrmLeadFields, 'status'>,\n filter: LeadFilter,\n): boolean {\n if (filter === 'all') return true\n if (filter === 'open') return isCrmLeadOpen(lead)\n return crmLeadStatus(lead) === filter\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/** Whether a lead belongs in an email filter's view. */\nexport function leadMatchesEmailFilter(\n lead: Readonly<Record<string, unknown>>,\n filter: LeadEmailFilter,\n): boolean {\n if (filter === 'any') return true\n const state = readEmailState(lead)\n if (filter === 'none') return state === null\n if (filter === 'problem') return emailStateForbidsEmail(state)\n return state?.status === filter\n}\n\n/**\n * The Leads section's `Campaign` control (AGL-3254): every lead, or the\n * ones filed under one campaign — read off the lead's own `campaignIds`,\n * the field every campaign member carries. `''` is every lead, which is\n * what the control opens on; the ids are the site's containers', and the\n * section resolves their names for the menu.\n */\nexport function leadMatchesCampaignFilter(\n lead: Readonly<Record<string, unknown>>,\n campaignId: string,\n): boolean {\n if (!campaignId) return true\n return readCampaignIds(lead).includes(campaignId)\n}\n\n/**\n * The fields the Leads section's search box reads (AGL-3246): who the lead\n * is and where they work — the words a person types to find one — plus the\n * tags, which are how an import names its batch (`sal-15`). `name` and\n * `email` are the capture door's fields rather than the CRM's, so the row is\n * read as a record rather than through `CrmLeadFields`.\n */\nexport const LEAD_SEARCH_FIELDS = ['name', 'email', 'company', 'jobTitle', 'tags'] as const\n\n/**\n * Whether a lead answers a search term.\n *\n * Case-insensitive, and every whitespace-separated word of the term must\n * appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`\n * both find Morgan Lamphere, `morgan smith` does not. A blank term matches\n * every lead, so the box emptied is the list unfiltered.\n */\nexport function leadMatchesSearch(\n lead: Readonly<Record<string, unknown>>,\n term: string,\n): boolean {\n // Tags are a list; a stray string there is no tag of the lead's.\n const tags = Array.isArray(lead['tags']) ? lead['tags'] : undefined\n return listRowMatchesSearch({ ...lead, tags }, LEAD_SEARCH_FIELDS, term.trim().split(/\\s+/))\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/**\n * The Leads section's `Lead source` control (AGL-3298): every lead (`''`),\n * the ones holding one value — compared the way the picklist compares\n * labels, so a value typed in another case before the list existed is\n * still found — or the ones holding none.\n */\nexport function leadMatchesLeadSourceFilter(\n lead: Readonly<Record<string, unknown>>,\n filter: string,\n): boolean {\n if (!filter) return true\n const held = normalizeCrmPicklistLabel(lead['leadSource']).toLowerCase()\n if (filter === LEAD_SOURCE_FILTER_NONE) return held === ''\n return held === normalizeCrmPicklistLabel(filter).toLowerCase()\n}\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n *\n * Every one narrows the loaded window, never the query — see the section's\n * `LEADS_WINDOW` for why a status cannot be asked of Firestore.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n campaignIds: 'Campaign',\n}\n\n/** The choices for Status: Open (new or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\nconst listed = (clause: CrmViewFilterClause): string[] =>\n clause.op === 'isAnyOf'\n ? clause.value.split(',').map((entry) => entry.trim()).filter(Boolean)\n : [clause.value]\n\n/** Whether a lead answers every clause the grid shows. */\nexport function leadMatchesClauses(\n lead: Readonly<Record<string, unknown>> & Pick<CrmLeadFields, 'status'>,\n clauses: readonly CrmViewFilterClause[],\n): boolean {\n return clauses.every((clause) => {\n switch (clause.field) {\n case 'status':\n return listed(clause).some((value) =>\n (LEAD_FILTERS as readonly string[]).includes(value)\n ? leadMatchesFilter(lead, value as LeadFilter)\n : true,\n )\n case 'emailState':\n return listed(clause).some((value) =>\n (LEAD_EMAIL_FILTERS as readonly string[]).includes(value)\n ? leadMatchesEmailFilter(lead, value as LeadEmailFilter)\n : true,\n )\n case 'campaignIds':\n return listed(clause).some((value) => leadMatchesCampaignFilter(lead, value))\n case 'leadSource':\n return clause.op === 'isEmpty'\n ? leadMatchesLeadSourceFilter(lead, LEAD_SOURCE_FILTER_NONE)\n : listed(clause).some((value) => leadMatchesLeadSourceFilter(lead, value))\n case 'ownerUid':\n return listed(clause).includes(String(lead['ownerUid'] ?? ''))\n default:\n // A clause on a field this list does not answer narrows nothing,\n // rather than emptying the list.\n return true\n }\n })\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n"],"names":["EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","CRM_LEAD_STATUS_LABELS","emailStateForbidsEmail","crmLeadStatus","isCrmLeadOpen","normalizeCrmPicklistLabel","readCampaignIds","readEmailState","listSelectCodec","listRowMatchesSearch","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","working","qualified","unqualified","leadMatchesFilter","lead","filter","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","leadMatchesEmailFilter","state","status","leadMatchesCampaignFilter","campaignId","includes","LEAD_SEARCH_FIELDS","leadMatchesSearch","term","tags","Array","isArray","undefined","trim","split","LEAD_SOURCE_FILTER_NONE","leadMatchesLeadSourceFilter","held","toLowerCase","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_LIST_FILTER_HEADERS","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","listed","entry","Boolean","leadMatchesClauses","clauses","every","some","String","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.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\nimport {\n EMAIL_STATE_LABELS,\n EMAIL_STATE_STATUSES,\n CRM_LEAD_STATUS_LABELS,\n CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n CRM_LEAD_STATUSES,\n type EmailStateStatus,\n type CrmLeadStatus,\n crmLeadSourceKey,\n} from '@aglyn/aglyn'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport { SCOPED_SEARCH_JOIN } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n crmSelectField,\n} from './crm-list-query'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — asked of the query as `status in [new, working]`,\n * which every lead answers because every lead writer stores a status, `new`\n * for one nobody has touched (`crmLeadListFields`, AGL-3321).\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n *\n * Every one is asked of the Leads query (AGL-3321) through the field a\n * writer stores for it — see {@link leadQueryClause}.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n campaignIds: 'Campaign',\n}\n\n/** The choices for Status: Open (new or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n\n/*==========================================\n * THE LEADS QUERY (AGL-3321).\n *\n * `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the\n * `visibleTo` scope clause. Each grid clause is asked through a field every\n * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):\n *\n * status `status`, stored `new` on a lead nobody has touched, so\n * Open is `status in [new, working]`\n * emailState `emailStatus`, the verdict's status or `none`; \"Cannot be\n * emailed\" is every verdict but `ok`\n * leadSource `leadSourceKey`, the label as the picklist compares it, or\n * `null` for none\n * ownerUid as stored\n * campaignIds `array-contains` on the lead's own campaigns; under a site,\n * `scopedCampaignIds` — the campaign behind each of the\n * site's scope tokens — which stands in the scope clause's\n * place, so the one array clause answers both\n *\n * and the search box reads `searchTokens` — word prefixes of the name,\n * address, company, title and tags. Every equality rides one\n * `(field, lastSeenAtMs DESC)` composite; the campaign and the search are\n * array clauses, so the two do not stand together (nor under a site, where\n * the scope clause is the query's one array clause, save the search, which\n * folds into it for a reader who may drop it).\n *=========================================*/\n\n/** The one order the Leads list takes: newest seen first. */\nexport const LEAD_LIST_SORTS: readonly ListQuerySort[] = [{ path: 'lastSeenAtMs', direction: 'desc' }]\n\n/** The fields the Leads query asks — each a stored field, as named. */\nexport const LEAD_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n crmSelectField('emailStatus'),\n crmSelectField('ownerUid'),\n {\n column: 'leadSourceKey',\n kind: 'exact',\n path: 'leadSourceKey',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty'],\n },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n tokensPath: 'campaignIds',\n operators: ['contains'],\n },\n {\n // A campaign under a site: the lead's campaigns behind its scope.\n column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n kind: 'exact',\n path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n operators: ['isAnyOf'],\n },\n]\n\nexport const LEAD_LIST_DECLARATION: ListQueryDeclaration = {\n fields: LEAD_QUERY_FIELDS,\n sorts: LEAD_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the address, beside the scope clause — the `(visibleTo, email)`\n * composite the Inbox's leads list reads too.\n */\nexport const LEAD_PREFIX_SEARCH = {\n field: {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n } satisfies ListFilterField,\n notice: 'Search matches the start of a lead’s address for access limited to specific sites.',\n}\n\n/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */\nexport const leadClauseImpliesScope = (clause: ListFilterRequest): boolean =>\n clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD\n\n/** The verdicts a member must not email to: every one but `ok`. */\nconst EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status) => status !== 'ok')\n\n/** Who is asking, for the one clause whose shape depends on it: the Campaign. */\nexport interface LeadQueryReader {\n /** The site's scope tokens, or `null` at the organization level. */\n scopeTokens: readonly string[] | null\n /** Whether the reader may run a query without the scope clause (`useCrmFoldsScope`). */\n foldsScope: boolean\n}\n\nconst ORG_READER: LeadQueryReader = { scopeTokens: null, foldsScope: true }\n\n/**\n * One grid clause as the clause the Leads query asks — see the block above.\n * A value the list does not know is refused by name rather than dropped, so\n * a stored view that names a retired choice says so.\n */\nexport function leadQueryClause(\n clause: CrmViewFilterClause | ListFilterRequest,\n reader: LeadQueryReader = ORG_READER,\n): CrmClauseAsked {\n const values = crmClauseValues(clause)\n switch (clause.field) {\n case 'status': {\n const statuses: string[] = []\n for (const value of values) {\n if (value === 'open') statuses.push('new', 'working')\n else if ((CRM_LEAD_STATUSES as readonly string[]).includes(value)) statuses.push(value)\n else return { refused: `${value} is not a lead status` }\n }\n return crmAnyOf('status', statuses)\n }\n case 'emailState': {\n const keys: string[] = []\n for (const value of values) {\n if (value === 'problem') keys.push(...EMAIL_PROBLEMS)\n else if (value === 'none') keys.push('none')\n else if ((EMAIL_STATE_STATUSES as readonly string[]).includes(value)) keys.push(value)\n else return { refused: `${value} is not an email verdict` }\n }\n return crmAnyOf('emailStatus', keys)\n }\n case 'leadSource': {\n if (clause.op === 'isEmpty' || (values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE)) {\n return { field: 'leadSourceKey', op: 'isEmpty', value: '' }\n }\n if (values.includes(LEAD_SOURCE_FILTER_NONE)) {\n return { refused: '\"No lead source\" cannot be picked beside a value' }\n }\n const keys = values.map((value) => crmLeadSourceKey(value)).filter((key): key is string => Boolean(key))\n return crmAnyOf('leadSourceKey', keys)\n }\n case 'ownerUid':\n return crmAnyOf('ownerUid', values)\n case 'campaignIds': {\n if (values.length !== 1) return { refused: 'pick one campaign' }\n if (!reader.scopeTokens) return { field: 'campaignIds', op: 'contains', value: values[0] }\n if (!reader.foldsScope) {\n return {\n refused:\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it',\n }\n }\n return {\n field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n op: 'isAnyOf',\n value: reader.scopeTokens.map((token) => `${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(','),\n }\n }\n default:\n return { refused: 'this list does not filter by that' }\n }\n}\n\n/**\n * The query shape the Leads list can send beyond its own declaration, for\n * the index pin (AGL-3321): a collaborator's address prefix beside the\n * equalities they may set.\n */\nexport function leadIndexShapes(): ListQueryDeclaration[] {\n const equalities = LEAD_QUERY_FIELDS.filter((field) => !field.tokensPath)\n return [{ fields: [LEAD_PREFIX_SEARCH.field, ...equalities], sorts: [] }]\n}\n"],"names":["EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","CRM_LEAD_STATUS_LABELS","CRM_LEAD_SCOPED_CAMPAIGNS_FIELD","CRM_LEAD_STATUSES","crmLeadSourceKey","SCOPED_SEARCH_JOIN","listSelectCodec","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","working","qualified","unqualified","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","LEAD_SOURCE_FILTER_NONE","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_LIST_FILTER_HEADERS","status","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","filter","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS","LEAD_LIST_SORTS","direction","LEAD_QUERY_FIELDS","presence","tokensPath","LEAD_LIST_DECLARATION","fields","sorts","search","LEAD_PREFIX_SEARCH","lowerPath","notice","leadClauseImpliesScope","EMAIL_PROBLEMS","ORG_READER","scopeTokens","foldsScope","leadQueryClause","reader","values","statuses","push","includes","refused","keys","length","key","Boolean","token","join","leadIndexShapes","equalities"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { CrmListCollection } from '@aglyn/aglyn';
|
|
18
|
+
import { type MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token';
|
|
19
|
+
import type { CrmTaskRouteScope } from './task-routes';
|
|
20
|
+
/**
|
|
21
|
+
* After a client-direct write the browser cannot follow up itself: ask the
|
|
22
|
+
* list-fields route to restamp the records (AGL-3321). Never throws — the
|
|
23
|
+
* write already happened, and a record left stale is what the backfill and
|
|
24
|
+
* the next write bring level.
|
|
25
|
+
*/
|
|
26
|
+
export declare function restampCrmListFields(user: MaybeTokenSource, scope: CrmTaskRouteScope | null, collection: CrmListCollection, ids: readonly string[]): Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
18
|
+
import { CRM_LIST_FIELDS_IDS_MAX, crmListFieldsRouteUrl } from "./list-fields.js";
|
|
19
|
+
/**
|
|
20
|
+
* After a client-direct write the browser cannot follow up itself: ask the
|
|
21
|
+
* list-fields route to restamp the records (AGL-3321). Never throws — the
|
|
22
|
+
* write already happened, and a record left stale is what the backfill and
|
|
23
|
+
* the next write bring level.
|
|
24
|
+
*/ export async function restampCrmListFields(user, scope, collection, ids) {
|
|
25
|
+
const unique = [
|
|
26
|
+
...new Set(ids.filter(Boolean))
|
|
27
|
+
].slice(0, CRM_LIST_FIELDS_IDS_MAX);
|
|
28
|
+
if (!scope || !unique.length) return;
|
|
29
|
+
const body = _extends({}, scope, {
|
|
30
|
+
collection,
|
|
31
|
+
ids: unique
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
const response = await authorizedFetch(user, crmListFieldsRouteUrl(), {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json'
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify(body)
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) console.warn('[crm] list fields were not restamped', response.status);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn('[crm] list fields were not restamped', error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=list-fields-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/list-fields-api.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\nimport type { CrmListCollection } from '@aglyn/aglyn'\nimport {\n authorizedFetch,\n type MaybeTokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\nimport {\n CRM_LIST_FIELDS_IDS_MAX,\n type CrmListFieldsRequest,\n crmListFieldsRouteUrl,\n} from './list-fields'\nimport type { CrmTaskRouteScope } from './task-routes'\n\n/**\n * After a client-direct write the browser cannot follow up itself: ask the\n * list-fields route to restamp the records (AGL-3321). Never throws — the\n * write already happened, and a record left stale is what the backfill and\n * the next write bring level.\n */\nexport async function restampCrmListFields(\n user: MaybeTokenSource,\n scope: CrmTaskRouteScope | null,\n collection: CrmListCollection,\n ids: readonly string[],\n): Promise<void> {\n const unique = [...new Set(ids.filter(Boolean))].slice(0, CRM_LIST_FIELDS_IDS_MAX)\n if (!scope || !unique.length) return\n const body: CrmListFieldsRequest = { ...scope, collection, ids: unique }\n try {\n const response = await authorizedFetch(user, crmListFieldsRouteUrl(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!response.ok) console.warn('[crm] list fields were not restamped', response.status)\n } catch (error) {\n console.warn('[crm] list fields were not restamped', error)\n }\n}\n"],"names":["authorizedFetch","CRM_LIST_FIELDS_IDS_MAX","crmListFieldsRouteUrl","restampCrmListFields","user","scope","collection","ids","unique","Set","filter","Boolean","slice","length","body","response","method","headers","JSON","stringify","ok","console","warn","status","error"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,eAAe,QAEV,2CAA0C;AACjD,SACEC,uBAAuB,EAEvBC,qBAAqB,QAChB,mBAAe;AAGtB;;;;;CAKC,GACD,OAAO,eAAeC,qBACpBC,IAAsB,EACtBC,KAA+B,EAC/BC,UAA6B,EAC7BC,GAAsB;IAEtB,MAAMC,SAAS;WAAI,IAAIC,IAAIF,IAAIG,MAAM,CAACC;KAAU,CAACC,KAAK,CAAC,GAAGX;IAC1D,IAAI,CAACI,SAAS,CAACG,OAAOK,MAAM,EAAE;IAC9B,MAAMC,OAA6B,aAAKT;QAAOC;QAAYC,KAAKC;;IAChE,IAAI;QACF,MAAMO,WAAW,MAAMf,gBAAgBI,MAAMF,yBAAyB;YACpEc,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CH,MAAMI,KAAKC,SAAS,CAACL;QACvB;QACA,IAAI,CAACC,SAASK,EAAE,EAAEC,QAAQC,IAAI,CAAC,wCAAwCP,SAASQ,MAAM;IACxF,EAAE,OAAOC,OAAO;QACdH,QAAQC,IAAI,CAAC,wCAAwCE;IACvD;AACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { CrmListCollection } from '@aglyn/aglyn';
|
|
18
|
+
import type { CrmTaskRouteScope } from './task-routes';
|
|
19
|
+
/**
|
|
20
|
+
* The contract between the console and the plugin's list-fields route
|
|
21
|
+
* (AGL-3321), the door a CLIENT-DIRECT write uses to keep the fields a CRM
|
|
22
|
+
* list queries honest.
|
|
23
|
+
*
|
|
24
|
+
* Those fields — the search tokens, a contact's facet keys — are derived
|
|
25
|
+
* from the record (`crmListFields` in `@aglyn/aglyn`). A client that
|
|
26
|
+
* creates a record, or edits one from the listener's copy, stamps them
|
|
27
|
+
* itself. A write the browser may not follow up — a holder letting go of a
|
|
28
|
+
* contact, which the rules narrow to removing its own half — asks this
|
|
29
|
+
* route to read the record back and restamp it. Best effort, like the
|
|
30
|
+
* next-activity route beside it: the write already happened.
|
|
31
|
+
*/
|
|
32
|
+
export declare const CRM_LIST_FIELDS_ROUTE: "crm/list-fields";
|
|
33
|
+
export declare const crmListFieldsRouteUrl: () => string;
|
|
34
|
+
/** The most records one request restamps — the bulk bar's own window. */
|
|
35
|
+
export declare const CRM_LIST_FIELDS_IDS_MAX = 200;
|
|
36
|
+
/** The collections the route restamps. */
|
|
37
|
+
export declare const CRM_LIST_FIELDS_COLLECTIONS: readonly CrmListCollection[];
|
|
38
|
+
export type CrmListFieldsRequest = CrmTaskRouteScope & {
|
|
39
|
+
collection: CrmListCollection;
|
|
40
|
+
ids: string[];
|
|
41
|
+
};
|
|
42
|
+
export interface CrmListFieldsResponse {
|
|
43
|
+
ok: true;
|
|
44
|
+
restamped: number;
|
|
45
|
+
current: number;
|
|
46
|
+
missing: number;
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The contract between the console and the plugin's list-fields route
|
|
18
|
+
* (AGL-3321), the door a CLIENT-DIRECT write uses to keep the fields a CRM
|
|
19
|
+
* list queries honest.
|
|
20
|
+
*
|
|
21
|
+
* Those fields — the search tokens, a contact's facet keys — are derived
|
|
22
|
+
* from the record (`crmListFields` in `@aglyn/aglyn`). A client that
|
|
23
|
+
* creates a record, or edits one from the listener's copy, stamps them
|
|
24
|
+
* itself. A write the browser may not follow up — a holder letting go of a
|
|
25
|
+
* contact, which the rules narrow to removing its own half — asks this
|
|
26
|
+
* route to read the record back and restamp it. Best effort, like the
|
|
27
|
+
* next-activity route beside it: the write already happened.
|
|
28
|
+
*/ export const CRM_LIST_FIELDS_ROUTE = 'crm/list-fields';
|
|
29
|
+
export const crmListFieldsRouteUrl = ()=>`/api/${CRM_LIST_FIELDS_ROUTE}`;
|
|
30
|
+
/** The most records one request restamps — the bulk bar's own window. */ export const CRM_LIST_FIELDS_IDS_MAX = 200;
|
|
31
|
+
/** The collections the route restamps. */ export const CRM_LIST_FIELDS_COLLECTIONS = [
|
|
32
|
+
'leads',
|
|
33
|
+
'contacts',
|
|
34
|
+
'companies',
|
|
35
|
+
'deals',
|
|
36
|
+
'crmTasks'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=list-fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/list-fields.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\nimport type { CrmListCollection } from '@aglyn/aglyn'\nimport type { CrmTaskRouteScope } from './task-routes'\n\n/**\n * The contract between the console and the plugin's list-fields route\n * (AGL-3321), the door a CLIENT-DIRECT write uses to keep the fields a CRM\n * list queries honest.\n *\n * Those fields — the search tokens, a contact's facet keys — are derived\n * from the record (`crmListFields` in `@aglyn/aglyn`). A client that\n * creates a record, or edits one from the listener's copy, stamps them\n * itself. A write the browser may not follow up — a holder letting go of a\n * contact, which the rules narrow to removing its own half — asks this\n * route to read the record back and restamp it. Best effort, like the\n * next-activity route beside it: the write already happened.\n */\nexport const CRM_LIST_FIELDS_ROUTE = 'crm/list-fields' as const\n\nexport const crmListFieldsRouteUrl = (): string => `/api/${CRM_LIST_FIELDS_ROUTE}`\n\n/** The most records one request restamps — the bulk bar's own window. */\nexport const CRM_LIST_FIELDS_IDS_MAX = 200\n\n/** The collections the route restamps. */\nexport const CRM_LIST_FIELDS_COLLECTIONS: readonly CrmListCollection[] = [\n 'leads',\n 'contacts',\n 'companies',\n 'deals',\n 'crmTasks',\n]\n\nexport type CrmListFieldsRequest = CrmTaskRouteScope & {\n collection: CrmListCollection\n ids: string[]\n}\n\nexport interface CrmListFieldsResponse {\n ok: true\n restamped: number\n current: number\n missing: number\n}\n"],"names":["CRM_LIST_FIELDS_ROUTE","crmListFieldsRouteUrl","CRM_LIST_FIELDS_IDS_MAX","CRM_LIST_FIELDS_COLLECTIONS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAKD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMA,wBAAwB,kBAA0B;AAE/D,OAAO,MAAMC,wBAAwB,IAAc,CAAC,KAAK,EAAED,uBAAuB,CAAA;AAElF,uEAAuE,GACvE,OAAO,MAAME,0BAA0B,IAAG;AAE1C,wCAAwC,GACxC,OAAO,MAAMC,8BAA4D;IACvE;IACA;IACA;IACA;IACA;CACD,CAAA"}
|