@aglyn/aglyn 1.0.0-beta.232 → 1.0.0-beta.234
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/admin-audit-index.d.ts +3 -1
- package/src/lib/app-utils/admin-audit-index.js +9 -1
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/analytics-summary.d.ts +95 -0
- package/src/lib/app-utils/analytics-summary.js +113 -0
- package/src/lib/app-utils/analytics-summary.js.map +1 -0
- package/src/lib/app-utils/artifact-list-keys.js +12 -0
- package/src/lib/app-utils/artifact-list-keys.js.map +1 -1
- package/src/lib/app-utils/artifact-list-queries.d.ts +16 -0
- package/src/lib/app-utils/artifact-list-queries.js +122 -4
- package/src/lib/app-utils/artifact-list-queries.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +30 -2
- package/src/lib/app-utils/crm.js +77 -15
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +17 -5
- package/src/lib/app-utils/docs-help.generated.js +35 -1
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +139 -6
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/lockdown.js +1 -1
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/message-search.js +5 -2
- package/src/lib/app-utils/message-search.js.map +1 -1
- package/src/lib/app-utils/name-search.js +15 -3
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/app-utils/screen-analytics-aggregate.d.ts +70 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js +82 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -0
- package/src/lib/app-utils/screen-kind.d.ts +25 -0
- package/src/lib/app-utils/screen-kind.js +25 -0
- package/src/lib/app-utils/screen-kind.js.map +1 -0
- package/src/lib/app-utils/screen-route.d.ts +1 -5
- package/src/lib/app-utils/screen-route.js +2 -4
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/site-journey-steps.d.ts +38 -0
- package/src/lib/app-utils/site-journey-steps.js +54 -0
- package/src/lib/app-utils/site-journey-steps.js.map +1 -0
- package/src/lib/app-utils/site-journey.d.ts +2 -22
- package/src/lib/app-utils/site-journey.js +2 -32
- package/src/lib/app-utils/site-journey.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +33 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +20 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-checkout-credits.d.ts +288 -0
- package/src/lib/plugin-manager/plugin-checkout-credits.js +196 -0
- package/src/lib/plugin-manager/plugin-checkout-credits.js.map +1 -0
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +87 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js +34 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -0
- package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -1
- package/src/lib/plugin-manager/stock-photo-provider.d.ts +141 -0
- package/src/lib/plugin-manager/stock-photo-provider.js +59 -0
- package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -0
|
@@ -58,7 +58,32 @@
|
|
|
58
58
|
* with the search anyway.
|
|
59
59
|
* - `Created` ranges: a second range order would double the composites for
|
|
60
60
|
* a question `Updated` answers nearly as well; one range per list.
|
|
61
|
-
*
|
|
61
|
+
*
|
|
62
|
+
* ## The header sorts (AGL-3680)
|
|
63
|
+
*
|
|
64
|
+
* Every column sorts on the QUERY, by a stored value every writer stamps:
|
|
65
|
+
*
|
|
66
|
+
* Display name `nameLower` (above; a starter page's is its starter's
|
|
67
|
+
* name, which is the name its library row shows);
|
|
68
|
+
* ID the walk itself, either way;
|
|
69
|
+
* Updated, `updatedAt` / `createdAt` — stamped by every create, and by
|
|
70
|
+
* Created `backfill-artifacts-list-keys.mjs` on a document that
|
|
71
|
+
* predates either;
|
|
72
|
+
* Used in, Kind the stored `kind`; Source the stored `source.type`;
|
|
73
|
+
* Description a layout's or component's `description`, stored as `null`
|
|
74
|
+
* when it has none (`artifactCreateListKeys` + the backfill),
|
|
75
|
+
* because an `orderBy` DROPS a document that lacks the field.
|
|
76
|
+
* A template row's description is its starter's when it
|
|
77
|
+
* leads one, which is no stored field: it sorts the page.
|
|
78
|
+
*
|
|
79
|
+
* Each is `alone` — served only while no filter or search is on, falling
|
|
80
|
+
* back to the list's default header order with a notice — so on the layouts
|
|
81
|
+
* and components subcollections it costs no composite. Updated newest-first
|
|
82
|
+
* is not `alone`: it is the order
|
|
83
|
+
* the Updated range already imposes, so it pairs with every equality and
|
|
84
|
+
* its composites are the ones the range needed anyway. The templates
|
|
85
|
+
* library's `libraryRow` scope is an equality every order pairs with, so
|
|
86
|
+
* each of its orders has one `(libraryRow, order)` composite.
|
|
62
87
|
*/ /** The walk every artifact list keeps: the document name, ascending. */ export const ARTIFACT_LIST_ORDER = {
|
|
63
88
|
path: LIST_QUERY_ID_PATH,
|
|
64
89
|
direction: 'asc'
|
|
@@ -94,6 +119,79 @@
|
|
|
94
119
|
/** The quick search every artifact list and gallery shelf offers: the name's word prefixes. */ export const ARTIFACT_NAME_SEARCH = {
|
|
95
120
|
tokensPath: 'nameTokens'
|
|
96
121
|
};
|
|
122
|
+
/** One header's two orders, `alone` (see "The header sorts"). */ const headerSorts = (column, path, label)=>[
|
|
123
|
+
{
|
|
124
|
+
path,
|
|
125
|
+
direction: 'asc',
|
|
126
|
+
column,
|
|
127
|
+
label,
|
|
128
|
+
alone: true
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
path,
|
|
132
|
+
direction: 'desc',
|
|
133
|
+
column,
|
|
134
|
+
label,
|
|
135
|
+
alone: true
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
/**
|
|
139
|
+
* The ID header: the walk ascending — the list's default, served under every
|
|
140
|
+
* filter — and descending, `alone`.
|
|
141
|
+
*/ const ID_SORTS = [
|
|
142
|
+
{
|
|
143
|
+
path: LIST_QUERY_ID_PATH,
|
|
144
|
+
direction: 'asc',
|
|
145
|
+
column: '$id',
|
|
146
|
+
label: 'ID'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
path: LIST_QUERY_ID_PATH,
|
|
150
|
+
direction: 'desc',
|
|
151
|
+
column: '$id',
|
|
152
|
+
label: 'ID',
|
|
153
|
+
alone: true
|
|
154
|
+
}
|
|
155
|
+
];
|
|
156
|
+
/**
|
|
157
|
+
* Updated newest first FIRST: it is the order the Updated range imposes
|
|
158
|
+
* (`planListQuery` takes the first declared order on the range's field), so
|
|
159
|
+
* it is not `alone` — the range's composites already serve it.
|
|
160
|
+
*/ const TIME_SORTS = [
|
|
161
|
+
{
|
|
162
|
+
path: 'updatedAt',
|
|
163
|
+
direction: 'desc',
|
|
164
|
+
column: 'updatedAt',
|
|
165
|
+
label: 'Updated'
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
path: 'updatedAt',
|
|
169
|
+
direction: 'asc',
|
|
170
|
+
column: 'updatedAt',
|
|
171
|
+
label: 'Updated',
|
|
172
|
+
alone: true
|
|
173
|
+
},
|
|
174
|
+
...headerSorts('createdAt', 'createdAt', 'Created')
|
|
175
|
+
];
|
|
176
|
+
const NAME_SORTS = headerSorts('displayName', 'nameLower', 'Display name');
|
|
177
|
+
const DESCRIPTION_SORTS = headerSorts('description', 'description', 'Description');
|
|
178
|
+
/** The layouts page's header sorts; the ID ascending one is the default. */ export const LAYOUT_LIST_SORTS = [
|
|
179
|
+
...ID_SORTS,
|
|
180
|
+
...NAME_SORTS,
|
|
181
|
+
...DESCRIPTION_SORTS,
|
|
182
|
+
...TIME_SORTS
|
|
183
|
+
];
|
|
184
|
+
/**
|
|
185
|
+
* The layouts list's scope: live layouts. Every create stores `deletedAt: null`
|
|
186
|
+
* (`artifactCreateListKeys`) and a delete stamps the time, so the query leaves
|
|
187
|
+
* tombstones out instead of the page dropping them after the read (AGL-3680).
|
|
188
|
+
*/ export const LAYOUT_LIST_BASE = [
|
|
189
|
+
{
|
|
190
|
+
path: 'deletedAt',
|
|
191
|
+
op: '==',
|
|
192
|
+
value: null
|
|
193
|
+
}
|
|
194
|
+
];
|
|
97
195
|
/** `hosts/{hostId}/layouts`. */ export const LAYOUT_LIST_QUERY = {
|
|
98
196
|
fields: [
|
|
99
197
|
NAME_FIELD,
|
|
@@ -101,7 +199,8 @@
|
|
|
101
199
|
UPDATED_FIELD
|
|
102
200
|
],
|
|
103
201
|
sorts: [
|
|
104
|
-
ARTIFACT_LIST_ORDER
|
|
202
|
+
ARTIFACT_LIST_ORDER,
|
|
203
|
+
...LAYOUT_LIST_SORTS
|
|
105
204
|
],
|
|
106
205
|
search: ARTIFACT_NAME_SEARCH
|
|
107
206
|
};
|
|
@@ -110,6 +209,13 @@ export const LAYOUT_LIST_HEADERS = {
|
|
|
110
209
|
$id: 'ID',
|
|
111
210
|
updatedAt: 'Updated'
|
|
112
211
|
};
|
|
212
|
+
/** The components card's header sorts; the ID ascending one is the default. */ export const COMPONENT_LIST_SORTS = [
|
|
213
|
+
...ID_SORTS,
|
|
214
|
+
...NAME_SORTS,
|
|
215
|
+
...headerSorts('kind', 'kind', 'Used in'),
|
|
216
|
+
...DESCRIPTION_SORTS,
|
|
217
|
+
...TIME_SORTS
|
|
218
|
+
];
|
|
113
219
|
/** `hosts/{hostId}/components`. */ export const COMPONENT_LIST_QUERY = {
|
|
114
220
|
fields: [
|
|
115
221
|
NAME_FIELD,
|
|
@@ -126,7 +232,8 @@ export const LAYOUT_LIST_HEADERS = {
|
|
|
126
232
|
UPDATED_FIELD
|
|
127
233
|
],
|
|
128
234
|
sorts: [
|
|
129
|
-
ARTIFACT_LIST_ORDER
|
|
235
|
+
ARTIFACT_LIST_ORDER,
|
|
236
|
+
...COMPONENT_LIST_SORTS
|
|
130
237
|
],
|
|
131
238
|
search: ARTIFACT_NAME_SEARCH
|
|
132
239
|
};
|
|
@@ -151,6 +258,16 @@ export const COMPONENT_LIST_HEADERS = {
|
|
|
151
258
|
value: true
|
|
152
259
|
}
|
|
153
260
|
];
|
|
261
|
+
/**
|
|
262
|
+
* The templates library's header sorts. It shows no ID column, so the walk
|
|
263
|
+
* stays the unlabelled default; Description sorts the page (see "The header
|
|
264
|
+
* sorts").
|
|
265
|
+
*/ export const TEMPLATE_LIST_SORTS = [
|
|
266
|
+
...NAME_SORTS,
|
|
267
|
+
...headerSorts('kind', 'kind', 'Kind'),
|
|
268
|
+
...headerSorts('source', 'source.type', 'Source'),
|
|
269
|
+
...TIME_SORTS
|
|
270
|
+
];
|
|
154
271
|
export const TEMPLATE_LIST_QUERY = {
|
|
155
272
|
fields: [
|
|
156
273
|
NAME_FIELD,
|
|
@@ -175,7 +292,8 @@ export const TEMPLATE_LIST_QUERY = {
|
|
|
175
292
|
UPDATED_FIELD
|
|
176
293
|
],
|
|
177
294
|
sorts: [
|
|
178
|
-
ARTIFACT_LIST_ORDER
|
|
295
|
+
ARTIFACT_LIST_ORDER,
|
|
296
|
+
...TEMPLATE_LIST_SORTS
|
|
179
297
|
],
|
|
180
298
|
search: ARTIFACT_NAME_SEARCH
|
|
181
299
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-list-queries.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 { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport {\n LIST_QUERY_ID_PATH,\n type ListQueryDeclaration,\n type ListQueryFilter,\n type ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * WHAT THE SITE ARTIFACT LISTS ASK FIRESTORE (AGL-3321).\n *\n * The layouts page, the components card and the templates library put every\n * clause their Filters panel accepts, and the quick-search word, on the query\n * — composed by `planListQuery` — and page what it answers. Nothing is matched\n * over the rows a page happened to read. The template gallery's shelves ask\n * the same keys (`template-gallery-dialog.component.tsx`), and so does the\n * email plugin's templates list, from its own declaration.\n *\n * ## The keys are WRITTEN\n *\n * A query finds a document by a stored value, so each list reads keys every\n * writer stamps (`artifactCreateListKeys`/`artifactRenameListKeys` in\n * `@aglyn/aglyn/app-utils/artifact-list-keys`) and\n * `tools/scripts/backfill-artifacts-list-keys.mjs` stamps on the documents\n * written before them:\n *\n * nameTokens the name's word prefixes — the search box and `contains`;\n * nameLower the normalized name — `equals`;\n * kind a component's `site`/`email`, a template's page/component/\n * layout, stored even where a reader used to infer it;\n * source.type a template's provenance, `authored` where there was none;\n * libraryRow a template that is its own library row (below).\n *\n * ## The order is the document NAME, and that is what keeps it cheap\n *\n * `hostArtifactQuery` explains why these lists walk the document id: it is\n * the one order no writer can omit, so the walk is total. It is also the\n * order index merging serves with Firestore's automatic single-field\n * indexes: any mix of the equalities and the one array clause above, ordered\n * by `__name__`, needs no composite at all. The composites below exist only\n * for the one range each list offers — `Updated` — which must lead the order\n * it ranges over, so it pairs with each equality once.\n *\n * ## What is not offered, and why\n *\n * - `startsWith`/`endsWith` on the name: a prefix range must lead the order,\n * and these lists are not sorted by name.\n * - `description` (a filter and a search target before): no token field,\n * and a second token array could not share the query's one array clause\n * with the search anyway.\n * - `Created` ranges: a second range order would double the composites for\n * a question `Updated` answers nearly as well; one range per list.\n * - Column sorting: the only declared order is the walk.\n */\n\n/** The walk every artifact list keeps: the document name, ascending. */\nexport const ARTIFACT_LIST_ORDER: ListQuerySort = {\n path: LIST_QUERY_ID_PATH,\n direction: 'asc',\n}\n\n/** The name, by word prefix (`contains`) or whole (`equals`). */\nconst NAME_FIELD: ListFilterField = {\n column: 'displayName',\n kind: 'text',\n path: 'displayName',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n operators: ['contains', 'equals'],\n}\n\n/** The document id, which the walk already orders by. */\nconst ID_FIELD: ListFilterField = {\n column: '$id',\n kind: 'id',\n path: LIST_QUERY_ID_PATH,\n}\n\n/** The one range: `updatedAt`, which every writer stamps. */\nconst UPDATED_FIELD: ListFilterField = {\n column: 'updatedAt',\n kind: 'date',\n path: 'updatedAt',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n}\n\n/** The quick search every artifact list and gallery shelf offers: the name's word prefixes. */\nexport const ARTIFACT_NAME_SEARCH = { tokensPath: 'nameTokens' }\n\n/** `hosts/{hostId}/layouts`. */\nexport const LAYOUT_LIST_QUERY: ListQueryDeclaration = {\n fields: [NAME_FIELD, ID_FIELD, UPDATED_FIELD],\n sorts: [ARTIFACT_LIST_ORDER],\n search: ARTIFACT_NAME_SEARCH,\n}\n\nexport const LAYOUT_LIST_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n $id: 'ID',\n updatedAt: 'Updated',\n}\n\n/** `hosts/{hostId}/components`. */\nexport const COMPONENT_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n NAME_FIELD,\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals', 'isAnyOf'] },\n ID_FIELD,\n UPDATED_FIELD,\n ],\n sorts: [ARTIFACT_LIST_ORDER],\n search: ARTIFACT_NAME_SEARCH,\n}\n\nexport const COMPONENT_LIST_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n kind: 'Used in',\n $id: 'ID',\n updatedAt: 'Updated',\n}\n\n/*\n * `hosts/{hostId}/templates`, as the library lists it: one row per template,\n * except that a multi-page STARTER is one row (AGL-696) — its first live page\n * carries `libraryRow: true` and the rest `false`, and a deleted template\n * `false`, so the base clause below is both \"one row per starter\" and \"no\n * tombstones\" without a single row being dropped after the read. Every page\n * of a starter carries the starter's name keys, so a search for it finds the\n * row whichever page leads it.\n */\nexport const TEMPLATE_LIST_BASE: readonly ListQueryFilter[] = [\n { path: 'libraryRow', op: '==', value: true },\n]\n\nexport const TEMPLATE_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n NAME_FIELD,\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals', 'isAnyOf'] },\n {\n column: 'source',\n kind: 'exact',\n path: 'source.type',\n operators: ['equals', 'isAnyOf'],\n },\n UPDATED_FIELD,\n ],\n sorts: [ARTIFACT_LIST_ORDER],\n search: ARTIFACT_NAME_SEARCH,\n}\n\nexport const TEMPLATE_LIST_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n kind: 'Kind',\n source: 'Source',\n updatedAt: 'Updated',\n}\n\n/** The template kinds, by the stored `kind`. */\nexport const TEMPLATE_KIND_OPTIONS: readonly ListFilterOption[] = [\n { value: 'page', label: 'Page' },\n { value: 'component', label: 'Component' },\n { value: 'layout', label: 'Layout' },\n]\n"],"names":["LIST_QUERY_ID_PATH","ARTIFACT_LIST_ORDER","path","direction","NAME_FIELD","column","kind","lowerPath","tokensPath","operators","ID_FIELD","UPDATED_FIELD","ARTIFACT_NAME_SEARCH","LAYOUT_LIST_QUERY","fields","sorts","search","LAYOUT_LIST_HEADERS","displayName","$id","updatedAt","COMPONENT_LIST_QUERY","COMPONENT_LIST_HEADERS","TEMPLATE_LIST_BASE","op","value","TEMPLATE_LIST_QUERY","TEMPLATE_LIST_HEADERS","source","TEMPLATE_KIND_OPTIONS","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SACEA,kBAAkB,QAIb,sDAAqD;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CC,GAED,sEAAsE,GACtE,OAAO,MAAMC,sBAAqC;IAChDC,MAAMF;IACNG,WAAW;AACb,EAAC;AAED,+DAA+D,GAC/D,MAAMC,aAA8B;IAClCC,QAAQ;IACRC,MAAM;IACNJ,MAAM;IACNK,WAAW;IACXC,YAAY;IACZC,WAAW;QAAC;QAAY;KAAS;AACnC;AAEA,uDAAuD,GACvD,MAAMC,WAA4B;IAChCL,QAAQ;IACRC,MAAM;IACNJ,MAAMF;AACR;AAEA,2DAA2D,GAC3D,MAAMW,gBAAiC;IACrCN,QAAQ;IACRC,MAAM;IACNJ,MAAM;IACNO,WAAW;QAAC;QAAM;QAAS;QAAa;QAAU;KAAa;AACjE;AAEA,6FAA6F,GAC7F,OAAO,MAAMG,uBAAuB;IAAEJ,YAAY;AAAa,EAAC;AAEhE,8BAA8B,GAC9B,OAAO,MAAMK,oBAA0C;IACrDC,QAAQ;QAACV;QAAYM;QAAUC;KAAc;IAC7CI,OAAO;QAACd;KAAoB;IAC5Be,QAAQJ;AACV,EAAC;AAED,OAAO,MAAMK,sBAAwD;IACnEC,aAAa;IACbC,KAAK;IACLC,WAAW;AACb,EAAC;AAED,iCAAiC,GACjC,OAAO,MAAMC,uBAA6C;IACxDP,QAAQ;QACNV;QACA;YAAEC,QAAQ;YAAQC,MAAM;YAASJ,MAAM;YAAQO,WAAW;gBAAC;gBAAU;aAAU;QAAC;QAChFC;QACAC;KACD;IACDI,OAAO;QAACd;KAAoB;IAC5Be,QAAQJ;AACV,EAAC;AAED,OAAO,MAAMU,yBAA2D;IACtEJ,aAAa;IACbZ,MAAM;IACNa,KAAK;IACLC,WAAW;AACb,EAAC;AAED;;;;;;;;CAQC,GACD,OAAO,MAAMG,qBAAiD;IAC5D;QAAErB,MAAM;QAAcsB,IAAI;QAAMC,OAAO;IAAK;CAC7C,CAAA;AAED,OAAO,MAAMC,sBAA4C;IACvDZ,QAAQ;QACNV;QACA;YAAEC,QAAQ;YAAQC,MAAM;YAASJ,MAAM;YAAQO,WAAW;gBAAC;gBAAU;aAAU;QAAC;QAChF;YACEJ,QAAQ;YACRC,MAAM;YACNJ,MAAM;YACNO,WAAW;gBAAC;gBAAU;aAAU;QAClC;QACAE;KACD;IACDI,OAAO;QAACd;KAAoB;IAC5Be,QAAQJ;AACV,EAAC;AAED,OAAO,MAAMe,wBAA0D;IACrET,aAAa;IACbZ,MAAM;IACNsB,QAAQ;IACRR,WAAW;AACb,EAAC;AAED,8CAA8C,GAC9C,OAAO,MAAMS,wBAAqD;IAChE;QAAEJ,OAAO;QAAQK,OAAO;IAAO;IAC/B;QAAEL,OAAO;QAAaK,OAAO;IAAY;IACzC;QAAEL,OAAO;QAAUK,OAAO;IAAS;CACpC,CAAA"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-list-queries.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 { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport {\n LIST_QUERY_ID_PATH,\n type ListQueryDeclaration,\n type ListQueryFilter,\n type ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * WHAT THE SITE ARTIFACT LISTS ASK FIRESTORE (AGL-3321).\n *\n * The layouts page, the components card and the templates library put every\n * clause their Filters panel accepts, and the quick-search word, on the query\n * — composed by `planListQuery` — and page what it answers. Nothing is matched\n * over the rows a page happened to read. The template gallery's shelves ask\n * the same keys (`template-gallery-dialog.component.tsx`), and so does the\n * email plugin's templates list, from its own declaration.\n *\n * ## The keys are WRITTEN\n *\n * A query finds a document by a stored value, so each list reads keys every\n * writer stamps (`artifactCreateListKeys`/`artifactRenameListKeys` in\n * `@aglyn/aglyn/app-utils/artifact-list-keys`) and\n * `tools/scripts/backfill-artifacts-list-keys.mjs` stamps on the documents\n * written before them:\n *\n * nameTokens the name's word prefixes — the search box and `contains`;\n * nameLower the normalized name — `equals`;\n * kind a component's `site`/`email`, a template's page/component/\n * layout, stored even where a reader used to infer it;\n * source.type a template's provenance, `authored` where there was none;\n * libraryRow a template that is its own library row (below).\n *\n * ## The order is the document NAME, and that is what keeps it cheap\n *\n * `hostArtifactQuery` explains why these lists walk the document id: it is\n * the one order no writer can omit, so the walk is total. It is also the\n * order index merging serves with Firestore's automatic single-field\n * indexes: any mix of the equalities and the one array clause above, ordered\n * by `__name__`, needs no composite at all. The composites below exist only\n * for the one range each list offers — `Updated` — which must lead the order\n * it ranges over, so it pairs with each equality once.\n *\n * ## What is not offered, and why\n *\n * - `startsWith`/`endsWith` on the name: a prefix range must lead the order,\n * and these lists are not sorted by name.\n * - `description` (a filter and a search target before): no token field,\n * and a second token array could not share the query's one array clause\n * with the search anyway.\n * - `Created` ranges: a second range order would double the composites for\n * a question `Updated` answers nearly as well; one range per list.\n *\n * ## The header sorts (AGL-3680)\n *\n * Every column sorts on the QUERY, by a stored value every writer stamps:\n *\n * Display name `nameLower` (above; a starter page's is its starter's\n * name, which is the name its library row shows);\n * ID the walk itself, either way;\n * Updated, `updatedAt` / `createdAt` — stamped by every create, and by\n * Created `backfill-artifacts-list-keys.mjs` on a document that\n * predates either;\n * Used in, Kind the stored `kind`; Source the stored `source.type`;\n * Description a layout's or component's `description`, stored as `null`\n * when it has none (`artifactCreateListKeys` + the backfill),\n * because an `orderBy` DROPS a document that lacks the field.\n * A template row's description is its starter's when it\n * leads one, which is no stored field: it sorts the page.\n *\n * Each is `alone` — served only while no filter or search is on, falling\n * back to the list's default header order with a notice — so on the layouts\n * and components subcollections it costs no composite. Updated newest-first\n * is not `alone`: it is the order\n * the Updated range already imposes, so it pairs with every equality and\n * its composites are the ones the range needed anyway. The templates\n * library's `libraryRow` scope is an equality every order pairs with, so\n * each of its orders has one `(libraryRow, order)` composite.\n */\n\n/** The walk every artifact list keeps: the document name, ascending. */\nexport const ARTIFACT_LIST_ORDER: ListQuerySort = {\n path: LIST_QUERY_ID_PATH,\n direction: 'asc',\n}\n\n/** The name, by word prefix (`contains`) or whole (`equals`). */\nconst NAME_FIELD: ListFilterField = {\n column: 'displayName',\n kind: 'text',\n path: 'displayName',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n operators: ['contains', 'equals'],\n}\n\n/** The document id, which the walk already orders by. */\nconst ID_FIELD: ListFilterField = {\n column: '$id',\n kind: 'id',\n path: LIST_QUERY_ID_PATH,\n}\n\n/** The one range: `updatedAt`, which every writer stamps. */\nconst UPDATED_FIELD: ListFilterField = {\n column: 'updatedAt',\n kind: 'date',\n path: 'updatedAt',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n}\n\n/** The quick search every artifact list and gallery shelf offers: the name's word prefixes. */\nexport const ARTIFACT_NAME_SEARCH = { tokensPath: 'nameTokens' }\n\n/** One header's two orders, `alone` (see \"The header sorts\"). */\nconst headerSorts = (column: string, path: string, label: string): ListQuerySort[] => [\n { path, direction: 'asc', column, label, alone: true },\n { path, direction: 'desc', column, label, alone: true },\n]\n\n/**\n * The ID header: the walk ascending — the list's default, served under every\n * filter — and descending, `alone`.\n */\nconst ID_SORTS: readonly ListQuerySort[] = [\n { path: LIST_QUERY_ID_PATH, direction: 'asc', column: '$id', label: 'ID' },\n { path: LIST_QUERY_ID_PATH, direction: 'desc', column: '$id', label: 'ID', alone: true },\n]\n\n/**\n * Updated newest first FIRST: it is the order the Updated range imposes\n * (`planListQuery` takes the first declared order on the range's field), so\n * it is not `alone` — the range's composites already serve it.\n */\nconst TIME_SORTS: readonly ListQuerySort[] = [\n { path: 'updatedAt', direction: 'desc', column: 'updatedAt', label: 'Updated' },\n { path: 'updatedAt', direction: 'asc', column: 'updatedAt', label: 'Updated', alone: true },\n ...headerSorts('createdAt', 'createdAt', 'Created'),\n]\n\nconst NAME_SORTS = headerSorts('displayName', 'nameLower', 'Display name')\nconst DESCRIPTION_SORTS = headerSorts('description', 'description', 'Description')\n\n/** The layouts page's header sorts; the ID ascending one is the default. */\nexport const LAYOUT_LIST_SORTS: readonly ListQuerySort[] = [\n ...ID_SORTS,\n ...NAME_SORTS,\n ...DESCRIPTION_SORTS,\n ...TIME_SORTS,\n]\n\n/**\n * The layouts list's scope: live layouts. Every create stores `deletedAt: null`\n * (`artifactCreateListKeys`) and a delete stamps the time, so the query leaves\n * tombstones out instead of the page dropping them after the read (AGL-3680).\n */\nexport const LAYOUT_LIST_BASE: readonly ListQueryFilter[] = [\n { path: 'deletedAt', op: '==', value: null },\n]\n\n/** `hosts/{hostId}/layouts`. */\nexport const LAYOUT_LIST_QUERY: ListQueryDeclaration = {\n fields: [NAME_FIELD, ID_FIELD, UPDATED_FIELD],\n sorts: [ARTIFACT_LIST_ORDER, ...LAYOUT_LIST_SORTS],\n search: ARTIFACT_NAME_SEARCH,\n}\n\nexport const LAYOUT_LIST_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n $id: 'ID',\n updatedAt: 'Updated',\n}\n\n/** The components card's header sorts; the ID ascending one is the default. */\nexport const COMPONENT_LIST_SORTS: readonly ListQuerySort[] = [\n ...ID_SORTS,\n ...NAME_SORTS,\n ...headerSorts('kind', 'kind', 'Used in'),\n ...DESCRIPTION_SORTS,\n ...TIME_SORTS,\n]\n\n/** `hosts/{hostId}/components`. */\nexport const COMPONENT_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n NAME_FIELD,\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals', 'isAnyOf'] },\n ID_FIELD,\n UPDATED_FIELD,\n ],\n sorts: [ARTIFACT_LIST_ORDER, ...COMPONENT_LIST_SORTS],\n search: ARTIFACT_NAME_SEARCH,\n}\n\nexport const COMPONENT_LIST_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n kind: 'Used in',\n $id: 'ID',\n updatedAt: 'Updated',\n}\n\n/*\n * `hosts/{hostId}/templates`, as the library lists it: one row per template,\n * except that a multi-page STARTER is one row (AGL-696) — its first live page\n * carries `libraryRow: true` and the rest `false`, and a deleted template\n * `false`, so the base clause below is both \"one row per starter\" and \"no\n * tombstones\" without a single row being dropped after the read. Every page\n * of a starter carries the starter's name keys, so a search for it finds the\n * row whichever page leads it.\n */\nexport const TEMPLATE_LIST_BASE: readonly ListQueryFilter[] = [\n { path: 'libraryRow', op: '==', value: true },\n]\n\n/**\n * The templates library's header sorts. It shows no ID column, so the walk\n * stays the unlabelled default; Description sorts the page (see \"The header\n * sorts\").\n */\nexport const TEMPLATE_LIST_SORTS: readonly ListQuerySort[] = [\n ...NAME_SORTS,\n ...headerSorts('kind', 'kind', 'Kind'),\n ...headerSorts('source', 'source.type', 'Source'),\n ...TIME_SORTS,\n]\n\nexport const TEMPLATE_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n NAME_FIELD,\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals', 'isAnyOf'] },\n {\n column: 'source',\n kind: 'exact',\n path: 'source.type',\n operators: ['equals', 'isAnyOf'],\n },\n UPDATED_FIELD,\n ],\n sorts: [ARTIFACT_LIST_ORDER, ...TEMPLATE_LIST_SORTS],\n search: ARTIFACT_NAME_SEARCH,\n}\n\nexport const TEMPLATE_LIST_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n kind: 'Kind',\n source: 'Source',\n updatedAt: 'Updated',\n}\n\n/** The template kinds, by the stored `kind`. */\nexport const TEMPLATE_KIND_OPTIONS: readonly ListFilterOption[] = [\n { value: 'page', label: 'Page' },\n { value: 'component', label: 'Component' },\n { value: 'layout', label: 'Layout' },\n]\n"],"names":["LIST_QUERY_ID_PATH","ARTIFACT_LIST_ORDER","path","direction","NAME_FIELD","column","kind","lowerPath","tokensPath","operators","ID_FIELD","UPDATED_FIELD","ARTIFACT_NAME_SEARCH","headerSorts","label","alone","ID_SORTS","TIME_SORTS","NAME_SORTS","DESCRIPTION_SORTS","LAYOUT_LIST_SORTS","LAYOUT_LIST_BASE","op","value","LAYOUT_LIST_QUERY","fields","sorts","search","LAYOUT_LIST_HEADERS","displayName","$id","updatedAt","COMPONENT_LIST_SORTS","COMPONENT_LIST_QUERY","COMPONENT_LIST_HEADERS","TEMPLATE_LIST_BASE","TEMPLATE_LIST_SORTS","TEMPLATE_LIST_QUERY","TEMPLATE_LIST_HEADERS","source","TEMPLATE_KIND_OPTIONS"],"mappings":"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"}
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
* Pure data module: types, constants and the small helpers every surface
|
|
44
44
|
* would otherwise write for itself. No Firestore, no React.
|
|
45
45
|
*/
|
|
46
|
-
import { type AglynPostalAddress
|
|
46
|
+
import { type AglynPostalAddress } from '../foundation/definitions/contact.types';
|
|
47
|
+
import type { OrgCrmAssignmentRule } from '../foundation/definitions/org-billing.types';
|
|
47
48
|
import { type ConsentGroup } from './consent-groups';
|
|
48
49
|
import { type ContactLifecycleStage, type CrmActivityKind, type CrmTaskKind } from './crm-kinds';
|
|
49
50
|
import { type ContactInteraction, type ContactSegment, type ContactSource } from './contacts';
|
|
@@ -2986,6 +2987,17 @@ export interface CrmListFieldsContext {
|
|
|
2986
2987
|
*/
|
|
2987
2988
|
leadSources?: PicklistValueSet | null;
|
|
2988
2989
|
}
|
|
2990
|
+
/** A text value as a header sort compares it: its key, `null` for none. */
|
|
2991
|
+
export declare function crmSortKey(value: unknown): string | null;
|
|
2992
|
+
/** A number as a header sort reads it: itself when finite, `null` for none. */
|
|
2993
|
+
export declare function crmSortNumber(value: unknown): number | null;
|
|
2994
|
+
/**
|
|
2995
|
+
* The key a person's row sorts by under its Name header: the name, or the
|
|
2996
|
+
* address the row shows in its place when there is none.
|
|
2997
|
+
*/
|
|
2998
|
+
export declare function crmPersonSortKey(record: Record<string, unknown>): string | null;
|
|
2999
|
+
/** The field a lead's and a contact's Name header sorts by. */
|
|
3000
|
+
export declare const CRM_NAME_SORT_FIELD = "nameSortKey";
|
|
2989
3001
|
/** What a lead's search box reads (AGL-3246). */
|
|
2990
3002
|
export declare const CRM_LEAD_SEARCH_SOURCES: readonly ["name", "email", "company", "jobTitle", "tags"];
|
|
2991
3003
|
/**
|
|
@@ -3013,6 +3025,9 @@ export declare function crmLeadListFields(record: object, context?: CrmListField
|
|
|
3013
3025
|
emailStatus: string;
|
|
3014
3026
|
industryKey: string | null;
|
|
3015
3027
|
ratingKey: string | null;
|
|
3028
|
+
nameSortKey: string | null;
|
|
3029
|
+
companyLower: string | null;
|
|
3030
|
+
jobTitleLower: string | null;
|
|
3016
3031
|
};
|
|
3017
3032
|
/** What a company's search box reads: its name and its domain. */
|
|
3018
3033
|
export declare const CRM_COMPANY_SEARCH_SOURCES: readonly ["name", "domain"];
|
|
@@ -3045,6 +3060,8 @@ export declare function crmDealListFields(record: object): {
|
|
|
3045
3060
|
contactRoleContactIds: string[];
|
|
3046
3061
|
scopedContactRoleContactIds: string[];
|
|
3047
3062
|
contactRoleKeys: string[];
|
|
3063
|
+
amountSortCents: number | null;
|
|
3064
|
+
expectedCloseSortAtMs: number | null;
|
|
3048
3065
|
};
|
|
3049
3066
|
/**
|
|
3050
3067
|
* The arrays a deal's contact roles are found by (AGL-3521): every contact
|
|
@@ -3059,10 +3076,20 @@ export declare function crmDealContactRoleListFields(deal: Record<string, unknow
|
|
|
3059
3076
|
scopedContactRoleContactIds: string[];
|
|
3060
3077
|
contactRoleKeys: string[];
|
|
3061
3078
|
};
|
|
3062
|
-
/**
|
|
3079
|
+
/** A task's priorities, lowest first: the rank its Priority header sorts by (AGL-3680). */
|
|
3080
|
+
export declare const CRM_TASK_PRIORITY_RANK: readonly CrmTaskPriority[];
|
|
3081
|
+
/** A task's priority rank — `normal`'s for one that names none. */
|
|
3082
|
+
export declare function crmTaskPriorityRank(priority: unknown): number;
|
|
3083
|
+
/**
|
|
3084
|
+
* What a task's search box reads — its title — and what its Task and
|
|
3085
|
+
* Priority headers sort by (AGL-3680): the title's key and the priority's
|
|
3086
|
+
* rank.
|
|
3087
|
+
*/
|
|
3063
3088
|
export declare function crmTaskListFields(record: object): {
|
|
3064
3089
|
searchTokens: string[];
|
|
3065
3090
|
scopedSearchTokens: string[];
|
|
3091
|
+
titleLower: string | null;
|
|
3092
|
+
priorityRank: number;
|
|
3066
3093
|
};
|
|
3067
3094
|
export declare const CRM_CONTACT_FACET_KEYS_FIELD = "facetKeys";
|
|
3068
3095
|
/** The group a key names at the organization level: any holder. */
|
|
@@ -3096,6 +3123,7 @@ export declare function crmContactListFields(record: object): {
|
|
|
3096
3123
|
scopedSearchTokens: string[];
|
|
3097
3124
|
facetKeys: string[];
|
|
3098
3125
|
emailStatus: string;
|
|
3126
|
+
nameSortKey: string | null;
|
|
3099
3127
|
};
|
|
3100
3128
|
/**
|
|
3101
3129
|
* The stored fields each collection's list fields are computed from. A
|
package/src/lib/app-utils/crm.js
CHANGED
|
@@ -42,7 +42,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
42
42
|
*
|
|
43
43
|
* Pure data module: types, constants and the small helpers every surface
|
|
44
44
|
* would otherwise write for itself. No Firestore, no React.
|
|
45
|
-
*/ import { normalizeAddress, normalizePhone } from "../foundation/
|
|
45
|
+
*/ import { normalizeAddress, normalizePhone } from "../foundation/definitions/contact.types.js";
|
|
46
46
|
import { consentGroupScope } from "./consent-groups.js";
|
|
47
47
|
import { CONTACT_LIFECYCLE_STAGES, CRM_TASK_KINDS, isContactLifecycleStage } from "./crm-kinds.js";
|
|
48
48
|
import { CONTACT_ALTERNATE_EMAILS_FIELD, CONTACT_FACETS_FIELD, CONTACT_SOURCE_LABELS, composeContactName, normalizeContactEmail, readContactFacet } from "./contacts.js";
|
|
@@ -1472,8 +1472,8 @@ const blank = (value)=>value === null || typeof value === 'string' && value.trim
|
|
|
1472
1472
|
*/ export const COMPANY_CONTACTS_COUNT_FIELD = 'contactsCount';
|
|
1473
1473
|
/** The link state of a stored contact, read for one holder. */ export function readContactCompanyLink(contact, groupId) {
|
|
1474
1474
|
var _readContactFacet_companyId;
|
|
1475
|
-
const
|
|
1476
|
-
const facets =
|
|
1475
|
+
const stored = contact != null ? contact : {};
|
|
1476
|
+
const facets = stored[CONTACT_FACETS_FIELD];
|
|
1477
1477
|
const heldElsewhere = new Set();
|
|
1478
1478
|
if (facets && typeof facets === 'object' && !Array.isArray(facets)) {
|
|
1479
1479
|
for (const [holder, facet] of Object.entries(facets)){
|
|
@@ -1482,9 +1482,9 @@ const blank = (value)=>value === null || typeof value === 'string' && value.trim
|
|
|
1482
1482
|
if (typeof named === 'string' && named) heldElsewhere.add(named);
|
|
1483
1483
|
}
|
|
1484
1484
|
}
|
|
1485
|
-
const mirror =
|
|
1485
|
+
const mirror = stored[CONTACT_COMPANY_IDS_FIELD];
|
|
1486
1486
|
return {
|
|
1487
|
-
companyId: (_readContactFacet_companyId = readContactFacet(
|
|
1487
|
+
companyId: (_readContactFacet_companyId = readContactFacet(stored, groupId).companyId) != null ? _readContactFacet_companyId : null,
|
|
1488
1488
|
companyIds: Array.isArray(mirror) ? mirror.filter((id)=>typeof id === 'string' && !!id) : [],
|
|
1489
1489
|
heldElsewhere: [
|
|
1490
1490
|
...heldElsewhere
|
|
@@ -3880,10 +3880,14 @@ const SEARCH_KEY = nameSearchKey;
|
|
|
3880
3880
|
*/ export function crmSearchTokens(values) {
|
|
3881
3881
|
const tokens = new Set();
|
|
3882
3882
|
const addWord = (word)=>{
|
|
3883
|
-
|
|
3883
|
+
// By codepoint, as `nameSearchTokens` cuts: a UTF-16 slice leaves a lone
|
|
3884
|
+
// surrogate of an emoji, and Firestore refuses the write.
|
|
3885
|
+
const capped = [
|
|
3886
|
+
...word
|
|
3887
|
+
].slice(0, NAME_TOKEN_MAX_PREFIX);
|
|
3884
3888
|
for(let end = 1; end <= capped.length; end += 1){
|
|
3885
3889
|
if (tokens.size >= CRM_SEARCH_TOKENS_MAX) return;
|
|
3886
|
-
tokens.add(capped.slice(0, end));
|
|
3890
|
+
tokens.add(capped.slice(0, end).join(''));
|
|
3887
3891
|
}
|
|
3888
3892
|
};
|
|
3889
3893
|
const addText = (text)=>{
|
|
@@ -3982,6 +3986,33 @@ export function isCrmLeadSourceDirection(value) {
|
|
|
3982
3986
|
leadSourceDirection: crmLeadSourceDirection(context.leadSources, lead['leadSource'])
|
|
3983
3987
|
} : {};
|
|
3984
3988
|
}
|
|
3989
|
+
/*------------------------------------------
|
|
3990
|
+
* THE HEADER SORT KEYS (AGL-3680).
|
|
3991
|
+
*
|
|
3992
|
+
* Every CRM table sorts by its column headers, and a header backed by a
|
|
3993
|
+
* stored field sorts on the QUERY — but Firestore orders text by its bytes
|
|
3994
|
+
* (every capital before every lower-case letter) and leaves a document
|
|
3995
|
+
* WITHOUT the ordered field out of the answer. So a text column sorts by a
|
|
3996
|
+
* key stored beside it on every record: the value as a reader compares it
|
|
3997
|
+
* (lower-cased, single-spaced — `nameSearchKey`), `null` for none, never
|
|
3998
|
+
* absent. A meaning with an order of its own (a task's priority) sorts by
|
|
3999
|
+
* its rank. Computed from the record like every other list field, so each
|
|
4000
|
+
* writer that stamps `crmListFields` stamps these, and the restamp after a
|
|
4001
|
+
* patch keeps them level.
|
|
4002
|
+
*-----------------------------------------*/ /** A text value as a header sort compares it: its key, `null` for none. */ export function crmSortKey(value) {
|
|
4003
|
+
return typeof value === 'string' && SEARCH_KEY(value) || null;
|
|
4004
|
+
}
|
|
4005
|
+
/** A number as a header sort reads it: itself when finite, `null` for none. */ export function crmSortNumber(value) {
|
|
4006
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
4007
|
+
}
|
|
4008
|
+
/**
|
|
4009
|
+
* The key a person's row sorts by under its Name header: the name, or the
|
|
4010
|
+
* address the row shows in its place when there is none.
|
|
4011
|
+
*/ export function crmPersonSortKey(record) {
|
|
4012
|
+
var _crmSortKey;
|
|
4013
|
+
return (_crmSortKey = crmSortKey(record['name'])) != null ? _crmSortKey : crmSortKey(record['email']);
|
|
4014
|
+
}
|
|
4015
|
+
/** The field a lead's and a contact's Name header sorts by. */ export const CRM_NAME_SORT_FIELD = 'nameSortKey';
|
|
3985
4016
|
/** What a lead's search box reads (AGL-3246). */ export const CRM_LEAD_SEARCH_SOURCES = [
|
|
3986
4017
|
'name',
|
|
3987
4018
|
'email',
|
|
@@ -4015,7 +4046,11 @@ function leadCampaignIds(lead) {
|
|
|
4015
4046
|
emailStatus: crmEmailStatusKey(lead),
|
|
4016
4047
|
// The Industry and Rating the Leads list filters by (AGL-3513).
|
|
4017
4048
|
industryKey: crmPicklistKey(lead['industry']),
|
|
4018
|
-
ratingKey: crmPicklistKey(lead['rating'])
|
|
4049
|
+
ratingKey: crmPicklistKey(lead['rating']),
|
|
4050
|
+
// The Lead, Company and Title headers' sort keys (AGL-3680).
|
|
4051
|
+
nameSortKey: crmPersonSortKey(lead),
|
|
4052
|
+
companyLower: crmSortKey(lead['company']),
|
|
4053
|
+
jobTitleLower: crmSortKey(lead['jobTitle'])
|
|
4019
4054
|
});
|
|
4020
4055
|
}
|
|
4021
4056
|
/** What a company's search box reads: its name and its domain. */ export const CRM_COMPANY_SEARCH_SOURCES = [
|
|
@@ -4049,7 +4084,12 @@ function leadCampaignIds(lead) {
|
|
|
4049
4084
|
titleLower: SEARCH_KEY(typeof deal['title'] === 'string' ? deal['title'] : ''),
|
|
4050
4085
|
typeKey: crmPicklistKey(deal['type']),
|
|
4051
4086
|
leadSourceKey: crmPicklistKey(deal['leadSource'])
|
|
4052
|
-
}, crmDealContactRoleListFields(deal)
|
|
4087
|
+
}, crmDealContactRoleListFields(deal), {
|
|
4088
|
+
// The Amount and Expected close headers' sort keys (AGL-3680): both
|
|
4089
|
+
// optional on a deal, so stored beside them as `null` for none.
|
|
4090
|
+
amountSortCents: crmSortNumber(deal['amountCents']),
|
|
4091
|
+
expectedCloseSortAtMs: crmSortNumber(deal['expectedCloseAtMs'])
|
|
4092
|
+
});
|
|
4053
4093
|
}
|
|
4054
4094
|
/**
|
|
4055
4095
|
* The arrays a deal's contact roles are found by (AGL-3521): every contact
|
|
@@ -4074,11 +4114,27 @@ function leadCampaignIds(lead) {
|
|
|
4074
4114
|
]
|
|
4075
4115
|
};
|
|
4076
4116
|
}
|
|
4077
|
-
/**
|
|
4117
|
+
/** A task's priorities, lowest first: the rank its Priority header sorts by (AGL-3680). */ export const CRM_TASK_PRIORITY_RANK = [
|
|
4118
|
+
'low',
|
|
4119
|
+
'normal',
|
|
4120
|
+
'high'
|
|
4121
|
+
];
|
|
4122
|
+
/** A task's priority rank — `normal`'s for one that names none. */ export function crmTaskPriorityRank(priority) {
|
|
4123
|
+
const at = CRM_TASK_PRIORITY_RANK.indexOf(priority);
|
|
4124
|
+
return at === -1 ? CRM_TASK_PRIORITY_RANK.indexOf('normal') : at;
|
|
4125
|
+
}
|
|
4126
|
+
/**
|
|
4127
|
+
* What a task's search box reads — its title — and what its Task and
|
|
4128
|
+
* Priority headers sort by (AGL-3680): the title's key and the priority's
|
|
4129
|
+
* rank.
|
|
4130
|
+
*/ export function crmTaskListFields(record) {
|
|
4078
4131
|
const task = record;
|
|
4079
|
-
return crmSearchFields(task['visibleTo'], [
|
|
4132
|
+
return _extends({}, crmSearchFields(task['visibleTo'], [
|
|
4080
4133
|
task['title']
|
|
4081
|
-
])
|
|
4134
|
+
]), {
|
|
4135
|
+
titleLower: crmSortKey(task['title']),
|
|
4136
|
+
priorityRank: crmTaskPriorityRank(task['priority'])
|
|
4137
|
+
});
|
|
4082
4138
|
}
|
|
4083
4139
|
/*------------------------------------------
|
|
4084
4140
|
* A CONTACT'S FACET KEYS.
|
|
@@ -4200,7 +4256,9 @@ export function crmContactListFields(record) {
|
|
|
4200
4256
|
crmPhoneSearchWords(contact['phone'])
|
|
4201
4257
|
]), {
|
|
4202
4258
|
facetKeys: crmContactFacetKeys(contact),
|
|
4203
|
-
emailStatus: crmEmailStatusKey(contact)
|
|
4259
|
+
emailStatus: crmEmailStatusKey(contact),
|
|
4260
|
+
// The Contact header's sort key (AGL-3680): the canonical name, or the address.
|
|
4261
|
+
nameSortKey: crmPersonSortKey(contact)
|
|
4204
4262
|
});
|
|
4205
4263
|
}
|
|
4206
4264
|
/**
|
|
@@ -4240,11 +4298,15 @@ export function crmContactListFields(record) {
|
|
|
4240
4298
|
'type',
|
|
4241
4299
|
'leadSource',
|
|
4242
4300
|
'contactRoles',
|
|
4243
|
-
'contactId'
|
|
4301
|
+
'contactId',
|
|
4302
|
+
// The header sort keys (AGL-3680).
|
|
4303
|
+
'amountCents',
|
|
4304
|
+
'expectedCloseAtMs'
|
|
4244
4305
|
],
|
|
4245
4306
|
crmTasks: [
|
|
4246
4307
|
'visibleTo',
|
|
4247
|
-
'title'
|
|
4308
|
+
'title',
|
|
4309
|
+
'priority'
|
|
4248
4310
|
]
|
|
4249
4311
|
};
|
|
4250
4312
|
/**
|