@aglyn/plugins-crm 1.0.0-beta.232 → 1.0.0-beta.234
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/companies-section.js +43 -8
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +3 -1
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +6 -1
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contacts-section.js +37 -8
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-email-check.d.ts +14 -0
- package/src/lib/components/crm-email-check.js +38 -1
- package/src/lib/components/crm-email-check.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +5 -4
- package/src/lib/components/crm-next-activity-column.js +5 -4
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +3 -1
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-section.js +50 -6
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +9 -3
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/leads-section.js +61 -11
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/tasks-section.js +61 -9
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/company-filters.js +40 -2
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +10 -1
- package/src/lib/constants/contact-filters.js +62 -2
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.js +78 -0
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/hooks/use-crm-column-sort.d.ts +67 -0
- package/src/lib/hooks/use-crm-column-sort.js +156 -0
- package/src/lib/hooks/use-crm-column-sort.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +6 -1
- package/src/lib/hooks/use-crm-tasks.js +10 -8
- package/src/lib/hooks/use-crm-tasks.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +6 -0
- package/src/lib/model/crm-list-query.js +13 -0
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +19 -2
- package/src/lib/model/lead-filters.js +112 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/native-vocabulary.d.ts +41 -0
- package/src/lib/model/native-vocabulary.js +59 -0
- package/src/lib/model/native-vocabulary.js.map +1 -0
- package/src/lib/model/task-views.d.ts +15 -2
- package/src/lib/model/task-views.js +50 -4
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/email-check.d.ts +4 -1
- package/src/lib/server/email-check.js +18 -6
- package/src/lib/server/email-check.js.map +1 -1
- package/src/lib/transfer/tasks.js +2 -1
- package/src/lib/transfer/tasks.js.map +1 -1
|
@@ -40,6 +40,84 @@ import { LEAD_SOURCE_CODEC, LEAD_SOURCE_FILTER_NONE } from "../model/lead-filter
|
|
|
40
40
|
{
|
|
41
41
|
path: 'updatedAt',
|
|
42
42
|
direction: 'desc'
|
|
43
|
+
},
|
|
44
|
+
/*
|
|
45
|
+
* The header orders (AGL-3680), each `alone` — served with no filter or
|
|
46
|
+
* search on, beside the pipeline by one `(pipelineId, field)` and one
|
|
47
|
+
* `(visibleTo, field)` composite per direction: the title's key, the
|
|
48
|
+
* amount and expected close as stored beside them (`null` for none), the
|
|
49
|
+
* status and the next activity. Stage (the pipeline's order), Owner, Type
|
|
50
|
+
* and Lead source (the org's picklist order) and custom values sort the
|
|
51
|
+
* page.
|
|
52
|
+
*/ {
|
|
53
|
+
path: 'titleLower',
|
|
54
|
+
direction: 'asc',
|
|
55
|
+
column: 'title',
|
|
56
|
+
label: 'Deal',
|
|
57
|
+
alone: true
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
path: 'titleLower',
|
|
61
|
+
direction: 'desc',
|
|
62
|
+
column: 'title',
|
|
63
|
+
label: 'Deal',
|
|
64
|
+
alone: true
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
path: 'amountSortCents',
|
|
68
|
+
direction: 'asc',
|
|
69
|
+
column: 'amountCents',
|
|
70
|
+
label: 'Amount',
|
|
71
|
+
alone: true
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
path: 'amountSortCents',
|
|
75
|
+
direction: 'desc',
|
|
76
|
+
column: 'amountCents',
|
|
77
|
+
label: 'Amount',
|
|
78
|
+
alone: true
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
path: 'expectedCloseSortAtMs',
|
|
82
|
+
direction: 'asc',
|
|
83
|
+
column: 'expectedCloseAtMs',
|
|
84
|
+
label: 'Expected close',
|
|
85
|
+
alone: true
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: 'expectedCloseSortAtMs',
|
|
89
|
+
direction: 'desc',
|
|
90
|
+
column: 'expectedCloseAtMs',
|
|
91
|
+
label: 'Expected close',
|
|
92
|
+
alone: true
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
path: 'status',
|
|
96
|
+
direction: 'asc',
|
|
97
|
+
column: 'status',
|
|
98
|
+
label: 'Status',
|
|
99
|
+
alone: true
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
path: 'status',
|
|
103
|
+
direction: 'desc',
|
|
104
|
+
column: 'status',
|
|
105
|
+
label: 'Status',
|
|
106
|
+
alone: true
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
path: 'nextTaskAtMs',
|
|
110
|
+
direction: 'asc',
|
|
111
|
+
column: 'nextTaskAtMs',
|
|
112
|
+
label: 'Next activity',
|
|
113
|
+
alone: true
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
path: 'nextTaskAtMs',
|
|
117
|
+
direction: 'desc',
|
|
118
|
+
column: 'nextTaskAtMs',
|
|
119
|
+
label: 'Next activity',
|
|
120
|
+
alone: true
|
|
43
121
|
}
|
|
44
122
|
];
|
|
45
123
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/deal-filters.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, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListGridFilterCodec } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport { crmPicklistKey, type CrmViewFilterClause } from '@aglyn/aglyn/app-utils/crm'\nimport { CRM_NEXT_ACTIVITY_FILTER_FIELD } from '../model/crm-next-activity'\nimport {\n CRM_LIST_SEARCH,\n crmAnyOf,\n type CrmClauseAsked,\n crmClauseValues,\n crmSelectField,\n} from '../model/crm-list-query'\nimport { LEAD_SOURCE_CODEC, LEAD_SOURCE_FILTER_NONE } from '../model/lead-filters'\n\n/*\n * THE DEALS TABLE'S QUERY (AGL-3321).\n *\n * `orgs/{orgId}/deals` in the chosen pipeline — the pipeline is the query's\n * base beside the scope clause — newest change first, paged by that query.\n * The status, \"No next activity\" (`nextTaskAtMs == null`, which every deal\n * is created carrying) and the search word are predicates on the same\n * query. The search reads `searchTokens`, the word prefixes of the title\n * every deal writer stamps (`crmDealListFields`), so \"renewal\" finds \"Acme\n * renewal\" on any page. Under a site the scope clause is the query's one\n * array clause and the search folds into it for a reader who may drop it.\n *\n * The board is not this: it draws a pipeline's open deals whole, bounded,\n * and filters nothing.\n *\n * Type and Lead source (AGL-3516) are asked through the keys every deal\n * writer stores beside the labels (`typeKey`, `leadSourceKey` in\n * `crmDealListFields`) — `null` for none, so \"No type\" is `== null` — the\n * way the Leads list asks its lead source (`dealQueryClause`).\n */\n\n/** The table's one order: newest change first. */\nexport const DEAL_LIST_SORTS: readonly ListQuerySort[] = [\n { path: 'updatedAt', direction: 'desc' },\n]\n\n/**\n * The Type and Lead source controls' \"none\" choice (AGL-3516). Held in the\n * saved view as an `isEmpty` clause, never as this string.\n */\nexport const DEAL_PICKLIST_FILTER_NONE = LEAD_SOURCE_FILTER_NONE\n\n/**\n * The Type and Lead source clauses' codec: \"none\" is an `isEmpty` clause,\n * shown as a choice of the select — the Leads list's own.\n */\nexport const DEAL_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n type: LEAD_SOURCE_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n\n/** What the deals table's Filters panel offers — every one asked of the query. */\nexport const DEAL_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n crmSelectField('type'),\n crmSelectField('leadSource'),\n]\n\n/** A picklist key the query asks: `null` for none, as every deal writer stores it. */\nconst keyField = (column: string): ListFilterField => ({\n column,\n kind: 'exact',\n path: column,\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty'],\n})\n\n/** The fields the Deals query asks — each a stored field, as named. */\nexport const DEAL_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n keyField('typeKey'),\n keyField('leadSourceKey'),\n]\n\nexport const DEAL_LIST_DECLARATION: ListQueryDeclaration = {\n fields: DEAL_QUERY_FIELDS,\n sorts: DEAL_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/** The picklist fields the panel shows by label and the query asks by key. */\nconst DEAL_KEYED_FIELDS: Readonly<Record<string, string>> = {\n type: 'typeKey',\n leadSource: 'leadSourceKey',\n}\n\n/**\n * One grid clause as the clause the Deals query asks (AGL-3516): a Type or\n * a Lead source by its key — \"none\" as `isEmpty` — and every other clause\n * as it is stored.\n */\nexport function dealQueryClause(clause: CrmViewFilterClause | ListFilterRequest): CrmClauseAsked {\n const key = DEAL_KEYED_FIELDS[clause.field]\n if (!key) return clause\n const values = crmClauseValues(clause)\n if (clause.op === 'isEmpty' || (values.length === 1 && values[0] === DEAL_PICKLIST_FILTER_NONE)) {\n return { field: key, op: 'isEmpty', value: '' }\n }\n if (values.includes(DEAL_PICKLIST_FILTER_NONE)) {\n return { refused: '\"None\" cannot be picked beside a value' }\n }\n return crmAnyOf(\n key,\n values.map((value) => crmPicklistKey(value)).filter((entry): entry is string => Boolean(entry)),\n )\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the title, `titleLower`, beside the scope clause.\n */\nexport const DEAL_PREFIX_SEARCH = {\n field: {\n column: 'title',\n kind: 'text',\n path: 'title',\n lowerPath: 'titleLower',\n presence: 'always',\n operators: ['startsWith'],\n } satisfies ListFilterField,\n notice: 'Search matches the start of a deal’s title for access limited to specific sites.',\n}\n\n/** The chosen pipeline, as the query's base beside the scope clause. */\nexport function dealPipelineBase(pipelineId: string | null): ListQueryFilter[] {\n return pipelineId ? [{ path: 'pipelineId', op: '==', value: pipelineId }] : []\n}\n\n/** The base's shape for `listQueryIndexes`: the pipeline, an equality. */\nexport const DEAL_LIST_PIPELINE_BASE_INDEX = [{ path: 'pipelineId' }] as const\n"],"names":["crmPicklistKey","CRM_NEXT_ACTIVITY_FILTER_FIELD","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","LEAD_SOURCE_CODEC","LEAD_SOURCE_FILTER_NONE","DEAL_LIST_SORTS","path","direction","DEAL_PICKLIST_FILTER_NONE","DEAL_FILTER_CODECS","type","leadSource","DEAL_LIST_FILTER_FIELDS","keyField","column","kind","presence","operators","DEAL_QUERY_FIELDS","DEAL_LIST_DECLARATION","fields","sorts","search","DEAL_KEYED_FIELDS","dealQueryClause","clause","key","field","values","op","length","value","includes","refused","map","filter","entry","Boolean","DEAL_PREFIX_SEARCH","lowerPath","notice","dealPipelineBase","pipelineId","DEAL_LIST_PIPELINE_BASE_INDEX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GASD,SAASA,cAAc,QAAkC,6BAA4B;AACrF,SAASC,8BAA8B,QAAQ,gCAA4B;AAC3E,SACEC,eAAe,EACfC,QAAQ,EAERC,eAAe,EACfC,cAAc,QACT,6BAAyB;AAChC,SAASC,iBAAiB,EAAEC,uBAAuB,QAAQ,2BAAuB;AAElF;;;;;;;;;;;;;;;;;;;CAmBC,GAED,gDAAgD,GAChD,OAAO,MAAMC,kBAA4C;IACvD;QAAEC,MAAM;QAAaC,WAAW;IAAO;CACxC,CAAA;AAED;;;CAGC,GACD,OAAO,MAAMC,4BAA4BJ,wBAAuB;AAEhE;;;CAGC,GACD,OAAO,MAAMK,qBAAoE;IAC/EC,MAAMP;IACNQ,YAAYR;AACd,EAAC;AAED,gFAAgF,GAChF,OAAO,MAAMS,0BAAsD;IACjEV,eAAe;IACfJ;IACAI,eAAe;IACfA,eAAe;CAChB,CAAA;AAED,oFAAoF,GACpF,MAAMW,WAAW,CAACC,SAAqC,CAAA;QACrDA;QACAC,MAAM;QACNT,MAAMQ;QACNE,UAAU;QACVC,WAAW;YAAC;YAAU;YAAW;SAAU;IAC7C,CAAA;AAEA,qEAAqE,GACrE,OAAO,MAAMC,oBAAgD;IAC3DhB,eAAe;IACfJ;IACAe,SAAS;IACTA,SAAS;CACV,CAAA;AAED,OAAO,MAAMM,wBAA8C;IACzDC,QAAQF;IACRG,OAAOhB;IACPiB,QAAQvB;AACV,EAAC;AAED,4EAA4E,GAC5E,MAAMwB,oBAAsD;IAC1Db,MAAM;IACNC,YAAY;AACd;AAEA;;;;CAIC,GACD,OAAO,SAASa,gBAAgBC,MAA+C;IAC7E,MAAMC,MAAMH,iBAAiB,CAACE,OAAOE,KAAK,CAAC;IAC3C,IAAI,CAACD,KAAK,OAAOD;IACjB,MAAMG,SAAS3B,gBAAgBwB;IAC/B,IAAIA,OAAOI,EAAE,KAAK,aAAcD,OAAOE,MAAM,KAAK,KAAKF,MAAM,CAAC,EAAE,KAAKpB,2BAA4B;QAC/F,OAAO;YAAEmB,OAAOD;YAAKG,IAAI;YAAWE,OAAO;QAAG;IAChD;IACA,IAAIH,OAAOI,QAAQ,CAACxB,4BAA4B;QAC9C,OAAO;YAAEyB,SAAS;QAAyC;IAC7D;IACA,OAAOjC,SACL0B,KACAE,OAAOM,GAAG,CAAC,CAACH,QAAUlC,eAAekC,QAAQI,MAAM,CAAC,CAACC,QAA2BC,QAAQD;AAE5F;AAEA;;;CAGC,GACD,OAAO,MAAME,qBAAqB;IAChCX,OAAO;QACLb,QAAQ;QACRC,MAAM;QACNT,MAAM;QACNiC,WAAW;QACXvB,UAAU;QACVC,WAAW;YAAC;SAAa;IAC3B;IACAuB,QAAQ;AACV,EAAC;AAED,sEAAsE,GACtE,OAAO,SAASC,iBAAiBC,UAAyB;IACxD,OAAOA,aAAa;QAAC;YAAEpC,MAAM;YAAcuB,IAAI;YAAME,OAAOW;QAAW;KAAE,GAAG,EAAE;AAChF;AAEA,wEAAwE,GACxE,OAAO,MAAMC,gCAAgC;IAAC;QAAErC,MAAM;IAAa;CAAE,CAAS"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/deal-filters.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, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListGridFilterCodec } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport { crmPicklistKey, type CrmViewFilterClause } from '@aglyn/aglyn/app-utils/crm'\nimport { CRM_NEXT_ACTIVITY_FILTER_FIELD } from '../model/crm-next-activity'\nimport {\n CRM_LIST_SEARCH,\n crmAnyOf,\n type CrmClauseAsked,\n crmClauseValues,\n crmSelectField,\n} from '../model/crm-list-query'\nimport { LEAD_SOURCE_CODEC, LEAD_SOURCE_FILTER_NONE } from '../model/lead-filters'\n\n/*\n * THE DEALS TABLE'S QUERY (AGL-3321).\n *\n * `orgs/{orgId}/deals` in the chosen pipeline — the pipeline is the query's\n * base beside the scope clause — newest change first, paged by that query.\n * The status, \"No next activity\" (`nextTaskAtMs == null`, which every deal\n * is created carrying) and the search word are predicates on the same\n * query. The search reads `searchTokens`, the word prefixes of the title\n * every deal writer stamps (`crmDealListFields`), so \"renewal\" finds \"Acme\n * renewal\" on any page. Under a site the scope clause is the query's one\n * array clause and the search folds into it for a reader who may drop it.\n *\n * The board is not this: it draws a pipeline's open deals whole, bounded,\n * and filters nothing.\n *\n * Type and Lead source (AGL-3516) are asked through the keys every deal\n * writer stores beside the labels (`typeKey`, `leadSourceKey` in\n * `crmDealListFields`) — `null` for none, so \"No type\" is `== null` — the\n * way the Leads list asks its lead source (`dealQueryClause`).\n */\n\n/** The table's one order: newest change first. */\nexport const DEAL_LIST_SORTS: readonly ListQuerySort[] = [\n { path: 'updatedAt', direction: 'desc' },\n /*\n * The header orders (AGL-3680), each `alone` — served with no filter or\n * search on, beside the pipeline by one `(pipelineId, field)` and one\n * `(visibleTo, field)` composite per direction: the title's key, the\n * amount and expected close as stored beside them (`null` for none), the\n * status and the next activity. Stage (the pipeline's order), Owner, Type\n * and Lead source (the org's picklist order) and custom values sort the\n * page.\n */\n { path: 'titleLower', direction: 'asc', column: 'title', label: 'Deal', alone: true },\n { path: 'titleLower', direction: 'desc', column: 'title', label: 'Deal', alone: true },\n { path: 'amountSortCents', direction: 'asc', column: 'amountCents', label: 'Amount', alone: true },\n { path: 'amountSortCents', direction: 'desc', column: 'amountCents', label: 'Amount', alone: true },\n {\n path: 'expectedCloseSortAtMs',\n direction: 'asc',\n column: 'expectedCloseAtMs',\n label: 'Expected close',\n alone: true,\n },\n {\n path: 'expectedCloseSortAtMs',\n direction: 'desc',\n column: 'expectedCloseAtMs',\n label: 'Expected close',\n alone: true,\n },\n { path: 'status', direction: 'asc', column: 'status', label: 'Status', alone: true },\n { path: 'status', direction: 'desc', column: 'status', label: 'Status', alone: true },\n { path: 'nextTaskAtMs', direction: 'asc', column: 'nextTaskAtMs', label: 'Next activity', alone: true },\n { path: 'nextTaskAtMs', direction: 'desc', column: 'nextTaskAtMs', label: 'Next activity', alone: true },\n]\n\n/**\n * The Type and Lead source controls' \"none\" choice (AGL-3516). Held in the\n * saved view as an `isEmpty` clause, never as this string.\n */\nexport const DEAL_PICKLIST_FILTER_NONE = LEAD_SOURCE_FILTER_NONE\n\n/**\n * The Type and Lead source clauses' codec: \"none\" is an `isEmpty` clause,\n * shown as a choice of the select — the Leads list's own.\n */\nexport const DEAL_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n type: LEAD_SOURCE_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n\n/** What the deals table's Filters panel offers — every one asked of the query. */\nexport const DEAL_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n crmSelectField('type'),\n crmSelectField('leadSource'),\n]\n\n/** A picklist key the query asks: `null` for none, as every deal writer stores it. */\nconst keyField = (column: string): ListFilterField => ({\n column,\n kind: 'exact',\n path: column,\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty'],\n})\n\n/** The fields the Deals query asks — each a stored field, as named. */\nexport const DEAL_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n keyField('typeKey'),\n keyField('leadSourceKey'),\n]\n\nexport const DEAL_LIST_DECLARATION: ListQueryDeclaration = {\n fields: DEAL_QUERY_FIELDS,\n sorts: DEAL_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/** The picklist fields the panel shows by label and the query asks by key. */\nconst DEAL_KEYED_FIELDS: Readonly<Record<string, string>> = {\n type: 'typeKey',\n leadSource: 'leadSourceKey',\n}\n\n/**\n * One grid clause as the clause the Deals query asks (AGL-3516): a Type or\n * a Lead source by its key — \"none\" as `isEmpty` — and every other clause\n * as it is stored.\n */\nexport function dealQueryClause(clause: CrmViewFilterClause | ListFilterRequest): CrmClauseAsked {\n const key = DEAL_KEYED_FIELDS[clause.field]\n if (!key) return clause\n const values = crmClauseValues(clause)\n if (clause.op === 'isEmpty' || (values.length === 1 && values[0] === DEAL_PICKLIST_FILTER_NONE)) {\n return { field: key, op: 'isEmpty', value: '' }\n }\n if (values.includes(DEAL_PICKLIST_FILTER_NONE)) {\n return { refused: '\"None\" cannot be picked beside a value' }\n }\n return crmAnyOf(\n key,\n values.map((value) => crmPicklistKey(value)).filter((entry): entry is string => Boolean(entry)),\n )\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the title, `titleLower`, beside the scope clause.\n */\nexport const DEAL_PREFIX_SEARCH = {\n field: {\n column: 'title',\n kind: 'text',\n path: 'title',\n lowerPath: 'titleLower',\n presence: 'always',\n operators: ['startsWith'],\n } satisfies ListFilterField,\n notice: 'Search matches the start of a deal’s title for access limited to specific sites.',\n}\n\n/** The chosen pipeline, as the query's base beside the scope clause. */\nexport function dealPipelineBase(pipelineId: string | null): ListQueryFilter[] {\n return pipelineId ? [{ path: 'pipelineId', op: '==', value: pipelineId }] : []\n}\n\n/** The base's shape for `listQueryIndexes`: the pipeline, an equality. */\nexport const DEAL_LIST_PIPELINE_BASE_INDEX = [{ path: 'pipelineId' }] as const\n"],"names":["crmPicklistKey","CRM_NEXT_ACTIVITY_FILTER_FIELD","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","LEAD_SOURCE_CODEC","LEAD_SOURCE_FILTER_NONE","DEAL_LIST_SORTS","path","direction","column","label","alone","DEAL_PICKLIST_FILTER_NONE","DEAL_FILTER_CODECS","type","leadSource","DEAL_LIST_FILTER_FIELDS","keyField","kind","presence","operators","DEAL_QUERY_FIELDS","DEAL_LIST_DECLARATION","fields","sorts","search","DEAL_KEYED_FIELDS","dealQueryClause","clause","key","field","values","op","length","value","includes","refused","map","filter","entry","Boolean","DEAL_PREFIX_SEARCH","lowerPath","notice","dealPipelineBase","pipelineId","DEAL_LIST_PIPELINE_BASE_INDEX"],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { CrmViewSort } from '@aglyn/aglyn/app-utils/crm';
|
|
18
|
+
import { type ListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort';
|
|
19
|
+
import type { ListPageSort } from '@aglyn/shared-util-tools/list-query/list-column-sort';
|
|
20
|
+
import type { ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan';
|
|
21
|
+
import type { GridColDef } from '@mui/x-data-grid';
|
|
22
|
+
import type { CrmSavedViewController } from './use-crm-saved-view';
|
|
23
|
+
export interface CrmViewQuerySort {
|
|
24
|
+
/** The declared order the view asks, or the list's default. */
|
|
25
|
+
sort: ListQuerySort;
|
|
26
|
+
/** The view's own sort when no declared order serves it — the list said so. */
|
|
27
|
+
unserved: CrmViewSort | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A view's stored sort as one of the list's declared header orders, or the
|
|
31
|
+
* list's default when it names none — `unserved` holds what it named.
|
|
32
|
+
*/
|
|
33
|
+
export declare function crmViewQuerySort(sorts: readonly ListQuerySort[], viewSort: CrmViewSort | null | undefined, defaultSort: ListQuerySort): CrmViewQuerySort;
|
|
34
|
+
/**
|
|
35
|
+
* One page sort per column the query does not order (strategy 3), each
|
|
36
|
+
* reading the cell the way the grid reads it — through the column's own
|
|
37
|
+
* `valueGetter` — so a header sorts by what the reader sees. Skips the
|
|
38
|
+
* query's columns, the actions column and `unsortable`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function crmPageSorts<Row>(columns: readonly GridColDef[], skip: ReadonlySet<string>): Record<string, ListPageSort<Row>>;
|
|
41
|
+
export interface CrmColumnSortOptions<Row> {
|
|
42
|
+
/** The view the sort is kept in. */
|
|
43
|
+
views: Pick<CrmSavedViewController, 'setSort'>;
|
|
44
|
+
/** The list's declared orders; the ones with a `column` sort a header on the query. */
|
|
45
|
+
sorts: readonly ListQuerySort[];
|
|
46
|
+
/** The order asked while no header is. */
|
|
47
|
+
defaultSort: ListQuerySort;
|
|
48
|
+
/** What the view asks — `crmViewQuerySort`. */
|
|
49
|
+
asked: CrmViewQuerySort;
|
|
50
|
+
/** The order the query reads in: the plan's `orderBy`. */
|
|
51
|
+
orderBy: ListQuerySort;
|
|
52
|
+
rows: readonly Row[];
|
|
53
|
+
/** The grid's columns, whose other headers sort the page. */
|
|
54
|
+
columns: readonly GridColDef[];
|
|
55
|
+
/** A page sort a column needs other than its cell value — a picklist's rank. */
|
|
56
|
+
pageSorts?: Readonly<Record<string, ListPageSort<Row>>>;
|
|
57
|
+
/** Columns that are row controls, like the actions column (a task's done box). */
|
|
58
|
+
unsortable?: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A CRM list's header sorts: `useListColumnSort` over the list's declared
|
|
62
|
+
* orders and a page sort for every other column, the asked order kept in the
|
|
63
|
+
* view. Hand the result to `ListTable` as `columnSort`, and its `notices` to
|
|
64
|
+
* `ListQueryNotices` beside the plan's.
|
|
65
|
+
*/
|
|
66
|
+
export declare function useCrmColumnSort<Row>(options: CrmColumnSortOptions<Row>): ListColumnSort<Row>;
|
|
67
|
+
export default useCrmColumnSort;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { LIST_ACTIONS_FIELD } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
19
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
20
|
+
import { useCallback, useMemo } from "react";
|
|
21
|
+
/*
|
|
22
|
+
* EVERY CRM TABLE SORTS BY ITS COLUMN HEADERS (AGL-3680).
|
|
23
|
+
*
|
|
24
|
+
* The five CRM lists — Leads, Contacts, Companies, Deals, Tasks — each page
|
|
25
|
+
* a Firestore query, and their headers used to be switched off (a header
|
|
26
|
+
* sort over one page reads as the whole list's order) or, on Companies and
|
|
27
|
+
* Deals, left on over the page with nothing saying so. Each header now sorts
|
|
28
|
+
* by the one strategy honest for it (`list-column-sort.ts` in
|
|
29
|
+
* `@aglyn/shared-util-tools`):
|
|
30
|
+
*
|
|
31
|
+
* - a column backed by a stored field sorts the QUERY: the list declares a
|
|
32
|
+
* `ListQuerySort` with that `column` beside its default order;
|
|
33
|
+
* - every other column — a name joined from the roster, a picklist label
|
|
34
|
+
* in the org's order, a holder's facet value — sorts the PAGE, and the
|
|
35
|
+
* header and a notice say so.
|
|
36
|
+
*
|
|
37
|
+
* The order a header asks is the saved VIEW'S (AGL-2617): a view stores
|
|
38
|
+
* `{ field, direction }`, and `crmViewQuerySort` reads it back as one of the
|
|
39
|
+
* list's declared orders before the query is built. A view saved sorted by a
|
|
40
|
+
* column the query cannot order — a page column, or a column since removed
|
|
41
|
+
* — opens in the list's default order and says so.
|
|
42
|
+
*/ const same = (a, b)=>Boolean(a && b && a.path === b.path && a.direction === b.direction);
|
|
43
|
+
/**
|
|
44
|
+
* A view's stored sort as one of the list's declared header orders, or the
|
|
45
|
+
* list's default when it names none — `unserved` holds what it named.
|
|
46
|
+
*/ export function crmViewQuerySort(sorts, viewSort, defaultSort) {
|
|
47
|
+
if (!viewSort) return {
|
|
48
|
+
sort: defaultSort,
|
|
49
|
+
unserved: null
|
|
50
|
+
};
|
|
51
|
+
const declared = sorts.find((sort)=>sort.column === viewSort.field && sort.direction === viewSort.direction);
|
|
52
|
+
return declared ? {
|
|
53
|
+
sort: declared,
|
|
54
|
+
unserved: null
|
|
55
|
+
} : {
|
|
56
|
+
sort: defaultSort,
|
|
57
|
+
unserved: viewSort
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** A cell value as a page sort compares it. */ function sortValueOf(raw) {
|
|
61
|
+
if (raw === null || raw === undefined) return null;
|
|
62
|
+
if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean' || raw instanceof Date) {
|
|
63
|
+
return raw;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(raw)) return raw.map((entry)=>String(entry != null ? entry : '')).join(', ');
|
|
66
|
+
// A Firestore Timestamp, as a listener hands it over.
|
|
67
|
+
const stamp = raw;
|
|
68
|
+
if (typeof stamp.toMillis === 'function') return stamp.toMillis();
|
|
69
|
+
if (typeof stamp.seconds === 'number') return stamp.seconds * 1000;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* One page sort per column the query does not order (strategy 3), each
|
|
74
|
+
* reading the cell the way the grid reads it — through the column's own
|
|
75
|
+
* `valueGetter` — so a header sorts by what the reader sees. Skips the
|
|
76
|
+
* query's columns, the actions column and `unsortable`.
|
|
77
|
+
*/ export function crmPageSorts(columns, skip) {
|
|
78
|
+
const sorts = {};
|
|
79
|
+
for (const column of columns){
|
|
80
|
+
if (skip.has(column.field) || column.field === LIST_ACTIONS_FIELD) continue;
|
|
81
|
+
const getter = column.valueGetter;
|
|
82
|
+
sorts[column.field] = (row)=>{
|
|
83
|
+
const raw = row[column.field];
|
|
84
|
+
return sortValueOf(getter ? getter(raw, row, column, undefined) : raw);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return sorts;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A CRM list's header sorts: `useListColumnSort` over the list's declared
|
|
91
|
+
* orders and a page sort for every other column, the asked order kept in the
|
|
92
|
+
* view. Hand the result to `ListTable` as `columnSort`, and its `notices` to
|
|
93
|
+
* `ListQueryNotices` beside the plan's.
|
|
94
|
+
*/ export function useCrmColumnSort(options) {
|
|
95
|
+
const { views, sorts, defaultSort, asked, orderBy, rows, columns, pageSorts, unsortable } = options;
|
|
96
|
+
const { setSort } = views;
|
|
97
|
+
const queryColumns = useMemo(()=>new Set(sorts.flatMap((sort)=>sort.column ? [
|
|
98
|
+
sort.column
|
|
99
|
+
] : [])), [
|
|
100
|
+
sorts
|
|
101
|
+
]);
|
|
102
|
+
const allPageSorts = useMemo(()=>_extends({}, crmPageSorts(columns, new Set([
|
|
103
|
+
...queryColumns,
|
|
104
|
+
...unsortable != null ? unsortable : []
|
|
105
|
+
])), pageSorts), [
|
|
106
|
+
columns,
|
|
107
|
+
queryColumns,
|
|
108
|
+
unsortable,
|
|
109
|
+
pageSorts
|
|
110
|
+
]);
|
|
111
|
+
const headers = useMemo(()=>Object.fromEntries(columns.map((column)=>[
|
|
112
|
+
column.field,
|
|
113
|
+
column.headerName || column.field
|
|
114
|
+
])), [
|
|
115
|
+
columns
|
|
116
|
+
]);
|
|
117
|
+
// The view keeps a header's order; the default is kept as no sort at all.
|
|
118
|
+
const onSortChange = useCallback((next)=>setSort((next == null ? void 0 : next.column) && !same(next, defaultSort) ? {
|
|
119
|
+
field: next.column,
|
|
120
|
+
direction: next.direction
|
|
121
|
+
} : null), [
|
|
122
|
+
setSort,
|
|
123
|
+
defaultSort
|
|
124
|
+
]);
|
|
125
|
+
const columnSort = useListColumnSort({
|
|
126
|
+
sorts,
|
|
127
|
+
defaultSort,
|
|
128
|
+
sort: asked.sort,
|
|
129
|
+
onSortChange,
|
|
130
|
+
orderBy,
|
|
131
|
+
rows,
|
|
132
|
+
pageSorts: allPageSorts,
|
|
133
|
+
headers
|
|
134
|
+
});
|
|
135
|
+
const unserved = asked.unserved;
|
|
136
|
+
const notices = useMemo(()=>{
|
|
137
|
+
var _headers_unserved_field;
|
|
138
|
+
return unserved ? [
|
|
139
|
+
...columnSort.notices,
|
|
140
|
+
`This view is saved sorted by ${(_headers_unserved_field = headers[unserved.field]) != null ? _headers_unserved_field : unserved.field}, which the list's query cannot order by: it is shown in the list's own order. Click the header to sort the rows on this page.`
|
|
141
|
+
] : columnSort.notices;
|
|
142
|
+
}, [
|
|
143
|
+
columnSort.notices,
|
|
144
|
+
unserved,
|
|
145
|
+
headers
|
|
146
|
+
]);
|
|
147
|
+
return useMemo(()=>_extends({}, columnSort, {
|
|
148
|
+
notices
|
|
149
|
+
}), [
|
|
150
|
+
columnSort,
|
|
151
|
+
notices
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
export default useCrmColumnSort;
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=use-crm-column-sort.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-column-sort.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\n'use client'\n\nimport type { CrmViewSort } from '@aglyn/aglyn/app-utils/crm'\nimport { LIST_ACTIONS_FIELD } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n type ListColumnSort,\n useListColumnSort,\n} from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport type {\n ListPageSort,\n ListSortValue,\n} from '@aglyn/shared-util-tools/list-query/list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useCallback, useMemo } from 'react'\nimport type { CrmSavedViewController } from './use-crm-saved-view'\n\n/*\n * EVERY CRM TABLE SORTS BY ITS COLUMN HEADERS (AGL-3680).\n *\n * The five CRM lists — Leads, Contacts, Companies, Deals, Tasks — each page\n * a Firestore query, and their headers used to be switched off (a header\n * sort over one page reads as the whole list's order) or, on Companies and\n * Deals, left on over the page with nothing saying so. Each header now sorts\n * by the one strategy honest for it (`list-column-sort.ts` in\n * `@aglyn/shared-util-tools`):\n *\n * - a column backed by a stored field sorts the QUERY: the list declares a\n * `ListQuerySort` with that `column` beside its default order;\n * - every other column — a name joined from the roster, a picklist label\n * in the org's order, a holder's facet value — sorts the PAGE, and the\n * header and a notice say so.\n *\n * The order a header asks is the saved VIEW'S (AGL-2617): a view stores\n * `{ field, direction }`, and `crmViewQuerySort` reads it back as one of the\n * list's declared orders before the query is built. A view saved sorted by a\n * column the query cannot order — a page column, or a column since removed\n * — opens in the list's default order and says so.\n */\n\nconst same = (a: ListQuerySort | null | undefined, b: ListQuerySort | null | undefined) =>\n Boolean(a && b && a.path === b.path && a.direction === b.direction)\n\nexport interface CrmViewQuerySort {\n /** The declared order the view asks, or the list's default. */\n sort: ListQuerySort\n /** The view's own sort when no declared order serves it — the list said so. */\n unserved: CrmViewSort | null\n}\n\n/**\n * A view's stored sort as one of the list's declared header orders, or the\n * list's default when it names none — `unserved` holds what it named.\n */\nexport function crmViewQuerySort(\n sorts: readonly ListQuerySort[],\n viewSort: CrmViewSort | null | undefined,\n defaultSort: ListQuerySort,\n): CrmViewQuerySort {\n if (!viewSort) return { sort: defaultSort, unserved: null }\n const declared = sorts.find(\n (sort) => sort.column === viewSort.field && sort.direction === viewSort.direction,\n )\n return declared ? { sort: declared, unserved: null } : { sort: defaultSort, unserved: viewSort }\n}\n\n/** A cell value as a page sort compares it. */\nfunction sortValueOf(raw: unknown): ListSortValue {\n if (raw === null || raw === undefined) return null\n if (\n typeof raw === 'string' ||\n typeof raw === 'number' ||\n typeof raw === 'boolean' ||\n raw instanceof Date\n ) {\n return raw\n }\n if (Array.isArray(raw)) return raw.map((entry) => String(entry ?? '')).join(', ')\n // A Firestore Timestamp, as a listener hands it over.\n const stamp = raw as { toMillis?: () => number; seconds?: number }\n if (typeof stamp.toMillis === 'function') return stamp.toMillis()\n if (typeof stamp.seconds === 'number') return stamp.seconds * 1000\n return null\n}\n\n/**\n * One page sort per column the query does not order (strategy 3), each\n * reading the cell the way the grid reads it — through the column's own\n * `valueGetter` — so a header sorts by what the reader sees. Skips the\n * query's columns, the actions column and `unsortable`.\n */\nexport function crmPageSorts<Row>(\n columns: readonly GridColDef[],\n skip: ReadonlySet<string>,\n): Record<string, ListPageSort<Row>> {\n const sorts: Record<string, ListPageSort<Row>> = {}\n for (const column of columns) {\n if (skip.has(column.field) || column.field === LIST_ACTIONS_FIELD) continue\n const getter = column.valueGetter as\n | ((value: unknown, row: Row, column: GridColDef, apiRef: never) => unknown)\n | undefined\n sorts[column.field] = (row: Row) => {\n const raw = (row as Record<string, unknown>)[column.field]\n return sortValueOf(getter ? getter(raw, row, column, undefined as never) : raw)\n }\n }\n return sorts\n}\n\nexport interface CrmColumnSortOptions<Row> {\n /** The view the sort is kept in. */\n views: Pick<CrmSavedViewController, 'setSort'>\n /** The list's declared orders; the ones with a `column` sort a header on the query. */\n sorts: readonly ListQuerySort[]\n /** The order asked while no header is. */\n defaultSort: ListQuerySort\n /** What the view asks — `crmViewQuerySort`. */\n asked: CrmViewQuerySort\n /** The order the query reads in: the plan's `orderBy`. */\n orderBy: ListQuerySort\n rows: readonly Row[]\n /** The grid's columns, whose other headers sort the page. */\n columns: readonly GridColDef[]\n /** A page sort a column needs other than its cell value — a picklist's rank. */\n pageSorts?: Readonly<Record<string, ListPageSort<Row>>>\n /** Columns that are row controls, like the actions column (a task's done box). */\n unsortable?: readonly string[]\n}\n\n/**\n * A CRM list's header sorts: `useListColumnSort` over the list's declared\n * orders and a page sort for every other column, the asked order kept in the\n * view. Hand the result to `ListTable` as `columnSort`, and its `notices` to\n * `ListQueryNotices` beside the plan's.\n */\nexport function useCrmColumnSort<Row>(options: CrmColumnSortOptions<Row>): ListColumnSort<Row> {\n const { views, sorts, defaultSort, asked, orderBy, rows, columns, pageSorts, unsortable } =\n options\n const { setSort } = views\n const queryColumns = useMemo(\n () => new Set(sorts.flatMap((sort) => (sort.column ? [sort.column] : []))),\n [sorts],\n )\n const allPageSorts = useMemo(\n () => ({\n ...crmPageSorts<Row>(columns, new Set([...queryColumns, ...(unsortable ?? [])])),\n ...pageSorts,\n }),\n [columns, queryColumns, unsortable, pageSorts],\n )\n const headers = useMemo(\n () =>\n Object.fromEntries(columns.map((column) => [column.field, column.headerName || column.field])),\n [columns],\n )\n // The view keeps a header's order; the default is kept as no sort at all.\n const onSortChange = useCallback(\n (next: ListQuerySort | null) =>\n setSort(\n next?.column && !same(next, defaultSort)\n ? { field: next.column, direction: next.direction }\n : null,\n ),\n [setSort, defaultSort],\n )\n const columnSort = useListColumnSort<Row>({\n sorts,\n defaultSort,\n sort: asked.sort,\n onSortChange,\n orderBy,\n rows,\n pageSorts: allPageSorts,\n headers,\n })\n const unserved = asked.unserved\n const notices = useMemo(\n () =>\n unserved\n ? [\n ...columnSort.notices,\n `This view is saved sorted by ${headers[unserved.field] ?? unserved.field}, which the list's query cannot order by: it is shown in the list's own order. Click the header to sort the rows on this page.`,\n ]\n : columnSort.notices,\n [columnSort.notices, unserved, headers],\n )\n return useMemo(() => ({ ...columnSort, notices }), [columnSort, notices])\n}\n\nexport default useCrmColumnSort\n"],"names":["LIST_ACTIONS_FIELD","useListColumnSort","useCallback","useMemo","same","a","b","Boolean","path","direction","crmViewQuerySort","sorts","viewSort","defaultSort","sort","unserved","declared","find","column","field","sortValueOf","raw","undefined","Date","Array","isArray","map","entry","String","join","stamp","toMillis","seconds","crmPageSorts","columns","skip","has","getter","valueGetter","row","useCrmColumnSort","options","views","asked","orderBy","rows","pageSorts","unsortable","setSort","queryColumns","Set","flatMap","allPageSorts","headers","Object","fromEntries","headerName","onSortChange","next","columnSort","notices"],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type CrmTask } from '@aglyn/aglyn';
|
|
18
18
|
import type { ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
19
|
-
import type { ListQueryPlan } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
19
|
+
import type { ListQueryPlan, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
20
20
|
import { type CrmTaskView } from '../model/task-views';
|
|
21
21
|
/** A task as the console lists it: the document plus its id. */
|
|
22
22
|
export type CrmTaskRow = CrmTask & {
|
|
@@ -74,6 +74,11 @@ export declare function useCrmTaskList(options: {
|
|
|
74
74
|
/** The Filters panel's clauses, the view's own excluded. */
|
|
75
75
|
clauses?: readonly ListFilterRequest[];
|
|
76
76
|
search?: readonly string[];
|
|
77
|
+
/**
|
|
78
|
+
* A header's order (AGL-3680), one of `TASK_LIST_SORTS`; absent, the
|
|
79
|
+
* view's own — Due soonest first, Done most recently due first.
|
|
80
|
+
*/
|
|
81
|
+
sort?: ListQuerySort | null;
|
|
77
82
|
}): CrmTaskPageResult;
|
|
78
83
|
/**
|
|
79
84
|
* How many of one record's tasks a card reads. A person with more than fifty
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
|
|
18
18
|
import { collection, limit, orderBy, query, where } from "firebase/firestore";
|
|
19
19
|
import { useEffect, useMemo, useState } from "react";
|
|
20
|
-
import { crmTaskViewBase, crmTaskViewPlan, orderTaskRows, TASK_LIST_DECLARATION } from "../model/task-views.js";
|
|
20
|
+
import { crmTaskViewBase, crmTaskViewPlan, crmTaskViewSort, orderTaskRows, TASK_LIST_DECLARATION } from "../model/task-views.js";
|
|
21
21
|
import { useCrmFoldsScope, useCrmListQuery } from "./use-crm-list-query.js";
|
|
22
22
|
import { crmVisibleToClause, useCrmScope } from "./use-crm-scope.js";
|
|
23
23
|
/**
|
|
@@ -46,6 +46,7 @@ import { crmVisibleToClause, useCrmScope } from "./use-crm-scope.js";
|
|
|
46
46
|
* query for `assigneeUid == ''` would be a real read that could only answer
|
|
47
47
|
* nothing.
|
|
48
48
|
*/ export function useCrmTaskList(options) {
|
|
49
|
+
var _options_sort;
|
|
49
50
|
const { hostId, org, view, uid, nowMs, clauses = NO_CLAUSES, search = NO_WORDS } = options;
|
|
50
51
|
// The org root and the reader's tokens from the one scope hook (AGL-2614)
|
|
51
52
|
// — the same expression the contacts list runs, so a task listed beside a
|
|
@@ -65,12 +66,10 @@ import { crmVisibleToClause, useCrmScope } from "./use-crm-scope.js";
|
|
|
65
66
|
const base = useMemo(()=>crmTaskViewBase(plan), [
|
|
66
67
|
plan
|
|
67
68
|
]);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
direction: plan.direction
|
|
71
|
-
}), [
|
|
72
|
-
plan.direction
|
|
69
|
+
const viewSort = useMemo(()=>crmTaskViewSort(plan), [
|
|
70
|
+
plan
|
|
73
71
|
]);
|
|
72
|
+
const sort = (_options_sort = options.sort) != null ? _options_sort : viewSort;
|
|
74
73
|
const foldsScope = useCrmFoldsScope(orgId, readTokens);
|
|
75
74
|
const paged = useCrmListQuery({
|
|
76
75
|
scope,
|
|
@@ -84,8 +83,11 @@ import { crmVisibleToClause, useCrmScope } from "./use-crm-scope.js";
|
|
|
84
83
|
base,
|
|
85
84
|
enabled: plan.assigneeUid === undefined || Boolean(plan.assigneeUid)
|
|
86
85
|
});
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
// Undated last only under the Due order; a header's order is the query's.
|
|
87
|
+
const orderedByDue = paged.plan.orderBy.path === 'dueAtMs';
|
|
88
|
+
const tasks = useMemo(()=>orderedByDue ? orderTaskRows(paged.rows) : paged.rows, [
|
|
89
|
+
paged.rows,
|
|
90
|
+
orderedByDue
|
|
89
91
|
]);
|
|
90
92
|
return {
|
|
91
93
|
tasks,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-tasks.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 { CRM_COLLECTIONS, type CrmTask } from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport type { ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListQueryPlan } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n type CrmTaskView,\n crmTaskViewBase,\n crmTaskViewPlan,\n orderTaskRows,\n TASK_LIST_DECLARATION,\n} from '../model/task-views'\nimport { useCrmFoldsScope, useCrmListQuery } from './use-crm-list-query'\nimport { crmVisibleToClause, useCrmScope } from './use-crm-scope'\n\n/** A task as the console lists it: the document plus its id. */\nexport type CrmTaskRow = CrmTask & { $id: string }\n\n/**\n * The reader's clock, refreshed once a minute (AGL-2599).\n *\n * \"Overdue\" and \"today\" are computed at read time against this value, so a\n * tab left open across midnight repaints yesterday's work red without a\n * reload. A minute is coarse enough that the re-render is invisible and fine\n * enough that a task due at 9:00 does not read as upcoming at 9:05.\n */\nexport function useNowMs(): number {\n const [nowMs, setNowMs] = useState(() => Date.now())\n useEffect(() => {\n const timer = setInterval(() => setNowMs(Date.now()), 60_000)\n return () => clearInterval(timer)\n }, [])\n return nowMs\n}\n\nexport interface CrmTaskListResult {\n tasks: CrmTaskRow[]\n status: 'loading' | 'success' | 'error'\n /** The rows are unconfirmed by the server — what a guarded write reads. */\n fromCache: boolean\n /** The window filled up; there are tasks this view is not showing. */\n truncated: boolean\n scope: readonly [string, string] | null\n orgId: string | null | undefined\n /** The reader's tokens, or `null` at the organization level — no clause (AGL-2630). */\n readTokens: readonly string[] | null\n}\n\n/** One page of the tasks list, and what its query held. */\nexport interface CrmTaskPageResult extends CrmTaskListResult {\n /** What the query holds, what it refused and why (AGL-3321). */\n plan: ListQueryPlan\n hasMore: boolean\n page: number\n setPage: (page: number) => void\n pageSize: number\n setPageSize: (pageSize: number) => void\n}\n\n/**\n * One view of the tasks section, PAGED BY ITS QUERY (AGL-3321).\n *\n * The plan comes from `crmTaskViewPlan`, so the hook holds no opinion about\n * what \"today\" means; the view's predicates are the query's base beside the\n * scope clause, and Kind, Priority, Assignee and the search word are planned\n * onto the same query — every page a page of matches, none narrowed in\n * memory. \"My tasks\" opens no listener until the reader's uid is known — a\n * query for `assigneeUid == ''` would be a real read that could only answer\n * nothing.\n */\nexport function useCrmTaskList(options: {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Record<string, unknown> | null\n view: CrmTaskView\n uid: string | null | undefined\n nowMs: number\n /** The Filters panel's clauses, the view's own excluded. */\n clauses?: readonly ListFilterRequest[]\n search?: readonly string[]\n}): CrmTaskPageResult {\n const { hostId, org, view, uid, nowMs, clauses = NO_CLAUSES, search = NO_WORDS } = options\n // The org root and the reader's tokens from the one scope hook (AGL-2614)\n // — the same expression the contacts list runs, so a task listed beside a\n // contact is scoped by exactly the predicate that admitted the contact.\n const { scope, orgId, visibleTo: readTokens } = useCrmScope({ hostId, org })\n const plan = useMemo(\n () => crmTaskViewPlan(view, { nowMs, uid }),\n [view, nowMs, uid],\n )\n const base = useMemo(() => crmTaskViewBase(plan), [plan])\n const sort = useMemo(\n () => ({ path: 'dueAtMs', direction: plan.direction }),\n [plan.direction],\n )\n const foldsScope = useCrmFoldsScope(orgId, readTokens)\n const paged = useCrmListQuery<CrmTaskRow>({\n scope,\n collection: CRM_COLLECTIONS.tasks,\n visibleTo: readTokens,\n foldsScope,\n declaration: TASK_LIST_DECLARATION,\n clauses,\n search,\n sort,\n base,\n enabled: plan.assigneeUid === undefined || Boolean(plan.assigneeUid),\n })\n const tasks = useMemo(() => orderTaskRows(paged.rows), [paged.rows])\n return {\n tasks,\n status: paged.status,\n fromCache: paged.fromCache,\n truncated: paged.hasMore,\n scope,\n orgId,\n readTokens,\n plan: paged.plan,\n hasMore: paged.hasMore,\n page: paged.page,\n setPage: paged.setPage,\n pageSize: paged.pageSize,\n setPageSize: paged.setPageSize,\n }\n}\n\nconst NO_CLAUSES: readonly ListFilterRequest[] = []\nconst NO_WORDS: readonly string[] = []\n\n/**\n * How many of one record's tasks a card reads. A person with more than fifty\n * open tasks about one contact has a different problem than a short list.\n */\nexport const CRM_RECORD_TASK_LIMIT = 50\n\n/** Which record a task list is FOR — exactly one of the three. */\nexport interface CrmRecordRef {\n contactId?: string\n companyId?: string\n dealId?: string\n}\n\n/**\n * The tasks that hang off one contact, company or deal, open and done\n * together, soonest due first.\n *\n * Rides the per-record indexes — `(visibleTo, contactId, dueAtMs)` and its\n * two siblings — with no status clause, because the card shows the open ones\n * and counts the rest, and two listeners for one short list is one too many.\n */\nexport function useCrmRecordTasks(options: {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Record<string, unknown> | null\n record: CrmRecordRef\n}): CrmTaskListResult {\n const { hostId, org, record } = options\n const firestore = useFirestore()\n const { scope, orgId, visibleTo: readTokens } = useCrmScope({ hostId, org })\n const field = record.contactId\n ? 'contactId'\n : record.companyId\n ? 'companyId'\n : record.dealId\n ? 'dealId'\n : null\n const id = field ? record[field] : undefined\n const { data, status, fromCache } = useFirestoreCollection<CrmTaskRow>(\n () => {\n if (!scope || !field || !id) return null\n return query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.tasks),\n ...crmVisibleToClause(readTokens),\n where(field, '==', id),\n orderBy('dueAtMs', 'asc'),\n limit(CRM_RECORD_TASK_LIMIT),\n )\n },\n [firestore, scope, readTokens, field, id],\n { idField: '$id' },\n )\n const tasks = useMemo(() => orderTaskRows(data ?? []), [data])\n return {\n tasks,\n status,\n fromCache,\n truncated: (data?.length ?? 0) >= CRM_RECORD_TASK_LIMIT,\n scope,\n orgId,\n readTokens,\n }\n}\n"],"names":["CRM_COLLECTIONS","useFirestore","useFirestoreCollection","collection","limit","orderBy","query","where","useEffect","useMemo","useState","crmTaskViewBase","crmTaskViewPlan","orderTaskRows","TASK_LIST_DECLARATION","useCrmFoldsScope","useCrmListQuery","crmVisibleToClause","useCrmScope","useNowMs","nowMs","setNowMs","Date","now","timer","setInterval","clearInterval","useCrmTaskList","options","hostId","org","view","uid","clauses","NO_CLAUSES","search","NO_WORDS","scope","orgId","visibleTo","readTokens","plan","base","sort","path","direction","foldsScope","paged","tasks","declaration","enabled","assigneeUid","undefined","Boolean","rows","status","fromCache","truncated","hasMore","page","setPage","pageSize","setPageSize","CRM_RECORD_TASK_LIMIT","useCrmRecordTasks","record","firestore","field","contactId","companyId","dealId","id","data","idField","length"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-tasks.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 { CRM_COLLECTIONS, type CrmTask } from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport type { ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListQueryPlan, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n type CrmTaskView,\n crmTaskViewBase,\n crmTaskViewPlan,\n crmTaskViewSort,\n orderTaskRows,\n TASK_LIST_DECLARATION,\n} from '../model/task-views'\nimport { useCrmFoldsScope, useCrmListQuery } from './use-crm-list-query'\nimport { crmVisibleToClause, useCrmScope } from './use-crm-scope'\n\n/** A task as the console lists it: the document plus its id. */\nexport type CrmTaskRow = CrmTask & { $id: string }\n\n/**\n * The reader's clock, refreshed once a minute (AGL-2599).\n *\n * \"Overdue\" and \"today\" are computed at read time against this value, so a\n * tab left open across midnight repaints yesterday's work red without a\n * reload. A minute is coarse enough that the re-render is invisible and fine\n * enough that a task due at 9:00 does not read as upcoming at 9:05.\n */\nexport function useNowMs(): number {\n const [nowMs, setNowMs] = useState(() => Date.now())\n useEffect(() => {\n const timer = setInterval(() => setNowMs(Date.now()), 60_000)\n return () => clearInterval(timer)\n }, [])\n return nowMs\n}\n\nexport interface CrmTaskListResult {\n tasks: CrmTaskRow[]\n status: 'loading' | 'success' | 'error'\n /** The rows are unconfirmed by the server — what a guarded write reads. */\n fromCache: boolean\n /** The window filled up; there are tasks this view is not showing. */\n truncated: boolean\n scope: readonly [string, string] | null\n orgId: string | null | undefined\n /** The reader's tokens, or `null` at the organization level — no clause (AGL-2630). */\n readTokens: readonly string[] | null\n}\n\n/** One page of the tasks list, and what its query held. */\nexport interface CrmTaskPageResult extends CrmTaskListResult {\n /** What the query holds, what it refused and why (AGL-3321). */\n plan: ListQueryPlan\n hasMore: boolean\n page: number\n setPage: (page: number) => void\n pageSize: number\n setPageSize: (pageSize: number) => void\n}\n\n/**\n * One view of the tasks section, PAGED BY ITS QUERY (AGL-3321).\n *\n * The plan comes from `crmTaskViewPlan`, so the hook holds no opinion about\n * what \"today\" means; the view's predicates are the query's base beside the\n * scope clause, and Kind, Priority, Assignee and the search word are planned\n * onto the same query — every page a page of matches, none narrowed in\n * memory. \"My tasks\" opens no listener until the reader's uid is known — a\n * query for `assigneeUid == ''` would be a real read that could only answer\n * nothing.\n */\nexport function useCrmTaskList(options: {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Record<string, unknown> | null\n view: CrmTaskView\n uid: string | null | undefined\n nowMs: number\n /** The Filters panel's clauses, the view's own excluded. */\n clauses?: readonly ListFilterRequest[]\n search?: readonly string[]\n /**\n * A header's order (AGL-3680), one of `TASK_LIST_SORTS`; absent, the\n * view's own — Due soonest first, Done most recently due first.\n */\n sort?: ListQuerySort | null\n}): CrmTaskPageResult {\n const { hostId, org, view, uid, nowMs, clauses = NO_CLAUSES, search = NO_WORDS } = options\n // The org root and the reader's tokens from the one scope hook (AGL-2614)\n // — the same expression the contacts list runs, so a task listed beside a\n // contact is scoped by exactly the predicate that admitted the contact.\n const { scope, orgId, visibleTo: readTokens } = useCrmScope({ hostId, org })\n const plan = useMemo(\n () => crmTaskViewPlan(view, { nowMs, uid }),\n [view, nowMs, uid],\n )\n const base = useMemo(() => crmTaskViewBase(plan), [plan])\n const viewSort = useMemo(() => crmTaskViewSort(plan), [plan])\n const sort = options.sort ?? viewSort\n const foldsScope = useCrmFoldsScope(orgId, readTokens)\n const paged = useCrmListQuery<CrmTaskRow>({\n scope,\n collection: CRM_COLLECTIONS.tasks,\n visibleTo: readTokens,\n foldsScope,\n declaration: TASK_LIST_DECLARATION,\n clauses,\n search,\n sort,\n base,\n enabled: plan.assigneeUid === undefined || Boolean(plan.assigneeUid),\n })\n // Undated last only under the Due order; a header's order is the query's.\n const orderedByDue = paged.plan.orderBy.path === 'dueAtMs'\n const tasks = useMemo(\n () => (orderedByDue ? orderTaskRows(paged.rows) : paged.rows),\n [paged.rows, orderedByDue],\n )\n return {\n tasks,\n status: paged.status,\n fromCache: paged.fromCache,\n truncated: paged.hasMore,\n scope,\n orgId,\n readTokens,\n plan: paged.plan,\n hasMore: paged.hasMore,\n page: paged.page,\n setPage: paged.setPage,\n pageSize: paged.pageSize,\n setPageSize: paged.setPageSize,\n }\n}\n\nconst NO_CLAUSES: readonly ListFilterRequest[] = []\nconst NO_WORDS: readonly string[] = []\n\n/**\n * How many of one record's tasks a card reads. A person with more than fifty\n * open tasks about one contact has a different problem than a short list.\n */\nexport const CRM_RECORD_TASK_LIMIT = 50\n\n/** Which record a task list is FOR — exactly one of the three. */\nexport interface CrmRecordRef {\n contactId?: string\n companyId?: string\n dealId?: string\n}\n\n/**\n * The tasks that hang off one contact, company or deal, open and done\n * together, soonest due first.\n *\n * Rides the per-record indexes — `(visibleTo, contactId, dueAtMs)` and its\n * two siblings — with no status clause, because the card shows the open ones\n * and counts the rest, and two listeners for one short list is one too many.\n */\nexport function useCrmRecordTasks(options: {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Record<string, unknown> | null\n record: CrmRecordRef\n}): CrmTaskListResult {\n const { hostId, org, record } = options\n const firestore = useFirestore()\n const { scope, orgId, visibleTo: readTokens } = useCrmScope({ hostId, org })\n const field = record.contactId\n ? 'contactId'\n : record.companyId\n ? 'companyId'\n : record.dealId\n ? 'dealId'\n : null\n const id = field ? record[field] : undefined\n const { data, status, fromCache } = useFirestoreCollection<CrmTaskRow>(\n () => {\n if (!scope || !field || !id) return null\n return query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.tasks),\n ...crmVisibleToClause(readTokens),\n where(field, '==', id),\n orderBy('dueAtMs', 'asc'),\n limit(CRM_RECORD_TASK_LIMIT),\n )\n },\n [firestore, scope, readTokens, field, id],\n { idField: '$id' },\n )\n const tasks = useMemo(() => orderTaskRows(data ?? []), [data])\n return {\n tasks,\n status,\n fromCache,\n truncated: (data?.length ?? 0) >= CRM_RECORD_TASK_LIMIT,\n scope,\n orgId,\n readTokens,\n }\n}\n"],"names":["CRM_COLLECTIONS","useFirestore","useFirestoreCollection","collection","limit","orderBy","query","where","useEffect","useMemo","useState","crmTaskViewBase","crmTaskViewPlan","crmTaskViewSort","orderTaskRows","TASK_LIST_DECLARATION","useCrmFoldsScope","useCrmListQuery","crmVisibleToClause","useCrmScope","useNowMs","nowMs","setNowMs","Date","now","timer","setInterval","clearInterval","useCrmTaskList","options","hostId","org","view","uid","clauses","NO_CLAUSES","search","NO_WORDS","scope","orgId","visibleTo","readTokens","plan","base","viewSort","sort","foldsScope","paged","tasks","declaration","enabled","assigneeUid","undefined","Boolean","orderedByDue","path","rows","status","fromCache","truncated","hasMore","page","setPage","pageSize","setPageSize","CRM_RECORD_TASK_LIMIT","useCrmRecordTasks","record","firestore","field","contactId","companyId","dealId","id","data","idField","length"],"mappings":"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"}
|
|
@@ -90,6 +90,12 @@ export declare function crmSelectField(column: string, path?: string): ListFilte
|
|
|
90
90
|
export declare function crmClientListFields(collection: CrmListCollection, record: Readonly<Record<string, unknown>>, patch: Readonly<Record<string, unknown>>, fields: readonly string[], context?: CrmListFieldsContext): Record<string, unknown>;
|
|
91
91
|
/** The search fields every CRM record carries — what a client edit of searched text rewrites. */
|
|
92
92
|
export declare const CRM_CLIENT_SEARCH_FIELDS: readonly ["searchTokens", "scopedSearchTokens"];
|
|
93
|
+
/**
|
|
94
|
+
* The header sort keys a client edit of their inputs rewrites (AGL-3680) —
|
|
95
|
+
* see `crmSortKey` in `@aglyn/aglyn`. `crmClientListFields` writes only the
|
|
96
|
+
* ones the collection computes, so one list serves every edit.
|
|
97
|
+
*/
|
|
98
|
+
export declare const CRM_CLIENT_SORT_FIELDS: readonly ["nameSortKey", "companyLower", "jobTitleLower", "titleLower", "amountSortCents", "expectedCloseSortAtMs", "priorityRank"];
|
|
93
99
|
/**
|
|
94
100
|
* The list fields a deal's contact roles are found by (AGL-3521) — what a
|
|
95
101
|
* client write of `contactRoles` or `contactId` rewrites beside them.
|
|
@@ -179,6 +179,19 @@ const sameClause = (a, b)=>a.field === b.field && a.op === b.op && a.value === b
|
|
|
179
179
|
'searchTokens',
|
|
180
180
|
'scopedSearchTokens'
|
|
181
181
|
];
|
|
182
|
+
/**
|
|
183
|
+
* The header sort keys a client edit of their inputs rewrites (AGL-3680) —
|
|
184
|
+
* see `crmSortKey` in `@aglyn/aglyn`. `crmClientListFields` writes only the
|
|
185
|
+
* ones the collection computes, so one list serves every edit.
|
|
186
|
+
*/ export const CRM_CLIENT_SORT_FIELDS = [
|
|
187
|
+
'nameSortKey',
|
|
188
|
+
'companyLower',
|
|
189
|
+
'jobTitleLower',
|
|
190
|
+
'titleLower',
|
|
191
|
+
'amountSortCents',
|
|
192
|
+
'expectedCloseSortAtMs',
|
|
193
|
+
'priorityRank'
|
|
194
|
+
];
|
|
182
195
|
/**
|
|
183
196
|
* The list fields a deal's contact roles are found by (AGL-3521) — what a
|
|
184
197
|
* client write of `contactRoles` or `contactId` rewrites beside them.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-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 {\n CRM_SCOPED_SEARCH_TOKENS_FIELD,\n CRM_SEARCH_TOKENS_FIELD,\n type CrmListCollection,\n type CrmListFieldsContext,\n crmListFields,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport { listQueryRefusals } from '@aglyn/shared-util-tools/list-query/list-query-refusals'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryPlan,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * THE CRM LISTS ON THEIR QUERIES (AGL-3321).\n *\n * Every CRM list — Leads, Contacts, Companies, Deals, Tasks — declares its\n * filterable fields, its one order and its search as a\n * `ListQueryDeclaration`, and `planListQuery` puts every clause and the\n * search word on ONE Firestore query paged by `useListQuery`. What a query\n * cannot hold is refused by name (`ListQueryNotices`), never matched over\n * the rows a page happened to load.\n *\n * This module is what the five share: the search every CRM record carries\n * (`searchTokens`, and `scopedSearchTokens` under a site — see\n * `crmListFields` in `@aglyn/aglyn`), the scope clause as the plan's base,\n * a stored clause translated to the one its query asks, and the refusals\n * read back through the clause the reader set.\n */\n\n/** Every CRM list's quick search: the tokens every writer stamps. */\nexport const CRM_LIST_SEARCH: NonNullable<ListQueryDeclaration['search']> = {\n tokensPath: CRM_SEARCH_TOKENS_FIELD,\n scoped: scopedSearch(CRM_SCOPED_SEARCH_TOKENS_FIELD),\n}\n\n/**\n * The scope clause as the plan's base: `visibleTo array-contains-any` over\n * the reader's tokens under a site, nothing at the organization level\n * (`null` tokens), where an org-wide member's listeners carry none.\n */\nexport function crmListBase(visibleTo: readonly string[] | null): ListQueryFilter[] {\n return visibleTo ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...visibleTo] }] : []\n}\n\n/** The base's shape, for `listQueryIndexes`: the scope clause is an array clause. */\nexport const CRM_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\n/**\n * The declaration a reader may run.\n *\n * Under a site the search folds into the scope clause (`search.scoped`),\n * and a query without `visibleTo` is one the rules can prove only for an\n * ORG-WIDE member — `canReadScoped()` short-circuits on it, and for anybody\n * else inspects `visibleTo`, which the folded query no longer constrains.\n * A member whose reach is some sites keeps the scope clause, and the plan\n * refuses their search by name rather than sending a query the rules deny.\n */\nexport function crmListDeclarationFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n): ListQueryDeclaration {\n if (foldsScope || !declaration.search?.scoped) return declaration\n return { ...declaration, search: { tokensPath: declaration.search.tokensPath } }\n}\n\n/*------------------------------------------\n * WHAT A LIST STORES, AND WHAT IT ASKS.\n *\n * A saved view stores clauses in the grid's grammar — \"Status is Open\",\n * \"Owner is Dana\", \"Lead source is none\" — and some of them name no stored\n * field one query can ask as it is: Open is two statuses, an owner under a\n * site is that holder's facet key, a lead source is compared by its key. A\n * list translates each stored clause into the clause its query asks, plans\n * those, and reads the plan's refusals back through the stored clause, so\n * the notice names what the reader set (\"Status is Open is not applied\")\n * and never the field it was translated to.\n *-----------------------------------------*/\n\n/** How one stored clause is asked: a query clause, or why it cannot be. */\nexport type CrmClauseAsked = ListFilterRequest | { refused: string }\n\nexport interface CrmAskedClauses {\n /** What the plan is asked, in the order the reader set them. */\n clauses: ListFilterRequest[]\n /** The stored clause each asked one stands for, index for index. */\n origins: ListFilterRequest[]\n /** Stored clauses refused before the plan saw them, with why. */\n refused: Array<{ clause: ListFilterRequest; reason: string }>\n}\n\n/** Every stored clause through `ask`, keeping where each came from. */\nexport function crmAskClauses(\n stored: readonly ListFilterRequest[],\n ask: (clause: ListFilterRequest) => CrmClauseAsked,\n): CrmAskedClauses {\n const clauses: ListFilterRequest[] = []\n const origins: ListFilterRequest[] = []\n const refused: CrmAskedClauses['refused'] = []\n for (const clause of stored) {\n const asked = ask(clause)\n if ('refused' in asked) {\n refused.push({ clause, reason: asked.refused })\n } else {\n clauses.push(asked)\n origins.push(clause)\n }\n }\n return { clauses, origins, refused }\n}\n\nconst sameClause = (a: ListFilterRequest, b: ListFilterRequest): boolean =>\n a.field === b.field && a.op === b.op && a.value === b.value\n\n/**\n * The plan's refusals and the translation's, each named by the clause the\n * reader SET, as `ListQueryNotices` takes them — through the platform's one\n * wording (`listQueryRefusals`), so a notice and its chip read alike.\n */\nexport function crmQueryRefusals(\n plan: Pick<ListQueryPlan, 'refused'>,\n asked: CrmAskedClauses,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): Array<{ label: string; reason: string }> {\n const planned = plan.refused.map((entry) => {\n if (entry.clause === 'search') return entry\n const clause = entry.clause\n const at = asked.clauses.findIndex((candidate) => sameClause(candidate, clause))\n return { clause: at >= 0 ? asked.origins[at] : clause, reason: entry.reason }\n })\n return listQueryRefusals([...asked.refused, ...planned], context)\n}\n\n/** The comma-separated values of an `isAnyOf`, or the one value of anything else. */\nexport function crmClauseValues(clause: Pick<ListFilterRequest, 'op' | 'value'>): string[] {\n const raw = clause.op === 'isAnyOf' ? clause.value.split(',') : [clause.value]\n return [...new Set(raw.map((value) => value.trim()).filter(Boolean))]\n}\n\n/** One value as `equals`, several as `isAnyOf` — the shape the plan takes a list in. */\nexport function crmAnyOf(field: string, values: readonly string[]): CrmClauseAsked {\n const unique = [...new Set(values)]\n if (!unique.length) return { refused: 'no value yet' }\n return unique.length === 1\n ? { field, op: 'equals', value: unique[0] }\n : { field, op: 'isAnyOf', value: unique.join(',') }\n}\n\n/** A `select` field: equality and \"any of\" over stored values, nothing else. */\nexport function crmSelectField(column: string, path: string = column): ListFilterField {\n return { column, kind: 'exact', path, operators: ['equals', 'isAnyOf'] }\n}\n\n/**\n * The list fields a CLIENT edit writes beside its patch (AGL-3321): the\n * record as the listener holds it with the patch laid over — a `null` or a\n * sentinel in the patch reads as the field removed — through\n * `crmListFields`, narrowed to `fields`. A client never writes a field the\n * rules keep for the server (a lead's `emailStatus`), which is why the\n * caller names the ones it may. A lead's direction is read off the org's\n * lead source list, which the caller passes in `context` once it has it.\n */\nexport function crmClientListFields(\n collection: CrmListCollection,\n record: Readonly<Record<string, unknown>>,\n patch: Readonly<Record<string, unknown>>,\n fields: readonly string[],\n context: CrmListFieldsContext = {},\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...record }\n for (const [key, value] of Object.entries(patch)) {\n // A dotted path is a map entry (`custom.tier`), never a searched field.\n if (key.includes('.')) continue\n merged[key] = value === null || isSentinel(value) ? undefined : value\n }\n const computed = crmListFields(collection, merged, context)\n return Object.fromEntries(fields.filter((field) => field in computed).map((field) => [field, computed[field]]))\n}\n\n/** A web-SDK `FieldValue` — `deleteField()`, the one a client edit of these fields sends. */\nconst isSentinel = (value: unknown): boolean =>\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date) &&\n typeof (value as { isEqual?: unknown }).isEqual === 'function'\n\n/** The search fields every CRM record carries — what a client edit of searched text rewrites. */\nexport const CRM_CLIENT_SEARCH_FIELDS = ['searchTokens', 'scopedSearchTokens'] as const\n\n\n/**\n * The list fields a deal's contact roles are found by (AGL-3521) — what a\n * client write of `contactRoles` or `contactId` rewrites beside them.\n */\nexport const CRM_DEAL_CONTACT_ROLE_LIST_FIELDS = [\n 'contactRoleContactIds',\n 'scopedContactRoleContactIds',\n 'contactRoleKeys',\n] as const\n"],"names":["CRM_SCOPED_SEARCH_TOKENS_FIELD","CRM_SEARCH_TOKENS_FIELD","crmListFields","scopedSearch","listQueryRefusals","CRM_LIST_SEARCH","tokensPath","scoped","crmListBase","visibleTo","path","op","value","CRM_LIST_BASE_INDEX","array","crmListDeclarationFor","declaration","foldsScope","search","crmAskClauses","stored","ask","clauses","origins","refused","clause","asked","push","reason","sameClause","a","b","field","crmQueryRefusals","plan","context","planned","map","entry","at","findIndex","candidate","crmClauseValues","raw","split","Set","trim","filter","Boolean","crmAnyOf","values","unique","length","join","crmSelectField","column","kind","operators","crmClientListFields","collection","record","patch","fields","merged","key","Object","entries","includes","isSentinel","undefined","computed","fromEntries","Array","isArray","Date","isEqual","CRM_CLIENT_SEARCH_FIELDS","CRM_DEAL_CONTACT_ROLE_LIST_FIELDS"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-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 {\n CRM_SCOPED_SEARCH_TOKENS_FIELD,\n CRM_SEARCH_TOKENS_FIELD,\n type CrmListCollection,\n type CrmListFieldsContext,\n crmListFields,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport { listQueryRefusals } from '@aglyn/shared-util-tools/list-query/list-query-refusals'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryPlan,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * THE CRM LISTS ON THEIR QUERIES (AGL-3321).\n *\n * Every CRM list — Leads, Contacts, Companies, Deals, Tasks — declares its\n * filterable fields, its one order and its search as a\n * `ListQueryDeclaration`, and `planListQuery` puts every clause and the\n * search word on ONE Firestore query paged by `useListQuery`. What a query\n * cannot hold is refused by name (`ListQueryNotices`), never matched over\n * the rows a page happened to load.\n *\n * This module is what the five share: the search every CRM record carries\n * (`searchTokens`, and `scopedSearchTokens` under a site — see\n * `crmListFields` in `@aglyn/aglyn`), the scope clause as the plan's base,\n * a stored clause translated to the one its query asks, and the refusals\n * read back through the clause the reader set.\n */\n\n/** Every CRM list's quick search: the tokens every writer stamps. */\nexport const CRM_LIST_SEARCH: NonNullable<ListQueryDeclaration['search']> = {\n tokensPath: CRM_SEARCH_TOKENS_FIELD,\n scoped: scopedSearch(CRM_SCOPED_SEARCH_TOKENS_FIELD),\n}\n\n/**\n * The scope clause as the plan's base: `visibleTo array-contains-any` over\n * the reader's tokens under a site, nothing at the organization level\n * (`null` tokens), where an org-wide member's listeners carry none.\n */\nexport function crmListBase(visibleTo: readonly string[] | null): ListQueryFilter[] {\n return visibleTo ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...visibleTo] }] : []\n}\n\n/** The base's shape, for `listQueryIndexes`: the scope clause is an array clause. */\nexport const CRM_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\n/**\n * The declaration a reader may run.\n *\n * Under a site the search folds into the scope clause (`search.scoped`),\n * and a query without `visibleTo` is one the rules can prove only for an\n * ORG-WIDE member — `canReadScoped()` short-circuits on it, and for anybody\n * else inspects `visibleTo`, which the folded query no longer constrains.\n * A member whose reach is some sites keeps the scope clause, and the plan\n * refuses their search by name rather than sending a query the rules deny.\n */\nexport function crmListDeclarationFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n): ListQueryDeclaration {\n if (foldsScope || !declaration.search?.scoped) return declaration\n return { ...declaration, search: { tokensPath: declaration.search.tokensPath } }\n}\n\n/*------------------------------------------\n * WHAT A LIST STORES, AND WHAT IT ASKS.\n *\n * A saved view stores clauses in the grid's grammar — \"Status is Open\",\n * \"Owner is Dana\", \"Lead source is none\" — and some of them name no stored\n * field one query can ask as it is: Open is two statuses, an owner under a\n * site is that holder's facet key, a lead source is compared by its key. A\n * list translates each stored clause into the clause its query asks, plans\n * those, and reads the plan's refusals back through the stored clause, so\n * the notice names what the reader set (\"Status is Open is not applied\")\n * and never the field it was translated to.\n *-----------------------------------------*/\n\n/** How one stored clause is asked: a query clause, or why it cannot be. */\nexport type CrmClauseAsked = ListFilterRequest | { refused: string }\n\nexport interface CrmAskedClauses {\n /** What the plan is asked, in the order the reader set them. */\n clauses: ListFilterRequest[]\n /** The stored clause each asked one stands for, index for index. */\n origins: ListFilterRequest[]\n /** Stored clauses refused before the plan saw them, with why. */\n refused: Array<{ clause: ListFilterRequest; reason: string }>\n}\n\n/** Every stored clause through `ask`, keeping where each came from. */\nexport function crmAskClauses(\n stored: readonly ListFilterRequest[],\n ask: (clause: ListFilterRequest) => CrmClauseAsked,\n): CrmAskedClauses {\n const clauses: ListFilterRequest[] = []\n const origins: ListFilterRequest[] = []\n const refused: CrmAskedClauses['refused'] = []\n for (const clause of stored) {\n const asked = ask(clause)\n if ('refused' in asked) {\n refused.push({ clause, reason: asked.refused })\n } else {\n clauses.push(asked)\n origins.push(clause)\n }\n }\n return { clauses, origins, refused }\n}\n\nconst sameClause = (a: ListFilterRequest, b: ListFilterRequest): boolean =>\n a.field === b.field && a.op === b.op && a.value === b.value\n\n/**\n * The plan's refusals and the translation's, each named by the clause the\n * reader SET, as `ListQueryNotices` takes them — through the platform's one\n * wording (`listQueryRefusals`), so a notice and its chip read alike.\n */\nexport function crmQueryRefusals(\n plan: Pick<ListQueryPlan, 'refused'>,\n asked: CrmAskedClauses,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): Array<{ label: string; reason: string }> {\n const planned = plan.refused.map((entry) => {\n if (entry.clause === 'search') return entry\n const clause = entry.clause\n const at = asked.clauses.findIndex((candidate) => sameClause(candidate, clause))\n return { clause: at >= 0 ? asked.origins[at] : clause, reason: entry.reason }\n })\n return listQueryRefusals([...asked.refused, ...planned], context)\n}\n\n/** The comma-separated values of an `isAnyOf`, or the one value of anything else. */\nexport function crmClauseValues(clause: Pick<ListFilterRequest, 'op' | 'value'>): string[] {\n const raw = clause.op === 'isAnyOf' ? clause.value.split(',') : [clause.value]\n return [...new Set(raw.map((value) => value.trim()).filter(Boolean))]\n}\n\n/** One value as `equals`, several as `isAnyOf` — the shape the plan takes a list in. */\nexport function crmAnyOf(field: string, values: readonly string[]): CrmClauseAsked {\n const unique = [...new Set(values)]\n if (!unique.length) return { refused: 'no value yet' }\n return unique.length === 1\n ? { field, op: 'equals', value: unique[0] }\n : { field, op: 'isAnyOf', value: unique.join(',') }\n}\n\n/** A `select` field: equality and \"any of\" over stored values, nothing else. */\nexport function crmSelectField(column: string, path: string = column): ListFilterField {\n return { column, kind: 'exact', path, operators: ['equals', 'isAnyOf'] }\n}\n\n/**\n * The list fields a CLIENT edit writes beside its patch (AGL-3321): the\n * record as the listener holds it with the patch laid over — a `null` or a\n * sentinel in the patch reads as the field removed — through\n * `crmListFields`, narrowed to `fields`. A client never writes a field the\n * rules keep for the server (a lead's `emailStatus`), which is why the\n * caller names the ones it may. A lead's direction is read off the org's\n * lead source list, which the caller passes in `context` once it has it.\n */\nexport function crmClientListFields(\n collection: CrmListCollection,\n record: Readonly<Record<string, unknown>>,\n patch: Readonly<Record<string, unknown>>,\n fields: readonly string[],\n context: CrmListFieldsContext = {},\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...record }\n for (const [key, value] of Object.entries(patch)) {\n // A dotted path is a map entry (`custom.tier`), never a searched field.\n if (key.includes('.')) continue\n merged[key] = value === null || isSentinel(value) ? undefined : value\n }\n const computed = crmListFields(collection, merged, context)\n return Object.fromEntries(fields.filter((field) => field in computed).map((field) => [field, computed[field]]))\n}\n\n/** A web-SDK `FieldValue` — `deleteField()`, the one a client edit of these fields sends. */\nconst isSentinel = (value: unknown): boolean =>\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date) &&\n typeof (value as { isEqual?: unknown }).isEqual === 'function'\n\n/** The search fields every CRM record carries — what a client edit of searched text rewrites. */\nexport const CRM_CLIENT_SEARCH_FIELDS = ['searchTokens', 'scopedSearchTokens'] as const\n\n/**\n * The header sort keys a client edit of their inputs rewrites (AGL-3680) —\n * see `crmSortKey` in `@aglyn/aglyn`. `crmClientListFields` writes only the\n * ones the collection computes, so one list serves every edit.\n */\nexport const CRM_CLIENT_SORT_FIELDS = [\n 'nameSortKey',\n 'companyLower',\n 'jobTitleLower',\n 'titleLower',\n 'amountSortCents',\n 'expectedCloseSortAtMs',\n 'priorityRank',\n] as const\n\n\n/**\n * The list fields a deal's contact roles are found by (AGL-3521) — what a\n * client write of `contactRoles` or `contactId` rewrites beside them.\n */\nexport const CRM_DEAL_CONTACT_ROLE_LIST_FIELDS = [\n 'contactRoleContactIds',\n 'scopedContactRoleContactIds',\n 'contactRoleKeys',\n] as const\n"],"names":["CRM_SCOPED_SEARCH_TOKENS_FIELD","CRM_SEARCH_TOKENS_FIELD","crmListFields","scopedSearch","listQueryRefusals","CRM_LIST_SEARCH","tokensPath","scoped","crmListBase","visibleTo","path","op","value","CRM_LIST_BASE_INDEX","array","crmListDeclarationFor","declaration","foldsScope","search","crmAskClauses","stored","ask","clauses","origins","refused","clause","asked","push","reason","sameClause","a","b","field","crmQueryRefusals","plan","context","planned","map","entry","at","findIndex","candidate","crmClauseValues","raw","split","Set","trim","filter","Boolean","crmAnyOf","values","unique","length","join","crmSelectField","column","kind","operators","crmClientListFields","collection","record","patch","fields","merged","key","Object","entries","includes","isSentinel","undefined","computed","fromEntries","Array","isArray","Date","isEqual","CRM_CLIENT_SEARCH_FIELDS","CRM_CLIENT_SORT_FIELDS","CRM_DEAL_CONTACT_ROLE_LIST_FIELDS"],"mappings":"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"}
|