@aglyn/plugins-marketing 1.0.0-beta.230 → 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-marketing",
3
- "version": "1.0.0-beta.230",
3
+ "version": "1.0.0-beta.231",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,20 +25,21 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.230",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.230",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.230",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.230",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.230",
33
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.230",
34
- "@aglyn/shared-ui-next": "1.0.0-beta.230",
35
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.230",
36
- "@aglyn/shared-util-email": "1.0.0-beta.230",
37
- "@aglyn/shared-util-http": "1.0.0-beta.230",
38
- "@aglyn/shared-util-timestamp": "1.0.0-beta.230",
39
- "@aglyn/tenant-data-admin": "1.0.0-beta.230",
40
- "@aglyn/tenant-feature-instance": "1.0.0-beta.230",
41
- "@aglyn/tenant-runtime": "1.0.0-beta.230",
28
+ "@aglyn/aglyn": "1.0.0-beta.231",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.231",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.231",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.231",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.231",
33
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.231",
34
+ "@aglyn/shared-ui-next": "1.0.0-beta.231",
35
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.231",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.231",
37
+ "@aglyn/shared-util-http": "1.0.0-beta.231",
38
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.231",
39
+ "@aglyn/shared-util-tools": "1.0.0-beta.231",
40
+ "@aglyn/tenant-data-admin": "1.0.0-beta.231",
41
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.231",
42
+ "@aglyn/tenant-runtime": "1.0.0-beta.231",
42
43
  "@swc/helpers": "0.5.23"
43
44
  },
44
45
  "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 { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
18
- import type { ListQueryDeclaration, ListQueryFilter } from '@aglyn/shared-ui-jsx/const/list-query-plan';
17
+ import type { ListFilterClause } from '@aglyn/shared-util-tools/list-query/list-filter-codecs';
18
+ import type { ListQueryDeclaration, ListQueryFilter } from '@aglyn/shared-util-tools/list-query/list-query-plan';
19
19
  /** The send's subject, as word prefixes. */
20
20
  export declare const CAMPAIGN_SEND_SUBJECT_TOKENS = "subjectTokens";
