@aglyn/plugins-forms 1.0.0-beta.199 → 1.0.0-beta.201
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 +13 -12
- package/src/lib/components/form-detail-card.js +24 -6
- package/src/lib/components/form-detail-card.js.map +1 -1
- package/src/lib/components/form-list-query.d.ts +53 -0
- package/src/lib/components/form-list-query.js +242 -0
- package/src/lib/components/form-list-query.js.map +1 -0
- package/src/lib/components/form-metrics-card.component.d.ts +5 -4
- package/src/lib/components/form-metrics-card.component.js +7 -6
- package/src/lib/components/form-metrics-card.component.js.map +1 -1
- package/src/lib/components/host-forms-card.component.d.ts +7 -11
- package/src/lib/components/host-forms-card.component.js +187 -213
- package/src/lib/components/host-forms-card.component.js.map +1 -1
- package/src/lib/components/use-form-stats-recount-api.d.ts +38 -0
- package/src/lib/components/use-form-stats-recount-api.js +49 -0
- package/src/lib/components/use-form-stats-recount-api.js.map +1 -0
- package/src/lib/declarations.server.d.ts +41 -0
- package/src/lib/declarations.server.js +89 -0
- package/src/lib/declarations.server.js.map +1 -0
- package/src/lib/server/form-stats.d.ts +85 -0
- package/src/lib/server/form-stats.js +111 -0
- package/src/lib/server/form-stats.js.map +1 -0
- package/src/lib/server.d.ts +42 -0
- package/src/lib/server.js +122 -0
- package/src/lib/server.js.map +1 -0
|
@@ -17,120 +17,43 @@
|
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
19
|
import * as Aglyn from "@aglyn/aglyn";
|
|
20
|
-
import { buildRoute, isFormArchived, PageHeaderActions, pluginDocsHelp,
|
|
20
|
+
import { buildRoute, isFormArchived, PageHeaderActions, pluginDocsHelp, Route } from "@aglyn/aglyn";
|
|
21
21
|
import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
|
|
22
22
|
import { ICON_VARIANT_SHOW_DETAIL } from "@aglyn/shared-data-enums";
|
|
23
23
|
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiContentCopy, mdiEyeOutline, mdiVectorSquare } from "@aglyn/shared-data-mdi";
|
|
24
24
|
import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
25
25
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
26
26
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
27
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
27
28
|
import ListTable, { ListRowActions, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
28
|
-
import {
|
|
29
|
+
import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
30
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
29
31
|
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
30
|
-
import {
|
|
32
|
+
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
31
33
|
import QuotaReadoutComponent from "@aglyn/shared-ui-jsx/components/quota-readout.component";
|
|
32
34
|
import { CreateArtifactDrawer } from "@aglyn/shared-ui-jsx-forms";
|
|
35
|
+
import { Timestamp } from "@aglyn/shared-util-timestamp";
|
|
33
36
|
import { Alert, Button, Stack, Typography } from "@mui/material";
|
|
34
|
-
import { collection, doc, updateDoc } from "firebase/firestore";
|
|
35
|
-
import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostCampaigns, useHostResourceApi, useLiveArtifactCount
|
|
36
|
-
import {
|
|
37
|
+
import { collection, doc, getCountFromServer, query, updateDoc, where } from "firebase/firestore";
|
|
38
|
+
import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostCampaigns, useHostResourceApi, useLiveArtifactCount } from "@aglyn/tenant-feature-instance";
|
|
39
|
+
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
37
40
|
import { useRouter } from "next/navigation";
|
|
38
|
-
import { useCallback, useMemo, useState } from "react";
|
|
41
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
39
42
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
* listener over the document id (`collectionPage`), so a filter matches over
|
|
47
|
-
* what the window has read, which the first filter widens
|
|
48
|
-
* (`usePagedRowsFilter`) and the caption under the chips owns up to. The
|
|
49
|
-
* query never changes shape, so no filter here needs an index. The figures
|
|
50
|
-
* are the `stats` counters `/api/forms/submit` keeps on the form document
|
|
51
|
-
* itself, read with the row.
|
|
52
|
-
*
|
|
53
|
-
* - Submissions and Leads are numbers. Leads is ABSENT on a form that does
|
|
54
|
-
* not route to leads — the column's dash — so `is empty` finds those and
|
|
55
|
-
* `= 0` finds a routed form that has produced none.
|
|
56
|
-
* - Last submission, Updated and Created are days. Created is a column
|
|
57
|
-
* hidden by default: `/api/hosts/resources` stamps it on every form it
|
|
58
|
-
* makes.
|
|
59
|
-
* - Status (active or retired), Lead routing (`routing.lead`) and Campaign
|
|
60
|
-
* (`campaignIds`) are hidden columns, matched on keys derived by
|
|
61
|
-
* {@link formFilterRow} so the row keeps the stored fields its menu reads.
|
|
62
|
-
* A form's campaigns are ids, matched member by member and byte for byte,
|
|
63
|
-
* as the campaigns list matches its `listIds`.
|
|
64
|
-
*
|
|
65
|
-
* Nothing else a form stores is a filter. It keeps no type and no template,
|
|
66
|
-
* and its plan slot is not its own: a retired form keeps one, which is the
|
|
67
|
-
* Status filter's answer.
|
|
68
|
-
*/ export const FORM_FILTER_FIELDS = [
|
|
69
|
-
inMemoryListField('displayName', 'text'),
|
|
70
|
-
inMemoryListField('slug', 'text'),
|
|
71
|
-
inMemoryListField('submissions', 'number', 'stats.submissions'),
|
|
72
|
-
inMemoryListField('leads', 'number', 'stats.leads'),
|
|
73
|
-
inMemoryListField('lastSubmission', 'date', 'stats.lastSubmissionAtMs'),
|
|
74
|
-
inMemoryListField('updatedAt', 'date'),
|
|
75
|
-
inMemoryListField('createdAt', 'date'),
|
|
76
|
-
inMemoryListField('status', 'select', 'statusKey'),
|
|
77
|
-
inMemoryListField('leadRouting', 'select', 'leadRoutingKey'),
|
|
78
|
-
_extends({}, inMemoryListField('campaignIds', 'select', 'campaignKeys'), {
|
|
79
|
-
tokensPath: 'campaignKeys',
|
|
80
|
-
verbatimTokens: true
|
|
81
|
-
})
|
|
82
|
-
];
|
|
83
|
-
export const FORM_FILTER_HEADERS = {
|
|
84
|
-
displayName: 'Display name',
|
|
85
|
-
slug: 'Slug',
|
|
86
|
-
submissions: 'Submissions',
|
|
87
|
-
leads: 'Leads',
|
|
88
|
-
lastSubmission: 'Last submission',
|
|
89
|
-
updatedAt: 'Updated',
|
|
90
|
-
createdAt: 'Created',
|
|
91
|
-
status: 'Status',
|
|
92
|
-
leadRouting: 'Lead routing',
|
|
93
|
-
campaignIds: 'Campaign'
|
|
94
|
-
};
|
|
95
|
-
const FORM_STATUS_OPTIONS = [
|
|
96
|
-
{
|
|
97
|
-
value: 'active',
|
|
98
|
-
label: 'Active'
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
value: 'retired',
|
|
102
|
-
label: 'Retired'
|
|
103
|
-
}
|
|
104
|
-
];
|
|
105
|
-
const FORM_LEAD_ROUTING_OPTIONS = [
|
|
106
|
-
{
|
|
107
|
-
value: 'on',
|
|
108
|
-
label: 'On'
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
value: 'off',
|
|
112
|
-
label: 'Off'
|
|
113
|
-
}
|
|
114
|
-
];
|
|
115
|
-
/** What the quick search reads: the name, the slug and the form's id. */ export const FORM_SEARCH_FIELDS = [
|
|
43
|
+
import { 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 } from "./form-list-query.js";
|
|
44
|
+
/**
|
|
45
|
+
* The filterable columns the table shows. The rest of the declaration —
|
|
46
|
+
* Status, Lead routing, Campaign, In a campaign — reaches the Filters panel as hidden
|
|
47
|
+
* columns (`hiddenFilterColumns`).
|
|
48
|
+
*/ const FORM_VISIBLE_FILTER_COLUMNS = [
|
|
116
49
|
'displayName',
|
|
117
50
|
'slug',
|
|
118
|
-
'
|
|
51
|
+
'submissions',
|
|
52
|
+
'leads',
|
|
53
|
+
'lastSubmission',
|
|
54
|
+
'updatedAt'
|
|
119
55
|
];
|
|
120
|
-
const FORM_HIDDEN_COLUMNS =
|
|
121
|
-
createdAt: false,
|
|
122
|
-
status: false,
|
|
123
|
-
leadRouting: false,
|
|
124
|
-
campaignIds: false
|
|
125
|
-
};
|
|
126
|
-
/** A form row with the keys its hidden filters match on. */ export const formFilterRow = (form)=>{
|
|
127
|
-
var _form_routing;
|
|
128
|
-
return _extends({}, form, {
|
|
129
|
-
statusKey: isFormArchived(form) ? 'retired' : 'active',
|
|
130
|
-
leadRoutingKey: (form == null ? void 0 : (_form_routing = form.routing) == null ? void 0 : _form_routing.lead) === true ? 'on' : 'off',
|
|
131
|
-
campaignKeys: readCampaignIds(form)
|
|
132
|
-
});
|
|
133
|
-
};
|
|
56
|
+
const FORM_HIDDEN_COLUMNS = hiddenFilterVisibility(FORM_LIST_FILTER_FIELDS, FORM_VISIBLE_FILTER_COLUMNS);
|
|
134
57
|
/**
|
|
135
58
|
* THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.
|
|
136
59
|
*
|
|
@@ -153,11 +76,13 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
153
76
|
* ## The two numeric columns, and when one is honestly blank
|
|
154
77
|
*
|
|
155
78
|
* `stats.submissions` and `stats.leads` are both incremented by
|
|
156
|
-
* `/api/forms/submit` on a write that was happening anyway,
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
79
|
+
* `/api/forms/submit` on a write that was happening anyway, and recounted
|
|
80
|
+
* from their rows after a removal (AGL-3330), so both are real numbers.
|
|
81
|
+
* `leads` is counted only for a form whose `routing.lead` is set — it holds
|
|
82
|
+
* `0` there until the first lead — so a form that has never routed to leads
|
|
83
|
+
* carries no figure at all and renders as a dash rather than as `0`. A zero
|
|
84
|
+
* would say this form has produced no leads, which for an unrouted form is a
|
|
85
|
+
* measurement nobody took.
|
|
161
86
|
*/ export function HostFormsCard(props) {
|
|
162
87
|
const { hostId, basePath, org } = props;
|
|
163
88
|
const router = useRouter();
|
|
@@ -193,6 +118,7 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
193
118
|
const [createError, setCreateError] = useState(null);
|
|
194
119
|
const [creating, setCreating] = useState(false);
|
|
195
120
|
const [retireError, setRetireError] = useState(null);
|
|
121
|
+
/** Bumped by each retire or restore, so the retired count is asked again. */ const [retiredEpoch, setRetiredEpoch] = useState(0);
|
|
196
122
|
/**
|
|
197
123
|
* Retire a form, or put it back (AGL-2671).
|
|
198
124
|
*
|
|
@@ -213,8 +139,15 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
213
139
|
setRetireError(null);
|
|
214
140
|
try {
|
|
215
141
|
await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {
|
|
216
|
-
archivedAt: retired ? Date.now() : null
|
|
142
|
+
archivedAt: retired ? Date.now() : null,
|
|
143
|
+
// The mirror the list's query asks (AGL-3330).
|
|
144
|
+
retired,
|
|
145
|
+
// Retiring and restoring are edits, and Updated says when the form
|
|
146
|
+
// was last edited (AGL-3330): a write that skipped it left a form
|
|
147
|
+
// retired today reading as untouched for weeks.
|
|
148
|
+
updatedAt: Timestamp.now()
|
|
217
149
|
});
|
|
150
|
+
setRetiredEpoch((epoch)=>epoch + 1);
|
|
218
151
|
} catch (unused) {
|
|
219
152
|
setRetireError(retired ? 'Could not retire that form. Try again shortly.' : 'Could not restore that form. Try again shortly.');
|
|
220
153
|
}
|
|
@@ -222,116 +155,110 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
222
155
|
firestore,
|
|
223
156
|
hostId
|
|
224
157
|
]);
|
|
225
|
-
|
|
226
|
-
*
|
|
158
|
+
/*
|
|
159
|
+
* THE LIST IS ITS QUERY (AGL-3330, on the AGL-3321 list query plan).
|
|
160
|
+
*
|
|
161
|
+
* The grid's Filters panel and search box edit the clauses and the words;
|
|
162
|
+
* `useListQuery` puts every one of them on ONE Firestore query and pages
|
|
163
|
+
* that query, so a page is a page of matches and a form whose only match
|
|
164
|
+
* is past the first page is still found. What one query cannot hold — a
|
|
165
|
+
* second range, a second array clause — is refused by name above the list
|
|
166
|
+
* (`ListQueryNotices`) rather than matched over the rows read. See
|
|
167
|
+
* `form-list-query.ts` for every field and the indexes they cost.
|
|
227
168
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* documents that HAVE the field, and the resources route stores an
|
|
231
|
-
* allow-list it never checks for presence, so ordering on a name would hide
|
|
232
|
-
* every form created without one rather than mis-sorting the list.
|
|
169
|
+
* Unfiltered it reads in document order, as it always has: `orderBy` on a
|
|
170
|
+
* name would drop every form stored without one rather than mis-sort it.
|
|
233
171
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
|
|
172
|
+
* A retired form is a TOMBSTONE, not a row, unless the reader asks for
|
|
173
|
+
* Status (AGL-2671). The query leaves them out (`retired == false`), so a
|
|
174
|
+
* page is a page of forms in use; a Status clause replaces that with the
|
|
175
|
+
* reader's own choice. Retiring a form removes it from the list, and
|
|
176
|
+
* without the Status filter that would be a one-way door: the row is the
|
|
177
|
+
* only route back to Restore.
|
|
178
|
+
*/ const [formClauses, setFormClauses] = useState([]);
|
|
179
|
+
const gridFilter = useListGridFilter({
|
|
180
|
+
clauses: formClauses,
|
|
181
|
+
onChange: setFormClauses,
|
|
182
|
+
selectFields: FORM_LIST_SELECT_FIELDS
|
|
183
|
+
});
|
|
184
|
+
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0;
|
|
185
|
+
const formsCollection = useMemo(()=>collection(firestore, 'hosts', hostId, 'forms'), [
|
|
237
186
|
firestore,
|
|
238
187
|
hostId
|
|
239
|
-
]
|
|
188
|
+
]);
|
|
189
|
+
const { status, rows: forms, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
|
|
190
|
+
collection: formsCollection,
|
|
191
|
+
declaration: FORM_LIST_QUERY,
|
|
192
|
+
request: formListRequest(gridFilter.clauses, gridFilter.searchWords),
|
|
193
|
+
deps: [
|
|
194
|
+
firestore,
|
|
195
|
+
hostId
|
|
196
|
+
],
|
|
240
197
|
idField: '$id'
|
|
241
198
|
});
|
|
242
199
|
/*
|
|
243
|
-
* A retired form is a TOMBSTONE, not a row — unless the reader asks for the
|
|
244
|
-
* tombstones (AGL-2671). Client-side because it has to be: Firestore cannot
|
|
245
|
-
* ask for the ABSENCE of a field, so a form created through the resources
|
|
246
|
-
* route (which carries no `archivedAt` at all) and one archived later are
|
|
247
|
-
* not one value to filter on. The cost is that a tombstone spends a slot in
|
|
248
|
-
* whichever page it falls in.
|
|
249
|
-
*
|
|
250
|
-
* The Filters panel's Status is how the reader asks (AGL-3317). With no
|
|
251
|
-
* Status clause the list leaves the tombstones out, as it always has, and
|
|
252
|
-
* reads one page; a Status clause hands the panel every row the window
|
|
253
|
-
* read, retired included, and the clause itself picks which to show. That
|
|
254
|
-
* default is the absence of a clause rather than a clause the list starts
|
|
255
|
-
* with, because a starting clause would widen the window on every visit to
|
|
256
|
-
* answer a question nobody asked. Retiring a form removes it from the list,
|
|
257
|
-
* and without the Status filter that would be a one-way door: the row is
|
|
258
|
-
* the only route back to Restore.
|
|
259
|
-
*/ const [formClauses, setFormClauses] = useState([]);
|
|
260
|
-
const askedStatus = formClauses.some((clause)=>clause.field === 'status');
|
|
261
|
-
const formRows = useMemo(()=>formWindow.filter((form)=>askedStatus || !isFormArchived(form)).map(formFilterRow), [
|
|
262
|
-
formWindow,
|
|
263
|
-
askedStatus
|
|
264
|
-
]);
|
|
265
|
-
const formWindowRows = useMemo(()=>formData == null ? void 0 : formData.filter((form)=>askedStatus || !isFormArchived(form)).map(formFilterRow), [
|
|
266
|
-
formData,
|
|
267
|
-
askedStatus
|
|
268
|
-
]);
|
|
269
|
-
/*
|
|
270
200
|
* The Campaign filter's choices: the campaigns placed on this site, by
|
|
271
|
-
* name, and any campaign a
|
|
272
|
-
*
|
|
273
|
-
* filed under it can still be found.
|
|
201
|
+
* name, and any campaign a clause names that the list does not, by its id,
|
|
202
|
+
* so the chip can still be read and cleared.
|
|
274
203
|
*
|
|
275
|
-
* Read
|
|
276
|
-
* the org lookup and up to fifty campaign documents, which a
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
], [
|
|
282
|
-
formData
|
|
283
|
-
]);
|
|
204
|
+
* Read once the reader opens the Filters panel, not on every visit: the
|
|
205
|
+
* names cost the org lookup and up to fifty campaign documents, which a
|
|
206
|
+
* reader who came to open one form would pay for a filter they never
|
|
207
|
+
* touched. Until they land the column has no choices, and the panel lists
|
|
208
|
+
* it a moment later.
|
|
209
|
+
*/ const [wantsCampaigns, setWantsCampaigns] = useState(false);
|
|
284
210
|
const siteCampaigns = useHostCampaigns(hostId, {
|
|
285
|
-
enabled:
|
|
211
|
+
enabled: wantsCampaigns || formClauses.some((clause)=>clause.field === 'campaignIds')
|
|
286
212
|
});
|
|
287
213
|
const formFilterOptions = useMemo(()=>{
|
|
288
214
|
const named = siteCampaigns.options.map((option)=>({
|
|
289
215
|
value: option.value,
|
|
290
216
|
label: option.label
|
|
291
217
|
}));
|
|
292
|
-
const unnamed =
|
|
293
|
-
value
|
|
294
|
-
label:
|
|
295
|
-
}))
|
|
218
|
+
const unnamed = formClauses.filter((clause)=>clause.field === 'campaignIds').flatMap((clause)=>clause.value.split(',')).map((value)=>value.trim()).filter((value)=>value && !named.some((option)=>option.value === value)).map((value)=>({
|
|
219
|
+
value,
|
|
220
|
+
label: value
|
|
221
|
+
}));
|
|
296
222
|
return {
|
|
297
223
|
status: FORM_STATUS_OPTIONS,
|
|
298
224
|
leadRouting: FORM_LEAD_ROUTING_OPTIONS,
|
|
299
225
|
campaignIds: [
|
|
300
226
|
...named,
|
|
301
227
|
...unnamed
|
|
302
|
-
]
|
|
228
|
+
],
|
|
229
|
+
inCampaign: FORM_IN_CAMPAIGN_OPTIONS
|
|
303
230
|
};
|
|
304
231
|
}, [
|
|
305
232
|
siteCampaigns.options,
|
|
306
|
-
|
|
307
|
-
filedCampaignIds
|
|
233
|
+
formClauses
|
|
308
234
|
]);
|
|
309
|
-
const formFilter = usePagedRowsFilter({
|
|
310
|
-
data: formWindowRows,
|
|
311
|
-
rows: formRows,
|
|
312
|
-
hasMore,
|
|
313
|
-
page,
|
|
314
|
-
setPage,
|
|
315
|
-
pageSize,
|
|
316
|
-
setPageSize
|
|
317
|
-
}, {
|
|
318
|
-
fields: FORM_FILTER_FIELDS,
|
|
319
|
-
options: formFilterOptions,
|
|
320
|
-
headers: FORM_FILTER_HEADERS,
|
|
321
|
-
search: FORM_SEARCH_FIELDS,
|
|
322
|
-
clauses: formClauses,
|
|
323
|
-
onChange: setFormClauses
|
|
324
|
-
});
|
|
325
|
-
// The rows on screen: the page, or the page of the matches.
|
|
326
|
-
const forms = formFilter.rows;
|
|
327
235
|
/*
|
|
328
236
|
* The COUNT is a server aggregate, not the length of a page. `forms` is one
|
|
329
237
|
* page, and publishing that as the site's form count would read as room to
|
|
330
238
|
* spare on a site that is already at the ceiling.
|
|
331
239
|
*/ const liveFormCount = useLiveArtifactCount(hostId, 'forms');
|
|
332
|
-
// Pending or refused, the page
|
|
333
|
-
//
|
|
334
|
-
const formsUsed = liveFormCount != null ? liveFormCount :
|
|
240
|
+
// Pending or refused, the page stands in: a LOWER bound, never a confident
|
|
241
|
+
// zero.
|
|
242
|
+
const formsUsed = liveFormCount != null ? liveFormCount : forms.length;
|
|
243
|
+
/*
|
|
244
|
+
* How many forms are retired, for the "keep their slot" line (AGL-2674).
|
|
245
|
+
* An aggregate, because the list no longer reads a retired row unless it is
|
|
246
|
+
* asked for one; asked again after this card retires or restores a form.
|
|
247
|
+
*/ const [retiredCount, setRetiredCount] = useState(null);
|
|
248
|
+
useEffect(()=>{
|
|
249
|
+
let active = true;
|
|
250
|
+
getCountFromServer(query(formsCollection, where('retired', '==', true))).then((snapshot)=>{
|
|
251
|
+
if (active) setRetiredCount(snapshot.data().count);
|
|
252
|
+
}).catch(()=>{
|
|
253
|
+
// The line is a courtesy; without the count it is simply not shown.
|
|
254
|
+
});
|
|
255
|
+
return ()=>{
|
|
256
|
+
active = false;
|
|
257
|
+
};
|
|
258
|
+
}, [
|
|
259
|
+
formsCollection,
|
|
260
|
+
retiredEpoch
|
|
261
|
+
]);
|
|
335
262
|
/**
|
|
336
263
|
* Name first, then create (AGL-700).
|
|
337
264
|
*
|
|
@@ -437,12 +364,13 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
437
364
|
align: 'right',
|
|
438
365
|
headerAlign: 'right',
|
|
439
366
|
/*
|
|
440
|
-
* A dash rather than a `0` whenever the field
|
|
367
|
+
* A dash rather than a `0` whenever the field holds no number.
|
|
441
368
|
*
|
|
442
|
-
* `stats.leads` is
|
|
443
|
-
*
|
|
444
|
-
* Rendering `0` there would read as "this
|
|
445
|
-
* which is a claim about a measurement
|
|
369
|
+
* `stats.leads` is counted only for a form whose `routing.lead` is set
|
|
370
|
+
* (`0` there until its first lead), so a form that has never routed to
|
|
371
|
+
* leads has no recorded figure. Rendering `0` there would read as "this
|
|
372
|
+
* form has produced no leads", which is a claim about a measurement
|
|
373
|
+
* nobody took.
|
|
446
374
|
*/ valueGetter: (_value, row)=>{
|
|
447
375
|
var _row_stats;
|
|
448
376
|
return typeof (row == null ? void 0 : (_row_stats = row.stats) == null ? void 0 : _row_stats.leads) === 'number' ? row.stats.leads : null;
|
|
@@ -482,23 +410,37 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
482
410
|
return (value == null ? void 0 : (_value_toLocaleString = value.toLocaleString) == null ? void 0 : _value_toLocaleString.call(value)) || '--';
|
|
483
411
|
}
|
|
484
412
|
},
|
|
413
|
+
/*
|
|
414
|
+
* Status and Lead routing, hidden until Columns shows them (AGL-3330).
|
|
415
|
+
* Each is a select the Filters panel offers either way, and draws the
|
|
416
|
+
* choice its value names. Status reads the FACT, `archivedAt`, rather
|
|
417
|
+
* than the `retired` mirror the query asks, so a form the backfill has
|
|
418
|
+
* not reached yet still reads true.
|
|
419
|
+
*/ {
|
|
420
|
+
field: 'status',
|
|
421
|
+
headerName: 'Status',
|
|
422
|
+
minWidth: 110,
|
|
423
|
+
valueGetter: (_value, row)=>String(isFormArchived(row))
|
|
424
|
+
},
|
|
485
425
|
{
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
valueGetter: (value)=>{
|
|
493
|
-
var _ref;
|
|
494
|
-
var _value_toDate;
|
|
495
|
-
return (_ref = value == null ? void 0 : (_value_toDate = value.toDate) == null ? void 0 : _value_toDate.call(value)) != null ? _ref : null;
|
|
496
|
-
},
|
|
497
|
-
valueFormatter: (value)=>{
|
|
498
|
-
var _value_toLocaleString;
|
|
499
|
-
return (value == null ? void 0 : (_value_toLocaleString = value.toLocaleString) == null ? void 0 : _value_toLocaleString.call(value)) || '--';
|
|
426
|
+
field: 'leadRouting',
|
|
427
|
+
headerName: 'Lead routing',
|
|
428
|
+
minWidth: 120,
|
|
429
|
+
valueGetter: (_value, row)=>{
|
|
430
|
+
var _row_routing;
|
|
431
|
+
return String((row == null ? void 0 : (_row_routing = row.routing) == null ? void 0 : _row_routing.lead) === true);
|
|
500
432
|
}
|
|
501
433
|
},
|
|
434
|
+
/*
|
|
435
|
+
* In a campaign, hidden like the two above. Reads the ids the form is
|
|
436
|
+
* filed under — the fact `inCampaign` mirrors for the query — so a form
|
|
437
|
+
* the backfill has not reached still draws the truth.
|
|
438
|
+
*/ {
|
|
439
|
+
field: 'inCampaign',
|
|
440
|
+
headerName: 'In a campaign',
|
|
441
|
+
minWidth: 130,
|
|
442
|
+
valueGetter: (_value, row)=>String(Aglyn.readCampaignIds(row).length > 0)
|
|
443
|
+
},
|
|
502
444
|
listActionsColumn((row)=>{
|
|
503
445
|
var _form_displayName;
|
|
504
446
|
const form = _extends({}, row, {
|
|
@@ -613,7 +555,7 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
613
555
|
limit: Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit,
|
|
614
556
|
noun: "form"
|
|
615
557
|
}),
|
|
616
|
-
|
|
558
|
+
retiredCount ? /*#__PURE__*/ _jsx(Typography, {
|
|
617
559
|
variant: "caption",
|
|
618
560
|
color: "text.secondary",
|
|
619
561
|
children: 'Retired forms keep their slot'
|
|
@@ -656,21 +598,43 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
656
598
|
},
|
|
657
599
|
children: retireError
|
|
658
600
|
}) : null,
|
|
659
|
-
/*#__PURE__*/ _jsx(ListFilterChips,
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
601
|
+
/*#__PURE__*/ _jsx(ListFilterChips, {
|
|
602
|
+
fields: FORM_LIST_FILTER_FIELDS,
|
|
603
|
+
headers: FORM_LIST_FILTER_HEADERS,
|
|
604
|
+
clauses: gridFilter.clauses,
|
|
605
|
+
onChange: gridFilter.setClauses,
|
|
606
|
+
options: formFilterOptions
|
|
607
|
+
}),
|
|
608
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
609
|
+
refused: listQueryRefusals(plan.refused, {
|
|
610
|
+
fields: FORM_LIST_FILTER_FIELDS,
|
|
611
|
+
headers: FORM_LIST_FILTER_HEADERS,
|
|
612
|
+
options: formFilterOptions
|
|
613
|
+
}),
|
|
614
|
+
notices: plan.notices
|
|
615
|
+
}),
|
|
665
616
|
/*#__PURE__*/ _jsx(ListTable, _extends({
|
|
666
617
|
rowHeight: TABLE_ROW_HEIGHT,
|
|
667
|
-
columns:
|
|
618
|
+
columns: listFilterGridColumns(columns, FORM_LIST_FILTER_FIELDS, formFilterOptions, FORM_LIST_FILTER_HEADERS),
|
|
668
619
|
initialState: {
|
|
669
620
|
columns: {
|
|
670
621
|
columnVisibilityModel: FORM_HIDDEN_COLUMNS
|
|
671
622
|
}
|
|
623
|
+
},
|
|
624
|
+
/*
|
|
625
|
+
* The grid must NOT also filter. The query answers the panel and
|
|
626
|
+
* the search, so a second pass in the browser could only drop rows
|
|
627
|
+
* that already matched — it compares what a column DRAWS, and the
|
|
628
|
+
* query compares what the document stores.
|
|
629
|
+
*/ filterMode: "server",
|
|
630
|
+
filterModel: gridFilter.filterModel,
|
|
631
|
+
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
632
|
+
quickFilter: true,
|
|
633
|
+
// The Campaign filter's names are read once the panel is opened.
|
|
634
|
+
onPreferencePanelOpen: (params)=>{
|
|
635
|
+
if (params.openedPanelValue === 'filters') setWantsCampaigns(true);
|
|
672
636
|
}
|
|
673
|
-
},
|
|
637
|
+
}, filtering ? {
|
|
674
638
|
noRowsLabel: 'No forms match these filters'
|
|
675
639
|
} : {
|
|
676
640
|
noRowsLabel: 'No forms yet',
|
|
@@ -689,9 +653,19 @@ const FORM_HIDDEN_COLUMNS = {
|
|
|
689
653
|
// rather than "these are on their way".
|
|
690
654
|
loading: status === 'loading',
|
|
691
655
|
// Paged by the footer below, so the grid must not also slice.
|
|
692
|
-
hideFooter: true
|
|
656
|
+
hideFooter: true,
|
|
657
|
+
// The rows keep the query's order; a header sort would order only
|
|
658
|
+
// the page on screen and read as the whole list's.
|
|
659
|
+
disableColumnSorting: true
|
|
693
660
|
})),
|
|
694
|
-
/*#__PURE__*/ _jsx(ListPagination,
|
|
661
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
662
|
+
page: page,
|
|
663
|
+
pageSize: pageSize,
|
|
664
|
+
rowCount: forms.length,
|
|
665
|
+
hasMore: hasMore,
|
|
666
|
+
onPageChange: setPage,
|
|
667
|
+
onPageSizeChange: setPageSize
|
|
668
|
+
}),
|
|
695
669
|
/*#__PURE__*/ _jsx(CreateArtifactDrawer, {
|
|
696
670
|
open: createOpen,
|
|
697
671
|
onClose: ()=>setCreateOpen(false),
|