@aglyn/plugins-forms 1.0.0-beta.232 → 1.0.0-beta.233
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/form-list-query.d.ts +15 -3
- package/src/lib/components/form-list-query.js +47 -5
- package/src/lib/components/form-list-query.js.map +1 -1
- package/src/lib/components/host-forms-card.component.js +31 -8
- package/src/lib/components/host-forms-card.component.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-forms",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.233",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.233",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.233",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.233",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.233",
|
|
32
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.233",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.233",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.233",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.233",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.233",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.233",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.233",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.233",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.233",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -16,10 +16,22 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
18
18
|
import type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
|
|
19
|
-
import type { ListQueryDeclaration, ListQueryFilter, ListQueryRequest } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
19
|
+
import type { ListQueryDeclaration, ListQueryFilter, ListQueryRequest, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
20
20
|
/** Every filter the forms grid offers, in the `ListFilterField` grammar. */
|
|
21
21
|
export declare const FORM_LIST_FILTER_FIELDS: readonly ListFilterField[];
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* EVERY HEADER SORTS ON THE QUERY (AGL-3680), by a field every form stores
|
|
24
|
+
* (see the list's header comment: `newFormListFields` at create,
|
|
25
|
+
* `backfill-form-list-fields.mjs` before) — the name by `nameLower`, and a
|
|
26
|
+
* counter nobody counted is `null`, which an order keeps. Status is not
|
|
27
|
+
* here: the list in use is all Active, so it sorts the page.
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ A ranged field's FIRST order is the one its range takes
|
|
30
|
+
* (`rangeOrder`), so the dates lead newest first and Submissions smallest
|
|
31
|
+
* first — the orders the range composites below were built for.
|
|
32
|
+
*/
|
|
33
|
+
export declare const FORM_LIST_COLUMN_SORTS: readonly ListQuerySort[];
|
|
34
|
+
/** The forms list's query: its fields, its orders, and its search. */
|
|
23
35
|
export declare const FORM_LIST_QUERY: ListQueryDeclaration;
|
|
24
36
|
/** How each field reads on a chip and as a hidden column's header. */
|
|
25
37
|
export declare const FORM_LIST_FILTER_HEADERS: Readonly<Record<string, string>>;
|
|
@@ -50,4 +62,4 @@ export declare const FORM_IN_USE: ListQueryFilter;
|
|
|
50
62
|
* What the forms list asks this time: the clauses in force, the search
|
|
51
63
|
* words, and the forms in use unless Status is one of the clauses.
|
|
52
64
|
*/
|
|
53
|
-
export declare function formListRequest(clauses: readonly ListFilterClause[], search: readonly string[]): ListQueryRequest;
|
|
65
|
+
export declare function formListRequest(clauses: readonly ListFilterClause[], search: readonly string[], sort?: ListQuerySort | null): ListQueryRequest;
|
|
@@ -154,14 +154,54 @@
|
|
|
154
154
|
]
|
|
155
155
|
}
|
|
156
156
|
];
|
|
157
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* A header's two orders, both `alone` (AGL-3680): served while nothing
|
|
159
|
+
* narrows the list past the forms in use ({@link FORM_IN_USE}), so each costs
|
|
160
|
+
* one `(retired, field)` composite per direction and no more. Asked under a
|
|
161
|
+
* filter or the search, the list keeps document order and says so.
|
|
162
|
+
*/ const aloneSorts = (column, path, label, first = 'asc')=>(first === 'asc' ? [
|
|
163
|
+
'asc',
|
|
164
|
+
'desc'
|
|
165
|
+
] : [
|
|
166
|
+
'desc',
|
|
167
|
+
'asc'
|
|
168
|
+
]).map((direction)=>({
|
|
169
|
+
path,
|
|
170
|
+
direction,
|
|
171
|
+
column,
|
|
172
|
+
label,
|
|
173
|
+
alone: true
|
|
174
|
+
}));
|
|
175
|
+
/**
|
|
176
|
+
* EVERY HEADER SORTS ON THE QUERY (AGL-3680), by a field every form stores
|
|
177
|
+
* (see the list's header comment: `newFormListFields` at create,
|
|
178
|
+
* `backfill-form-list-fields.mjs` before) — the name by `nameLower`, and a
|
|
179
|
+
* counter nobody counted is `null`, which an order keeps. Status is not
|
|
180
|
+
* here: the list in use is all Active, so it sorts the page.
|
|
181
|
+
*
|
|
182
|
+
* ⚠️ A ranged field's FIRST order is the one its range takes
|
|
183
|
+
* (`rangeOrder`), so the dates lead newest first and Submissions smallest
|
|
184
|
+
* first — the orders the range composites below were built for.
|
|
185
|
+
*/ export const FORM_LIST_COLUMN_SORTS = [
|
|
186
|
+
...aloneSorts('displayName', 'nameLower', 'Display name'),
|
|
187
|
+
...aloneSorts('slug', 'slug', 'Slug'),
|
|
188
|
+
...aloneSorts('submissions', 'stats.submissions', 'Submissions'),
|
|
189
|
+
...aloneSorts('leads', 'stats.leads', 'Leads'),
|
|
190
|
+
...aloneSorts('lastSubmission', 'stats.lastSubmissionAtMs', 'Last submission', 'desc'),
|
|
191
|
+
...aloneSorts('updatedAt', 'updatedAt', 'Updated', 'desc'),
|
|
192
|
+
...aloneSorts('leadRouting', 'routing.lead', 'Lead routing'),
|
|
193
|
+
...aloneSorts('inCampaign', 'inCampaign', 'In a campaign')
|
|
194
|
+
];
|
|
195
|
+
/** The forms list's query: its fields, its orders, and its search. */ export const FORM_LIST_QUERY = {
|
|
158
196
|
fields: FORM_LIST_FILTER_FIELDS,
|
|
159
|
-
// Document order, as the list has always read, which needs no composite
|
|
197
|
+
// Document order, as the list has always read, which needs no composite;
|
|
198
|
+
// then the header orders.
|
|
160
199
|
sorts: [
|
|
161
200
|
{
|
|
162
201
|
path: '__name__',
|
|
163
202
|
direction: 'asc'
|
|
164
|
-
}
|
|
203
|
+
},
|
|
204
|
+
...FORM_LIST_COLUMN_SORTS
|
|
165
205
|
],
|
|
166
206
|
search: {
|
|
167
207
|
tokensPath: 'searchTokens'
|
|
@@ -227,12 +267,14 @@
|
|
|
227
267
|
/**
|
|
228
268
|
* What the forms list asks this time: the clauses in force, the search
|
|
229
269
|
* words, and the forms in use unless Status is one of the clauses.
|
|
230
|
-
*/ export function formListRequest(clauses, search) {
|
|
270
|
+
*/ export function formListRequest(clauses, search, sort = null) {
|
|
231
271
|
const askedStatus = clauses.some((clause)=>clause.field === 'status');
|
|
232
272
|
return _extends({
|
|
233
273
|
clauses,
|
|
234
274
|
search
|
|
235
|
-
},
|
|
275
|
+
}, sort ? {
|
|
276
|
+
sort
|
|
277
|
+
} : {}, askedStatus ? {} : {
|
|
236
278
|
base: [
|
|
237
279
|
FORM_IN_USE
|
|
238
280
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-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 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 ListQueryRequest,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*==========================================\n * WHAT THE FORMS LIST FILTERS AND SEARCHES BY — ALL OF IT ON THE QUERY\n * (AGL-3330, on the AGL-3321 list query plan).\n *\n * The list pages a site's catalog (`hosts/{hostId}/forms`), so a filter\n * matched over the rows already read would answer \"no such form\" for one on\n * a later page. Every clause and the search word go on ONE Firestore query\n * (`planListQuery`), paged by that query (`useListQuery`); what one query\n * cannot hold is refused by name, never matched in memory.\n *\n * Each field is one every form stores: the create paths write it\n * (`newFormListFields`), a rename rewrites the search keys\n * (`formListFields`), and `tools/scripts/backfill-form-list-fields.mjs`\n * stamps the forms made before.\n *\n * - Display name: a word of it (`nameTokens`), the shared name-search\n * prefixes every `displayName`-named document carries.\n * - Slug: stored normalized (`normalizeFormSlug`), so it is its own key.\n * - Submissions: the counter `/api/forms/submit` increments on the form, a\n * number or a range. Leads: the same counter's equality — a count, or\n * none. A form that counted nothing holds `null`, never zero, so \"is\n * empty\" is an equality.\n * - Last submission (epoch ms) and Updated: days, before or after.\n * - Status: `retired`, the queryable mirror of `archivedAt`.\n * - Lead routing: `routing.lead`, a boolean on every form.\n * - Campaign: `campaignIds`, any of the picked campaigns, as stored.\n * - In a campaign: `inCampaign`, the boolean every writer of `campaignIds`\n * stamps beside it (`formCampaignFields`), because \"in no campaign\" is an\n * empty array, which no query can ask for.\n *\n * ## The index budget, and what it bought\n *\n * One query holds any number of equalities, one array clause and one range,\n * and a range leads the order. Unfiltered, and under equalities alone, the\n * list reads in document order, which Firestore serves by merging its\n * single-field indexes: no composite. A composite is needed only where an\n * equality or array field meets a RANGE, one per (field, range order) pair\n * (`listQueryIndexes`), so the count is (equality fields) × (range orders).\n *\n * Three ranges — Submissions, Last submission, Updated — are the visible\n * columns with no other honest operator. Leads is asked by equality (a\n * count, or none — a lead-routing form holds `0` until its first lead, so\n * \"= 0\" and \"is empty\" are different questions), Display name by a word,\n * Slug exactly, so none of those adds an order; a Leads range would be a\n * fourth order and more composites. Nine equality fields (the search tokens,\n * the name tokens, Slug, the two counters' equalities, `retired`, Lead\n * routing, Campaign, In a campaign) times three orders, less the Submissions\n * equality against its own order, is 26 — In a campaign is the last three,\n * the whole of what \"in no campaign\" costs. `form-list-query.spec.ts` pins\n * every one against the index file.\n *=========================================*/\n\n/** Every filter the forms grid offers, in the `ListFilterField` grammar. */\nexport const FORM_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n {\n column: 'displayName',\n kind: 'text',\n path: 'displayName',\n tokensPath: 'nameTokens',\n operators: ['contains'],\n },\n {\n column: 'slug',\n kind: 'text',\n path: 'slug',\n lowerPath: 'slug',\n operators: ['equals'],\n },\n { column: 'submissions', kind: 'number', path: 'stats.submissions', presence: 'nullable' },\n {\n column: 'leads',\n kind: 'number',\n path: 'stats.leads',\n presence: 'nullable',\n operators: ['=', 'isEmpty'],\n },\n {\n column: 'lastSubmission',\n kind: 'date',\n path: 'stats.lastSubmissionAtMs',\n storedAs: 'millis',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore', 'isNotEmpty'],\n },\n { column: 'updatedAt', kind: 'date', path: 'updatedAt', presence: 'always' },\n { column: 'status', kind: 'boolean', path: 'retired', operators: ['equals'] },\n { column: 'leadRouting', kind: 'boolean', path: 'routing.lead', operators: ['equals'] },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n // The array itself: \"any of\" is one `array-contains-any`.\n tokensPath: 'campaignIds',\n operators: ['isAnyOf'],\n },\n { column: 'inCampaign', kind: 'boolean', path: 'inCampaign', operators: ['equals'] },\n]\n\n/** The forms list's query: its fields, its one order, and its search. */\nexport const FORM_LIST_QUERY: ListQueryDeclaration = {\n fields: FORM_LIST_FILTER_FIELDS,\n // Document order, as the list has always read, which needs no composite.\n sorts: [{ path: '__name__', direction: 'asc' }],\n search: { tokensPath: 'searchTokens' },\n}\n\n/** How each field reads on a chip and as a hidden column's header. */\nexport const FORM_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n slug: 'Slug',\n submissions: 'Submissions',\n leads: 'Leads',\n lastSubmission: 'Last submission',\n updatedAt: 'Updated',\n status: 'Status',\n leadRouting: 'Lead routing',\n campaignIds: 'Campaign',\n inCampaign: 'In a campaign',\n}\n\n/** Status is the stored `retired`, picked by name. */\nexport const FORM_STATUS_OPTIONS = [\n { value: 'false', label: 'Active' },\n { value: 'true', label: 'Retired' },\n]\n\n/** Lead routing is the stored switch, picked by name. */\nexport const FORM_LEAD_ROUTING_OPTIONS = [\n { value: 'true', label: 'On' },\n { value: 'false', label: 'Off' },\n]\n\n/** Whether a form is in any campaign, picked by name. */\nexport const FORM_IN_CAMPAIGN_OPTIONS = [\n { value: 'true', label: 'Yes' },\n { value: 'false', label: 'No' },\n]\n\n/** The fields picked from choices rather than typed. */\nexport const FORM_LIST_SELECT_FIELDS = [\n 'status',\n 'leadRouting',\n 'campaignIds',\n 'inCampaign',\n] as const\n\n/**\n * A retired form is a tombstone the list leaves out unless the reader asks\n * for Status (AGL-2671). On the query, so a page is a page of forms in use\n * rather than one with retired rows dropped from it.\n */\nexport const FORM_IN_USE: ListQueryFilter = { path: 'retired', op: '==', value: false }\n\n/**\n * What the forms list asks this time: the clauses in force, the search\n * words, and the forms in use unless Status is one of the clauses.\n */\nexport function formListRequest(\n clauses: readonly ListFilterClause[],\n search: readonly string[],\n): ListQueryRequest {\n const askedStatus = clauses.some((clause) => clause.field === 'status')\n return {\n clauses,\n search,\n ...(askedStatus ? {} : { base: [FORM_IN_USE] }),\n }\n}\n"],"names":["FORM_LIST_FILTER_FIELDS","column","kind","path","tokensPath","operators","lowerPath","presence","storedAs","FORM_LIST_QUERY","fields","sorts","direction","search","FORM_LIST_FILTER_HEADERS","displayName","slug","submissions","leads","lastSubmission","updatedAt","status","leadRouting","campaignIds","inCampaign","FORM_STATUS_OPTIONS","value","label","FORM_LEAD_ROUTING_OPTIONS","FORM_IN_CAMPAIGN_OPTIONS","FORM_LIST_SELECT_FIELDS","FORM_IN_USE","op","formListRequest","clauses","askedStatus","some","clause","field","base"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAkD2C,GAE3C,0EAA0E,GAC1E,OAAO,MAAMA,0BAAsD;IACjE;QACEC,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNC,YAAY;QACZC,WAAW;YAAC;SAAW;IACzB;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNG,WAAW;QACXD,WAAW;YAAC;SAAS;IACvB;IACA;QAAEJ,QAAQ;QAAeC,MAAM;QAAUC,MAAM;QAAqBI,UAAU;IAAW;IACzF;QACEN,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNI,UAAU;QACVF,WAAW;YAAC;YAAK;SAAU;IAC7B;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNK,UAAU;QACVH,WAAW;YAAC;YAAM;YAAS;YAAa;YAAU;YAAc;SAAa;IAC/E;IACA;QAAEJ,QAAQ;QAAaC,MAAM;QAAQC,MAAM;QAAaI,UAAU;IAAS;IAC3E;QAAEN,QAAQ;QAAUC,MAAM;QAAWC,MAAM;QAAWE,WAAW;YAAC;SAAS;IAAC;IAC5E;QAAEJ,QAAQ;QAAeC,MAAM;QAAWC,MAAM;QAAgBE,WAAW;YAAC;SAAS;IAAC;IACtF;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACN,0DAA0D;QAC1DC,YAAY;QACZC,WAAW;YAAC;SAAU;IACxB;IACA;QAAEJ,QAAQ;QAAcC,MAAM;QAAWC,MAAM;QAAcE,WAAW;YAAC;SAAS;IAAC;CACpF,CAAA;AAED,uEAAuE,GACvE,OAAO,MAAMI,kBAAwC;IACnDC,QAAQV;IACR,yEAAyE;IACzEW,OAAO;QAAC;YAAER,MAAM;YAAYS,WAAW;QAAM;KAAE;IAC/CC,QAAQ;QAAET,YAAY;IAAe;AACvC,EAAC;AAED,oEAAoE,GACpE,OAAO,MAAMU,2BAA6D;IACxEC,aAAa;IACbC,MAAM;IACNC,aAAa;IACbC,OAAO;IACPC,gBAAgB;IAChBC,WAAW;IACXC,QAAQ;IACRC,aAAa;IACbC,aAAa;IACbC,YAAY;AACd,EAAC;AAED,oDAAoD,GACpD,OAAO,MAAMC,sBAAsB;IACjC;QAAEC,OAAO;QAASC,OAAO;IAAS;IAClC;QAAED,OAAO;QAAQC,OAAO;IAAU;CACnC,CAAA;AAED,uDAAuD,GACvD,OAAO,MAAMC,4BAA4B;IACvC;QAAEF,OAAO;QAAQC,OAAO;IAAK;IAC7B;QAAED,OAAO;QAASC,OAAO;IAAM;CAChC,CAAA;AAED,uDAAuD,GACvD,OAAO,MAAME,2BAA2B;IACtC;QAAEH,OAAO;QAAQC,OAAO;IAAM;IAC9B;QAAED,OAAO;QAASC,OAAO;IAAK;CAC/B,CAAA;AAED,sDAAsD,GACtD,OAAO,MAAMG,0BAA0B;IACrC;IACA;IACA;IACA;CACD,CAAS;AAEV;;;;CAIC,GACD,OAAO,MAAMC,cAA+B;IAAE5B,MAAM;IAAW6B,IAAI;IAAMN,OAAO;AAAM,EAAC;AAEvF;;;CAGC,GACD,OAAO,SAASO,gBACdC,OAAoC,EACpCrB,MAAyB;IAEzB,MAAMsB,cAAcD,QAAQE,IAAI,CAAC,CAACC,SAAWA,OAAOC,KAAK,KAAK;IAC9D,OAAO;QACLJ;QACArB;OACIsB,cAAc,CAAC,IAAI;QAAEI,MAAM;YAACR;SAAY;IAAC;AAEjD"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/form-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 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 ListQueryRequest,\n ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*==========================================\n * WHAT THE FORMS LIST FILTERS AND SEARCHES BY — ALL OF IT ON THE QUERY\n * (AGL-3330, on the AGL-3321 list query plan).\n *\n * The list pages a site's catalog (`hosts/{hostId}/forms`), so a filter\n * matched over the rows already read would answer \"no such form\" for one on\n * a later page. Every clause and the search word go on ONE Firestore query\n * (`planListQuery`), paged by that query (`useListQuery`); what one query\n * cannot hold is refused by name, never matched in memory.\n *\n * Each field is one every form stores: the create paths write it\n * (`newFormListFields`), a rename rewrites the search keys\n * (`formListFields`), and `tools/scripts/backfill-form-list-fields.mjs`\n * stamps the forms made before.\n *\n * - Display name: a word of it (`nameTokens`), the shared name-search\n * prefixes every `displayName`-named document carries.\n * - Slug: stored normalized (`normalizeFormSlug`), so it is its own key.\n * - Submissions: the counter `/api/forms/submit` increments on the form, a\n * number or a range. Leads: the same counter's equality — a count, or\n * none. A form that counted nothing holds `null`, never zero, so \"is\n * empty\" is an equality.\n * - Last submission (epoch ms) and Updated: days, before or after.\n * - Status: `retired`, the queryable mirror of `archivedAt`.\n * - Lead routing: `routing.lead`, a boolean on every form.\n * - Campaign: `campaignIds`, any of the picked campaigns, as stored.\n * - In a campaign: `inCampaign`, the boolean every writer of `campaignIds`\n * stamps beside it (`formCampaignFields`), because \"in no campaign\" is an\n * empty array, which no query can ask for.\n *\n * ## The index budget, and what it bought\n *\n * One query holds any number of equalities, one array clause and one range,\n * and a range leads the order. Unfiltered, and under equalities alone, the\n * list reads in document order, which Firestore serves by merging its\n * single-field indexes: no composite. A composite is needed only where an\n * equality or array field meets a RANGE, one per (field, range order) pair\n * (`listQueryIndexes`), so the count is (equality fields) × (range orders).\n *\n * Three ranges — Submissions, Last submission, Updated — are the visible\n * columns with no other honest operator. Leads is asked by equality (a\n * count, or none — a lead-routing form holds `0` until its first lead, so\n * \"= 0\" and \"is empty\" are different questions), Display name by a word,\n * Slug exactly, so none of those adds an order; a Leads range would be a\n * fourth order and more composites. Nine equality fields (the search tokens,\n * the name tokens, Slug, the two counters' equalities, `retired`, Lead\n * routing, Campaign, In a campaign) times three orders, less the Submissions\n * equality against its own order, is 26 — In a campaign is the last three,\n * the whole of what \"in no campaign\" costs. `form-list-query.spec.ts` pins\n * every one against the index file.\n *=========================================*/\n\n/** Every filter the forms grid offers, in the `ListFilterField` grammar. */\nexport const FORM_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n {\n column: 'displayName',\n kind: 'text',\n path: 'displayName',\n tokensPath: 'nameTokens',\n operators: ['contains'],\n },\n {\n column: 'slug',\n kind: 'text',\n path: 'slug',\n lowerPath: 'slug',\n operators: ['equals'],\n },\n { column: 'submissions', kind: 'number', path: 'stats.submissions', presence: 'nullable' },\n {\n column: 'leads',\n kind: 'number',\n path: 'stats.leads',\n presence: 'nullable',\n operators: ['=', 'isEmpty'],\n },\n {\n column: 'lastSubmission',\n kind: 'date',\n path: 'stats.lastSubmissionAtMs',\n storedAs: 'millis',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore', 'isNotEmpty'],\n },\n { column: 'updatedAt', kind: 'date', path: 'updatedAt', presence: 'always' },\n { column: 'status', kind: 'boolean', path: 'retired', operators: ['equals'] },\n { column: 'leadRouting', kind: 'boolean', path: 'routing.lead', operators: ['equals'] },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n // The array itself: \"any of\" is one `array-contains-any`.\n tokensPath: 'campaignIds',\n operators: ['isAnyOf'],\n },\n { column: 'inCampaign', kind: 'boolean', path: 'inCampaign', operators: ['equals'] },\n]\n\n/**\n * A header's two orders, both `alone` (AGL-3680): served while nothing\n * narrows the list past the forms in use ({@link FORM_IN_USE}), so each costs\n * one `(retired, field)` composite per direction and no more. Asked under a\n * filter or the search, the list keeps document order and says so.\n */\nconst aloneSorts = (\n column: string,\n path: string,\n label: string,\n first: ListQuerySort['direction'] = 'asc',\n): ListQuerySort[] =>\n (first === 'asc' ? (['asc', 'desc'] as const) : (['desc', 'asc'] as const)).map(\n (direction) => ({ path, direction, column, label, alone: true }),\n )\n\n/**\n * EVERY HEADER SORTS ON THE QUERY (AGL-3680), by a field every form stores\n * (see the list's header comment: `newFormListFields` at create,\n * `backfill-form-list-fields.mjs` before) — the name by `nameLower`, and a\n * counter nobody counted is `null`, which an order keeps. Status is not\n * here: the list in use is all Active, so it sorts the page.\n *\n * ⚠️ A ranged field's FIRST order is the one its range takes\n * (`rangeOrder`), so the dates lead newest first and Submissions smallest\n * first — the orders the range composites below were built for.\n */\nexport const FORM_LIST_COLUMN_SORTS: readonly ListQuerySort[] = [\n ...aloneSorts('displayName', 'nameLower', 'Display name'),\n ...aloneSorts('slug', 'slug', 'Slug'),\n ...aloneSorts('submissions', 'stats.submissions', 'Submissions'),\n ...aloneSorts('leads', 'stats.leads', 'Leads'),\n ...aloneSorts('lastSubmission', 'stats.lastSubmissionAtMs', 'Last submission', 'desc'),\n ...aloneSorts('updatedAt', 'updatedAt', 'Updated', 'desc'),\n ...aloneSorts('leadRouting', 'routing.lead', 'Lead routing'),\n ...aloneSorts('inCampaign', 'inCampaign', 'In a campaign'),\n]\n\n/** The forms list's query: its fields, its orders, and its search. */\nexport const FORM_LIST_QUERY: ListQueryDeclaration = {\n fields: FORM_LIST_FILTER_FIELDS,\n // Document order, as the list has always read, which needs no composite;\n // then the header orders.\n sorts: [{ path: '__name__', direction: 'asc' }, ...FORM_LIST_COLUMN_SORTS],\n search: { tokensPath: 'searchTokens' },\n}\n\n/** How each field reads on a chip and as a hidden column's header. */\nexport const FORM_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n slug: 'Slug',\n submissions: 'Submissions',\n leads: 'Leads',\n lastSubmission: 'Last submission',\n updatedAt: 'Updated',\n status: 'Status',\n leadRouting: 'Lead routing',\n campaignIds: 'Campaign',\n inCampaign: 'In a campaign',\n}\n\n/** Status is the stored `retired`, picked by name. */\nexport const FORM_STATUS_OPTIONS = [\n { value: 'false', label: 'Active' },\n { value: 'true', label: 'Retired' },\n]\n\n/** Lead routing is the stored switch, picked by name. */\nexport const FORM_LEAD_ROUTING_OPTIONS = [\n { value: 'true', label: 'On' },\n { value: 'false', label: 'Off' },\n]\n\n/** Whether a form is in any campaign, picked by name. */\nexport const FORM_IN_CAMPAIGN_OPTIONS = [\n { value: 'true', label: 'Yes' },\n { value: 'false', label: 'No' },\n]\n\n/** The fields picked from choices rather than typed. */\nexport const FORM_LIST_SELECT_FIELDS = [\n 'status',\n 'leadRouting',\n 'campaignIds',\n 'inCampaign',\n] as const\n\n/**\n * A retired form is a tombstone the list leaves out unless the reader asks\n * for Status (AGL-2671). On the query, so a page is a page of forms in use\n * rather than one with retired rows dropped from it.\n */\nexport const FORM_IN_USE: ListQueryFilter = { path: 'retired', op: '==', value: false }\n\n/**\n * What the forms list asks this time: the clauses in force, the search\n * words, and the forms in use unless Status is one of the clauses.\n */\nexport function formListRequest(\n clauses: readonly ListFilterClause[],\n search: readonly string[],\n sort: ListQuerySort | null = null,\n): ListQueryRequest {\n const askedStatus = clauses.some((clause) => clause.field === 'status')\n return {\n clauses,\n search,\n ...(sort ? { sort } : {}),\n ...(askedStatus ? {} : { base: [FORM_IN_USE] }),\n }\n}\n"],"names":["FORM_LIST_FILTER_FIELDS","column","kind","path","tokensPath","operators","lowerPath","presence","storedAs","aloneSorts","label","first","map","direction","alone","FORM_LIST_COLUMN_SORTS","FORM_LIST_QUERY","fields","sorts","search","FORM_LIST_FILTER_HEADERS","displayName","slug","submissions","leads","lastSubmission","updatedAt","status","leadRouting","campaignIds","inCampaign","FORM_STATUS_OPTIONS","value","FORM_LEAD_ROUTING_OPTIONS","FORM_IN_CAMPAIGN_OPTIONS","FORM_LIST_SELECT_FIELDS","FORM_IN_USE","op","formListRequest","clauses","sort","askedStatus","some","clause","field","base"],"mappings":"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"}
|
|
@@ -37,6 +37,7 @@ import { Alert, Button, Stack, Typography } from "@mui/material";
|
|
|
37
37
|
import { collection, doc, getCountFromServer, query, updateDoc, where } from "firebase/firestore";
|
|
38
38
|
import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useSiteContainerOptions, useHostResourceApi, useLiveArtifactCount } from "@aglyn/tenant-feature-instance";
|
|
39
39
|
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
40
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
40
41
|
import { useRouter } from "next/navigation";
|
|
41
42
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
42
43
|
import { BUNDLE_ID, FORMS_DOCUMENT_SEGMENT } from "../constants/bundle-common.js";
|
|
@@ -47,7 +48,13 @@ import { HOST_FORMS_ZONE } from "./form-zones.js";
|
|
|
47
48
|
* The filterable columns the table shows. The rest of the declaration —
|
|
48
49
|
* Status, Lead routing, Campaign, In a campaign — reaches the Filters panel as hidden
|
|
49
50
|
* columns (`hiddenFilterColumns`).
|
|
50
|
-
*/ const
|
|
51
|
+
*/ /** Status sorts the loaded page (AGL-3680): Active before Retired. */ const FORM_PAGE_SORTS = {
|
|
52
|
+
status: (row)=>isFormArchived(row)
|
|
53
|
+
};
|
|
54
|
+
const FORM_PAGE_SORT_HEADERS = {
|
|
55
|
+
status: 'Status'
|
|
56
|
+
};
|
|
57
|
+
const FORM_VISIBLE_FILTER_COLUMNS = [
|
|
51
58
|
'displayName',
|
|
52
59
|
'slug',
|
|
53
60
|
'submissions',
|
|
@@ -197,16 +204,31 @@ const FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(FORM_LIST_FILTER_FIELDS, FORM
|
|
|
197
204
|
firestore,
|
|
198
205
|
hostId
|
|
199
206
|
]);
|
|
207
|
+
/*
|
|
208
|
+
* EVERY HEADER SORTS (AGL-3680): each stored column orders the QUERY
|
|
209
|
+
* (`FORM_LIST_COLUMN_SORTS`), served while no filter or search narrows the
|
|
210
|
+
* list — otherwise it keeps document order and the notices say why.
|
|
211
|
+
* Status sorts the page: the list in use is all Active.
|
|
212
|
+
*/ const [askedSort, setAskedSort] = useState(null);
|
|
200
213
|
const { status, rows: forms, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
|
|
201
214
|
collection: formsCollection,
|
|
202
215
|
declaration: FORM_LIST_QUERY,
|
|
203
|
-
request: formListRequest(gridFilter.clauses, gridFilter.searchWords),
|
|
216
|
+
request: formListRequest(gridFilter.clauses, gridFilter.searchWords, askedSort),
|
|
204
217
|
deps: [
|
|
205
218
|
firestore,
|
|
206
219
|
hostId
|
|
207
220
|
],
|
|
208
221
|
idField: '$id'
|
|
209
222
|
});
|
|
223
|
+
const columnSort = useListColumnSort({
|
|
224
|
+
sorts: FORM_LIST_QUERY.sorts,
|
|
225
|
+
sort: askedSort,
|
|
226
|
+
onSortChange: setAskedSort,
|
|
227
|
+
orderBy: plan.orderBy,
|
|
228
|
+
rows: forms,
|
|
229
|
+
pageSorts: FORM_PAGE_SORTS,
|
|
230
|
+
headers: FORM_PAGE_SORT_HEADERS
|
|
231
|
+
});
|
|
210
232
|
/*
|
|
211
233
|
* The Campaign filter's choices: the campaigns placed on this site, by
|
|
212
234
|
* name, and any campaign a clause names that the list does not, by its id,
|
|
@@ -642,9 +664,13 @@ const FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(FORM_LIST_FILTER_FIELDS, FORM
|
|
|
642
664
|
headers: FORM_LIST_FILTER_HEADERS,
|
|
643
665
|
options: formFilterOptions
|
|
644
666
|
}),
|
|
645
|
-
notices:
|
|
667
|
+
notices: [
|
|
668
|
+
...plan.notices,
|
|
669
|
+
...columnSort.notices
|
|
670
|
+
]
|
|
646
671
|
}),
|
|
647
672
|
/*#__PURE__*/ _jsx(ListTable, _extends({
|
|
673
|
+
columnSort: columnSort,
|
|
648
674
|
rowHeight: TABLE_ROW_HEIGHT,
|
|
649
675
|
columns: listFilterGridColumns(columns, FORM_LIST_FILTER_FIELDS, formFilterOptions, FORM_LIST_FILTER_HEADERS),
|
|
650
676
|
initialState: {
|
|
@@ -690,7 +716,7 @@ const FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(FORM_LIST_FILTER_FIELDS, FORM
|
|
|
690
716
|
]
|
|
691
717
|
})
|
|
692
718
|
}, {
|
|
693
|
-
rows:
|
|
719
|
+
rows: columnSort.rows,
|
|
694
720
|
// The whole row opens the detail page; the action cluster stops
|
|
695
721
|
// propagation so a menu click never navigates underneath it.
|
|
696
722
|
onOpen: (id)=>router.push(formHref(String(id))),
|
|
@@ -698,10 +724,7 @@ const FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(FORM_LIST_FILTER_FIELDS, FORM
|
|
|
698
724
|
// rather than "these are on their way".
|
|
699
725
|
loading: status === 'loading',
|
|
700
726
|
// Paged by the footer below, so the grid must not also slice.
|
|
701
|
-
hideFooter: true
|
|
702
|
-
// The rows keep the query's order; a header sort would order only
|
|
703
|
-
// the page on screen and read as the whole list's.
|
|
704
|
-
disableColumnSorting: true
|
|
727
|
+
hideFooter: true
|
|
705
728
|
})),
|
|
706
729
|
/*#__PURE__*/ _jsx(ListPagination, {
|
|
707
730
|
page: page,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/host-forms-card.component.tsx"],"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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { AglynOrgBilling } from '@aglyn/aglyn'\nimport {\n buildRoute,\n isFormArchived,\n PageHeaderActions,\n pluginDocsHelp,\n Route,\n useTransferLauncher,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { ICON_VARIANT_SHOW_DETAIL } from '@aglyn/shared-data-enums'\nimport {\n mdiArchiveArrowDownOutline,\n mdiArchiveArrowUpOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiVectorSquare,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport ListTable, {\n ListRowActions,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n type ListFilterClause,\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n getCountFromServer,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useSiteContainerOptions,\n useHostResourceApi,\n useLiveArtifactCount,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { BUNDLE_ID, FORMS_DOCUMENT_SEGMENT } from '../constants/bundle-common'\nimport {\n FORM_IN_CAMPAIGN_OPTIONS,\n FORM_LEAD_ROUTING_OPTIONS,\n FORM_LIST_FILTER_FIELDS,\n FORM_LIST_FILTER_HEADERS,\n FORM_LIST_QUERY,\n FORM_LIST_SELECT_FIELDS,\n FORM_STATUS_OPTIONS,\n formListRequest,\n} from './form-list-query'\nimport { FORM_SUBMISSIONS_TRANSFER_KEY } from '../transfer/form-submissions-transfer-key'\nimport { HOST_FORMS_ZONE } from './form-zones'\n\n/**\n * The filterable columns the table shows. The rest of the declaration —\n * Status, Lead routing, Campaign, In a campaign — reaches the Filters panel as hidden\n * columns (`hiddenFilterColumns`).\n */\nconst FORM_VISIBLE_FILTER_COLUMNS = [\n 'displayName',\n 'slug',\n 'submissions',\n 'leads',\n 'lastSubmission',\n 'updatedAt',\n]\nconst FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(\n FORM_LIST_FILTER_FIELDS,\n FORM_VISIBLE_FILTER_COLUMNS,\n)\n\nexport interface HostFormsCardProps {\n hostId: string\n /**\n * The Forms surface's own absolute console path, from the shell.\n *\n * A row's link is this plus the form's id, resolved synchronously. The\n * alternative is `useConsoleHostRoute`, which answers `null` for a paint\n * while it reads two documents — and a table whose every row links to\n * `/null/...` on first render is worse than one that pays nothing.\n */\n basePath?: string\n /**\n * The org billing document, from the shell.\n *\n * The quota readout's denominator resolves from it. `undefined` means the\n * plan is not known rather than that it is Free — the shell holds this\n * surface behind a spinner until the org read settles, so it reads that way\n * only when the read failed — and the readout says so instead of naming a\n * cap it cannot support.\n */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.\n *\n * The list is the components list's table, deliberately and not\n * approximately: `ListTable` gives it the row grammar every artifact list has\n * — the row opens the detail page, rows are not selectable, one quick action\n * then the overflow — and `ListPagination` gives it the console's one footer.\n * A form is an artifact like a component, so a reader who has used one list\n * has used this one.\n *\n * ## Why the empty state is a table and not a sentence\n *\n * A paragraph and a button reads as a smaller feature than this is: a form is\n * a thing with a slug, a submission count and a version history, and none of\n * that would be visible until the reader had already committed to making one.\n * Rendering the columns with the empty overlay inside them teaches the shape\n * of the artifact BEFORE the first one exists, which is what the components\n * list has always done.\n *\n * ## The two numeric columns, and when one is honestly blank\n *\n * `stats.submissions` and `stats.leads` are both incremented by\n * `/api/forms/submit` on a write that was happening anyway, and recounted\n * from their rows after a removal (AGL-3330), so both are real numbers.\n * `leads` is counted only for a form whose `routing.lead` is set — it holds\n * `0` there until the first lead — so a form that has never routed to leads\n * carries no figure at all and renders as a dash rather than as `0`. A zero\n * would say this form has produced no leads, which for an unrouted form is a\n * measurement nobody took.\n *\n * ## Export submissions, in the card header\n *\n * Every form's submissions on this site, through the console's export dialog\n * (`forms.submissions`, unfiltered): each form's questions are a group of\n * columns, so one file holds the whole site. One form's alone is exported\n * from that form's page. There is no Import: a submission is what a visitor\n * sent, and a file cannot have sent one.\n */\nexport function HostFormsCard(props: HostFormsCardProps) {\n const { hostId, basePath, org } = props\n const router = useRouter()\n const transfer = useTransferLauncher()\n // Duplicate (AGL-2936): the copy is a form of its own — its submissions\n // start empty — and opens on its detail page once made.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) => router.push(formHref(copy.id)),\n })\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n /**\n * The shell's zone renderer (AGL-3043), for other ways to start a form; `null`\n * outside the console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n\n /**\n * One form's page, beneath this surface's own path.\n *\n * `basePath` is the shell's answer and needs no read; the route table is the\n * fallback for a caller that has none, and it is the one that can be `null`\n * for a paint.\n */\n const formHref = useCallback(\n (formId: string) =>\n basePath\n ? `${basePath}/${formId}`\n : buildRoute(Route.FORM_DETAILS, {\n orgSlug: orgSlug ?? '',\n host: host ?? '',\n formId,\n }),\n [basePath, orgSlug, host],\n )\n\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n const [retireError, setRetireError] = useState<string | null>(null)\n /** Bumped by each retire or restore, so the retired count is asked again. */\n const [retiredEpoch, setRetiredEpoch] = useState(0)\n\n /**\n * Retire a form, or put it back (AGL-2671).\n *\n * A write of one field, client-side like every other edit on this document:\n * the rules already admit it for a host writer, fencing off only `stats`.\n *\n * ⛔ NOT a delete, and the difference is the whole feature. The submissions,\n * the leads and the contact timeline they built are filed under this form,\n * so the document is what makes that history readable; deleting it to stop\n * collection would throw away the reason the merchant kept the form.\n *\n * `Date.now()` is the browser's clock rather than `serverTimestamp()`\n * because {@link isFormArchived} asks for a number greater than zero, and a\n * server sentinel reads as `null` on the local echo — the row would stay\n * looking live until the write round-tripped. The value is a marker, never\n * an audit fact: nothing computes a duration from it.\n */\n const setRetired = useCallback(\n async (formId: string, retired: boolean) => {\n setRetireError(null)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n archivedAt: retired ? Date.now() : null,\n // The mirror the list's query asks (AGL-3330).\n retired,\n // Retiring and restoring are edits, and Updated says when the form\n // was last edited (AGL-3330): a write that skipped it left a form\n // retired today reading as untouched for weeks.\n updatedAt: Timestamp.now(),\n })\n setRetiredEpoch((epoch) => epoch + 1)\n } catch {\n setRetireError(\n retired\n ? 'Could not retire that form. Try again shortly.'\n : 'Could not restore that form. Try again shortly.',\n )\n }\n },\n [firestore, hostId],\n )\n\n /*\n * THE LIST IS ITS QUERY (AGL-3330, on the AGL-3321 list query plan).\n *\n * The grid's Filters panel and search box edit the clauses and the words;\n * `useListQuery` puts every one of them on ONE Firestore query and pages\n * that query, so a page is a page of matches and a form whose only match\n * is past the first page is still found. What one query cannot hold — a\n * second range, a second array clause — is refused by name above the list\n * (`ListQueryNotices`) rather than matched over the rows read. See\n * `form-list-query.ts` for every field and the indexes they cost.\n *\n * Unfiltered it reads in document order, as it always has: `orderBy` on a\n * name would drop every form stored without one rather than mis-sort it.\n *\n * A retired form is a TOMBSTONE, not a row, unless the reader asks for\n * Status (AGL-2671). The query leaves them out (`retired == false`), so a\n * page is a page of forms in use; a Status clause replaces that with the\n * reader's own choice. Retiring a form removes it from the list, and\n * without the Status filter that would be a one-way door: the row is the\n * only route back to Restore.\n */\n const [formClauses, setFormClauses] = useState<ListFilterClause[]>([])\n const gridFilter = useListGridFilter({\n clauses: formClauses,\n onChange: setFormClauses,\n selectFields: FORM_LIST_SELECT_FIELDS,\n })\n const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0\n const formsCollection = useMemo(\n () => collection(firestore, 'hosts', hostId, 'forms'),\n [firestore, hostId],\n )\n const {\n status,\n rows: forms,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: formsCollection,\n declaration: FORM_LIST_QUERY,\n request: formListRequest(gridFilter.clauses, gridFilter.searchWords),\n deps: [firestore, hostId],\n idField: '$id',\n })\n\n /*\n * The Campaign filter's choices: the campaigns placed on this site, by\n * name, and any campaign a clause names that the list does not, by its id,\n * so the chip can still be read and cleared.\n *\n * Read once the reader opens the Filters panel, not on every visit: the\n * names cost the org lookup and up to fifty campaign documents, which a\n * reader who came to open one form would pay for a filter they never\n * touched. Until they land the column has no choices, and the panel lists\n * it a moment later.\n */\n const [wantsCampaigns, setWantsCampaigns] = useState(false)\n const siteCampaigns = useSiteContainerOptions('campaign', hostId, {\n enabled: wantsCampaigns || formClauses.some((clause) => clause.field === 'campaignIds'),\n })\n const formFilterOptions = useMemo<Record<string, ListFilterOption[]>>(() => {\n const named = siteCampaigns.options.map((option) => ({\n value: option.value,\n label: option.label,\n }))\n const unnamed = formClauses\n .filter((clause) => clause.field === 'campaignIds')\n .flatMap((clause) => clause.value.split(','))\n .map((value) => value.trim())\n .filter((value) => value && !named.some((option) => option.value === value))\n .map((value) => ({ value, label: value }))\n return {\n status: FORM_STATUS_OPTIONS,\n leadRouting: FORM_LEAD_ROUTING_OPTIONS,\n campaignIds: [...named, ...unnamed],\n inCampaign: FORM_IN_CAMPAIGN_OPTIONS,\n }\n }, [siteCampaigns.options, formClauses])\n\n /*\n * The COUNT is a server aggregate, not the length of a page. `forms` is one\n * page, and publishing that as the site's form count would read as room to\n * spare on a site that is already at the ceiling.\n */\n const liveFormCount = useLiveArtifactCount(hostId, 'forms')\n // Pending or refused, the page stands in: a LOWER bound, never a confident\n // zero.\n const formsUsed = liveFormCount ?? forms.length\n /*\n * How many forms are retired, for the \"keep their slot\" line (AGL-2674).\n * An aggregate, because the list no longer reads a retired row unless it is\n * asked for one; asked again after this card retires or restores a form.\n */\n const [retiredCount, setRetiredCount] = useState<number | null>(null)\n useEffect(() => {\n let active = true\n getCountFromServer(query(formsCollection, where('retired', '==', true)))\n .then((snapshot) => {\n if (active) setRetiredCount(snapshot.data().count)\n })\n .catch(() => {\n // The line is a courtesy; without the count it is simply not shown.\n })\n return () => {\n active = false\n }\n }, [formsCollection, retiredEpoch])\n\n /**\n * Name first, then create (AGL-700).\n *\n * A form is created with BOTH halves seeded. `fields` is the declaration the\n * submission path reads and starts empty; the canvas is the design, seeded\n * with a root and a form node already bound to this id — so the besigner\n * opens on something that satisfies `checkFormContract` rather than on a\n * blank page whose first publish is a list of violations.\n */\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n setCreating(true)\n setCreateError(null)\n try {\n const formId = Aglyn.createResourceUid()\n await createHostResource({\n hostId,\n resource: 'form',\n id: formId,\n data: {\n displayName: values['displayName'],\n slug: Aglyn.normalizeFormSlug(values['displayName']) || formId,\n fields: [],\n rootId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n nodes: {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: ['formRoot'],\n },\n formRoot: {\n $id: 'formRoot',\n componentId: 'form',\n pluginId: BUNDLE_ID,\n parentId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n props: { formId, formName: values['displayName'] },\n nodes: [],\n },\n },\n },\n })\n setCreateOpen(false)\n router.push(formHref(formId))\n } catch (error) {\n console.error(error)\n // The route's own words when it refused: a spent `formsPerHost`\n // allowance answers \"Your plan includes N forms — upgrade in Billing\n // for more\", which is the upgrade path a generic failure would hide.\n setCreateError(\n error instanceof Error && error.message && error.message !== 'Create failed'\n ? error.message\n : 'Could not create that form',\n )\n } finally {\n setCreating(false)\n }\n },\n [creating, createHostResource, hostId, router, formHref],\n )\n\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Display name',\n minWidth: 220,\n type: 'string',\n renderCell: ({ id, value }: any) => (\n <AppLink href={formHref(id as string)}>\n {value || (id as string)}\n </AppLink>\n ),\n },\n {\n field: 'slug',\n headerName: 'Slug',\n minWidth: 160,\n flex: 1,\n type: 'string',\n // Blank reads as a rendering gap; '--' reads as \"nothing here\".\n valueFormatter: (value: any) => value || '--',\n },\n {\n field: 'submissions',\n headerName: 'Submissions',\n minWidth: 130,\n type: 'number',\n // Head AND body. `type: 'number'` right-aligns both, and the explicit\n // pair says so at the call site rather than relying on a grid default\n // that a later `renderCell` would silently override.\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: any, row: any) => row?.stats?.submissions ?? null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'leads',\n headerName: 'Leads',\n minWidth: 100,\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n /*\n * A dash rather than a `0` whenever the field holds no number.\n *\n * `stats.leads` is counted only for a form whose `routing.lead` is set\n * (`0` there until its first lead), so a form that has never routed to\n * leads has no recorded figure. Rendering `0` there would read as \"this\n * form has produced no leads\", which is a claim about a measurement\n * nobody took.\n */\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.leads === 'number' ? row.stats.leads : null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'lastSubmission',\n headerName: 'Last submission',\n minWidth: 170,\n flex: 1,\n type: 'date',\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.lastSubmissionAtMs === 'number'\n ? new Date(row.stats.lastSubmissionAtMs)\n : null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n minWidth: 170,\n flex: 1,\n type: 'date',\n // MUI X v9 passes the value positionally. The v6 object form silently\n // destructures undefined off a Date and every row renders '--'.\n valueGetter: (value: any) => value?.toDate?.() ?? null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n /*\n * Status and Lead routing, hidden until Columns shows them (AGL-3330).\n * Each is a select the Filters panel offers either way, and draws the\n * choice its value names. Status reads the FACT, `archivedAt`, rather\n * than the `retired` mirror the query asks, so a form the backfill has\n * not reached yet still reads true.\n */\n {\n field: 'status',\n headerName: 'Status',\n minWidth: 110,\n valueGetter: (_value: any, row: any) => String(isFormArchived(row)),\n },\n {\n field: 'leadRouting',\n headerName: 'Lead routing',\n minWidth: 120,\n valueGetter: (_value: any, row: any) => String(row?.routing?.lead === true),\n },\n /*\n * In a campaign, hidden like the two above. Reads the ids the form is\n * filed under — the fact `inCampaign` mirrors for the query — so a form\n * the backfill has not reached still draws the truth.\n */\n {\n field: 'inCampaign',\n headerName: 'In a campaign',\n minWidth: 130,\n valueGetter: (_value: any, row: any) =>\n String(Aglyn.readContainerIds(row, 'campaign').length > 0),\n },\n listActionsColumn((row: any) => {\n const form = { ...row, $id: row.$id as string }\n const versionId = form.versionId as string | undefined\n return (\n <ListRowActions\n label={form.displayName ?? form.$id}\n quick={{\n icon: mdiEyeOutline.path,\n label: 'Preview',\n // A form with no version has never been opened in the besigner, so\n // there is no snapshot to render. Disabled and saying so, rather\n // than a link to an empty preview.\n ...(versionId && orgSlug && host\n ? {\n to: buildRoute(Route.PLUGIN_DOCUMENT_PREVIEW, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: form.$id,\n versionId,\n }),\n }\n : {\n unavailableReason: versionId\n ? 'Resolving this site’s address…'\n : 'Nothing to preview yet — open it in the besigner once.',\n }),\n }}\n items={[\n {\n key: 'details',\n label: 'View details',\n icon: <MdiIcon path={ICON_VARIANT_SHOW_DETAIL.path} size={0.8} />,\n href: formHref(form.$id),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiVectorSquare.path} size={0.8} />,\n /*\n * A LINK only once the form has a version. A form that has never\n * been opened has none, and opening it MINTS the first one —\n * that is a write, and the detail page is the one place that\n * decides what an initial version looks like. Sending the reader\n * there rather than minting a second way is what keeps the two\n * from drifting.\n */\n href:\n versionId && orgSlug && host\n ? buildRoute(Route.PLUGIN_DOCUMENT_BESIGNER, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: form.$id,\n versionId,\n })\n : formHref(form.$id),\n },\n /*\n * Retire / Restore (AGL-2671). Last in the menu, and the only item\n * here that writes.\n *\n * The label says what it DOES to the form rather than what it does\n * to this list — \"Archive\" reads like a filing action, and the\n * consequence a merchant needs to weigh is that the form stops\n * collecting. `/api/forms/submit` refuses a retired form, so one\n * still placed on a published page will turn visitors away.\n */\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('form', {\n id: form.$id,\n name: form.displayName ?? '',\n }),\n },\n isFormArchived(form)\n ? {\n key: 'restore',\n label: 'Restore — start collecting again',\n icon: <MdiIcon path={mdiArchiveArrowUpOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, false),\n }\n : {\n key: 'retire',\n label: 'Retire — stop collecting, keep the history',\n icon: <MdiIcon path={mdiArchiveArrowDownOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, true),\n },\n ]}\n />\n )\n }),\n ]\n\n return (\n <>\n {duplicate.dialog}\n {/*\n The readout leads the create button, in the PAGE header — where Sites,\n screens, layouts, components and templates put theirs. Forms declares\n no sections, so it has no vertical rail for a card-header cluster to\n belong beside, and the controls are about the whole surface rather\n than about anything the card is showing.\n Published from the card because the card owns what they say: the count\n comes from the listener already open here, and a page that counted for\n itself would be a second source for one fact and a second read for one\n collection. It publishes from the LIST, so a form's own detail route —\n a different component — leaves the header with nothing to create into.\n */}\n <PageHeaderActions>\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'center' }}>\n {/*\n The denominator is `formsPerHost` — the allowance\n `/api/hosts/resources` refuses the create at, resolved through the\n same `checkQuota` the route calls, so the number offered and the\n number enforced cannot drift apart. `FORMS_MAX_PER_HOST` bounds a\n READ of the catalog and sits above every plan's allowance, so\n quoting it advertises room the server will not honor.\n */}\n <QuotaReadoutComponent\n ready={org != null}\n used={formsUsed}\n limit={Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit}\n noun=\"form\"\n />\n {/*\n A retired form keeps its slot, and the readout says so rather than\n leaving a merchant to work out why a catalog of three is using five\n (AGL-2674). The count comes from the same aggregate\n `/api/hosts/resources` enforces on, which subtracts deleted forms\n and nothing else — so retirement is deliberately not a way to buy\n room back, and the number here is the number the server will\n refuse a create at.\n\n Shown only when the site has retired a form (an aggregate — the\n list reads no retired row unless asked): the line answers a\n question nobody is asking on a site that has never retired\n anything.\n */}\n {retiredCount ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Retired forms keep their slot'}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n {/*\n Other ways to start a form, from plugins (AGL-3043): the\n `hostForms` zone this plugin declares, drawn through the shell's\n own gated slot. A plugin page cannot mount that slot itself, so\n the shell hands it down, and a widget here passes the gates a\n console page's would.\n */}\n {CreateZone ? (\n <CreateZone slot={HOST_FORMS_ZONE.id} hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create Form'}\n </Button>\n </Stack>\n </Stack>\n </PageHeaderActions>\n <CardDisplay\n header=\"Forms\"\n help={pluginDocsHelp('forms', {\n anchor: '#find-a-form',\n excerpt:\n 'A form collects submissions into the Inbox, and its design is ' +\n 'drawn in the besigner like any other artifact.',\n })}\n HeaderProps={\n transfer?.can('export', { resource: FORM_SUBMISSIONS_TRANSFER_KEY, scope: 'host', hostId })\n ? {\n action: (\n <Button\n size=\"small\"\n onClick={() =>\n transfer?.openExport({\n resource: FORM_SUBMISSIONS_TRANSFER_KEY,\n scope: 'host',\n hostId,\n })\n }\n >\n {'Export submissions'}\n </Button>\n ),\n }\n : undefined\n }\n >\n {retireError ? (\n <Alert severity=\"error\" sx={{ mb: 2 }}>\n {retireError}\n </Alert>\n ) : null}\n <ListFilterChips\n fields={FORM_LIST_FILTER_FIELDS}\n headers={FORM_LIST_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={formFilterOptions}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: FORM_LIST_FILTER_FIELDS,\n headers: FORM_LIST_FILTER_HEADERS,\n options: formFilterOptions,\n })}\n notices={plan.notices}\n />\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={listFilterGridColumns(\n columns,\n FORM_LIST_FILTER_FIELDS,\n formFilterOptions,\n FORM_LIST_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: FORM_HIDDEN_COLUMNS } }}\n /*\n * The grid must NOT also filter. The query answers the panel and\n * the search, so a second pass in the browser could only drop rows\n * that already matched — it compares what a column DRAWS, and the\n * query compares what the document stores.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The Campaign filter's names are read once the panel is opened.\n onPreferencePanelOpen={(params) => {\n if (params.openedPanelValue === 'filters') setWantsCampaigns(true)\n }}\n {...(filtering\n ? { noRowsLabel: 'No forms match these filters' }\n : {\n noRowsLabel: 'No forms yet',\n noRowsDescription:\n 'A form collects submissions, dedupes the people who send them, and can route them to a lead. Its design is drawn in the besigner and published like any other artifact.',\n noRowsAction: (\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n {/* Other ways to start a form, the header's zone again. */}\n {CreateZone ? (\n <CreateZone slot={HOST_FORMS_ZONE.id} hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create your first form'}\n </Button>\n </Stack>\n ),\n })}\n rows={forms}\n // The whole row opens the detail page; the action cluster stops\n // propagation so a menu click never navigates underneath it.\n onOpen={(id) => router.push(formHref(String(id)))}\n // An empty table while the read is in flight reads as \"you have none\"\n // rather than \"these are on their way\".\n loading={status === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The rows keep the query's order; a header sort would order only\n // the page on screen and read as the whole list's.\n disableColumnSorting\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={forms.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. The empty state and the header open the\n SAME one, so the state that says \"creating…\" is one state.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new form\"\n // A form document stores no description: `/api/hosts/resources` filters\n // `data` through a per-kind allow-list, so one typed here is dropped\n // without a word.\n includeDescription={false}\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n </>\n )\n}\nHostFormsCard.displayName = 'HostFormsCard'\n\nexport default HostFormsCard\n"],"names":["Aglyn","buildRoute","isFormArchived","PageHeaderActions","pluginDocsHelp","Route","useTransferLauncher","useConsoleWidgetSlot","ICON_VARIANT_SHOW_DETAIL","mdiArchiveArrowDownOutline","mdiArchiveArrowUpOutline","mdiContentCopy","mdiEyeOutline","mdiVectorSquare","AppLink","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","ListRowActions","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","QuotaReadoutComponent","CreateArtifactDrawer","Timestamp","Alert","Button","Stack","Typography","collection","doc","getCountFromServer","query","updateDoc","where","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useSiteContainerOptions","useHostResourceApi","useLiveArtifactCount","useListQuery","useRouter","useCallback","useEffect","useMemo","useState","BUNDLE_ID","FORMS_DOCUMENT_SEGMENT","FORM_IN_CAMPAIGN_OPTIONS","FORM_LEAD_ROUTING_OPTIONS","FORM_LIST_FILTER_FIELDS","FORM_LIST_FILTER_HEADERS","FORM_LIST_QUERY","FORM_LIST_SELECT_FIELDS","FORM_STATUS_OPTIONS","formListRequest","FORM_SUBMISSIONS_TRANSFER_KEY","HOST_FORMS_ZONE","FORM_VISIBLE_FILTER_COLUMNS","FORM_HIDDEN_COLUMNS","HostFormsCard","props","hostId","basePath","org","router","transfer","duplicate","onDuplicated","_kind","copy","push","formHref","id","orgSlug","subdomain","host","firestore","createHostResource","CreateZone","formId","FORM_DETAILS","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","retireError","setRetireError","retiredEpoch","setRetiredEpoch","setRetired","retired","archivedAt","Date","now","updatedAt","epoch","formClauses","setFormClauses","gridFilter","clauses","onChange","selectFields","filtering","length","searchWords","formsCollection","status","rows","forms","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","deps","idField","wantsCampaigns","setWantsCampaigns","siteCampaigns","enabled","some","clause","field","formFilterOptions","named","options","map","option","value","label","unnamed","filter","flatMap","split","trim","leadRouting","campaignIds","inCampaign","liveFormCount","formsUsed","retiredCount","setRetiredCount","active","then","snapshot","data","count","catch","handleCreate","values","createResourceUid","resource","displayName","slug","normalizeFormSlug","fields","rootId","CANVAS_ROOT_ELEMENT_ID","nodes","$id","componentId","formRoot","pluginId","parentId","formName","error","console","Error","message","columns","headerName","minWidth","type","renderCell","href","flex","valueFormatter","align","headerAlign","valueGetter","_value","row","stats","submissions","toLocaleString","leads","lastSubmissionAtMs","toDate","String","routing","lead","readContainerIds","form","versionId","quick","icon","path","to","PLUGIN_DOCUMENT_PREVIEW","documentSegment","docId","unavailableReason","items","key","size","PLUGIN_DOCUMENT_BESIGNER","onClick","name","dialog","direction","spacing","sx","alignItems","ready","used","limit","checkQuota","noun","variant","color","slot","orgId","disabled","header","help","anchor","excerpt","HeaderProps","can","scope","action","openExport","undefined","severity","mb","headers","setClauses","refused","notices","rowHeight","initialState","columnVisibilityModel","filterMode","filterModel","onFilterModelChange","quickFilter","onPreferencePanelOpen","params","openedPanelValue","noRowsLabel","noRowsDescription","noRowsAction","xs","sm","onOpen","loading","hideFooter","disableColumnSorting","rowCount","onPageChange","onPageSizeChange","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,YAAYA,WAAW,eAAc;AAErC,SACEC,UAAU,EACVC,cAAc,EACdC,iBAAiB,EACjBC,cAAc,EACdC,KAAK,EACLC,mBAAmB,QACd,eAAc;AACrB,SAASC,oBAAoB,QAAQ,qDAAoD;AACzF,SAASC,wBAAwB,QAAQ,2BAA0B;AACnE,SACEC,0BAA0B,EAC1BC,wBAAwB,EACxBC,cAAc,EACdC,aAAa,EACbC,eAAe,QACV,yBAAwB;AAC/B,SAASC,OAAO,EAAEC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AACpE,OAAOC,qBAAqB,8DAA6D;AACzF,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,OAAOC,oBACLC,iBAAiB,QACZ,+DAA8D;AACrE,OAAOC,aACLC,cAAc,EACdC,iBAAiB,QACZ,uDAAsD;AAC7D,SAASC,sBAAsB,QAAQ,yCAAwC;AAC/E,SAGEC,qBAAqB,QAChB,8CAA6C;AACpD,SAASC,gBAAgB,QAAQ,8CAA6C;AAC9E,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,OAAOC,2BAA2B,0DAAyD;AAC3F,SAASC,oBAAoB,QAAQ,6BAA4B;AACjE,SAASC,SAAS,QAAQ,+BAA8B;AACxD,SAASC,KAAK,EAAEC,MAAM,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAEhE,SACEC,UAAU,EACVC,GAAG,EACHC,kBAAkB,EAClBC,KAAK,EACLC,SAAS,EACTC,KAAK,QACA,qBAAoB;AAC3B,SACEC,oBAAoB,EACpBC,mBAAmB,EACnBC,oBAAoB,EACpBC,YAAY,EACZC,uBAAuB,EACvBC,kBAAkB,EAClBC,oBAAoB,QACf,iCAAgC;AACvC,SAASC,YAAY,QAAQ,sDAAqD;AAClF,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACjE,SAASC,SAAS,EAAEC,sBAAsB,QAAQ,gCAA4B;AAC9E,SACEC,wBAAwB,EACxBC,yBAAyB,EACzBC,uBAAuB,EACvBC,wBAAwB,EACxBC,eAAe,EACfC,uBAAuB,EACvBC,mBAAmB,EACnBC,eAAe,QACV,uBAAmB;AAC1B,SAASC,6BAA6B,QAAQ,+CAA2C;AACzF,SAASC,eAAe,QAAQ,kBAAc;AAE9C;;;;CAIC,GACD,MAAMC,8BAA8B;IAClC;IACA;IACA;IACA;IACA;IACA;CACD;AACD,MAAMC,sBAAsB3C,uBAC1BkC,yBACAQ;AA0BF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GACD,OAAO,SAASE,cAAcC,KAAyB;IACrD,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,GAAG,EAAE,GAAGH;IAClC,MAAMI,SAASxB;IACf,MAAMyB,WAAWpE;IACjB,wEAAwE;IACxE,wDAAwD;IACxD,MAAMqE,YAAYhC,qBAAqB;QACrC2B;QACAM,cAAc,CAACC,OAAOC,OAASL,OAAOM,IAAI,CAACC,SAASF,KAAKG,EAAE;IAC7D;IACA,MAAM,EAAEC,OAAO,EAAEC,WAAWC,IAAI,EAAE,GAAG1C,oBAAoB4B;IACzD,MAAMe,YAAYzC;IAClB,MAAM0C,qBAAqBxC;IAC3B;;;GAGC,GACD,MAAMyC,aAAahF;IAEnB;;;;;;GAMC,GACD,MAAMyE,WAAW9B,YACf,CAACsC,SACCjB,WACI,GAAGA,SAAS,CAAC,EAAEiB,QAAQ,GACvBvF,WAAWI,MAAMoF,YAAY,EAAE;YAC7BP,OAAO,EAAEA,kBAAAA,UAAW;YACpBE,IAAI,EAAEA,eAAAA,OAAQ;YACdI;QACF,IACN;QAACjB;QAAUW;QAASE;KAAK;IAG3B,MAAM,CAACM,YAAYC,cAAc,GAAGtC,SAAS;IAC7C,MAAM,CAACuC,aAAaC,eAAe,GAAGxC,SAAwB;IAC9D,MAAM,CAACyC,UAAUC,YAAY,GAAG1C,SAAS;IACzC,MAAM,CAAC2C,aAAaC,eAAe,GAAG5C,SAAwB;IAC9D,2EAA2E,GAC3E,MAAM,CAAC6C,cAAcC,gBAAgB,GAAG9C,SAAS;IAEjD;;;;;;;;;;;;;;;;GAgBC,GACD,MAAM+C,aAAalD,YACjB,OAAOsC,QAAgBa;QACrBJ,eAAe;QACf,IAAI;YACF,MAAM1D,UAAUH,IAAIiD,WAAW,SAASf,QAAQ,SAASkB,SAAS;gBAChEc,YAAYD,UAAUE,KAAKC,GAAG,KAAK;gBACnC,+CAA+C;gBAC/CH;gBACA,mEAAmE;gBACnE,kEAAkE;gBAClE,gDAAgD;gBAChDI,WAAW3E,UAAU0E,GAAG;YAC1B;YACAL,gBAAgB,CAACO,QAAUA,QAAQ;QACrC,EAAE,eAAM;YACNT,eACEI,UACI,mDACA;QAER;IACF,GACA;QAAChB;QAAWf;KAAO;IAGrB;;;;;;;;;;;;;;;;;;;;GAoBC,GACD,MAAM,CAACqC,aAAaC,eAAe,GAAGvD,SAA6B,EAAE;IACrE,MAAMwD,aAAalF,kBAAkB;QACnCmF,SAASH;QACTI,UAAUH;QACVI,cAAcnD;IAChB;IACA,MAAMoD,YAAYJ,WAAWC,OAAO,CAACI,MAAM,GAAG,KAAKL,WAAWM,WAAW,CAACD,MAAM,GAAG;IACnF,MAAME,kBAAkBhE,QACtB,IAAMjB,WAAWkD,WAAW,SAASf,QAAQ,UAC7C;QAACe;QAAWf;KAAO;IAErB,MAAM,EACJ+C,MAAM,EACNC,MAAMC,KAAK,EACXC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,IAAI,EACL,GAAG7E,aAAkB;QACpBb,YAAYiF;QACZU,aAAalE;QACbmE,SAAShE,gBAAgB8C,WAAWC,OAAO,EAAED,WAAWM,WAAW;QACnEa,MAAM;YAAC3C;YAAWf;SAAO;QACzB2D,SAAS;IACX;IAEA;;;;;;;;;;GAUC,GACD,MAAM,CAACC,gBAAgBC,kBAAkB,GAAG9E,SAAS;IACrD,MAAM+E,gBAAgBvF,wBAAwB,YAAYyB,QAAQ;QAChE+D,SAASH,kBAAkBvB,YAAY2B,IAAI,CAAC,CAACC,SAAWA,OAAOC,KAAK,KAAK;IAC3E;IACA,MAAMC,oBAAoBrF,QAA4C;QACpE,MAAMsF,QAAQN,cAAcO,OAAO,CAACC,GAAG,CAAC,CAACC,SAAY,CAAA;gBACnDC,OAAOD,OAAOC,KAAK;gBACnBC,OAAOF,OAAOE,KAAK;YACrB,CAAA;QACA,MAAMC,UAAUrC,YACbsC,MAAM,CAAC,CAACV,SAAWA,OAAOC,KAAK,KAAK,eACpCU,OAAO,CAAC,CAACX,SAAWA,OAAOO,KAAK,CAACK,KAAK,CAAC,MACvCP,GAAG,CAAC,CAACE,QAAUA,MAAMM,IAAI,IACzBH,MAAM,CAAC,CAACH,QAAUA,SAAS,CAACJ,MAAMJ,IAAI,CAAC,CAACO,SAAWA,OAAOC,KAAK,KAAKA,QACpEF,GAAG,CAAC,CAACE,QAAW,CAAA;gBAAEA;gBAAOC,OAAOD;YAAM,CAAA;QACzC,OAAO;YACLzB,QAAQvD;YACRuF,aAAa5F;YACb6F,aAAa;mBAAIZ;mBAAUM;aAAQ;YACnCO,YAAY/F;QACd;IACF,GAAG;QAAC4E,cAAcO,OAAO;QAAEhC;KAAY;IAEvC;;;;GAIC,GACD,MAAM6C,gBAAgBzG,qBAAqBuB,QAAQ;IACnD,2EAA2E;IAC3E,QAAQ;IACR,MAAMmF,YAAYD,wBAAAA,gBAAiBjC,MAAML,MAAM;IAC/C;;;;GAIC,GACD,MAAM,CAACwC,cAAcC,gBAAgB,GAAGtG,SAAwB;IAChEF,UAAU;QACR,IAAIyG,SAAS;QACbvH,mBAAmBC,MAAM8E,iBAAiB5E,MAAM,WAAW,MAAM,QAC9DqH,IAAI,CAAC,CAACC;YACL,IAAIF,QAAQD,gBAAgBG,SAASC,IAAI,GAAGC,KAAK;QACnD,GACCC,KAAK,CAAC;QACL,oEAAoE;QACtE;QACF,OAAO;YACLL,SAAS;QACX;IACF,GAAG;QAACxC;QAAiBlB;KAAa;IAElC;;;;;;;;GAQC,GACD,MAAMgE,eAAehH,YACnB,OAAOiH;QACL,IAAIrE,UAAU;QACdC,YAAY;QACZF,eAAe;QACf,IAAI;YACF,MAAML,SAASxF,MAAMoK,iBAAiB;YACtC,MAAM9E,mBAAmB;gBACvBhB;gBACA+F,UAAU;gBACVpF,IAAIO;gBACJuE,MAAM;oBACJO,aAAaH,MAAM,CAAC,cAAc;oBAClCI,MAAMvK,MAAMwK,iBAAiB,CAACL,MAAM,CAAC,cAAc,KAAK3E;oBACxDiF,QAAQ,EAAE;oBACVC,QAAQ1K,MAAM2K,sBAAsB;oBACpCC,OAAO;wBACL,CAAC5K,MAAM2K,sBAAsB,CAAC,EAAE;4BAC9BE,KAAK7K,MAAM2K,sBAAsB;4BACjCG,aAAa;4BACbF,OAAO;gCAAC;6BAAW;wBACrB;wBACAG,UAAU;4BACRF,KAAK;4BACLC,aAAa;4BACbE,UAAU1H;4BACV2H,UAAUjL,MAAM2K,sBAAsB;4BACtCtG,OAAO;gCAAEmB;gCAAQ0F,UAAUf,MAAM,CAAC,cAAc;4BAAC;4BACjDS,OAAO,EAAE;wBACX;oBACF;gBACF;YACF;YACAjF,cAAc;YACdlB,OAAOM,IAAI,CAACC,SAASQ;QACvB,EAAE,OAAO2F,OAAO;YACdC,QAAQD,KAAK,CAACA;YACd,gEAAgE;YAChE,qEAAqE;YACrE,qEAAqE;YACrEtF,eACEsF,iBAAiBE,SAASF,MAAMG,OAAO,IAAIH,MAAMG,OAAO,KAAK,kBACzDH,MAAMG,OAAO,GACb;QAER,SAAU;YACRvF,YAAY;QACd;IACF,GACA;QAACD;QAAUR;QAAoBhB;QAAQG;QAAQO;KAAS;IAG1D,MAAMuG,UAAwB;QAC5B;YACE/C,OAAO;YACPgD,YAAY;YACZC,UAAU;YACVC,MAAM;YACNC,YAAY,CAAC,EAAE1G,EAAE,EAAE6D,KAAK,EAAO,iBAC7B,KAAChI;oBAAQ8K,MAAM5G,SAASC;8BACrB6D,SAAU7D;;QAGjB;QACA;YACEuD,OAAO;YACPgD,YAAY;YACZC,UAAU;YACVI,MAAM;YACNH,MAAM;YACN,gEAAgE;YAChEI,gBAAgB,CAAChD,QAAeA,SAAS;QAC3C;QACA;YACEN,OAAO;YACPgD,YAAY;YACZC,UAAU;YACVC,MAAM;YACN,sEAAsE;YACtE,sEAAsE;YACtE,qDAAqD;YACrDK,OAAO;YACPC,aAAa;YACbC,aAAa,CAACC,QAAaC;;oBAAaA;+BAAAA,wBAAAA,aAAAA,IAAKC,KAAK,qBAAVD,WAAYE,WAAW,mBAAI;;YACnEP,gBAAgB,CAAChD,QACf,OAAOA,UAAU,WAAWA,MAAMwD,cAAc,KAAK;QACzD;QACA;YACE9D,OAAO;YACPgD,YAAY;YACZC,UAAU;YACVC,MAAM;YACNK,OAAO;YACPC,aAAa;YACb;;;;;;;;OAQC,GACDC,aAAa,CAACC,QAAaC;oBAClBA;uBAAP,QAAOA,wBAAAA,aAAAA,IAAKC,KAAK,qBAAVD,WAAYI,KAAK,MAAK,WAAWJ,IAAIC,KAAK,CAACG,KAAK,GAAG;;YAC5DT,gBAAgB,CAAChD,QACf,OAAOA,UAAU,WAAWA,MAAMwD,cAAc,KAAK;QACzD;QACA;YACE9D,OAAO;YACPgD,YAAY;YACZC,UAAU;YACVI,MAAM;YACNH,MAAM;YACNO,aAAa,CAACC,QAAaC;oBAClBA;uBAAP,QAAOA,wBAAAA,aAAAA,IAAKC,KAAK,qBAAVD,WAAYK,kBAAkB,MAAK,WACtC,IAAIjG,KAAK4F,IAAIC,KAAK,CAACI,kBAAkB,IACrC;;YACNV,gBAAgB,CAAChD;oBAAeA;uBAAAA,CAAAA,0BAAAA,wBAAAA,MAAOwD,cAAc,qBAArBxD,2BAAAA,WAA6B;;QAC/D;QACA;YACEN,OAAO;YACPgD,YAAY;YACZC,UAAU;YACVI,MAAM;YACNH,MAAM;YACN,sEAAsE;YACtE,gEAAgE;YAChEO,aAAa,CAACnD;;oBAAeA;+BAAAA,0BAAAA,gBAAAA,MAAO2D,MAAM,qBAAb3D,mBAAAA,yBAAqB;;YAClDgD,gBAAgB,CAAChD;oBAAeA;uBAAAA,CAAAA,0BAAAA,wBAAAA,MAAOwD,cAAc,qBAArBxD,2BAAAA,WAA6B;;QAC/D;QACA;;;;;;KAMC,GACD;YACEN,OAAO;YACPgD,YAAY;YACZC,UAAU;YACVQ,aAAa,CAACC,QAAaC,MAAaO,OAAOxM,eAAeiM;QAChE;QACA;YACE3D,OAAO;YACPgD,YAAY;YACZC,UAAU;YACVQ,aAAa,CAACC,QAAaC;oBAAoBA;uBAAPO,OAAOP,CAAAA,wBAAAA,eAAAA,IAAKQ,OAAO,qBAAZR,aAAcS,IAAI,MAAK;;QACxE;QACA;;;;KAIC,GACD;YACEpE,OAAO;YACPgD,YAAY;YACZC,UAAU;YACVQ,aAAa,CAACC,QAAaC,MACzBO,OAAO1M,MAAM6M,gBAAgB,CAACV,KAAK,YAAYjF,MAAM,GAAG;QAC5D;QACA3F,kBAAkB,CAAC4K;gBAKNW;YAJX,MAAMA,OAAO,aAAKX;gBAAKtB,KAAKsB,IAAItB,GAAG;;YACnC,MAAMkC,YAAYD,KAAKC,SAAS;YAChC,qBACE,KAACzL;gBACCyH,KAAK,GAAE+D,oBAAAA,KAAKxC,WAAW,YAAhBwC,oBAAoBA,KAAKjC,GAAG;gBACnCmC,OAAO;oBACLC,MAAMrM,cAAcsM,IAAI;oBACxBnE,OAAO;mBAIHgE,aAAa7H,WAAWE,OACxB;oBACE+H,IAAIlN,WAAWI,MAAM+M,uBAAuB,EAAE;wBAC5ClI;wBACAE;wBACAiI,iBAAiB9J;wBACjB+J,OAAOR,KAAKjC,GAAG;wBACfkC;oBACF;gBACF,IACA;oBACEQ,mBAAmBR,YACf,mCACA;gBACN;gBAENS,OAAO;oBACL;wBACEC,KAAK;wBACL1E,OAAO;wBACPkE,oBAAM,KAACjM;4BAAQkM,MAAM1M,yBAAyB0M,IAAI;4BAAEQ,MAAM;;wBAC1D9B,MAAM5G,SAAS8H,KAAKjC,GAAG;oBACzB;oBACA;wBACE4C,KAAK;wBACL1E,OAAO;wBACPkE,oBAAM,KAACjM;4BAAQkM,MAAMrM,gBAAgBqM,IAAI;4BAAEQ,MAAM;;wBACjD;;;;;;;eAOC,GACD9B,MACEmB,aAAa7H,WAAWE,OACpBnF,WAAWI,MAAMsN,wBAAwB,EAAE;4BACzCzI;4BACAE;4BACAiI,iBAAiB9J;4BACjB+J,OAAOR,KAAKjC,GAAG;4BACfkC;wBACF,KACA/H,SAAS8H,KAAKjC,GAAG;oBACzB;oBACA;;;;;;;;;aASC,GACD;wBACE4C,KAAK;wBACL1E,OAAOtG;wBACPwK,oBAAM,KAACjM;4BAAQkM,MAAMvM,eAAeuM,IAAI;4BAAEQ,MAAM;;wBAChDE,SAAS;gCAGCd;mCAFRnI,UAAUoD,OAAO,CAAC,QAAQ;gCACxB9C,IAAI6H,KAAKjC,GAAG;gCACZgD,IAAI,GAAEf,oBAAAA,KAAKxC,WAAW,YAAhBwC,oBAAoB;4BAC5B;;oBACJ;oBACA5M,eAAe4M,QACX;wBACEW,KAAK;wBACL1E,OAAO;wBACPkE,oBAAM,KAACjM;4BAAQkM,MAAMxM,yBAAyBwM,IAAI;4BAAEQ,MAAM;;wBAC1DE,SAAS,IAAM,KAAKxH,WAAW0G,KAAKjC,GAAG,EAAE;oBAC3C,IACA;wBACE4C,KAAK;wBACL1E,OAAO;wBACPkE,oBAAM,KAACjM;4BAAQkM,MAAMzM,2BAA2ByM,IAAI;4BAAEQ,MAAM;;wBAC5DE,SAAS,IAAM,KAAKxH,WAAW0G,KAAKjC,GAAG,EAAE;oBAC3C;iBACL;;QAGP;KACD;IAED,qBACE;;YACGlG,UAAUmJ,MAAM;0BAajB,KAAC3N;0BACC,cAAA,MAAC8B;oBAAM8L,WAAU;oBAAMC,SAAS;oBAAGC,IAAI;wBAAEC,YAAY;oBAAS;;sCAS5D,KAACtM;4BACCuM,OAAO3J,OAAO;4BACd4J,MAAM3E;4BACN4E,OAAOrO,MAAMsO,UAAU,CAAC9J,KAAK,gBAAgBiF,WAAW4E,KAAK;4BAC7DE,MAAK;;wBAgBN7E,6BACC,KAACxH;4BAAWsM,SAAQ;4BAAUC,OAAM;sCACjC;6BAED;sCACJ,MAACxM;4BAAM8L,WAAU;4BAAMC,SAAS;;gCAQ7BzI,2BACC,KAACA;oCAAWmJ,MAAMzK,gBAAgBgB,EAAE;oCAAEX,QAAQA;oCAAQqK,KAAK,EAAEnK,uBAAAA,IAAKqG,GAAG;qCACnE;8CACJ,KAAC7I;oCACC0L,MAAK;oCACLc,SAAQ;oCACRI,UAAU9I;oCACV8H,SAAS;wCACP/H,eAAe;wCACfF,cAAc;oCAChB;8CAECG,WAAW,cAAc;;;;;;;0BAKlC,MAAC/E;gBACC8N,QAAO;gBACPC,MAAM1O,eAAe,SAAS;oBAC5B2O,QAAQ;oBACRC,SACE,mEACA;gBACJ;gBACAC,aACEvK,CAAAA,4BAAAA,SAAUwK,GAAG,CAAC,UAAU;oBAAE7E,UAAUrG;oBAA+BmL,OAAO;oBAAQ7K;gBAAO,MACrF;oBACE8K,sBACE,KAACpN;wBACC0L,MAAK;wBACLE,SAAS,IACPlJ,4BAAAA,SAAU2K,UAAU,CAAC;gCACnBhF,UAAUrG;gCACVmL,OAAO;gCACP7K;4BACF;kCAGD;;gBAGP,IACAgL;;oBAGLtJ,4BACC,KAACjE;wBAAMwN,UAAS;wBAAQtB,IAAI;4BAAEuB,IAAI;wBAAE;kCACjCxJ;yBAED;kCACJ,KAAC/E;wBACCwJ,QAAQ/G;wBACR+L,SAAS9L;wBACTmD,SAASD,WAAWC,OAAO;wBAC3BC,UAAUF,WAAW6I,UAAU;wBAC/B/G,SAASF;;kCAEX,KAACtH;wBACCwO,SAASvO,kBAAkByG,KAAK8H,OAAO,EAAE;4BACvClF,QAAQ/G;4BACR+L,SAAS9L;4BACTgF,SAASF;wBACX;wBACAmH,SAAS/H,KAAK+H,OAAO;;kCAEvB,KAACvO;wBACCwO,WAAWnO;wBACX6J,SAAS9J,sBACP8J,SACA7H,yBACA+E,mBACA9E;wBAEFmM,cAAc;4BAAEvE,SAAS;gCAAEwE,uBAAuB5L;4BAAoB;wBAAE;wBACxE;;;;;WAKC,GACD6L,YAAW;wBACXC,aAAapJ,WAAWoJ,WAAW;wBACnCC,qBAAqBrJ,WAAWqJ,mBAAmB;wBACnDC,WAAW;wBACX,iEAAiE;wBACjEC,uBAAuB,CAACC;4BACtB,IAAIA,OAAOC,gBAAgB,KAAK,WAAWnI,kBAAkB;wBAC/D;uBACKlB,YACD;wBAAEsJ,aAAa;oBAA+B,IAC9C;wBACEA,aAAa;wBACbC,mBACE;wBACFC,4BACE,MAACxO;4BAAM8L,WAAW;gCAAE2C,IAAI;gCAAUC,IAAI;4BAAM;4BAAG3C,SAAS;;gCAErDzI,2BACC,KAACA;oCAAWmJ,MAAMzK,gBAAgBgB,EAAE;oCAAEX,QAAQA;oCAAQqK,KAAK,EAAEnK,uBAAAA,IAAKqG,GAAG;qCACnE;8CACJ,KAAC7I;oCAAOwM,SAAQ;oCAAYZ,SAAS,IAAMjI,cAAc;8CACtD;;;;oBAIT;wBACJ2B,MAAMC;wBACN,gEAAgE;wBAChE,6DAA6D;wBAC7DqJ,QAAQ,CAAC3L,KAAOR,OAAOM,IAAI,CAACC,SAAS0H,OAAOzH;wBAC5C,sEAAsE;wBACtE,wCAAwC;wBACxC4L,SAASxJ,WAAW;wBACpB,8DAA8D;wBAC9DyJ,UAAU;wBACV,kEAAkE;wBAClE,mDAAmD;wBACnDC,oBAAoB;;kCAEtB,KAAC7P;wBACCuG,MAAMA;wBACNE,UAAUA;wBACVqJ,UAAUzJ,MAAML,MAAM;wBACtBM,SAASA;wBACTyJ,cAAcvJ;wBACdwJ,kBAAkBtJ;;kCAOpB,KAAC/F;wBACCsP,MAAMzL;wBACN0L,SAAS,IAAMzL,cAAc;wBAC7B0L,OAAM;wBACN,wEAAwE;wBACxE,qEAAqE;wBACrE,kBAAkB;wBAClBC,oBAAoB;wBACpBC,UAAUrH;wBACVsH,WACE5L,4BACE,KAAC7D;4BAAMwN,UAAS;4BAAQtB,IAAI;gCAAEwD,IAAI;gCAAGjC,IAAI;4BAAE;sCACxC5J;6BAED;;;;;;AAMhB;AACAxB,cAAckG,WAAW,GAAG;AAE5B,eAAelG,cAAa"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/components/host-forms-card.component.tsx"],"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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { AglynOrgBilling } from '@aglyn/aglyn'\nimport {\n buildRoute,\n isFormArchived,\n PageHeaderActions,\n pluginDocsHelp,\n Route,\n useTransferLauncher,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { ICON_VARIANT_SHOW_DETAIL } from '@aglyn/shared-data-enums'\nimport {\n mdiArchiveArrowDownOutline,\n mdiArchiveArrowUpOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiVectorSquare,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport ListTable, {\n ListRowActions,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n type ListFilterClause,\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n getCountFromServer,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useSiteContainerOptions,\n useHostResourceApi,\n useLiveArtifactCount,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { BUNDLE_ID, FORMS_DOCUMENT_SEGMENT } from '../constants/bundle-common'\nimport {\n FORM_IN_CAMPAIGN_OPTIONS,\n FORM_LEAD_ROUTING_OPTIONS,\n FORM_LIST_FILTER_FIELDS,\n FORM_LIST_FILTER_HEADERS,\n FORM_LIST_QUERY,\n FORM_LIST_SELECT_FIELDS,\n FORM_STATUS_OPTIONS,\n formListRequest,\n} from './form-list-query'\nimport { FORM_SUBMISSIONS_TRANSFER_KEY } from '../transfer/form-submissions-transfer-key'\nimport { HOST_FORMS_ZONE } from './form-zones'\n\n/**\n * The filterable columns the table shows. The rest of the declaration —\n * Status, Lead routing, Campaign, In a campaign — reaches the Filters panel as hidden\n * columns (`hiddenFilterColumns`).\n */\n/** Status sorts the loaded page (AGL-3680): Active before Retired. */\nconst FORM_PAGE_SORTS = { status: (row: any) => isFormArchived(row) }\nconst FORM_PAGE_SORT_HEADERS = { status: 'Status' }\n\nconst FORM_VISIBLE_FILTER_COLUMNS = [\n 'displayName',\n 'slug',\n 'submissions',\n 'leads',\n 'lastSubmission',\n 'updatedAt',\n]\nconst FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(\n FORM_LIST_FILTER_FIELDS,\n FORM_VISIBLE_FILTER_COLUMNS,\n)\n\nexport interface HostFormsCardProps {\n hostId: string\n /**\n * The Forms surface's own absolute console path, from the shell.\n *\n * A row's link is this plus the form's id, resolved synchronously. The\n * alternative is `useConsoleHostRoute`, which answers `null` for a paint\n * while it reads two documents — and a table whose every row links to\n * `/null/...` on first render is worse than one that pays nothing.\n */\n basePath?: string\n /**\n * The org billing document, from the shell.\n *\n * The quota readout's denominator resolves from it. `undefined` means the\n * plan is not known rather than that it is Free — the shell holds this\n * surface behind a spinner until the org read settles, so it reads that way\n * only when the read failed — and the readout says so instead of naming a\n * cap it cannot support.\n */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.\n *\n * The list is the components list's table, deliberately and not\n * approximately: `ListTable` gives it the row grammar every artifact list has\n * — the row opens the detail page, rows are not selectable, one quick action\n * then the overflow — and `ListPagination` gives it the console's one footer.\n * A form is an artifact like a component, so a reader who has used one list\n * has used this one.\n *\n * ## Why the empty state is a table and not a sentence\n *\n * A paragraph and a button reads as a smaller feature than this is: a form is\n * a thing with a slug, a submission count and a version history, and none of\n * that would be visible until the reader had already committed to making one.\n * Rendering the columns with the empty overlay inside them teaches the shape\n * of the artifact BEFORE the first one exists, which is what the components\n * list has always done.\n *\n * ## The two numeric columns, and when one is honestly blank\n *\n * `stats.submissions` and `stats.leads` are both incremented by\n * `/api/forms/submit` on a write that was happening anyway, and recounted\n * from their rows after a removal (AGL-3330), so both are real numbers.\n * `leads` is counted only for a form whose `routing.lead` is set — it holds\n * `0` there until the first lead — so a form that has never routed to leads\n * carries no figure at all and renders as a dash rather than as `0`. A zero\n * would say this form has produced no leads, which for an unrouted form is a\n * measurement nobody took.\n *\n * ## Export submissions, in the card header\n *\n * Every form's submissions on this site, through the console's export dialog\n * (`forms.submissions`, unfiltered): each form's questions are a group of\n * columns, so one file holds the whole site. One form's alone is exported\n * from that form's page. There is no Import: a submission is what a visitor\n * sent, and a file cannot have sent one.\n */\nexport function HostFormsCard(props: HostFormsCardProps) {\n const { hostId, basePath, org } = props\n const router = useRouter()\n const transfer = useTransferLauncher()\n // Duplicate (AGL-2936): the copy is a form of its own — its submissions\n // start empty — and opens on its detail page once made.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) => router.push(formHref(copy.id)),\n })\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n /**\n * The shell's zone renderer (AGL-3043), for other ways to start a form; `null`\n * outside the console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n\n /**\n * One form's page, beneath this surface's own path.\n *\n * `basePath` is the shell's answer and needs no read; the route table is the\n * fallback for a caller that has none, and it is the one that can be `null`\n * for a paint.\n */\n const formHref = useCallback(\n (formId: string) =>\n basePath\n ? `${basePath}/${formId}`\n : buildRoute(Route.FORM_DETAILS, {\n orgSlug: orgSlug ?? '',\n host: host ?? '',\n formId,\n }),\n [basePath, orgSlug, host],\n )\n\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n const [retireError, setRetireError] = useState<string | null>(null)\n /** Bumped by each retire or restore, so the retired count is asked again. */\n const [retiredEpoch, setRetiredEpoch] = useState(0)\n\n /**\n * Retire a form, or put it back (AGL-2671).\n *\n * A write of one field, client-side like every other edit on this document:\n * the rules already admit it for a host writer, fencing off only `stats`.\n *\n * ⛔ NOT a delete, and the difference is the whole feature. The submissions,\n * the leads and the contact timeline they built are filed under this form,\n * so the document is what makes that history readable; deleting it to stop\n * collection would throw away the reason the merchant kept the form.\n *\n * `Date.now()` is the browser's clock rather than `serverTimestamp()`\n * because {@link isFormArchived} asks for a number greater than zero, and a\n * server sentinel reads as `null` on the local echo — the row would stay\n * looking live until the write round-tripped. The value is a marker, never\n * an audit fact: nothing computes a duration from it.\n */\n const setRetired = useCallback(\n async (formId: string, retired: boolean) => {\n setRetireError(null)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {\n archivedAt: retired ? Date.now() : null,\n // The mirror the list's query asks (AGL-3330).\n retired,\n // Retiring and restoring are edits, and Updated says when the form\n // was last edited (AGL-3330): a write that skipped it left a form\n // retired today reading as untouched for weeks.\n updatedAt: Timestamp.now(),\n })\n setRetiredEpoch((epoch) => epoch + 1)\n } catch {\n setRetireError(\n retired\n ? 'Could not retire that form. Try again shortly.'\n : 'Could not restore that form. Try again shortly.',\n )\n }\n },\n [firestore, hostId],\n )\n\n /*\n * THE LIST IS ITS QUERY (AGL-3330, on the AGL-3321 list query plan).\n *\n * The grid's Filters panel and search box edit the clauses and the words;\n * `useListQuery` puts every one of them on ONE Firestore query and pages\n * that query, so a page is a page of matches and a form whose only match\n * is past the first page is still found. What one query cannot hold — a\n * second range, a second array clause — is refused by name above the list\n * (`ListQueryNotices`) rather than matched over the rows read. See\n * `form-list-query.ts` for every field and the indexes they cost.\n *\n * Unfiltered it reads in document order, as it always has: `orderBy` on a\n * name would drop every form stored without one rather than mis-sort it.\n *\n * A retired form is a TOMBSTONE, not a row, unless the reader asks for\n * Status (AGL-2671). The query leaves them out (`retired == false`), so a\n * page is a page of forms in use; a Status clause replaces that with the\n * reader's own choice. Retiring a form removes it from the list, and\n * without the Status filter that would be a one-way door: the row is the\n * only route back to Restore.\n */\n const [formClauses, setFormClauses] = useState<ListFilterClause[]>([])\n const gridFilter = useListGridFilter({\n clauses: formClauses,\n onChange: setFormClauses,\n selectFields: FORM_LIST_SELECT_FIELDS,\n })\n const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0\n const formsCollection = useMemo(\n () => collection(firestore, 'hosts', hostId, 'forms'),\n [firestore, hostId],\n )\n /*\n * EVERY HEADER SORTS (AGL-3680): each stored column orders the QUERY\n * (`FORM_LIST_COLUMN_SORTS`), served while no filter or search narrows the\n * list — otherwise it keeps document order and the notices say why.\n * Status sorts the page: the list in use is all Active.\n */\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(null)\n const {\n status,\n rows: forms,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: formsCollection,\n declaration: FORM_LIST_QUERY,\n request: formListRequest(gridFilter.clauses, gridFilter.searchWords, askedSort),\n deps: [firestore, hostId],\n idField: '$id',\n })\n const columnSort = useListColumnSort<any>({\n sorts: FORM_LIST_QUERY.sorts,\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: plan.orderBy,\n rows: forms,\n pageSorts: FORM_PAGE_SORTS,\n headers: FORM_PAGE_SORT_HEADERS,\n })\n\n /*\n * The Campaign filter's choices: the campaigns placed on this site, by\n * name, and any campaign a clause names that the list does not, by its id,\n * so the chip can still be read and cleared.\n *\n * Read once the reader opens the Filters panel, not on every visit: the\n * names cost the org lookup and up to fifty campaign documents, which a\n * reader who came to open one form would pay for a filter they never\n * touched. Until they land the column has no choices, and the panel lists\n * it a moment later.\n */\n const [wantsCampaigns, setWantsCampaigns] = useState(false)\n const siteCampaigns = useSiteContainerOptions('campaign', hostId, {\n enabled: wantsCampaigns || formClauses.some((clause) => clause.field === 'campaignIds'),\n })\n const formFilterOptions = useMemo<Record<string, ListFilterOption[]>>(() => {\n const named = siteCampaigns.options.map((option) => ({\n value: option.value,\n label: option.label,\n }))\n const unnamed = formClauses\n .filter((clause) => clause.field === 'campaignIds')\n .flatMap((clause) => clause.value.split(','))\n .map((value) => value.trim())\n .filter((value) => value && !named.some((option) => option.value === value))\n .map((value) => ({ value, label: value }))\n return {\n status: FORM_STATUS_OPTIONS,\n leadRouting: FORM_LEAD_ROUTING_OPTIONS,\n campaignIds: [...named, ...unnamed],\n inCampaign: FORM_IN_CAMPAIGN_OPTIONS,\n }\n }, [siteCampaigns.options, formClauses])\n\n /*\n * The COUNT is a server aggregate, not the length of a page. `forms` is one\n * page, and publishing that as the site's form count would read as room to\n * spare on a site that is already at the ceiling.\n */\n const liveFormCount = useLiveArtifactCount(hostId, 'forms')\n // Pending or refused, the page stands in: a LOWER bound, never a confident\n // zero.\n const formsUsed = liveFormCount ?? forms.length\n /*\n * How many forms are retired, for the \"keep their slot\" line (AGL-2674).\n * An aggregate, because the list no longer reads a retired row unless it is\n * asked for one; asked again after this card retires or restores a form.\n */\n const [retiredCount, setRetiredCount] = useState<number | null>(null)\n useEffect(() => {\n let active = true\n getCountFromServer(query(formsCollection, where('retired', '==', true)))\n .then((snapshot) => {\n if (active) setRetiredCount(snapshot.data().count)\n })\n .catch(() => {\n // The line is a courtesy; without the count it is simply not shown.\n })\n return () => {\n active = false\n }\n }, [formsCollection, retiredEpoch])\n\n /**\n * Name first, then create (AGL-700).\n *\n * A form is created with BOTH halves seeded. `fields` is the declaration the\n * submission path reads and starts empty; the canvas is the design, seeded\n * with a root and a form node already bound to this id — so the besigner\n * opens on something that satisfies `checkFormContract` rather than on a\n * blank page whose first publish is a list of violations.\n */\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n setCreating(true)\n setCreateError(null)\n try {\n const formId = Aglyn.createResourceUid()\n await createHostResource({\n hostId,\n resource: 'form',\n id: formId,\n data: {\n displayName: values['displayName'],\n slug: Aglyn.normalizeFormSlug(values['displayName']) || formId,\n fields: [],\n rootId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n nodes: {\n [Aglyn.CANVAS_ROOT_ELEMENT_ID]: {\n $id: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: ['formRoot'],\n },\n formRoot: {\n $id: 'formRoot',\n componentId: 'form',\n pluginId: BUNDLE_ID,\n parentId: Aglyn.CANVAS_ROOT_ELEMENT_ID,\n props: { formId, formName: values['displayName'] },\n nodes: [],\n },\n },\n },\n })\n setCreateOpen(false)\n router.push(formHref(formId))\n } catch (error) {\n console.error(error)\n // The route's own words when it refused: a spent `formsPerHost`\n // allowance answers \"Your plan includes N forms — upgrade in Billing\n // for more\", which is the upgrade path a generic failure would hide.\n setCreateError(\n error instanceof Error && error.message && error.message !== 'Create failed'\n ? error.message\n : 'Could not create that form',\n )\n } finally {\n setCreating(false)\n }\n },\n [creating, createHostResource, hostId, router, formHref],\n )\n\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Display name',\n minWidth: 220,\n type: 'string',\n renderCell: ({ id, value }: any) => (\n <AppLink href={formHref(id as string)}>\n {value || (id as string)}\n </AppLink>\n ),\n },\n {\n field: 'slug',\n headerName: 'Slug',\n minWidth: 160,\n flex: 1,\n type: 'string',\n // Blank reads as a rendering gap; '--' reads as \"nothing here\".\n valueFormatter: (value: any) => value || '--',\n },\n {\n field: 'submissions',\n headerName: 'Submissions',\n minWidth: 130,\n type: 'number',\n // Head AND body. `type: 'number'` right-aligns both, and the explicit\n // pair says so at the call site rather than relying on a grid default\n // that a later `renderCell` would silently override.\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: any, row: any) => row?.stats?.submissions ?? null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'leads',\n headerName: 'Leads',\n minWidth: 100,\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n /*\n * A dash rather than a `0` whenever the field holds no number.\n *\n * `stats.leads` is counted only for a form whose `routing.lead` is set\n * (`0` there until its first lead), so a form that has never routed to\n * leads has no recorded figure. Rendering `0` there would read as \"this\n * form has produced no leads\", which is a claim about a measurement\n * nobody took.\n */\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.leads === 'number' ? row.stats.leads : null,\n valueFormatter: (value: any) =>\n typeof value === 'number' ? value.toLocaleString() : '--',\n },\n {\n field: 'lastSubmission',\n headerName: 'Last submission',\n minWidth: 170,\n flex: 1,\n type: 'date',\n valueGetter: (_value: any, row: any) =>\n typeof row?.stats?.lastSubmissionAtMs === 'number'\n ? new Date(row.stats.lastSubmissionAtMs)\n : null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n minWidth: 170,\n flex: 1,\n type: 'date',\n // MUI X v9 passes the value positionally. The v6 object form silently\n // destructures undefined off a Date and every row renders '--'.\n valueGetter: (value: any) => value?.toDate?.() ?? null,\n valueFormatter: (value: any) => value?.toLocaleString?.() || '--',\n },\n /*\n * Status and Lead routing, hidden until Columns shows them (AGL-3330).\n * Each is a select the Filters panel offers either way, and draws the\n * choice its value names. Status reads the FACT, `archivedAt`, rather\n * than the `retired` mirror the query asks, so a form the backfill has\n * not reached yet still reads true.\n */\n {\n field: 'status',\n headerName: 'Status',\n minWidth: 110,\n valueGetter: (_value: any, row: any) => String(isFormArchived(row)),\n },\n {\n field: 'leadRouting',\n headerName: 'Lead routing',\n minWidth: 120,\n valueGetter: (_value: any, row: any) => String(row?.routing?.lead === true),\n },\n /*\n * In a campaign, hidden like the two above. Reads the ids the form is\n * filed under — the fact `inCampaign` mirrors for the query — so a form\n * the backfill has not reached still draws the truth.\n */\n {\n field: 'inCampaign',\n headerName: 'In a campaign',\n minWidth: 130,\n valueGetter: (_value: any, row: any) =>\n String(Aglyn.readContainerIds(row, 'campaign').length > 0),\n },\n listActionsColumn((row: any) => {\n const form = { ...row, $id: row.$id as string }\n const versionId = form.versionId as string | undefined\n return (\n <ListRowActions\n label={form.displayName ?? form.$id}\n quick={{\n icon: mdiEyeOutline.path,\n label: 'Preview',\n // A form with no version has never been opened in the besigner, so\n // there is no snapshot to render. Disabled and saying so, rather\n // than a link to an empty preview.\n ...(versionId && orgSlug && host\n ? {\n to: buildRoute(Route.PLUGIN_DOCUMENT_PREVIEW, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: form.$id,\n versionId,\n }),\n }\n : {\n unavailableReason: versionId\n ? 'Resolving this site’s address…'\n : 'Nothing to preview yet — open it in the besigner once.',\n }),\n }}\n items={[\n {\n key: 'details',\n label: 'View details',\n icon: <MdiIcon path={ICON_VARIANT_SHOW_DETAIL.path} size={0.8} />,\n href: formHref(form.$id),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiVectorSquare.path} size={0.8} />,\n /*\n * A LINK only once the form has a version. A form that has never\n * been opened has none, and opening it MINTS the first one —\n * that is a write, and the detail page is the one place that\n * decides what an initial version looks like. Sending the reader\n * there rather than minting a second way is what keeps the two\n * from drifting.\n */\n href:\n versionId && orgSlug && host\n ? buildRoute(Route.PLUGIN_DOCUMENT_BESIGNER, {\n orgSlug,\n host,\n documentSegment: FORMS_DOCUMENT_SEGMENT,\n docId: form.$id,\n versionId,\n })\n : formHref(form.$id),\n },\n /*\n * Retire / Restore (AGL-2671). Last in the menu, and the only item\n * here that writes.\n *\n * The label says what it DOES to the form rather than what it does\n * to this list — \"Archive\" reads like a filing action, and the\n * consequence a merchant needs to weigh is that the form stops\n * collecting. `/api/forms/submit` refuses a retired form, so one\n * still placed on a published page will turn visitors away.\n */\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('form', {\n id: form.$id,\n name: form.displayName ?? '',\n }),\n },\n isFormArchived(form)\n ? {\n key: 'restore',\n label: 'Restore — start collecting again',\n icon: <MdiIcon path={mdiArchiveArrowUpOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, false),\n }\n : {\n key: 'retire',\n label: 'Retire — stop collecting, keep the history',\n icon: <MdiIcon path={mdiArchiveArrowDownOutline.path} size={0.8} />,\n onClick: () => void setRetired(form.$id, true),\n },\n ]}\n />\n )\n }),\n ]\n\n return (\n <>\n {duplicate.dialog}\n {/*\n The readout leads the create button, in the PAGE header — where Sites,\n screens, layouts, components and templates put theirs. Forms declares\n no sections, so it has no vertical rail for a card-header cluster to\n belong beside, and the controls are about the whole surface rather\n than about anything the card is showing.\n Published from the card because the card owns what they say: the count\n comes from the listener already open here, and a page that counted for\n itself would be a second source for one fact and a second read for one\n collection. It publishes from the LIST, so a form's own detail route —\n a different component — leaves the header with nothing to create into.\n */}\n <PageHeaderActions>\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'center' }}>\n {/*\n The denominator is `formsPerHost` — the allowance\n `/api/hosts/resources` refuses the create at, resolved through the\n same `checkQuota` the route calls, so the number offered and the\n number enforced cannot drift apart. `FORMS_MAX_PER_HOST` bounds a\n READ of the catalog and sits above every plan's allowance, so\n quoting it advertises room the server will not honor.\n */}\n <QuotaReadoutComponent\n ready={org != null}\n used={formsUsed}\n limit={Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit}\n noun=\"form\"\n />\n {/*\n A retired form keeps its slot, and the readout says so rather than\n leaving a merchant to work out why a catalog of three is using five\n (AGL-2674). The count comes from the same aggregate\n `/api/hosts/resources` enforces on, which subtracts deleted forms\n and nothing else — so retirement is deliberately not a way to buy\n room back, and the number here is the number the server will\n refuse a create at.\n\n Shown only when the site has retired a form (an aggregate — the\n list reads no retired row unless asked): the line answers a\n question nobody is asking on a site that has never retired\n anything.\n */}\n {retiredCount ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Retired forms keep their slot'}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n {/*\n Other ways to start a form, from plugins (AGL-3043): the\n `hostForms` zone this plugin declares, drawn through the shell's\n own gated slot. A plugin page cannot mount that slot itself, so\n the shell hands it down, and a widget here passes the gates a\n console page's would.\n */}\n {CreateZone ? (\n <CreateZone slot={HOST_FORMS_ZONE.id} hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create Form'}\n </Button>\n </Stack>\n </Stack>\n </PageHeaderActions>\n <CardDisplay\n header=\"Forms\"\n help={pluginDocsHelp('forms', {\n anchor: '#find-a-form',\n excerpt:\n 'A form collects submissions into the Inbox, and its design is ' +\n 'drawn in the besigner like any other artifact.',\n })}\n HeaderProps={\n transfer?.can('export', { resource: FORM_SUBMISSIONS_TRANSFER_KEY, scope: 'host', hostId })\n ? {\n action: (\n <Button\n size=\"small\"\n onClick={() =>\n transfer?.openExport({\n resource: FORM_SUBMISSIONS_TRANSFER_KEY,\n scope: 'host',\n hostId,\n })\n }\n >\n {'Export submissions'}\n </Button>\n ),\n }\n : undefined\n }\n >\n {retireError ? (\n <Alert severity=\"error\" sx={{ mb: 2 }}>\n {retireError}\n </Alert>\n ) : null}\n <ListFilterChips\n fields={FORM_LIST_FILTER_FIELDS}\n headers={FORM_LIST_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={formFilterOptions}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: FORM_LIST_FILTER_FIELDS,\n headers: FORM_LIST_FILTER_HEADERS,\n options: formFilterOptions,\n })}\n notices={[...plan.notices, ...columnSort.notices]}\n />\n <ListTable\n columnSort={columnSort}\n rowHeight={TABLE_ROW_HEIGHT}\n columns={listFilterGridColumns(\n columns,\n FORM_LIST_FILTER_FIELDS,\n formFilterOptions,\n FORM_LIST_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: FORM_HIDDEN_COLUMNS } }}\n /*\n * The grid must NOT also filter. The query answers the panel and\n * the search, so a second pass in the browser could only drop rows\n * that already matched — it compares what a column DRAWS, and the\n * query compares what the document stores.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The Campaign filter's names are read once the panel is opened.\n onPreferencePanelOpen={(params) => {\n if (params.openedPanelValue === 'filters') setWantsCampaigns(true)\n }}\n {...(filtering\n ? { noRowsLabel: 'No forms match these filters' }\n : {\n noRowsLabel: 'No forms yet',\n noRowsDescription:\n 'A form collects submissions, dedupes the people who send them, and can route them to a lead. Its design is drawn in the besigner and published like any other artifact.',\n noRowsAction: (\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n {/* Other ways to start a form, the header's zone again. */}\n {CreateZone ? (\n <CreateZone slot={HOST_FORMS_ZONE.id} hostId={hostId} orgId={org?.$id} />\n ) : null}\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create your first form'}\n </Button>\n </Stack>\n ),\n })}\n rows={columnSort.rows}\n // The whole row opens the detail page; the action cluster stops\n // propagation so a menu click never navigates underneath it.\n onOpen={(id) => router.push(formHref(String(id)))}\n // An empty table while the read is in flight reads as \"you have none\"\n // rather than \"these are on their way\".\n loading={status === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={forms.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. The empty state and the header open the\n SAME one, so the state that says \"creating…\" is one state.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new form\"\n // A form document stores no description: `/api/hosts/resources` filters\n // `data` through a per-kind allow-list, so one typed here is dropped\n // without a word.\n includeDescription={false}\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n </>\n )\n}\nHostFormsCard.displayName = 'HostFormsCard'\n\nexport default HostFormsCard\n"],"names":["Aglyn","buildRoute","isFormArchived","PageHeaderActions","pluginDocsHelp","Route","useTransferLauncher","useConsoleWidgetSlot","ICON_VARIANT_SHOW_DETAIL","mdiArchiveArrowDownOutline","mdiArchiveArrowUpOutline","mdiContentCopy","mdiEyeOutline","mdiVectorSquare","AppLink","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","ListRowActions","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","QuotaReadoutComponent","CreateArtifactDrawer","Timestamp","Alert","Button","Stack","Typography","collection","doc","getCountFromServer","query","updateDoc","where","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useSiteContainerOptions","useHostResourceApi","useLiveArtifactCount","useListQuery","useListColumnSort","useRouter","useCallback","useEffect","useMemo","useState","BUNDLE_ID","FORMS_DOCUMENT_SEGMENT","FORM_IN_CAMPAIGN_OPTIONS","FORM_LEAD_ROUTING_OPTIONS","FORM_LIST_FILTER_FIELDS","FORM_LIST_FILTER_HEADERS","FORM_LIST_QUERY","FORM_LIST_SELECT_FIELDS","FORM_STATUS_OPTIONS","formListRequest","FORM_SUBMISSIONS_TRANSFER_KEY","HOST_FORMS_ZONE","FORM_PAGE_SORTS","status","row","FORM_PAGE_SORT_HEADERS","FORM_VISIBLE_FILTER_COLUMNS","FORM_HIDDEN_COLUMNS","HostFormsCard","props","hostId","basePath","org","router","transfer","duplicate","onDuplicated","_kind","copy","push","formHref","id","orgSlug","subdomain","host","firestore","createHostResource","CreateZone","formId","FORM_DETAILS","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","retireError","setRetireError","retiredEpoch","setRetiredEpoch","setRetired","retired","archivedAt","Date","now","updatedAt","epoch","formClauses","setFormClauses","gridFilter","clauses","onChange","selectFields","filtering","length","searchWords","formsCollection","askedSort","setAskedSort","rows","forms","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","deps","idField","columnSort","sorts","sort","onSortChange","orderBy","pageSorts","headers","wantsCampaigns","setWantsCampaigns","siteCampaigns","enabled","some","clause","field","formFilterOptions","named","options","map","option","value","label","unnamed","filter","flatMap","split","trim","leadRouting","campaignIds","inCampaign","liveFormCount","formsUsed","retiredCount","setRetiredCount","active","then","snapshot","data","count","catch","handleCreate","values","createResourceUid","resource","displayName","slug","normalizeFormSlug","fields","rootId","CANVAS_ROOT_ELEMENT_ID","nodes","$id","componentId","formRoot","pluginId","parentId","formName","error","console","Error","message","columns","headerName","minWidth","type","renderCell","href","flex","valueFormatter","align","headerAlign","valueGetter","_value","stats","submissions","toLocaleString","leads","lastSubmissionAtMs","toDate","String","routing","lead","readContainerIds","form","versionId","quick","icon","path","to","PLUGIN_DOCUMENT_PREVIEW","documentSegment","docId","unavailableReason","items","key","size","PLUGIN_DOCUMENT_BESIGNER","onClick","name","dialog","direction","spacing","sx","alignItems","ready","used","limit","checkQuota","noun","variant","color","slot","orgId","disabled","header","help","anchor","excerpt","HeaderProps","can","scope","action","openExport","undefined","severity","mb","setClauses","refused","notices","rowHeight","initialState","columnVisibilityModel","filterMode","filterModel","onFilterModelChange","quickFilter","onPreferencePanelOpen","params","openedPanelValue","noRowsLabel","noRowsDescription","noRowsAction","xs","sm","onOpen","loading","hideFooter","rowCount","onPageChange","onPageSizeChange","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"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"}
|