21
21
  /** The container's name, as word prefixes. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/campaign-list-query.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 { nameSearchKey, nameSearchTokens } from '@aglyn/aglyn/app-utils/name-search'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * WHAT THE MARKETING LISTS ASK FIRESTORE (AGL-3321).\n *\n * Three tables read the org's campaign collections — the Campaigns list, a\n * campaign's own emails, and the Emails list — and every clause their\n * Filters panels offer, and their search boxes, is a predicate on ONE query,\n * newest first, paged by that query. Nothing is matched over the rows a read\n * happened to hold. Each field is here because a query can serve it.\n *\n * ## The fields the writers stamp\n *\n * A SEND (`orgs/{orgId}/campaigns`) carries:\n *\n * `createdAtMs` when the record was minted — the one date on every\n * send (a sent one has `sentAt`, a scheduled one\n * `sendAtMs`, a draft neither), so the order;\n * `subjectTokens` the subject's word prefixes, for search and\n * \"Subject contains\";\n * `emailCampaignId` the container it is filed under, or `null` for a\n * single send — stored null rather than left out,\n * because a query can ask for `== null` and can never\n * ask for a field that is missing.\n *\n * A CONTAINER (`orgs/{orgId}/emailCampaigns`) carries `createdAtMs`,\n * `nameTokens` (the org hub's search) and `nameLower` (a site hub's search,\n * as a prefix range — below).\n *\n * Every writer stamps them through the two helpers below, and\n * `tools/scripts/backfill-campaign-list-fields.mjs` stamps the records\n * written before them.\n *\n * ## What a site hub asks\n *\n * A send is sent AS one site, so a site hub's sends are `hostId == {site}` —\n * an equality, which merges with every other clause on single-field\n * indexes and leaves the query's one array clause for search. The security\n * rules admit that read for the site's collaborators (a send's `hostId` is\n * the site its `visibleTo` names). A container is placed on SEVERAL sites,\n * so its scope stays `visibleTo array-contains-any [org, host:{site}]` —\n * the query's one array clause, and the only one the rules can prove for a\n * site collaborator. So a site hub's campaign search is not a token lookup:\n * it is a PREFIX RANGE on `nameLower` beside the scope\n * (`campaignSiteSearchClause`), which orders the page by name while it\n * applies, and the list says search matches the start of a name. Folding\n * the word into the scope clause through scoped tokens would be refused by\n * the rules for such a reader: that query proves nothing about `visibleTo`.\n *\n * ## What is not offered\n *\n * - A send's STATE (Sending, Stopped, …) is derived from its counters at\n * read time (`campaignSendDisplay`); no query can ask it. Its stored\n * `status` is offered instead.\n * - A campaign's WINDOW (Upcoming, Running, Ended) is derived from its dates\n * against the clock; \"running\" is a range on two fields. Not offered.\n * - The site NAME a row shows is not stored on the record; the Site filter\n * asks by the site's id, and search reads the subject or the name only.\n * - Ranges: ONE, on `createdAtMs`, the field every list is ordered by — so\n * it needs no order of its own and no index beyond the ones above.\n */\n\n/** The send's subject, as word prefixes. */\nexport const CAMPAIGN_SEND_SUBJECT_TOKENS = 'subjectTokens'\n/** The container's name, as word prefixes. */\nexport const CAMPAIGN_NAME_TOKENS = 'nameTokens'\n/** The container's name, lower-cased whole, for a site hub's prefix search. */\nexport const CAMPAIGN_NAME_LOWER = 'nameLower'\n/** The one date on every send and container, and every list's order. */\nexport const CAMPAIGN_LIST_ORDER_FIELD = 'createdAtMs'\n\n/**\n * The Campaign filter's value for an email filed under no campaign. An\n * empty value reads as \"no value\" to the panel, so the choice needs one of\n * its own; the query asks `emailCampaignId == null` for it.\n */\nexport const SINGLE_SEND_FILTER_VALUE = '__single__'\n\n/**\n * The search field a send carries beside its `subject`, stamped by every\n * write that sets the subject.\n */\nexport function campaignSendSearchFields(subject: unknown): {\n subjectTokens: string[]\n} {\n return {\n subjectTokens: nameSearchTokens(typeof subject === 'string' ? subject : ''),\n }\n}\n\n/**\n * The search fields a container carries beside its `name`, stamped by every\n * write that sets the name.\n */\nexport function campaignContainerSearchFields(name: unknown): {\n nameLower: string\n nameTokens: string[]\n} {\n const text = typeof name === 'string' ? name : ''\n return { nameLower: nameSearchKey(text), nameTokens: nameSearchTokens(text) }\n}\n\nconst ORDER = [{ path: CAMPAIGN_LIST_ORDER_FIELD, direction: 'desc' as const }]\n\nconst CREATED: ListFilterField = {\n column: CAMPAIGN_LIST_ORDER_FIELD,\n kind: 'date',\n path: CAMPAIGN_LIST_ORDER_FIELD,\n storedAs: 'millis',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n}\n\nconst SUBJECT: ListFilterField = {\n column: 'subject',\n kind: 'text',\n path: 'subject',\n tokensPath: CAMPAIGN_SEND_SUBJECT_TOKENS,\n operators: ['contains'],\n}\n\nconst STATUS: ListFilterField = {\n column: 'status',\n kind: 'exact',\n path: 'status',\n operators: ['equals', 'isAnyOf'],\n}\n\n/**\n * The stored statuses a send can hold, for the Status filter. Stored\n * strings, not the State column's display states: an email between batches\n * is stored `scheduled` and SHOWN as Sending, which is why the filter is\n * headed Status and not State.\n */\nexport const CAMPAIGN_SEND_STATUS_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [\n { value: 'draft', label: 'Draft' },\n { value: 'scheduled', label: 'Scheduled' },\n { value: 'sending', label: 'Sending' },\n { value: 'sent', label: 'Sent' },\n { value: 'canceled', label: 'Canceled' },\n { value: 'failed', label: 'Failed' },\n]\n\n/**\n * The Emails list: every send, under a site or across the org.\n *\n * `site` is the org hub's only — under a site every row is that site's.\n */\nexport function campaignEmailsListQuery(orgHub: boolean): ListQueryDeclaration {\n return orgHub ? EMAILS_ORG : EMAILS_SITE\n}\n\nconst EMAIL_CAMPAIGN: ListFilterField = {\n column: 'emailCampaignId',\n kind: 'exact',\n path: 'emailCampaignId',\n presence: 'nullable',\n // `is` only: \"Single send\" is `== null`, which cannot sit in an `in`\n // beside campaign ids.\n operators: ['equals', 'isEmpty'],\n}\n\nconst EMAILS_SITE: ListQueryDeclaration = {\n fields: [SUBJECT, STATUS, EMAIL_CAMPAIGN, CREATED],\n sorts: ORDER,\n search: { tokensPath: CAMPAIGN_SEND_SUBJECT_TOKENS },\n}\nconst EMAILS_ORG: ListQueryDeclaration = {\n ...EMAILS_SITE,\n fields: [\n ...EMAILS_SITE.fields,\n { column: 'site', kind: 'exact', path: 'hostId', operators: ['equals', 'isAnyOf'] },\n ],\n}\n\n/** A campaign's own emails: the container is the query's base. */\nexport const CAMPAIGN_EMAILS_QUERY: ListQueryDeclaration = {\n fields: [SUBJECT, STATUS, CREATED],\n sorts: ORDER,\n search: { tokensPath: CAMPAIGN_SEND_SUBJECT_TOKENS },\n}\n\n/**\n * The Campaigns list's CAMPAIGNS: the containers. The columns are the\n * table's (`name`, `listIds`, `sitesLabel`); Lists and Sites are the org\n * hub's only — under a site the scope clause is the query's one array\n * clause, so an any-of on another array could never be served beside it.\n */\nexport function campaignContainersListQuery(orgHub: boolean): ListQueryDeclaration {\n return orgHub ? CONTAINERS_ORG : CONTAINERS_SITE\n}\n\nconst CONTAINER_NAME: ListFilterField = {\n column: 'name',\n kind: 'text',\n path: 'name',\n tokensPath: CAMPAIGN_NAME_TOKENS,\n operators: ['contains'],\n}\n/** A site hub's Campaign field: the name's start, a range beside the scope. */\nconst CONTAINER_NAME_PREFIX: ListFilterField = {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: CAMPAIGN_NAME_LOWER,\n operators: ['startsWith'],\n}\nconst CONTAINERS_SITE: ListQueryDeclaration = {\n fields: [CONTAINER_NAME_PREFIX, CREATED],\n sorts: ORDER,\n}\nconst CONTAINERS_ORG: ListQueryDeclaration = {\n sorts: ORDER,\n search: { tokensPath: CAMPAIGN_NAME_TOKENS },\n fields: [\n CONTAINER_NAME,\n { column: 'listIds', kind: 'exact', path: 'listIds', tokensPath: 'listIds', operators: ['isAnyOf'] },\n // Asked by scope token — `org` is \"every site\", `host:{id}` one site.\n { column: 'sitesLabel', kind: 'exact', path: 'visibleTo', tokensPath: 'visibleTo', operators: ['isAnyOf'] },\n CREATED,\n ],\n}\n\n/**\n * The Campaigns list's SINGLE SENDS: sends filed under no campaign, drawn\n * in the same table. The `name` column is the send's subject; `sitesLabel`\n * is the site it was sent as, on the org hub.\n */\nexport function campaignSingleSendsListQuery(orgHub: boolean): ListQueryDeclaration {\n return orgHub ? SINGLE_ORG : SINGLE_SITE\n}\n\nconst SINGLE_NAME: ListFilterField = { ...SUBJECT, column: 'name' }\nconst SINGLE_SITE: ListQueryDeclaration = {\n fields: [SINGLE_NAME, CREATED],\n sorts: ORDER,\n search: { tokensPath: CAMPAIGN_SEND_SUBJECT_TOKENS },\n}\nconst SINGLE_ORG: ListQueryDeclaration = {\n ...SINGLE_SITE,\n fields: [\n SINGLE_NAME,\n { column: 'sitesLabel', kind: 'exact', path: 'hostId', operators: ['equals', 'isAnyOf'] },\n CREATED,\n ],\n}\n\n/** A site hub's sends: the ones sent as it. Nothing on the org hub. */\nexport function campaignSendsScope(hostId: string | null): ListQueryFilter[] {\n return hostId ? [{ path: 'hostId', op: '==', value: hostId }] : []\n}\n\n/** Said above a site hub's campaigns while its search is applied. */\nexport const CAMPAIGN_SITE_SEARCH_NOTICE =\n 'Search on a site’s campaigns matches the start of a campaign’s name.'\n\n/**\n * A site hub's campaign search, as the clause its query can serve: the typed\n * words as \"Campaign starts with\", or null when nothing was typed. The list\n * passes it among its clauses and no search words — see the file header for\n * why a site's search is a prefix range and not a token lookup.\n */\nexport function campaignSiteSearchClause(\n words: readonly string[],\n): ListFilterClause | null {\n const typed = words.map((word) => word.trim()).filter(Boolean).join(' ')\n return typed ? { field: 'name', op: 'startsWith', value: typed } : null\n}\n\n/** A site hub's containers: the ones placed on every site or on it. */\nexport function campaignContainersScope(hostId: string | null): ListQueryFilter[] {\n return hostId\n ? [{ path: 'visibleTo', op: 'array-contains-any', value: scopeTokensForHost(hostId) }]\n : []\n}\n\n/** The sends filed under no campaign. */\nexport const SINGLE_SENDS_BASE: ListQueryFilter = {\n path: 'emailCampaignId',\n op: '==',\n value: null,\n}\n\n/**\n * The Campaign filter's clauses as the query asks them: \"Single send\" is\n * `emailCampaignId == null`, which the grammar spells `isEmpty`. The chips\n * keep reading the clause as the reader set it.\n */\nexport function emailCampaignQueryClauses(\n clauses: readonly ListFilterClause[],\n): ListFilterClause[] {\n return clauses.map((clause) =>\n clause.field === 'emailCampaignId' &&\n clause.op === 'equals' &&\n clause.value === SINGLE_SEND_FILTER_VALUE\n ? { ...clause, op: 'isEmpty', value: '' }\n : clause,\n )\n}\n"],"names":["nameSearchKey","nameSearchTokens","scopeTokensForHost","CAMPAIGN_SEND_SUBJECT_TOKENS","CAMPAIGN_NAME_TOKENS","CAMPAIGN_NAME_LOWER","CAMPAIGN_LIST_ORDER_FIELD","SINGLE_SEND_FILTER_VALUE","campaignSendSearchFields","subject","subjectTokens","campaignContainerSearchFields","name","text","nameLower","nameTokens","ORDER","path","direction","CREATED","column","kind","storedAs","operators","SUBJECT","tokensPath","STATUS","CAMPAIGN_SEND_STATUS_OPTIONS","value","label","campaignEmailsListQuery","orgHub","EMAILS_ORG","EMAILS_SITE","EMAIL_CAMPAIGN","presence","fields","sorts","search","CAMPAIGN_EMAILS_QUERY","campaignContainersListQuery","CONTAINERS_ORG","CONTAINERS_SITE","CONTAINER_NAME","CONTAINER_NAME_PREFIX","lowerPath","campaignSingleSendsListQuery","SINGLE_ORG","SINGLE_SITE","SINGLE_NAME","campaignSendsScope","hostId","op","CAMPAIGN_SITE_SEARCH_NOTICE","campaignSiteSearchClause","words","typed","map","word","trim","filter","Boolean","join","field","campaignContainersScope","SINGLE_SENDS_BASE","emailCampaignQueryClauses","clauses","clause"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/campaign-list-query.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 { nameSearchKey, nameSearchTokens } from '@aglyn/aglyn/app-utils/name-search'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListFilterClause } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * WHAT THE MARKETING LISTS ASK FIRESTORE (AGL-3321).\n *\n * Three tables read the org's campaign collections — the Campaigns list, a\n * campaign's own emails, and the Emails list — and every clause their\n * Filters panels offer, and their search boxes, is a predicate on ONE query,\n * newest first, paged by that query. Nothing is matched over the rows a read\n * happened to hold. Each field is here because a query can serve it.\n *\n * ## The fields the writers stamp\n *\n * A SEND (`orgs/{orgId}/campaigns`) carries:\n *\n * `createdAtMs` when the record was minted — the one date on every\n * send (a sent one has `sentAt`, a scheduled one\n * `sendAtMs`, a draft neither), so the order;\n * `subjectTokens` the subject's word prefixes, for search and\n * \"Subject contains\";\n * `emailCampaignId` the container it is filed under, or `null` for a\n * single send — stored null rather than left out,\n * because a query can ask for `== null` and can never\n * ask for a field that is missing.\n *\n * A CONTAINER (`orgs/{orgId}/emailCampaigns`) carries `createdAtMs`,\n * `nameTokens` (the org hub's search) and `nameLower` (a site hub's search,\n * as a prefix range — below).\n *\n * Every writer stamps them through the two helpers below, and\n * `tools/scripts/backfill-campaign-list-fields.mjs` stamps the records\n * written before them.\n *\n * ## What a site hub asks\n *\n * A send is sent AS one site, so a site hub's sends are `hostId == {site}` —\n * an equality, which merges with every other clause on single-field\n * indexes and leaves the query's one array clause for search. The security\n * rules admit that read for the site's collaborators (a send's `hostId` is\n * the site its `visibleTo` names). A container is placed on SEVERAL sites,\n * so its scope stays `visibleTo array-contains-any [org, host:{site}]` —\n * the query's one array clause, and the only one the rules can prove for a\n * site collaborator. So a site hub's campaign search is not a token lookup:\n * it is a PREFIX RANGE on `nameLower` beside the scope\n * (`campaignSiteSearchClause`), which orders the page by name while it\n * applies, and the list says search matches the start of a name. Folding\n * the word into the scope clause through scoped tokens would be refused by\n * the rules for such a reader: that query proves nothing about `visibleTo`.\n *\n * ## What is not offered\n *\n * - A send's STATE (Sending, Stopped, …) is derived from its counters at\n * read time (`campaignSendDisplay`); no query can ask it. Its stored\n * `status` is offered instead.\n * - A campaign's WINDOW (Upcoming, Running, Ended) is derived from its dates\n * against the clock; \"running\" is a range on two fields. Not offered.\n * - The site NAME a row shows is not stored on the record; the Site filter\n * asks by the site's id, and search reads the subject or the name only.\n * - Ranges: ONE, on `createdAtMs`, the field every list is ordered by — so\n * it needs no order of its own and no index beyond the ones above.\n */\n\n/** The send's subject, as word prefixes. */\nexport const CAMPAIGN_SEND_SUBJECT_TOKENS = 'subjectTokens'\n/** The container's name, as word prefixes. */\nexport const CAMPAIGN_NAME_TOKENS = 'nameTokens'\n/** The container's name, lower-cased whole, for a site hub's prefix search. */\nexport const CAMPAIGN_NAME_LOWER = 'nameLower'\n/** The one date on every send and container, and every list's order. */\nexport const CAMPAIGN_LIST_ORDER_FIELD = 'createdAtMs'\n\n/**\n * The Campaign filter's value for an email filed under no campaign. An\n * empty value reads as \"no value\" to the panel, so the choice needs one of\n * its own; the query asks `emailCampaignId == null` for it.\n */\nexport const SINGLE_SEND_FILTER_VALUE = '__single__'\n\n/**\n * The search field a send carries beside its `subject`, stamped by every\n * write that sets the subject.\n */\nexport function campaignSendSearchFields(subject: unknown): {\n subjectTokens: string[]\n} {\n return {\n subjectTokens: nameSearchTokens(typeof subject === 'string' ? subject : ''),\n }\n}\n\n/**\n * The search fields a container carries beside its `name`, stamped by every\n * write that sets the name.\n */\nexport function campaignContainerSearchFields(name: unknown): {\n nameLower: string\n nameTokens: string[]\n} {\n const text = typeof name === 'string' ? name : ''\n return { nameLower: nameSearchKey(text), nameTokens: nameSearchTokens(text) }\n}\n\nconst ORDER = [{ path: CAMPAIGN_LIST_ORDER_FIELD, direction: 'desc' as const }]\n\nconst CREATED: ListFilterField = {\n column: CAMPAIGN_LIST_ORDER_FIELD,\n kind: 'date',\n path: CAMPAIGN_LIST_ORDER_FIELD,\n storedAs: 'millis',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n}\n\nconst SUBJECT: ListFilterField = {\n column: 'subject',\n kind: 'text',\n path: 'subject',\n tokensPath: CAMPAIGN_SEND_SUBJECT_TOKENS,\n operators: ['contains'],\n}\n\nconst STATUS: ListFilterField = {\n column: 'status',\n kind: 'exact',\n path: 'status',\n operators: ['equals', 'isAnyOf'],\n}\n\n/**\n * The stored statuses a send can hold, for the Status filter. Stored\n * strings, not the State column's display states: an email between batches\n * is stored `scheduled` and SHOWN as Sending, which is why the filter is\n * headed Status and not State.\n */\nexport const CAMPAIGN_SEND_STATUS_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [\n { value: 'draft', label: 'Draft' },\n { value: 'scheduled', label: 'Scheduled' },\n { value: 'sending', label: 'Sending' },\n { value: 'sent', label: 'Sent' },\n { value: 'canceled', label: 'Canceled' },\n { value: 'failed', label: 'Failed' },\n]\n\n/**\n * The Emails list: every send, under a site or across the org.\n *\n * `site` is the org hub's only — under a site every row is that site's.\n */\nexport function campaignEmailsListQuery(orgHub: boolean): ListQueryDeclaration {\n return orgHub ? EMAILS_ORG : EMAILS_SITE\n}\n\nconst EMAIL_CAMPAIGN: ListFilterField = {\n column: 'emailCampaignId',\n kind: 'exact',\n path: 'emailCampaignId',\n presence: 'nullable',\n // `is` only: \"Single send\" is `== null`, which cannot sit in an `in`\n // beside campaign ids.\n operators: ['equals', 'isEmpty'],\n}\n\nconst EMAILS_SITE: ListQueryDeclaration = {\n fields: [SUBJECT, STATUS, EMAIL_CAMPAIGN, CREATED],\n sorts: ORDER,\n search: { tokensPath: CAMPAIGN_SEND_SUBJECT_TOKENS },\n}\nconst EMAILS_ORG: ListQueryDeclaration = {\n ...EMAILS_SITE,\n fields: [\n ...EMAILS_SITE.fields,\n { column: 'site', kind: 'exact', path: 'hostId', operators: ['equals', 'isAnyOf'] },\n ],\n}\n\n/** A campaign's own emails: the container is the query's base. */\nexport const CAMPAIGN_EMAILS_QUERY: ListQueryDeclaration = {\n fields: [SUBJECT, STATUS, CREATED],\n sorts: ORDER,\n search: { tokensPath: CAMPAIGN_SEND_SUBJECT_TOKENS },\n}\n\n/**\n * The Campaigns list's CAMPAIGNS: the containers. The columns are the\n * table's (`name`, `listIds`, `sitesLabel`); Lists and Sites are the org\n * hub's only — under a site the scope clause is the query's one array\n * clause, so an any-of on another array could never be served beside it.\n */\nexport function campaignContainersListQuery(orgHub: boolean): ListQueryDeclaration {\n return orgHub ? CONTAINERS_ORG : CONTAINERS_SITE\n}\n\nconst CONTAINER_NAME: ListFilterField = {\n column: 'name',\n kind: 'text',\n path: 'name',\n tokensPath: CAMPAIGN_NAME_TOKENS,\n operators: ['contains'],\n}\n/** A site hub's Campaign field: the name's start, a range beside the scope. */\nconst CONTAINER_NAME_PREFIX: ListFilterField = {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: CAMPAIGN_NAME_LOWER,\n operators: ['startsWith'],\n}\nconst CONTAINERS_SITE: ListQueryDeclaration = {\n fields: [CONTAINER_NAME_PREFIX, CREATED],\n sorts: ORDER,\n}\nconst CONTAINERS_ORG: ListQueryDeclaration = {\n sorts: ORDER,\n search: { tokensPath: CAMPAIGN_NAME_TOKENS },\n fields: [\n CONTAINER_NAME,\n { column: 'listIds', kind: 'exact', path: 'listIds', tokensPath: 'listIds', operators: ['isAnyOf'] },\n // Asked by scope token — `org` is \"every site\", `host:{id}` one site.\n { column: 'sitesLabel', kind: 'exact', path: 'visibleTo', tokensPath: 'visibleTo', operators: ['isAnyOf'] },\n CREATED,\n ],\n}\n\n/**\n * The Campaigns list's SINGLE SENDS: sends filed under no campaign, drawn\n * in the same table. The `name` column is the send's subject; `sitesLabel`\n * is the site it was sent as, on the org hub.\n */\nexport function campaignSingleSendsListQuery(orgHub: boolean): ListQueryDeclaration {\n return orgHub ? SINGLE_ORG : SINGLE_SITE\n}\n\nconst SINGLE_NAME: ListFilterField = { ...SUBJECT, column: 'name' }\nconst SINGLE_SITE: ListQueryDeclaration = {\n fields: [SINGLE_NAME, CREATED],\n sorts: ORDER,\n search: { tokensPath: CAMPAIGN_SEND_SUBJECT_TOKENS },\n}\nconst SINGLE_ORG: ListQueryDeclaration = {\n ...SINGLE_SITE,\n fields: [\n SINGLE_NAME,\n { column: 'sitesLabel', kind: 'exact', path: 'hostId', operators: ['equals', 'isAnyOf'] },\n CREATED,\n ],\n}\n\n/** A site hub's sends: the ones sent as it. Nothing on the org hub. */\nexport function campaignSendsScope(hostId: string | null): ListQueryFilter[] {\n return hostId ? [{ path: 'hostId', op: '==', value: hostId }] : []\n}\n\n/** Said above a site hub's campaigns while its search is applied. */\nexport const CAMPAIGN_SITE_SEARCH_NOTICE =\n 'Search on a site’s campaigns matches the start of a campaign’s name.'\n\n/**\n * A site hub's campaign search, as the clause its query can serve: the typed\n * words as \"Campaign starts with\", or null when nothing was typed. The list\n * passes it among its clauses and no search words — see the file header for\n * why a site's search is a prefix range and not a token lookup.\n */\nexport function campaignSiteSearchClause(\n words: readonly string[],\n): ListFilterClause | null {\n const typed = words.map((word) => word.trim()).filter(Boolean).join(' ')\n return typed ? { field: 'name', op: 'startsWith', value: typed } : null\n}\n\n/** A site hub's containers: the ones placed on every site or on it. */\nexport function campaignContainersScope(hostId: string | null): ListQueryFilter[] {\n return hostId\n ? [{ path: 'visibleTo', op: 'array-contains-any', value: scopeTokensForHost(hostId) }]\n : []\n}\n\n/** The sends filed under no campaign. */\nexport const SINGLE_SENDS_BASE: ListQueryFilter = {\n path: 'emailCampaignId',\n op: '==',\n value: null,\n}\n\n/**\n * The Campaign filter's clauses as the query asks them: \"Single send\" is\n * `emailCampaignId == null`, which the grammar spells `isEmpty`. The chips\n * keep reading the clause as the reader set it.\n */\nexport function emailCampaignQueryClauses(\n clauses: readonly ListFilterClause[],\n): ListFilterClause[] {\n return clauses.map((clause) =>\n clause.field === 'emailCampaignId' &&\n clause.op === 'equals' &&\n clause.value === SINGLE_SEND_FILTER_VALUE\n ? { ...clause, op: 'isEmpty', value: '' }\n : clause,\n )\n}\n"],"names":["nameSearchKey","nameSearchTokens","scopeTokensForHost","CAMPAIGN_SEND_SUBJECT_TOKENS","CAMPAIGN_NAME_TOKENS","CAMPAIGN_NAME_LOWER","CAMPAIGN_LIST_ORDER_FIELD","SINGLE_SEND_FILTER_VALUE","campaignSendSearchFields","subject","subjectTokens","campaignContainerSearchFields","name","text","nameLower","nameTokens","ORDER","path","direction","CREATED","column","kind","storedAs","operators","SUBJECT","tokensPath","STATUS","CAMPAIGN_SEND_STATUS_OPTIONS","value","label","campaignEmailsListQuery","orgHub","EMAILS_ORG","EMAILS_SITE","EMAIL_CAMPAIGN","presence","fields","sorts","search","CAMPAIGN_EMAILS_QUERY","campaignContainersListQuery","CONTAINERS_ORG","CONTAINERS_SITE","CONTAINER_NAME","CONTAINER_NAME_PREFIX","lowerPath","campaignSingleSendsListQuery","SINGLE_ORG","SINGLE_SITE","SINGLE_NAME","campaignSendsScope","hostId","op","CAMPAIGN_SITE_SEARCH_NOTICE","campaignSiteSearchClause","words","typed","map","word","trim","filter","Boolean","join","field","campaignContainersScope","SINGLE_SENDS_BASE","emailCampaignQueryClauses","clauses","clause"],"mappings":"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"}