@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.
Files changed (56) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/admin-audit-index.d.ts +3 -1
  3. package/src/lib/app-utils/admin-audit-index.js +9 -1
  4. package/src/lib/app-utils/admin-audit-index.js.map +1 -1
  5. package/src/lib/app-utils/analytics-summary.d.ts +95 -0
  6. package/src/lib/app-utils/analytics-summary.js +113 -0
  7. package/src/lib/app-utils/analytics-summary.js.map +1 -0
  8. package/src/lib/app-utils/artifact-list-keys.js +12 -0
  9. package/src/lib/app-utils/artifact-list-keys.js.map +1 -1
  10. package/src/lib/app-utils/artifact-list-queries.d.ts +16 -0
  11. package/src/lib/app-utils/artifact-list-queries.js +122 -4
  12. package/src/lib/app-utils/artifact-list-queries.js.map +1 -1
  13. package/src/lib/app-utils/crm.d.ts +30 -2
  14. package/src/lib/app-utils/crm.js +77 -15
  15. package/src/lib/app-utils/crm.js.map +1 -1
  16. package/src/lib/app-utils/docs-help.generated.d.ts +17 -5
  17. package/src/lib/app-utils/docs-help.generated.js +35 -1
  18. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  19. package/src/lib/app-utils/docs-index.generated.js +139 -6
  20. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  21. package/src/lib/app-utils/lockdown.js +1 -1
  22. package/src/lib/app-utils/lockdown.js.map +1 -1
  23. package/src/lib/app-utils/message-search.js +5 -2
  24. package/src/lib/app-utils/message-search.js.map +1 -1
  25. package/src/lib/app-utils/name-search.js +15 -3
  26. package/src/lib/app-utils/name-search.js.map +1 -1
  27. package/src/lib/app-utils/screen-analytics-aggregate.d.ts +70 -0
  28. package/src/lib/app-utils/screen-analytics-aggregate.js +82 -0
  29. package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -0
  30. package/src/lib/app-utils/screen-kind.d.ts +25 -0
  31. package/src/lib/app-utils/screen-kind.js +25 -0
  32. package/src/lib/app-utils/screen-kind.js.map +1 -0
  33. package/src/lib/app-utils/screen-route.d.ts +1 -5
  34. package/src/lib/app-utils/screen-route.js +2 -4
  35. package/src/lib/app-utils/screen-route.js.map +1 -1
  36. package/src/lib/app-utils/site-journey-steps.d.ts +38 -0
  37. package/src/lib/app-utils/site-journey-steps.js +54 -0
  38. package/src/lib/app-utils/site-journey-steps.js.map +1 -0
  39. package/src/lib/app-utils/site-journey.d.ts +2 -22
  40. package/src/lib/app-utils/site-journey.js +2 -32
  41. package/src/lib/app-utils/site-journey.js.map +1 -1
  42. package/src/lib/foundation/definitions/platform.types.d.ts +33 -0
  43. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  44. package/src/lib/plugin-manager/first-party-plugins.generated.js +20 -2
  45. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  46. package/src/lib/plugin-manager/plugin-checkout-credits.d.ts +288 -0
  47. package/src/lib/plugin-manager/plugin-checkout-credits.js +196 -0
  48. package/src/lib/plugin-manager/plugin-checkout-credits.js.map +1 -0
  49. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +87 -0
  50. package/src/lib/plugin-manager/plugin-media-ingest.js +34 -0
  51. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -0
  52. package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +10 -0
  53. package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -1
  54. package/src/lib/plugin-manager/stock-photo-provider.d.ts +141 -0
  55. package/src/lib/plugin-manager/stock-photo-provider.js +59 -0
  56. 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
- * - Column sorting: the only declared order is the walk.
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, type OrgCrmAssignmentRule } from '../foundation';
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
- /** What a task's search box reads: its title. */
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
@@ -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/index.js";
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 document = contact != null ? contact : {};
1476
- const facets = document[CONTACT_FACETS_FIELD];
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 = document[CONTACT_COMPANY_IDS_FIELD];
1485
+ const mirror = stored[CONTACT_COMPANY_IDS_FIELD];
1486
1486
  return {
1487
- companyId: (_readContactFacet_companyId = readContactFacet(document, groupId).companyId) != null ? _readContactFacet_companyId : null,
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
- const capped = word.slice(0, NAME_TOKEN_MAX_PREFIX);
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
- /** What a task's search box reads: its title. */ export function crmTaskListFields(record) {
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
  /**