@aglyn/plugins-inbox 1.0.0-beta.229 → 1.0.0-beta.231
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-inbox",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.231",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
35
|
-
"@aglyn/
|
|
36
|
-
"@aglyn/tenant-
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.231",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.231",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.231",
|
|
31
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.231",
|
|
32
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.231",
|
|
33
|
+
"@aglyn/shared-util-email": "1.0.0-beta.231",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.231",
|
|
35
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.231",
|
|
36
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.231",
|
|
37
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.231",
|
|
37
38
|
"@swc/helpers": "0.5.23"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type { ListFilterOption } from '@aglyn/shared-
|
|
18
|
-
import type { ListQueryDeclaration, ListQueryFilter, ListQueryRequest } from '@aglyn/shared-
|
|
17
|
+
import type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs';
|
|
18
|
+
import type { ListQueryDeclaration, ListQueryFilter, ListQueryRequest } from '@aglyn/shared-util-tools/list-query/list-query-plan';
|
|
19
19
|
export declare const SITE_MEMBER_LIST_QUERY: ListQueryDeclaration;
|
|
20
20
|
export declare const SITE_MEMBER_FILTER_HEADERS: Readonly<Record<string, string>>;
|
|
21
21
|
export declare const LEAD_LIST_QUERY: ListQueryDeclaration;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/constants/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 { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryRequest,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * WHAT EACH INBOX LIST CAN BE ASKED, ON ITS FIRESTORE QUERY (AGL-3321).\n *\n * Every clause the Filters panel offers and every quick-search word is a\n * predicate on the list's query, planned by `planListQuery`, so each page is\n * a page of the answer. A question no query can answer is not offered, and a\n * combination one query cannot hold is refused by name. `list-queries.spec.ts`\n * holds `cloud/firebase-firestore.indexes.json` to `listQueryIndexes` of each\n * declaration.\n */\n\n/*==========================================\n * SITE MEMBERS — `hosts/{hostId}/siteMembers`, newest first.\n *\n * The same fields and index shapes as the console's Site users list\n * (`apps/console/utils/site-account-query.ts`), so the composites are one\n * set. The search reads `searchTokens` — the start of any word of the\n * display name or of the address — which both writers of a member stamp\n * (`memberSearchTokens`, the commerce plugin's register and account routes)\n * and `backfill-site-member-search.mjs` stamps on the members written\n * before it. The register path lower-cases `email` before it stores it, so\n * the stored address is its own normalized key.\n *=========================================*/\nexport const SITE_MEMBER_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n },\n ],\n sorts: [{ path: 'createdAt', direction: 'desc' }],\n search: { tokensPath: 'searchTokens' },\n}\n\nexport const SITE_MEMBER_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Email',\n}\n\n/*==========================================\n * LEADS — `orgs/{orgId}/leads`, newest first.\n *\n * Under a site the list is narrowed to what that site may see\n * (`visibleTo array-contains-any`, {@link leadListBase}); on the\n * organization's Inbox an org-wide member reads it unscoped.\n *\n * `searchTokens` — the lead's name, address, company, title and tags as word\n * prefixes — and `scopedSearchTokens` — the same behind each `visibleTo`\n * token — are the CRM's list fields, stamped on every lead write by\n * `crmLeadListFields` (`@aglyn/aglyn`'s CRM app-utils) and backfilled by the\n * CRM's own lane. Under a site the search folds into the scope clause\n * (`scopedSearch`), which the rules prove only for an org-wide member: a\n * collaborator whose reach is some sites keeps the scope clause, and their\n * search is asked as the start of the address beside it\n * (`leadListQueryFor`).\n *\n * Every current lead writer normalizes the address (`normalizeContactEmail`)\n * before `addHostLead` stores it, so Email is an equality on `email`.\n *=========================================*/\nconst LEAD_EMAIL_FIELD: ListFilterField = {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n}\n\nexport const LEAD_LIST_QUERY: ListQueryDeclaration = {\n fields: [LEAD_EMAIL_FIELD],\n sorts: [{ path: 'createdAt', direction: 'desc' }],\n search: { tokensPath: 'searchTokens', scoped: scopedSearch('scopedSearchTokens') },\n}\n\n/**\n * The organization's Inbox adds what an unscoped read can ask: the surface\n * that captured the lead and the sites that did. Both are ARRAYS on the\n * lead (`sources`, `capturedByHostIds`, `arrayUnion`ed by `addHostLead`), so\n * each is the query's one array clause — \"any of\" — and cannot stand beside\n * the search or the other. Under a site the scope clause already holds that\n * one array clause, which is why neither is offered there.\n */\nexport const ORG_LEAD_LIST_QUERY: ListQueryDeclaration = {\n ...LEAD_LIST_QUERY,\n fields: [\n LEAD_EMAIL_FIELD,\n {\n column: 'sources',\n kind: 'exact',\n path: 'sources',\n tokensPath: 'sources',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n {\n column: 'capturedByHostIds',\n kind: 'exact',\n path: 'capturedByHostIds',\n tokensPath: 'capturedByHostIds',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n ],\n}\n\n/**\n * The column a site collaborator's search is asked through: the START of the\n * address, a prefix range on `email`. Not a Filters-panel field — the panel\n * reads the declaration the card shows, and this one rides only on the query.\n */\nexport const LEAD_ADDRESS_SEARCH_COLUMN = '$addressSearch'\n\nconst LEAD_ADDRESS_SEARCH_FIELD: ListFilterField = {\n column: LEAD_ADDRESS_SEARCH_COLUMN,\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n}\n\n/** The notice a site collaborator's search carries: what it matched, and the order. */\nexport const LEAD_ADDRESS_SEARCH_NOTICE =\n 'Search on this site’s leads matches the start of the address, and lists them by address.'\n\n/**\n * The query a reader may run for the Leads list, and whether its search was\n * asked as an address prefix (AGL-3321).\n *\n * An ORG-WIDE reader's search folds into the scope clause (`scopedSearch`):\n * a query without `visibleTo` is one the rules prove only for them. A reader\n * whose reach is some sites must keep `visibleTo array-contains-any` on the\n * query, which spends its one array clause — so their search is asked as a\n * prefix RANGE on the stored, normalized `email` beside the scope instead,\n * ordered by address (the `visibleTo, email` composite the lead list already\n * carries). Only the first word is asked.\n */\nexport function leadListQueryFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n request: ListQueryRequest,\n): { declaration: ListQueryDeclaration; request: ListQueryRequest; addressSearch: boolean } {\n if (foldsScope || !declaration.search?.scoped) return { declaration, request, addressSearch: false }\n const word = (request.search ?? []).join(' ').trim().split(/\\s+/)[0] ?? ''\n return {\n declaration: {\n ...declaration,\n fields: [...declaration.fields, LEAD_ADDRESS_SEARCH_FIELD],\n search: undefined,\n },\n request: {\n ...request,\n search: [],\n clauses: word\n ? [...request.clauses, { field: LEAD_ADDRESS_SEARCH_COLUMN, op: 'startsWith', value: word }]\n : request.clauses,\n },\n addressSearch: Boolean(word),\n }\n}\n\n/** The scope clause a site's lead list always carries, or none on the org's. */\nexport function leadListBase(scopeTokens: readonly string[] | null): ListQueryFilter[] {\n return scopeTokens ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...scopeTokens] }] : []\n}\n\n/** The base's shape for `listQueryIndexes`: the scope clause is an array clause. */\nexport const LEAD_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\nexport const LEAD_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Email',\n sources: 'Source',\n capturedByHostIds: 'Site',\n}\n\n/**\n * The capture surfaces a Source filter can name. A lead routed from a form\n * is filed as `form:{formId}` — one value per form, so no fixed choice can\n * ask for \"every form\" and none is offered; the rest are the fixed words the\n * doors write: a booking, a lead added by hand, over the API or from a file,\n * and `signup`, which member sign-ups wrote before they filed a contact.\n */\nexport const LEAD_SOURCE_OPTIONS: readonly ListFilterOption[] = [\n { value: 'booking', label: 'Booking' },\n { value: 'manual', label: 'Added by hand' },\n { value: 'api', label: 'API' },\n { value: 'import', label: 'Import' },\n { value: 'signup', label: 'Sign-up' },\n]\n\n/** How one capture surface reads on a row. */\nexport function leadSourceLabel(source: string): string {\n if (source === 'form' || source.startsWith('form:')) return 'Form'\n return LEAD_SOURCE_OPTIONS.find((option) => option.value === source)?.label ?? source\n}\n\n/*==========================================\n * FORM SUBMISSIONS — `hosts/{hostId}/formSubmissions` under a site, and the\n * `formSubmissions` collection group narrowed to `orgId` on the\n * organization's Inbox (the rules admit that group read only so narrowed).\n * Newest first.\n *\n * `senderTokens` and `searchTokens` are `messageSearchFields` of the\n * message, stamped by the submit route — the collection's only creator —\n * and by `backfill-form-submission-filters.mjs` on the rows before it; a\n * message's values never change after it arrives. `read` is a boolean on\n * every row: the route writes `false`, the reader and the API flip it, and\n * the same backfill stamps `false` on a row without one. `hostId` and\n * `orgId` are stamped by the route and frozen by the rules.\n *\n * ⛔ The form's NAME is not searched. The row keeps the name the form had\n * when the message arrived, and a rename would leave the search finding\n * rows by a name the reader no longer sees. Form is a filter by id instead,\n * which survives a rename.\n *=========================================*/\nconst SUBMISSION_FIELDS: readonly ListFilterField[] = [\n {\n column: 'from',\n kind: 'text',\n path: 'senderTokens',\n tokensPath: 'senderTokens',\n operators: ['contains'],\n },\n { column: 'read', kind: 'boolean', path: 'read', operators: ['equals'] },\n]\n\n/** Form: offered where a site's forms are read (not across every site). */\nconst FORM_FIELD: ListFilterField = {\n column: 'formId',\n kind: 'exact',\n path: 'formId',\n operators: ['equals'],\n}\n\n/** Site: the organization's Inbox only, where a row can be any site's. */\nconst SITE_FIELD: ListFilterField = {\n column: 'hostId',\n kind: 'exact',\n path: 'hostId',\n operators: ['equals', 'isAnyOf'],\n}\n\nconst SUBMISSION_SORTS = [{ path: 'createdAt', direction: 'desc' as const }]\nconst SUBMISSION_SEARCH = { tokensPath: 'searchTokens' }\n\n/** A site's Inbox. */\nexport const SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [...SUBMISSION_FIELDS, FORM_FIELD],\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** A card already narrowed to one form, where Form has nothing to pick. */\nexport const FORM_SCOPED_SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: SUBMISSION_FIELDS,\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** Every site's, on the organization's Inbox. */\nexport const ORG_SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [...SUBMISSION_FIELDS, SITE_FIELD],\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** The organization's Inbox's scope, which the rules require. */\nexport const orgSubmissionBase = (orgId: string): ListQueryFilter[] => [\n { path: 'orgId', op: '==', value: orgId },\n]\n\n/** A form-scoped card's scope. */\nexport const formSubmissionBase = (formId: string): ListQueryFilter[] => [\n { path: 'formId', op: '==', value: formId },\n]\n\nexport const SUBMISSION_FILTER_HEADERS: Readonly<Record<string, string>> = {\n from: 'From',\n read: 'Read',\n formId: 'Form',\n hostId: 'Site',\n}\n\n/** Read is the stored boolean, by the words a reader uses for it. */\nexport const SUBMISSION_READ_OPTIONS: readonly ListFilterOption[] = [\n { value: 'false', label: 'Unread' },\n { value: 'true', label: 'Read' },\n]\n"],"names":["scopedSearch","SITE_MEMBER_LIST_QUERY","fields","column","kind","path","lowerPath","presence","operators","sorts","direction","search","tokensPath","SITE_MEMBER_FILTER_HEADERS","email","LEAD_EMAIL_FIELD","LEAD_LIST_QUERY","scoped","ORG_LEAD_LIST_QUERY","verbatimTokens","LEAD_ADDRESS_SEARCH_COLUMN","LEAD_ADDRESS_SEARCH_FIELD","LEAD_ADDRESS_SEARCH_NOTICE","leadListQueryFor","declaration","foldsScope","request","addressSearch","word","join","trim","split","undefined","clauses","field","op","value","Boolean","leadListBase","scopeTokens","LEAD_LIST_BASE_INDEX","array","LEAD_FILTER_HEADERS","sources","capturedByHostIds","LEAD_SOURCE_OPTIONS","label","leadSourceLabel","source","startsWith","find","option","SUBMISSION_FIELDS","FORM_FIELD","SITE_FIELD","SUBMISSION_SORTS","SUBMISSION_SEARCH","SUBMISSION_LIST_QUERY","FORM_SCOPED_SUBMISSION_LIST_QUERY","ORG_SUBMISSION_LIST_QUERY","orgSubmissionBase","orgId","formSubmissionBase","formId","SUBMISSION_FILTER_HEADERS","from","read","hostId","SUBMISSION_READ_OPTIONS"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/constants/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 { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\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 type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryRequest,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * WHAT EACH INBOX LIST CAN BE ASKED, ON ITS FIRESTORE QUERY (AGL-3321).\n *\n * Every clause the Filters panel offers and every quick-search word is a\n * predicate on the list's query, planned by `planListQuery`, so each page is\n * a page of the answer. A question no query can answer is not offered, and a\n * combination one query cannot hold is refused by name. `list-queries.spec.ts`\n * holds `cloud/firebase-firestore.indexes.json` to `listQueryIndexes` of each\n * declaration.\n */\n\n/*==========================================\n * SITE MEMBERS — `hosts/{hostId}/siteMembers`, newest first.\n *\n * The same fields and index shapes as the console's Site users list\n * (`apps/console/utils/site-account-query.ts`), so the composites are one\n * set. The search reads `searchTokens` — the start of any word of the\n * display name or of the address — which both writers of a member stamp\n * (`memberSearchTokens`, the commerce plugin's register and account routes)\n * and `backfill-site-member-search.mjs` stamps on the members written\n * before it. The register path lower-cases `email` before it stores it, so\n * the stored address is its own normalized key.\n *=========================================*/\nexport const SITE_MEMBER_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n },\n ],\n sorts: [{ path: 'createdAt', direction: 'desc' }],\n search: { tokensPath: 'searchTokens' },\n}\n\nexport const SITE_MEMBER_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Email',\n}\n\n/*==========================================\n * LEADS — `orgs/{orgId}/leads`, newest first.\n *\n * Under a site the list is narrowed to what that site may see\n * (`visibleTo array-contains-any`, {@link leadListBase}); on the\n * organization's Inbox an org-wide member reads it unscoped.\n *\n * `searchTokens` — the lead's name, address, company, title and tags as word\n * prefixes — and `scopedSearchTokens` — the same behind each `visibleTo`\n * token — are the CRM's list fields, stamped on every lead write by\n * `crmLeadListFields` (`@aglyn/aglyn`'s CRM app-utils) and backfilled by the\n * CRM's own lane. Under a site the search folds into the scope clause\n * (`scopedSearch`), which the rules prove only for an org-wide member: a\n * collaborator whose reach is some sites keeps the scope clause, and their\n * search is asked as the start of the address beside it\n * (`leadListQueryFor`).\n *\n * Every current lead writer normalizes the address (`normalizeContactEmail`)\n * before `addHostLead` stores it, so Email is an equality on `email`.\n *=========================================*/\nconst LEAD_EMAIL_FIELD: ListFilterField = {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals'],\n}\n\nexport const LEAD_LIST_QUERY: ListQueryDeclaration = {\n fields: [LEAD_EMAIL_FIELD],\n sorts: [{ path: 'createdAt', direction: 'desc' }],\n search: { tokensPath: 'searchTokens', scoped: scopedSearch('scopedSearchTokens') },\n}\n\n/**\n * The organization's Inbox adds what an unscoped read can ask: the surface\n * that captured the lead and the sites that did. Both are ARRAYS on the\n * lead (`sources`, `capturedByHostIds`, `arrayUnion`ed by `addHostLead`), so\n * each is the query's one array clause — \"any of\" — and cannot stand beside\n * the search or the other. Under a site the scope clause already holds that\n * one array clause, which is why neither is offered there.\n */\nexport const ORG_LEAD_LIST_QUERY: ListQueryDeclaration = {\n ...LEAD_LIST_QUERY,\n fields: [\n LEAD_EMAIL_FIELD,\n {\n column: 'sources',\n kind: 'exact',\n path: 'sources',\n tokensPath: 'sources',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n {\n column: 'capturedByHostIds',\n kind: 'exact',\n path: 'capturedByHostIds',\n tokensPath: 'capturedByHostIds',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n ],\n}\n\n/**\n * The column a site collaborator's search is asked through: the START of the\n * address, a prefix range on `email`. Not a Filters-panel field — the panel\n * reads the declaration the card shows, and this one rides only on the query.\n */\nexport const LEAD_ADDRESS_SEARCH_COLUMN = '$addressSearch'\n\nconst LEAD_ADDRESS_SEARCH_FIELD: ListFilterField = {\n column: LEAD_ADDRESS_SEARCH_COLUMN,\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n}\n\n/** The notice a site collaborator's search carries: what it matched, and the order. */\nexport const LEAD_ADDRESS_SEARCH_NOTICE =\n 'Search on this site’s leads matches the start of the address, and lists them by address.'\n\n/**\n * The query a reader may run for the Leads list, and whether its search was\n * asked as an address prefix (AGL-3321).\n *\n * An ORG-WIDE reader's search folds into the scope clause (`scopedSearch`):\n * a query without `visibleTo` is one the rules prove only for them. A reader\n * whose reach is some sites must keep `visibleTo array-contains-any` on the\n * query, which spends its one array clause — so their search is asked as a\n * prefix RANGE on the stored, normalized `email` beside the scope instead,\n * ordered by address (the `visibleTo, email` composite the lead list already\n * carries). Only the first word is asked.\n */\nexport function leadListQueryFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n request: ListQueryRequest,\n): { declaration: ListQueryDeclaration; request: ListQueryRequest; addressSearch: boolean } {\n if (foldsScope || !declaration.search?.scoped) return { declaration, request, addressSearch: false }\n const word = (request.search ?? []).join(' ').trim().split(/\\s+/)[0] ?? ''\n return {\n declaration: {\n ...declaration,\n fields: [...declaration.fields, LEAD_ADDRESS_SEARCH_FIELD],\n search: undefined,\n },\n request: {\n ...request,\n search: [],\n clauses: word\n ? [...request.clauses, { field: LEAD_ADDRESS_SEARCH_COLUMN, op: 'startsWith', value: word }]\n : request.clauses,\n },\n addressSearch: Boolean(word),\n }\n}\n\n/** The scope clause a site's lead list always carries, or none on the org's. */\nexport function leadListBase(scopeTokens: readonly string[] | null): ListQueryFilter[] {\n return scopeTokens ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...scopeTokens] }] : []\n}\n\n/** The base's shape for `listQueryIndexes`: the scope clause is an array clause. */\nexport const LEAD_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\nexport const LEAD_FILTER_HEADERS: Readonly<Record<string, string>> = {\n email: 'Email',\n sources: 'Source',\n capturedByHostIds: 'Site',\n}\n\n/**\n * The capture surfaces a Source filter can name. A lead routed from a form\n * is filed as `form:{formId}` — one value per form, so no fixed choice can\n * ask for \"every form\" and none is offered; the rest are the fixed words the\n * doors write: a booking, a lead added by hand, over the API or from a file,\n * and `signup`, which member sign-ups wrote before they filed a contact.\n */\nexport const LEAD_SOURCE_OPTIONS: readonly ListFilterOption[] = [\n { value: 'booking', label: 'Booking' },\n { value: 'manual', label: 'Added by hand' },\n { value: 'api', label: 'API' },\n { value: 'import', label: 'Import' },\n { value: 'signup', label: 'Sign-up' },\n]\n\n/** How one capture surface reads on a row. */\nexport function leadSourceLabel(source: string): string {\n if (source === 'form' || source.startsWith('form:')) return 'Form'\n return LEAD_SOURCE_OPTIONS.find((option) => option.value === source)?.label ?? source\n}\n\n/*==========================================\n * FORM SUBMISSIONS — `hosts/{hostId}/formSubmissions` under a site, and the\n * `formSubmissions` collection group narrowed to `orgId` on the\n * organization's Inbox (the rules admit that group read only so narrowed).\n * Newest first.\n *\n * `senderTokens` and `searchTokens` are `messageSearchFields` of the\n * message, stamped by the submit route — the collection's only creator —\n * and by `backfill-form-submission-filters.mjs` on the rows before it; a\n * message's values never change after it arrives. `read` is a boolean on\n * every row: the route writes `false`, the reader and the API flip it, and\n * the same backfill stamps `false` on a row without one. `hostId` and\n * `orgId` are stamped by the route and frozen by the rules.\n *\n * ⛔ The form's NAME is not searched. The row keeps the name the form had\n * when the message arrived, and a rename would leave the search finding\n * rows by a name the reader no longer sees. Form is a filter by id instead,\n * which survives a rename.\n *=========================================*/\nconst SUBMISSION_FIELDS: readonly ListFilterField[] = [\n {\n column: 'from',\n kind: 'text',\n path: 'senderTokens',\n tokensPath: 'senderTokens',\n operators: ['contains'],\n },\n { column: 'read', kind: 'boolean', path: 'read', operators: ['equals'] },\n]\n\n/** Form: offered where a site's forms are read (not across every site). */\nconst FORM_FIELD: ListFilterField = {\n column: 'formId',\n kind: 'exact',\n path: 'formId',\n operators: ['equals'],\n}\n\n/** Site: the organization's Inbox only, where a row can be any site's. */\nconst SITE_FIELD: ListFilterField = {\n column: 'hostId',\n kind: 'exact',\n path: 'hostId',\n operators: ['equals', 'isAnyOf'],\n}\n\nconst SUBMISSION_SORTS = [{ path: 'createdAt', direction: 'desc' as const }]\nconst SUBMISSION_SEARCH = { tokensPath: 'searchTokens' }\n\n/** A site's Inbox. */\nexport const SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [...SUBMISSION_FIELDS, FORM_FIELD],\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** A card already narrowed to one form, where Form has nothing to pick. */\nexport const FORM_SCOPED_SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: SUBMISSION_FIELDS,\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** Every site's, on the organization's Inbox. */\nexport const ORG_SUBMISSION_LIST_QUERY: ListQueryDeclaration = {\n fields: [...SUBMISSION_FIELDS, SITE_FIELD],\n sorts: SUBMISSION_SORTS,\n search: SUBMISSION_SEARCH,\n}\n\n/** The organization's Inbox's scope, which the rules require. */\nexport const orgSubmissionBase = (orgId: string): ListQueryFilter[] => [\n { path: 'orgId', op: '==', value: orgId },\n]\n\n/** A form-scoped card's scope. */\nexport const formSubmissionBase = (formId: string): ListQueryFilter[] => [\n { path: 'formId', op: '==', value: formId },\n]\n\nexport const SUBMISSION_FILTER_HEADERS: Readonly<Record<string, string>> = {\n from: 'From',\n read: 'Read',\n formId: 'Form',\n hostId: 'Site',\n}\n\n/** Read is the stored boolean, by the words a reader uses for it. */\nexport const SUBMISSION_READ_OPTIONS: readonly ListFilterOption[] = [\n { value: 'false', label: 'Unread' },\n { value: 'true', label: 'Read' },\n]\n"],"names":["scopedSearch","SITE_MEMBER_LIST_QUERY","fields","column","kind","path","lowerPath","presence","operators","sorts","direction","search","tokensPath","SITE_MEMBER_FILTER_HEADERS","email","LEAD_EMAIL_FIELD","LEAD_LIST_QUERY","scoped","ORG_LEAD_LIST_QUERY","verbatimTokens","LEAD_ADDRESS_SEARCH_COLUMN","LEAD_ADDRESS_SEARCH_FIELD","LEAD_ADDRESS_SEARCH_NOTICE","leadListQueryFor","declaration","foldsScope","request","addressSearch","word","join","trim","split","undefined","clauses","field","op","value","Boolean","leadListBase","scopeTokens","LEAD_LIST_BASE_INDEX","array","LEAD_FILTER_HEADERS","sources","capturedByHostIds","LEAD_SOURCE_OPTIONS","label","leadSourceLabel","source","startsWith","find","option","SUBMISSION_FIELDS","FORM_FIELD","SITE_FIELD","SUBMISSION_SORTS","SUBMISSION_SEARCH","SUBMISSION_LIST_QUERY","FORM_SCOPED_SUBMISSION_LIST_QUERY","ORG_SUBMISSION_LIST_QUERY","orgSubmissionBase","orgId","formSubmissionBase","formId","SUBMISSION_FILTER_HEADERS","from","read","hostId","SUBMISSION_READ_OPTIONS"],"mappings":"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"}
|