@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -13,154 +14,179 @@
|
|
|
13
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
15
|
* See the License for the specific language governing permissions and
|
|
15
16
|
* limitations under the License.
|
|
16
|
-
*/ import { CRM_CONTACT_VIEW_FIELDS, crmContactCustomColumn } from "@aglyn/aglyn";
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
*/ import { CRM_CONTACT_VIEW_FIELDS, CRM_FACET_KEY_ANY_GROUP, crmContactCustomColumn, crmFacetKey, crmFacetKeyValue, EMAIL_STATE_STATUSES } from "@aglyn/aglyn";
|
|
18
|
+
import { CRM_LIST_SEARCH, crmAnyOf, crmClauseValues } from "../model/crm-list-query.js";
|
|
19
|
+
/*==========================================
|
|
20
|
+
* CONTACTS (`orgs/{orgId}/contacts`, read scoped to a host) — WHAT THE
|
|
21
|
+
* PANEL OFFERS, AND WHAT THE QUERY ASKS (AGL-3321).
|
|
19
22
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Every clause and the search word are on the list's one Firestore query,
|
|
24
|
+
* newest change first, paged by it. A contact is one row shared by every
|
|
25
|
+
* holder, and most of what a holder filters by — the owner, the stage, the
|
|
26
|
+
* sources, the company, the tags, a custom value, whether the person has
|
|
27
|
+
* bought — lives on that holder's FACET, a path per holder no index can
|
|
28
|
+
* reach. So every writer stamps `facetKeys` (`crmContactFacetKeys` in
|
|
29
|
+
* `@aglyn/aglyn`): `{group}:{field}={value}` for each holder's value,
|
|
30
|
+
* `{group}:{field}` for "has one", and the same under `*` for any holder.
|
|
31
|
+
* Under a site a facet clause asks the viewing holder's key; at the
|
|
32
|
+
* organization level, any holder's.
|
|
26
33
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
* What one query holds decides the rest:
|
|
35
|
+
*
|
|
36
|
+
* - ONE array clause. The scope clause (`visibleTo array-contains-any`),
|
|
37
|
+
* the search (`searchTokens`), a name word (`nameTokens`), a form
|
|
38
|
+
* (`formIds`) and a facet clause (`facetKeys`) all are one. The search
|
|
39
|
+
* folds into the scope clause for a reader who may drop it; a facet or
|
|
40
|
+
* form clause stands in the scope clause's place for that reader, since
|
|
41
|
+
* only a record the viewing holder keeps carries its key. Two of them
|
|
42
|
+
* at once is refused by name.
|
|
43
|
+
* - ONE range. `updatedAt`, the field the list is ordered by, composes
|
|
44
|
+
* with anything; Created and a name or address prefix order the list by
|
|
45
|
+
* their own field and so stand alone beside the scope clause
|
|
46
|
+
* (`contactSoloClause`), served by the `(visibleTo, field)` composites.
|
|
47
|
+
* - equalities (the address, the site, the verdict, "No next activity")
|
|
48
|
+
* compose freely, one `(field, updatedAt DESC)` composite each.
|
|
49
|
+
*
|
|
50
|
+
* The per-holder FIGURES (orders, lifetime value) live on each holder's
|
|
51
|
+
* facet, where no range reaches — a top-level `ordersCount` was never
|
|
52
|
+
* written — so they offer "is not empty" alone: "has bought".
|
|
53
|
+
*
|
|
54
|
+
* A collaborator's search is the start of the name (`CONTACT_PREFIX_SEARCH`).
|
|
55
|
+
*=========================================*/ /**
|
|
56
|
+
* A word of the canonical name (`nameTokens`), the whole of it, or its start
|
|
57
|
+
* or end — `nameLower`, which every contact carries, and `nameReversed`.
|
|
58
|
+
*/ const NAME_FIELD = {
|
|
59
|
+
column: 'name',
|
|
60
|
+
kind: 'text',
|
|
61
|
+
path: 'name',
|
|
62
|
+
lowerPath: 'nameLower',
|
|
63
|
+
tokensPath: 'nameTokens',
|
|
64
|
+
reversedPath: 'nameReversed',
|
|
65
|
+
operators: [
|
|
66
|
+
'contains',
|
|
67
|
+
'equals',
|
|
68
|
+
'startsWith',
|
|
69
|
+
'endsWith'
|
|
70
|
+
]
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* The address. `normalizeContactEmail` lower-cases before every write, so the
|
|
74
|
+
* stored value IS its own normalized key.
|
|
75
|
+
*/ const EMAIL_FIELD = {
|
|
76
|
+
column: 'email',
|
|
77
|
+
kind: 'text',
|
|
78
|
+
path: 'email',
|
|
79
|
+
lowerPath: 'email',
|
|
80
|
+
presence: 'always',
|
|
81
|
+
operators: [
|
|
82
|
+
'equals',
|
|
83
|
+
'startsWith'
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
/** When the person was first captured — a range the list is then ordered by. */ const CREATED_FIELD = {
|
|
87
|
+
column: CRM_CONTACT_VIEW_FIELDS.createdAt,
|
|
88
|
+
kind: 'date',
|
|
89
|
+
path: 'createdAt',
|
|
90
|
+
presence: 'always',
|
|
91
|
+
operators: [
|
|
92
|
+
'is',
|
|
93
|
+
'after',
|
|
94
|
+
'onOrAfter',
|
|
95
|
+
'before',
|
|
96
|
+
'onOrBefore'
|
|
97
|
+
]
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The forms a person came in through — the top-level `formIds` mirror of the
|
|
101
|
+
* interactions' `formId` (AGL-2612), matched whole and as typed because a
|
|
102
|
+
* form id is minted with mixed case. The form's own page links here with its
|
|
103
|
+
* id; nobody types one.
|
|
104
|
+
*/ const FORM_FIELD = {
|
|
105
|
+
column: 'formIds',
|
|
106
|
+
kind: 'text',
|
|
107
|
+
path: 'formIds',
|
|
108
|
+
tokensPath: 'formIds',
|
|
109
|
+
verbatimTokens: true,
|
|
110
|
+
operators: [
|
|
111
|
+
'contains'
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
/** What the Filters panel offers on a contact, in the grid's grammar. */ export const CONTACT_LIST_FILTER_FIELDS = [
|
|
115
|
+
NAME_FIELD,
|
|
116
|
+
EMAIL_FIELD,
|
|
48
117
|
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* because a tag is matched whole rather than by word prefix.
|
|
53
|
-
*/ column: 'tags',
|
|
54
|
-
kind: 'text',
|
|
118
|
+
// A holder's tags, matched whole and case-insensitively.
|
|
119
|
+
column: CRM_CONTACT_VIEW_FIELDS.tags,
|
|
120
|
+
kind: 'exact',
|
|
55
121
|
path: 'tags',
|
|
56
|
-
tokensPath: 'tags',
|
|
57
|
-
// Ordered by the timestamp the list already sorts on, not by the tag
|
|
58
|
-
// array — `orderBy` on the same field an `array-contains` matches is a
|
|
59
|
-
// composite index nobody wants to explain.
|
|
60
|
-
containsOrderBy: 'updatedAt',
|
|
61
|
-
// Newest first, as the unfiltered list, which is the composite declared
|
|
62
|
-
// for it (AGL-3321).
|
|
63
|
-
containsOrderDirection: 'desc',
|
|
64
|
-
// `isAnyOf` is a saved segment's "any of these tags" (AGL-2617). The
|
|
65
|
-
// query cannot serve it — `array-contains-any` is the scope clause's
|
|
66
|
-
// — so the translator refuses it and the list matches it over the
|
|
67
|
-
// window, which the caption says.
|
|
68
122
|
operators: [
|
|
69
123
|
'contains',
|
|
70
124
|
'isAnyOf',
|
|
71
125
|
'isNotEmpty'
|
|
72
126
|
]
|
|
73
127
|
},
|
|
74
|
-
|
|
75
|
-
/*
|
|
76
|
-
* The forms a person came in through — the top-level `formIds` mirror
|
|
77
|
-
* of the interactions' `formId` (AGL-2612), matched whole and as typed
|
|
78
|
-
* because a form id is minted with mixed case. The form's own page links
|
|
79
|
-
* here with its id; nobody types one. Ordered by the list's own
|
|
80
|
-
* timestamp for the reason `tags` is, served by the
|
|
81
|
-
* `(formIds CONTAINS, updatedAt DESC)` index.
|
|
82
|
-
*
|
|
83
|
-
* ⚠️ An `array-contains` on the mirror cannot share a query with the
|
|
84
|
-
* `visibleTo` scope clause, so the list drops that clause for this one
|
|
85
|
-
* filter and the rules admit the read to an org-wide member only — the
|
|
86
|
-
* caveat the company contacts card states, stated again on the list.
|
|
87
|
-
*/ column: 'formIds',
|
|
88
|
-
kind: 'text',
|
|
89
|
-
path: 'formIds',
|
|
90
|
-
tokensPath: 'formIds',
|
|
91
|
-
verbatimTokens: true,
|
|
92
|
-
containsOrderBy: 'updatedAt',
|
|
93
|
-
// Newest first, as the unfiltered list, which is the composite declared
|
|
94
|
-
// for it (AGL-3321).
|
|
95
|
-
containsOrderDirection: 'desc',
|
|
96
|
-
operators: [
|
|
97
|
-
'contains'
|
|
98
|
-
]
|
|
99
|
-
},
|
|
128
|
+
FORM_FIELD,
|
|
100
129
|
{
|
|
101
130
|
column: 'hostId',
|
|
102
131
|
kind: 'exact',
|
|
103
132
|
path: 'hostId',
|
|
104
133
|
presence: 'always'
|
|
105
134
|
},
|
|
135
|
+
// Per-holder figures: "has bought", never a range across holders.
|
|
106
136
|
{
|
|
107
|
-
column:
|
|
137
|
+
column: CRM_CONTACT_VIEW_FIELDS.orders,
|
|
108
138
|
kind: 'number',
|
|
109
|
-
path: 'ordersCount'
|
|
139
|
+
path: 'ordersCount',
|
|
140
|
+
operators: [
|
|
141
|
+
'isNotEmpty'
|
|
142
|
+
]
|
|
110
143
|
},
|
|
111
144
|
{
|
|
112
|
-
column:
|
|
145
|
+
column: CRM_CONTACT_VIEW_FIELDS.ltv,
|
|
113
146
|
kind: 'number',
|
|
114
|
-
path: 'ltvCents'
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
kind: 'date',
|
|
119
|
-
path: 'createdAt',
|
|
120
|
-
presence: 'always'
|
|
147
|
+
path: 'ltvCents',
|
|
148
|
+
operators: [
|
|
149
|
+
'isNotEmpty'
|
|
150
|
+
]
|
|
121
151
|
},
|
|
152
|
+
CREATED_FIELD,
|
|
122
153
|
{
|
|
123
|
-
column:
|
|
154
|
+
column: CRM_CONTACT_VIEW_FIELDS.updatedAt,
|
|
124
155
|
kind: 'date',
|
|
125
156
|
path: 'updatedAt',
|
|
126
|
-
presence: 'always'
|
|
157
|
+
presence: 'always',
|
|
158
|
+
operators: [
|
|
159
|
+
'is',
|
|
160
|
+
'after',
|
|
161
|
+
'onOrAfter',
|
|
162
|
+
'before',
|
|
163
|
+
'onOrBefore'
|
|
164
|
+
]
|
|
127
165
|
},
|
|
128
166
|
/*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* existed has none, and Firestore cannot query for absence. `nullable`
|
|
132
|
-
* is honest: the writer stores `null` for nothing scheduled, and the
|
|
133
|
-
* matcher reads absent the same way, so "is empty" is the "No next
|
|
134
|
-
* activity" filter over the loaded window.
|
|
167
|
+
* "No next activity" (AGL-2661): `nextTaskAtMs == null`, which every
|
|
168
|
+
* contact carries from its creation until a task is scheduled against it.
|
|
135
169
|
*/ {
|
|
136
170
|
column: 'nextTaskAtMs',
|
|
137
171
|
kind: 'date',
|
|
138
172
|
path: 'nextTaskAtMs',
|
|
139
|
-
|
|
140
|
-
|
|
173
|
+
presence: 'nullable',
|
|
174
|
+
storedAs: 'millis',
|
|
175
|
+
operators: [
|
|
176
|
+
'isEmpty'
|
|
177
|
+
]
|
|
141
178
|
},
|
|
142
179
|
/*
|
|
143
|
-
* THE FACET FIELDS (AGL-2617)
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* translator
|
|
149
|
-
* are declared for the menu and for a saved view, matched over the loaded
|
|
150
|
-
* window against the flattened row (`ContactRecord` carries each at the
|
|
151
|
-
* top), and never put on a query — `windowOnly` is what holds that line.
|
|
152
|
-
* The paths are the ROW'S, which is what the matcher reads.
|
|
153
|
-
*
|
|
154
|
-
* The values are picked, not typed — a uid from the roster, a stage from
|
|
155
|
-
* the fixed list, a source from its labels, a company from the picker —
|
|
156
|
-
* so `equals` and `isAnyOf` are what they offer; the section supplies
|
|
157
|
-
* the choices. The column names are the contract `CRM_CONTACT_VIEW_FIELDS`
|
|
158
|
-
* states, because the dynamic-list translator reads a view by them.
|
|
180
|
+
* THE FACET FIELDS (AGL-2617). The values are picked, not typed — a uid
|
|
181
|
+
* from the roster, a stage from the fixed list, a source from its labels,
|
|
182
|
+
* a company from the picker — so `equals` and `isAnyOf` are what they
|
|
183
|
+
* offer; the section supplies the choices. The column names are the
|
|
184
|
+
* contract `CRM_CONTACT_VIEW_FIELDS` states, because the dynamic-list
|
|
185
|
+
* translator reads a view by them. The paths are the flattened ROW'S.
|
|
159
186
|
*/ {
|
|
160
187
|
column: CRM_CONTACT_VIEW_FIELDS.owner,
|
|
161
188
|
kind: 'exact',
|
|
162
189
|
path: 'ownerUid',
|
|
163
|
-
windowOnly: true,
|
|
164
190
|
operators: [
|
|
165
191
|
'equals',
|
|
166
192
|
'isAnyOf',
|
|
@@ -171,7 +197,6 @@
|
|
|
171
197
|
column: CRM_CONTACT_VIEW_FIELDS.stage,
|
|
172
198
|
kind: 'exact',
|
|
173
199
|
path: 'lifecycleStage',
|
|
174
|
-
windowOnly: true,
|
|
175
200
|
operators: [
|
|
176
201
|
'equals',
|
|
177
202
|
'isAnyOf',
|
|
@@ -184,7 +209,6 @@
|
|
|
184
209
|
kind: 'exact',
|
|
185
210
|
path: 'sources',
|
|
186
211
|
keysOf: true,
|
|
187
|
-
windowOnly: true,
|
|
188
212
|
operators: [
|
|
189
213
|
'equals',
|
|
190
214
|
'isAnyOf'
|
|
@@ -194,21 +218,19 @@
|
|
|
194
218
|
column: CRM_CONTACT_VIEW_FIELDS.company,
|
|
195
219
|
kind: 'exact',
|
|
196
220
|
path: 'companyId',
|
|
197
|
-
windowOnly: true,
|
|
198
221
|
operators: [
|
|
199
222
|
'equals',
|
|
200
223
|
'isNotEmpty'
|
|
201
224
|
]
|
|
202
225
|
},
|
|
203
226
|
/*
|
|
204
|
-
* The verdict on the address (AGL-3245)
|
|
205
|
-
*
|
|
206
|
-
*
|
|
227
|
+
* The verdict on the address (AGL-3245), asked through `emailStatus` —
|
|
228
|
+
* `none` for a person nothing has been said about, which is what "is
|
|
229
|
+
* empty" asks.
|
|
207
230
|
*/ {
|
|
208
231
|
column: 'emailState',
|
|
209
232
|
kind: 'exact',
|
|
210
233
|
path: 'emailState.status',
|
|
211
|
-
windowOnly: true,
|
|
212
234
|
presence: 'nullable',
|
|
213
235
|
operators: [
|
|
214
236
|
'equals',
|
|
@@ -219,15 +241,11 @@
|
|
|
219
241
|
}
|
|
220
242
|
];
|
|
221
243
|
/**
|
|
222
|
-
* One filter field per active custom contact field (AGL-2617).
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* clause and a column agree on the name and a saved view's columns and
|
|
228
|
-
* filters name the same thing. The kind follows the definition's type, and
|
|
229
|
-
* `nullable` is how a cleared value is stored, which is what makes both
|
|
230
|
-
* empty operators honest here.
|
|
244
|
+
* One filter field per active custom contact field (AGL-2617). A custom
|
|
245
|
+
* value lives under the holder's `custom` map, so it is asked through the
|
|
246
|
+
* holder's facet key: equality on a picked, typed or ticked value, and
|
|
247
|
+
* "is not empty". Its column is the one the table shows it under —
|
|
248
|
+
* `crmContactCustomColumn(key)` — so a clause and a column agree on the name.
|
|
231
249
|
*/ export function contactCustomFilterFields(definitions) {
|
|
232
250
|
return definitions.filter((definition)=>!definition.retiredAt).map((definition)=>{
|
|
233
251
|
const column = crmContactCustomColumn(definition.key);
|
|
@@ -238,35 +256,34 @@
|
|
|
238
256
|
column,
|
|
239
257
|
kind: 'number',
|
|
240
258
|
path,
|
|
241
|
-
|
|
242
|
-
|
|
259
|
+
operators: [
|
|
260
|
+
'=',
|
|
261
|
+
'isNotEmpty'
|
|
262
|
+
]
|
|
243
263
|
};
|
|
244
264
|
case 'date':
|
|
245
265
|
return {
|
|
246
266
|
column,
|
|
247
267
|
kind: 'date',
|
|
248
268
|
path,
|
|
249
|
-
|
|
250
|
-
|
|
269
|
+
operators: [
|
|
270
|
+
'isNotEmpty'
|
|
271
|
+
]
|
|
251
272
|
};
|
|
252
273
|
case 'checkbox':
|
|
253
274
|
return {
|
|
254
275
|
column,
|
|
255
276
|
kind: 'boolean',
|
|
256
|
-
path
|
|
257
|
-
windowOnly: true
|
|
277
|
+
path
|
|
258
278
|
};
|
|
259
279
|
case 'select':
|
|
260
280
|
return {
|
|
261
281
|
column,
|
|
262
282
|
kind: 'exact',
|
|
263
283
|
path,
|
|
264
|
-
windowOnly: true,
|
|
265
|
-
presence: 'nullable',
|
|
266
284
|
operators: [
|
|
267
285
|
'equals',
|
|
268
286
|
'isAnyOf',
|
|
269
|
-
'isEmpty',
|
|
270
287
|
'isNotEmpty'
|
|
271
288
|
]
|
|
272
289
|
};
|
|
@@ -275,13 +292,9 @@
|
|
|
275
292
|
column,
|
|
276
293
|
kind: 'text',
|
|
277
294
|
path,
|
|
278
|
-
|
|
279
|
-
presence: 'nullable',
|
|
295
|
+
lowerPath: path,
|
|
280
296
|
operators: [
|
|
281
|
-
'contains',
|
|
282
297
|
'equals',
|
|
283
|
-
'startsWith',
|
|
284
|
-
'isEmpty',
|
|
285
298
|
'isNotEmpty'
|
|
286
299
|
]
|
|
287
300
|
};
|
|
@@ -314,5 +327,224 @@
|
|
|
314
327
|
[CRM_CONTACT_VIEW_FIELDS.company]: 'Company',
|
|
315
328
|
emailState: 'Email'
|
|
316
329
|
};
|
|
330
|
+
/*------------------------------------------
|
|
331
|
+
* THE QUERY THE CONTACTS LIST RUNS.
|
|
332
|
+
*-----------------------------------------*/ /** The list's one order: newest change first. */ export const CONTACT_LIST_SORTS = [
|
|
333
|
+
{
|
|
334
|
+
path: 'updatedAt',
|
|
335
|
+
direction: 'desc'
|
|
336
|
+
}
|
|
337
|
+
];
|
|
338
|
+
/** The stored array a facet clause is asked of. */ export const CONTACT_FACET_KEYS_PATH = 'facetKeys';
|
|
339
|
+
/** The fields the Contacts query asks — each a stored field, as named. */ export const CONTACT_QUERY_FIELDS = [
|
|
340
|
+
NAME_FIELD,
|
|
341
|
+
EMAIL_FIELD,
|
|
342
|
+
FORM_FIELD,
|
|
343
|
+
CREATED_FIELD,
|
|
344
|
+
{
|
|
345
|
+
column: 'hostId',
|
|
346
|
+
kind: 'exact',
|
|
347
|
+
path: 'hostId',
|
|
348
|
+
presence: 'always'
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
column: 'updatedAt',
|
|
352
|
+
kind: 'date',
|
|
353
|
+
path: 'updatedAt',
|
|
354
|
+
presence: 'always',
|
|
355
|
+
operators: [
|
|
356
|
+
'is',
|
|
357
|
+
'after',
|
|
358
|
+
'onOrAfter',
|
|
359
|
+
'before',
|
|
360
|
+
'onOrBefore'
|
|
361
|
+
]
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
column: 'nextTaskAtMs',
|
|
365
|
+
kind: 'date',
|
|
366
|
+
path: 'nextTaskAtMs',
|
|
367
|
+
presence: 'nullable',
|
|
368
|
+
storedAs: 'millis',
|
|
369
|
+
operators: [
|
|
370
|
+
'isEmpty'
|
|
371
|
+
]
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
column: 'emailStatus',
|
|
375
|
+
kind: 'exact',
|
|
376
|
+
path: 'emailStatus',
|
|
377
|
+
operators: [
|
|
378
|
+
'equals',
|
|
379
|
+
'isAnyOf'
|
|
380
|
+
]
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
column: CONTACT_FACET_KEYS_PATH,
|
|
384
|
+
kind: 'exact',
|
|
385
|
+
path: CONTACT_FACET_KEYS_PATH,
|
|
386
|
+
tokensPath: CONTACT_FACET_KEYS_PATH,
|
|
387
|
+
operators: [
|
|
388
|
+
'contains',
|
|
389
|
+
'isAnyOf'
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
];
|
|
393
|
+
export const CONTACT_LIST_DECLARATION = {
|
|
394
|
+
fields: CONTACT_QUERY_FIELDS,
|
|
395
|
+
sorts: CONTACT_LIST_SORTS,
|
|
396
|
+
search: CRM_LIST_SEARCH
|
|
397
|
+
};
|
|
398
|
+
/** The grid fields a stored clause passes to the query unchanged. */ const AS_STORED = new Set([
|
|
399
|
+
'name',
|
|
400
|
+
'email',
|
|
401
|
+
'formIds',
|
|
402
|
+
'hostId',
|
|
403
|
+
'createdAt',
|
|
404
|
+
'updatedAt',
|
|
405
|
+
'nextTaskAtMs'
|
|
406
|
+
]);
|
|
407
|
+
/**
|
|
408
|
+
* The ranges that stand ALONE beside the scope clause: Created and a name
|
|
409
|
+
* or address prefix each order the list by their own field, which the
|
|
410
|
+
* `(visibleTo, field)` composites serve and nothing beside them. Updated is
|
|
411
|
+
* not one: the list is ordered by it already, so it composes freely.
|
|
412
|
+
*/ export function contactSoloClause(clause) {
|
|
413
|
+
if (clause.field === 'createdAt') return true;
|
|
414
|
+
if (clause.field === 'name') return clause.op === 'startsWith' || clause.op === 'endsWith';
|
|
415
|
+
return clause.field === 'email' && clause.op === 'startsWith';
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the
|
|
419
|
+
* start of the contact's name, beside the scope clause.
|
|
420
|
+
*/ export const CONTACT_PREFIX_SEARCH = {
|
|
421
|
+
field: NAME_FIELD,
|
|
422
|
+
notice: 'Search matches the start of a contact’s name for access limited to specific sites.'
|
|
423
|
+
};
|
|
424
|
+
/** The facet field each grid column is keyed under. */ const FACET_FIELDS = {
|
|
425
|
+
[CRM_CONTACT_VIEW_FIELDS.owner]: 'owner',
|
|
426
|
+
[CRM_CONTACT_VIEW_FIELDS.stage]: 'stage',
|
|
427
|
+
[CRM_CONTACT_VIEW_FIELDS.source]: 'source',
|
|
428
|
+
[CRM_CONTACT_VIEW_FIELDS.company]: 'company',
|
|
429
|
+
[CRM_CONTACT_VIEW_FIELDS.tags]: 'tag',
|
|
430
|
+
[CRM_CONTACT_VIEW_FIELDS.orders]: 'orders',
|
|
431
|
+
[CRM_CONTACT_VIEW_FIELDS.ltv]: 'ltv'
|
|
432
|
+
};
|
|
433
|
+
/** Why a reader whose access is some sites cannot filter by a holder's field. */ const LIMITED_ACCESS = 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it';
|
|
434
|
+
/** The facet field a grid column names, or null for one that is not a facet field. */ function facetFieldOf(column) {
|
|
435
|
+
if (FACET_FIELDS[column]) return FACET_FIELDS[column];
|
|
436
|
+
if (column.startsWith(CRM_CONTACT_VIEW_FIELDS.custom)) {
|
|
437
|
+
return `custom.${column.slice(CRM_CONTACT_VIEW_FIELDS.custom.length)}`;
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* One grid clause as the clause the Contacts query asks — see the block at
|
|
443
|
+
* the top. A clause a query cannot serve for this reader is refused by
|
|
444
|
+
* name, never matched over the rows a page loaded.
|
|
445
|
+
*/ export function contactQueryClause(clause, reader) {
|
|
446
|
+
var _reader_groupId;
|
|
447
|
+
if (AS_STORED.has(clause.field)) {
|
|
448
|
+
if (clause.field === 'formIds' && !reader.foldsScope) return {
|
|
449
|
+
refused: LIMITED_ACCESS
|
|
450
|
+
};
|
|
451
|
+
return {
|
|
452
|
+
field: clause.field,
|
|
453
|
+
op: clause.op,
|
|
454
|
+
value: clause.value
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (clause.field === 'emailState') {
|
|
458
|
+
if (clause.op === 'isEmpty') return {
|
|
459
|
+
field: 'emailStatus',
|
|
460
|
+
op: 'equals',
|
|
461
|
+
value: 'none'
|
|
462
|
+
};
|
|
463
|
+
if (clause.op === 'isNotEmpty') return crmAnyOf('emailStatus', EMAIL_STATE_STATUSES);
|
|
464
|
+
const values = crmClauseValues(clause);
|
|
465
|
+
if (values.some((value)=>!EMAIL_STATE_STATUSES.includes(value))) {
|
|
466
|
+
return {
|
|
467
|
+
refused: 'pick one of the choices'
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return crmAnyOf('emailStatus', values);
|
|
471
|
+
}
|
|
472
|
+
const facet = facetFieldOf(clause.field);
|
|
473
|
+
if (!facet) return {
|
|
474
|
+
refused: 'this list does not filter by that'
|
|
475
|
+
};
|
|
476
|
+
if (!reader.foldsScope) return {
|
|
477
|
+
refused: LIMITED_ACCESS
|
|
478
|
+
};
|
|
479
|
+
const group = (_reader_groupId = reader.groupId) != null ? _reader_groupId : CRM_FACET_KEY_ANY_GROUP;
|
|
480
|
+
if (clause.op === 'isNotEmpty') {
|
|
481
|
+
return {
|
|
482
|
+
field: CONTACT_FACET_KEYS_PATH,
|
|
483
|
+
op: 'contains',
|
|
484
|
+
value: crmFacetKey(group, facet)
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const keys = [];
|
|
488
|
+
for (const value of crmClauseValues(clause)){
|
|
489
|
+
const key = crmFacetKeyValue(clause.op === '=' ? Number(value) : value);
|
|
490
|
+
if (key === null) return {
|
|
491
|
+
refused: 'no value yet'
|
|
492
|
+
};
|
|
493
|
+
keys.push(crmFacetKey(group, facet, key));
|
|
494
|
+
}
|
|
495
|
+
const asked = crmAnyOf(CONTACT_FACET_KEYS_PATH, keys);
|
|
496
|
+
// One key is an `array-contains`; several, an `array-contains-any`.
|
|
497
|
+
return 'refused' in asked || asked.op === 'isAnyOf' ? asked : _extends({}, asked, {
|
|
498
|
+
op: 'contains'
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Whether an asked clause stands in for the scope clause: a facet key of
|
|
503
|
+
* the viewing holder, which only a record that holder keeps carries, or a
|
|
504
|
+
* form, which only its site's captures carry.
|
|
505
|
+
*/ export function contactClauseImpliesScope(groupId) {
|
|
506
|
+
return (clause)=>{
|
|
507
|
+
if (clause.field === 'formIds') return true;
|
|
508
|
+
if (clause.field !== CONTACT_FACET_KEYS_PATH || !groupId) return false;
|
|
509
|
+
return crmClauseValues(clause).every((key)=>key.startsWith(`${groupId}:`));
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* The query shapes the Contacts list can send, as declarations the index
|
|
514
|
+
* pin enumerates (AGL-3321): the list's own, without its solo ranges, whose
|
|
515
|
+
* every equality rides `(field, updatedAt DESC)`; and each solo range beside
|
|
516
|
+
* the scope clause alone.
|
|
517
|
+
*/ export function contactIndexShapes() {
|
|
518
|
+
const soloOps = new Set([
|
|
519
|
+
'startsWith',
|
|
520
|
+
'endsWith'
|
|
521
|
+
]);
|
|
522
|
+
const main = _extends({}, CONTACT_LIST_DECLARATION, {
|
|
523
|
+
fields: CONTACT_QUERY_FIELDS.filter((field)=>field !== CREATED_FIELD).map((field)=>field.operators ? _extends({}, field, {
|
|
524
|
+
operators: field.operators.filter((op)=>!soloOps.has(op))
|
|
525
|
+
}) : field)
|
|
526
|
+
});
|
|
527
|
+
const solo = {
|
|
528
|
+
fields: [
|
|
529
|
+
_extends({}, NAME_FIELD, {
|
|
530
|
+
operators: [
|
|
531
|
+
'startsWith',
|
|
532
|
+
'endsWith'
|
|
533
|
+
]
|
|
534
|
+
}),
|
|
535
|
+
_extends({}, EMAIL_FIELD, {
|
|
536
|
+
operators: [
|
|
537
|
+
'startsWith'
|
|
538
|
+
]
|
|
539
|
+
}),
|
|
540
|
+
CREATED_FIELD
|
|
541
|
+
],
|
|
542
|
+
sorts: []
|
|
543
|
+
};
|
|
544
|
+
return [
|
|
545
|
+
main,
|
|
546
|
+
solo
|
|
547
|
+
];
|
|
548
|
+
}
|
|
317
549
|
|
|
318
550
|
//# sourceMappingURL=contact-filters.js.map
|