@aglyn/plugins-forms 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +35 -0
- package/package.json +52 -0
- package/src/index.d.ts +18 -0
- package/src/index.js +19 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/form-design-preview.component.d.ts +68 -0
- package/src/lib/components/form-design-preview.component.js +238 -0
- package/src/lib/components/form-design-preview.component.js.map +1 -0
- package/src/lib/components/form-detail-card.d.ts +53 -0
- package/src/lib/components/form-detail-card.js +827 -0
- package/src/lib/components/form-detail-card.js.map +1 -0
- package/src/lib/components/form-metrics-card.component.d.ts +67 -0
- package/src/lib/components/form-metrics-card.component.js +298 -0
- package/src/lib/components/form-metrics-card.component.js.map +1 -0
- package/src/lib/components/form-submissions-card.component.d.ts +40 -0
- package/src/lib/components/form-submissions-card.component.js +104 -0
- package/src/lib/components/form-submissions-card.component.js.map +1 -0
- package/src/lib/components/form-zones.d.ts +52 -0
- package/src/lib/components/form-zones.js +20 -0
- package/src/lib/components/form-zones.js.map +1 -0
- package/src/lib/components/form.d.ts +186 -0
- package/src/lib/components/form.js +1014 -0
- package/src/lib/components/form.js.map +1 -0
- package/src/lib/components/forms-console-page.d.ts +22 -0
- package/src/lib/components/forms-console-page.js +55 -0
- package/src/lib/components/forms-console-page.js.map +1 -0
- package/src/lib/components/host-forms-card.component.d.ts +56 -0
- package/src/lib/components/host-forms-card.component.js +533 -0
- package/src/lib/components/host-forms-card.component.js.map +1 -0
- package/src/lib/components/use-form-promote-api.d.ts +48 -0
- package/src/lib/components/use-form-promote-api.js +63 -0
- package/src/lib/components/use-form-promote-api.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +31 -0
- package/src/lib/constants/bundle-common.js +31 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/plugin.d.ts +25 -0
- package/src/lib/plugin.js +76 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/site.d.ts +53 -0
- package/src/lib/site.js +83 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +25 -0
- package/src/lib/utils/generate-preset-id.js +30 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
import * as Aglyn from "@aglyn/aglyn";
|
|
20
|
+
import { buildRoute, isFormArchived, PageHeaderActions, pluginDocsHelp, Route } from "@aglyn/aglyn";
|
|
21
|
+
import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
|
|
22
|
+
import { ICON_VARIANT_SHOW_DETAIL } from "@aglyn/shared-data-enums";
|
|
23
|
+
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiContentCopy, mdiEyeOutline, mdiVectorSquare } from "@aglyn/shared-data-mdi";
|
|
24
|
+
import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
25
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
26
|
+
import ListTable, { ListRowActions, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
27
|
+
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
28
|
+
import QuotaReadoutComponent from "@aglyn/shared-ui-jsx/components/quota-readout.component";
|
|
29
|
+
import { CreateArtifactDrawer } from "@aglyn/shared-ui-jsx-forms";
|
|
30
|
+
import { Alert, Button, Stack, Typography } from "@mui/material";
|
|
31
|
+
import { collection, doc, updateDoc } from "firebase/firestore";
|
|
32
|
+
import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostResourceApi, useLiveArtifactCount, usePagedCollection } from "@aglyn/tenant-feature-instance";
|
|
33
|
+
import { collectionPage } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
|
|
34
|
+
import { useRouter } from "next/navigation";
|
|
35
|
+
import { useCallback, useState } from "react";
|
|
36
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
37
|
+
/**
|
|
38
|
+
* THE FORM CATALOG, WITH ITS SHAPE VISIBLE BEFORE THERE IS ANYTHING IN IT.
|
|
39
|
+
*
|
|
40
|
+
* The list is the components list's table, deliberately and not
|
|
41
|
+
* approximately: `ListTable` gives it the row grammar every artifact list has
|
|
42
|
+
* — the row opens the detail page, rows are not selectable, one quick action
|
|
43
|
+
* then the overflow — and `ListPagination` gives it the console's one footer.
|
|
44
|
+
* A form is an artifact like a component, so a reader who has used one list
|
|
45
|
+
* has used this one.
|
|
46
|
+
*
|
|
47
|
+
* ## Why the empty state is a table and not a sentence
|
|
48
|
+
*
|
|
49
|
+
* A paragraph and a button reads as a smaller feature than this is: a form is
|
|
50
|
+
* a thing with a slug, a submission count and a version history, and none of
|
|
51
|
+
* that would be visible until the reader had already committed to making one.
|
|
52
|
+
* Rendering the columns with the empty overlay inside them teaches the shape
|
|
53
|
+
* of the artifact BEFORE the first one exists, which is what the components
|
|
54
|
+
* list has always done.
|
|
55
|
+
*
|
|
56
|
+
* ## The two numeric columns, and when one is honestly blank
|
|
57
|
+
*
|
|
58
|
+
* `stats.submissions` and `stats.leads` are both incremented by
|
|
59
|
+
* `/api/forms/submit` on a write that was happening anyway, so both are real
|
|
60
|
+
* numbers. `leads` is counted only for a form whose `routing.lead` is set, so
|
|
61
|
+
* a form that does not route to leads carries no figure at all and renders as
|
|
62
|
+
* a dash rather than as `0`. A zero would say this form has produced no leads,
|
|
63
|
+
* which for an unrouted form is a measurement nobody took.
|
|
64
|
+
*/ export function HostFormsCard(props) {
|
|
65
|
+
const { hostId, basePath, org } = props;
|
|
66
|
+
const router = useRouter();
|
|
67
|
+
// Duplicate (AGL-2936): the copy is a form of its own — its submissions
|
|
68
|
+
// start empty — and opens on its detail page once made.
|
|
69
|
+
const duplicate = useDuplicateResource({
|
|
70
|
+
hostId,
|
|
71
|
+
onDuplicated: (_kind, copy)=>router.push(formHref(copy.id))
|
|
72
|
+
});
|
|
73
|
+
const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId);
|
|
74
|
+
const firestore = useFirestore();
|
|
75
|
+
const createHostResource = useHostResourceApi();
|
|
76
|
+
/**
|
|
77
|
+
* The shell's zone renderer (AGL-3043), for other ways to start a form; `null`
|
|
78
|
+
* outside the console shell, where there is no workspace to gate on.
|
|
79
|
+
*/ const CreateZone = useConsoleWidgetSlot();
|
|
80
|
+
/**
|
|
81
|
+
* One form's page, beneath this surface's own path.
|
|
82
|
+
*
|
|
83
|
+
* `basePath` is the shell's answer and needs no read; the route table is the
|
|
84
|
+
* fallback for a caller that has none, and it is the one that can be `null`
|
|
85
|
+
* for a paint.
|
|
86
|
+
*/ const formHref = useCallback((formId)=>basePath ? `${basePath}/${formId}` : buildRoute(Route.FORM_DETAILS, {
|
|
87
|
+
orgSlug: orgSlug != null ? orgSlug : '',
|
|
88
|
+
host: host != null ? host : '',
|
|
89
|
+
formId
|
|
90
|
+
}), [
|
|
91
|
+
basePath,
|
|
92
|
+
orgSlug,
|
|
93
|
+
host
|
|
94
|
+
]);
|
|
95
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
96
|
+
const [createError, setCreateError] = useState(null);
|
|
97
|
+
const [creating, setCreating] = useState(false);
|
|
98
|
+
const [retireError, setRetireError] = useState(null);
|
|
99
|
+
/**
|
|
100
|
+
* Retire a form, or put it back (AGL-2671).
|
|
101
|
+
*
|
|
102
|
+
* A write of one field, client-side like every other edit on this document:
|
|
103
|
+
* the rules already admit it for a host writer, fencing off only `stats`.
|
|
104
|
+
*
|
|
105
|
+
* ⛔ NOT a delete, and the difference is the whole feature. The submissions,
|
|
106
|
+
* the leads and the contact timeline they built are filed under this form,
|
|
107
|
+
* so the document is what makes that history readable; deleting it to stop
|
|
108
|
+
* collection would throw away the reason the merchant kept the form.
|
|
109
|
+
*
|
|
110
|
+
* `Date.now()` is the browser's clock rather than `serverTimestamp()`
|
|
111
|
+
* because {@link isFormArchived} asks for a number greater than zero, and a
|
|
112
|
+
* server sentinel reads as `null` on the local echo — the row would stay
|
|
113
|
+
* looking live until the write round-tripped. The value is a marker, never
|
|
114
|
+
* an audit fact: nothing computes a duration from it.
|
|
115
|
+
*/ const setRetired = useCallback(async (formId, retired)=>{
|
|
116
|
+
setRetireError(null);
|
|
117
|
+
try {
|
|
118
|
+
await updateDoc(doc(firestore, 'hosts', hostId, 'forms', formId), {
|
|
119
|
+
archivedAt: retired ? Date.now() : null
|
|
120
|
+
});
|
|
121
|
+
} catch (unused) {
|
|
122
|
+
setRetireError(retired ? 'Could not retire that form. Try again shortly.' : 'Could not restore that form. Try again shortly.');
|
|
123
|
+
}
|
|
124
|
+
}, [
|
|
125
|
+
firestore,
|
|
126
|
+
hostId
|
|
127
|
+
]);
|
|
128
|
+
/**
|
|
129
|
+
* The list PAGES, over an ordered walk.
|
|
130
|
+
*
|
|
131
|
+
* `collectionPage` holds the ordering decision and the reason it is the
|
|
132
|
+
* document id rather than `displayName` — briefly, `orderBy` matches only
|
|
133
|
+
* documents that HAVE the field, and the resources route stores an
|
|
134
|
+
* allow-list it never checks for presence, so ordering on a name would hide
|
|
135
|
+
* every form created without one rather than mis-sorting the list.
|
|
136
|
+
*
|
|
137
|
+
* The page is NOT re-sorted: sorting a server-ordered window in the browser
|
|
138
|
+
* is what makes a pseudo-random sample look like the first page.
|
|
139
|
+
*/ const { status, rows: formWindow, hasMore, page, setPage, pageSize, setPageSize } = usePagedCollection((pageLimit)=>collectionPage(collection(firestore, 'hosts', hostId, 'forms'), pageLimit), [
|
|
140
|
+
firestore,
|
|
141
|
+
hostId
|
|
142
|
+
], {
|
|
143
|
+
idField: '$id'
|
|
144
|
+
});
|
|
145
|
+
/*
|
|
146
|
+
* A retired form is a TOMBSTONE, not a row — unless the reader asks for the
|
|
147
|
+
* tombstones (AGL-2671). Client-side because it has to be: Firestore cannot
|
|
148
|
+
* ask for the ABSENCE of a field, so a form created through the resources
|
|
149
|
+
* route (which carries no `archivedAt` at all) and one archived later are
|
|
150
|
+
* not one value to filter on. The cost is that a tombstone spends a slot in
|
|
151
|
+
* whichever page it falls in.
|
|
152
|
+
*
|
|
153
|
+
* The toggle is not decoration. Retiring a form removes it from this list,
|
|
154
|
+
* and a retirement with no way to see what has been retired is a one-way
|
|
155
|
+
* door: the row is the only route back to Restore.
|
|
156
|
+
*/ const [showArchived, setShowArchived] = useState(false);
|
|
157
|
+
const forms = showArchived ? formWindow : formWindow.filter((form)=>!isFormArchived(form));
|
|
158
|
+
/*
|
|
159
|
+
* The COUNT is a server aggregate, not the length of a page. `forms` is one
|
|
160
|
+
* page, and publishing that as the site's form count would read as room to
|
|
161
|
+
* spare on a site that is already at the ceiling.
|
|
162
|
+
*/ const liveFormCount = useLiveArtifactCount(hostId, 'forms');
|
|
163
|
+
// Pending or refused, the page window stands in: a LOWER bound, never a
|
|
164
|
+
// confident zero.
|
|
165
|
+
const formsUsed = liveFormCount != null ? liveFormCount : forms.length;
|
|
166
|
+
/**
|
|
167
|
+
* Name first, then create (AGL-700).
|
|
168
|
+
*
|
|
169
|
+
* A form is created with BOTH halves seeded. `fields` is the declaration the
|
|
170
|
+
* submission path reads and starts empty; the canvas is the design, seeded
|
|
171
|
+
* with a root and a form node already bound to this id — so the besigner
|
|
172
|
+
* opens on something that satisfies `checkFormContract` rather than on a
|
|
173
|
+
* blank page whose first publish is a list of violations.
|
|
174
|
+
*/ const handleCreate = useCallback(async (values)=>{
|
|
175
|
+
if (creating) return;
|
|
176
|
+
setCreating(true);
|
|
177
|
+
setCreateError(null);
|
|
178
|
+
try {
|
|
179
|
+
const formId = Aglyn.createResourceUid();
|
|
180
|
+
await createHostResource({
|
|
181
|
+
hostId,
|
|
182
|
+
resource: 'form',
|
|
183
|
+
id: formId,
|
|
184
|
+
data: {
|
|
185
|
+
displayName: values['displayName'],
|
|
186
|
+
slug: Aglyn.normalizeFormSlug(values['displayName']) || formId,
|
|
187
|
+
fields: [],
|
|
188
|
+
rootId: Aglyn.CANVAS_ROOT_ELEMENT_ID,
|
|
189
|
+
nodes: {
|
|
190
|
+
[Aglyn.CANVAS_ROOT_ELEMENT_ID]: {
|
|
191
|
+
$id: Aglyn.CANVAS_ROOT_ELEMENT_ID,
|
|
192
|
+
componentId: 'div',
|
|
193
|
+
nodes: [
|
|
194
|
+
'formRoot'
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
formRoot: {
|
|
198
|
+
$id: 'formRoot',
|
|
199
|
+
componentId: 'form',
|
|
200
|
+
pluginId: BUNDLE_ID,
|
|
201
|
+
parentId: Aglyn.CANVAS_ROOT_ELEMENT_ID,
|
|
202
|
+
props: {
|
|
203
|
+
formId,
|
|
204
|
+
formName: values['displayName']
|
|
205
|
+
},
|
|
206
|
+
nodes: []
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
setCreateOpen(false);
|
|
212
|
+
router.push(formHref(formId));
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error(error);
|
|
215
|
+
setCreateError('Could not create that form');
|
|
216
|
+
} finally{
|
|
217
|
+
setCreating(false);
|
|
218
|
+
}
|
|
219
|
+
}, [
|
|
220
|
+
creating,
|
|
221
|
+
createHostResource,
|
|
222
|
+
hostId,
|
|
223
|
+
router,
|
|
224
|
+
formHref
|
|
225
|
+
]);
|
|
226
|
+
const columns = [
|
|
227
|
+
{
|
|
228
|
+
field: 'displayName',
|
|
229
|
+
headerName: 'Display name',
|
|
230
|
+
minWidth: 220,
|
|
231
|
+
type: 'string',
|
|
232
|
+
renderCell: ({ id, value })=>/*#__PURE__*/ _jsx(AppLink, {
|
|
233
|
+
href: formHref(id),
|
|
234
|
+
children: value || id
|
|
235
|
+
})
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
field: 'slug',
|
|
239
|
+
headerName: 'Slug',
|
|
240
|
+
minWidth: 160,
|
|
241
|
+
flex: 1,
|
|
242
|
+
type: 'string',
|
|
243
|
+
// Blank reads as a rendering gap; '--' reads as "nothing here".
|
|
244
|
+
valueFormatter: (value)=>value || '--'
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
field: 'submissions',
|
|
248
|
+
headerName: 'Submissions',
|
|
249
|
+
minWidth: 130,
|
|
250
|
+
type: 'number',
|
|
251
|
+
// Head AND body. `type: 'number'` right-aligns both, and the explicit
|
|
252
|
+
// pair says so at the call site rather than relying on a grid default
|
|
253
|
+
// that a later `renderCell` would silently override.
|
|
254
|
+
align: 'right',
|
|
255
|
+
headerAlign: 'right',
|
|
256
|
+
valueGetter: (_value, row)=>{
|
|
257
|
+
var _ref;
|
|
258
|
+
var _row_stats;
|
|
259
|
+
return (_ref = row == null ? void 0 : (_row_stats = row.stats) == null ? void 0 : _row_stats.submissions) != null ? _ref : null;
|
|
260
|
+
},
|
|
261
|
+
valueFormatter: (value)=>typeof value === 'number' ? value.toLocaleString() : '--'
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
field: 'leads',
|
|
265
|
+
headerName: 'Leads',
|
|
266
|
+
minWidth: 100,
|
|
267
|
+
type: 'number',
|
|
268
|
+
align: 'right',
|
|
269
|
+
headerAlign: 'right',
|
|
270
|
+
/*
|
|
271
|
+
* A dash rather than a `0` whenever the field is absent.
|
|
272
|
+
*
|
|
273
|
+
* `stats.leads` is incremented only for a form whose `routing.lead` is
|
|
274
|
+
* set, so a form that does not route to leads has no recorded figure.
|
|
275
|
+
* Rendering `0` there would read as "this form has produced no leads",
|
|
276
|
+
* which is a claim about a measurement nobody took.
|
|
277
|
+
*/ valueGetter: (_value, row)=>{
|
|
278
|
+
var _row_stats;
|
|
279
|
+
return typeof (row == null ? void 0 : (_row_stats = row.stats) == null ? void 0 : _row_stats.leads) === 'number' ? row.stats.leads : null;
|
|
280
|
+
},
|
|
281
|
+
valueFormatter: (value)=>typeof value === 'number' ? value.toLocaleString() : '--'
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
field: 'lastSubmission',
|
|
285
|
+
headerName: 'Last submission',
|
|
286
|
+
minWidth: 170,
|
|
287
|
+
flex: 1,
|
|
288
|
+
type: 'date',
|
|
289
|
+
valueGetter: (_value, row)=>{
|
|
290
|
+
var _row_stats;
|
|
291
|
+
return typeof (row == null ? void 0 : (_row_stats = row.stats) == null ? void 0 : _row_stats.lastSubmissionAtMs) === 'number' ? new Date(row.stats.lastSubmissionAtMs) : null;
|
|
292
|
+
},
|
|
293
|
+
valueFormatter: (value)=>{
|
|
294
|
+
var _value_toLocaleString;
|
|
295
|
+
return (value == null ? void 0 : (_value_toLocaleString = value.toLocaleString) == null ? void 0 : _value_toLocaleString.call(value)) || '--';
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
field: 'updatedAt',
|
|
300
|
+
headerName: 'Updated',
|
|
301
|
+
minWidth: 170,
|
|
302
|
+
flex: 1,
|
|
303
|
+
type: 'date',
|
|
304
|
+
// MUI X v9 passes the value positionally. The v6 object form silently
|
|
305
|
+
// destructures undefined off a Date and every row renders '--'.
|
|
306
|
+
valueGetter: (value)=>{
|
|
307
|
+
var _ref;
|
|
308
|
+
var _value_toDate;
|
|
309
|
+
return (_ref = value == null ? void 0 : (_value_toDate = value.toDate) == null ? void 0 : _value_toDate.call(value)) != null ? _ref : null;
|
|
310
|
+
},
|
|
311
|
+
valueFormatter: (value)=>{
|
|
312
|
+
var _value_toLocaleString;
|
|
313
|
+
return (value == null ? void 0 : (_value_toLocaleString = value.toLocaleString) == null ? void 0 : _value_toLocaleString.call(value)) || '--';
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
listActionsColumn((row)=>{
|
|
317
|
+
var _form_displayName;
|
|
318
|
+
const form = _extends({}, row, {
|
|
319
|
+
$id: row.$id
|
|
320
|
+
});
|
|
321
|
+
const versionId = form.versionId;
|
|
322
|
+
return /*#__PURE__*/ _jsx(ListRowActions, {
|
|
323
|
+
label: (_form_displayName = form.displayName) != null ? _form_displayName : form.$id,
|
|
324
|
+
quick: _extends({
|
|
325
|
+
icon: mdiEyeOutline.path,
|
|
326
|
+
label: 'Preview'
|
|
327
|
+
}, versionId && orgSlug && host ? {
|
|
328
|
+
to: buildRoute(Route.FORM_PREVIEW, {
|
|
329
|
+
orgSlug,
|
|
330
|
+
host,
|
|
331
|
+
formId: form.$id,
|
|
332
|
+
versionId
|
|
333
|
+
})
|
|
334
|
+
} : {
|
|
335
|
+
unavailableReason: versionId ? 'Resolving this site’s address…' : 'Nothing to preview yet — open it in the besigner once.'
|
|
336
|
+
}),
|
|
337
|
+
items: [
|
|
338
|
+
{
|
|
339
|
+
key: 'details',
|
|
340
|
+
label: 'View details',
|
|
341
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
342
|
+
path: ICON_VARIANT_SHOW_DETAIL.path,
|
|
343
|
+
size: 0.8
|
|
344
|
+
}),
|
|
345
|
+
href: formHref(form.$id)
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
key: 'besigner',
|
|
349
|
+
label: 'Edit in besigner',
|
|
350
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
351
|
+
path: mdiVectorSquare.path,
|
|
352
|
+
size: 0.8
|
|
353
|
+
}),
|
|
354
|
+
/*
|
|
355
|
+
* A LINK only once the form has a version. A form that has never
|
|
356
|
+
* been opened has none, and opening it MINTS the first one —
|
|
357
|
+
* that is a write, and the detail page is the one place that
|
|
358
|
+
* decides what an initial version looks like. Sending the reader
|
|
359
|
+
* there rather than minting a second way is what keeps the two
|
|
360
|
+
* from drifting.
|
|
361
|
+
*/ href: versionId && orgSlug && host ? buildRoute(Route.FORM_BESIGNER, {
|
|
362
|
+
orgSlug,
|
|
363
|
+
host,
|
|
364
|
+
formId: form.$id,
|
|
365
|
+
versionId
|
|
366
|
+
}) : formHref(form.$id)
|
|
367
|
+
},
|
|
368
|
+
/*
|
|
369
|
+
* Retire / Restore (AGL-2671). Last in the menu, and the only item
|
|
370
|
+
* here that writes.
|
|
371
|
+
*
|
|
372
|
+
* The label says what it DOES to the form rather than what it does
|
|
373
|
+
* to this list — "Archive" reads like a filing action, and the
|
|
374
|
+
* consequence a merchant needs to weigh is that the form stops
|
|
375
|
+
* collecting. `/api/forms/submit` refuses a retired form, so one
|
|
376
|
+
* still placed on a published page will turn visitors away.
|
|
377
|
+
*/ {
|
|
378
|
+
key: 'duplicate',
|
|
379
|
+
label: DUPLICATE_MENU_LABEL,
|
|
380
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
381
|
+
path: mdiContentCopy.path,
|
|
382
|
+
size: 0.8
|
|
383
|
+
}),
|
|
384
|
+
onClick: ()=>{
|
|
385
|
+
var _form_displayName;
|
|
386
|
+
return duplicate.request('form', {
|
|
387
|
+
id: form.$id,
|
|
388
|
+
name: (_form_displayName = form.displayName) != null ? _form_displayName : ''
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
isFormArchived(form) ? {
|
|
393
|
+
key: 'restore',
|
|
394
|
+
label: 'Restore — start collecting again',
|
|
395
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
396
|
+
path: mdiArchiveArrowUpOutline.path,
|
|
397
|
+
size: 0.8
|
|
398
|
+
}),
|
|
399
|
+
onClick: ()=>void setRetired(form.$id, false)
|
|
400
|
+
} : {
|
|
401
|
+
key: 'retire',
|
|
402
|
+
label: 'Retire — stop collecting, keep the history',
|
|
403
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
404
|
+
path: mdiArchiveArrowDownOutline.path,
|
|
405
|
+
size: 0.8
|
|
406
|
+
}),
|
|
407
|
+
onClick: ()=>void setRetired(form.$id, true)
|
|
408
|
+
}
|
|
409
|
+
]
|
|
410
|
+
});
|
|
411
|
+
})
|
|
412
|
+
];
|
|
413
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
414
|
+
children: [
|
|
415
|
+
duplicate.dialog,
|
|
416
|
+
/*#__PURE__*/ _jsx(PageHeaderActions, {
|
|
417
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
418
|
+
direction: "row",
|
|
419
|
+
spacing: 2,
|
|
420
|
+
sx: {
|
|
421
|
+
alignItems: 'center'
|
|
422
|
+
},
|
|
423
|
+
children: [
|
|
424
|
+
/*#__PURE__*/ _jsx(QuotaReadoutComponent, {
|
|
425
|
+
ready: org != null,
|
|
426
|
+
used: formsUsed,
|
|
427
|
+
limit: Aglyn.checkQuota(org, 'formsPerHost', formsUsed).limit,
|
|
428
|
+
noun: "form"
|
|
429
|
+
}),
|
|
430
|
+
formWindow.some((form)=>isFormArchived(form)) ? /*#__PURE__*/ _jsx(Typography, {
|
|
431
|
+
variant: "caption",
|
|
432
|
+
color: "text.secondary",
|
|
433
|
+
children: 'Retired forms keep their slot'
|
|
434
|
+
}) : null,
|
|
435
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
436
|
+
size: "small",
|
|
437
|
+
variant: showArchived ? 'outlined' : 'text',
|
|
438
|
+
onClick: ()=>setShowArchived((shown)=>!shown),
|
|
439
|
+
children: showArchived ? 'Hide retired' : 'Show retired'
|
|
440
|
+
}),
|
|
441
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
442
|
+
direction: "row",
|
|
443
|
+
spacing: 1,
|
|
444
|
+
children: [
|
|
445
|
+
CreateZone ? /*#__PURE__*/ _jsx(CreateZone, {
|
|
446
|
+
slot: "hostForms",
|
|
447
|
+
hostId: hostId,
|
|
448
|
+
orgId: org == null ? void 0 : org.$id
|
|
449
|
+
}) : null,
|
|
450
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
451
|
+
size: "small",
|
|
452
|
+
variant: "contained",
|
|
453
|
+
disabled: creating,
|
|
454
|
+
onClick: ()=>{
|
|
455
|
+
setCreateError(null);
|
|
456
|
+
setCreateOpen(true);
|
|
457
|
+
},
|
|
458
|
+
children: creating ? 'Creating…' : 'Create Form'
|
|
459
|
+
})
|
|
460
|
+
]
|
|
461
|
+
})
|
|
462
|
+
]
|
|
463
|
+
})
|
|
464
|
+
}),
|
|
465
|
+
/*#__PURE__*/ _jsxs(CardDisplay, {
|
|
466
|
+
header: "Forms",
|
|
467
|
+
help: pluginDocsHelp('forms', {
|
|
468
|
+
anchor: '#build-a-form',
|
|
469
|
+
excerpt: 'A form collects submissions into the Inbox, and its design is ' + 'drawn in the besigner like any other artifact.'
|
|
470
|
+
}),
|
|
471
|
+
children: [
|
|
472
|
+
retireError ? /*#__PURE__*/ _jsx(Alert, {
|
|
473
|
+
severity: "error",
|
|
474
|
+
sx: {
|
|
475
|
+
mb: 2
|
|
476
|
+
},
|
|
477
|
+
children: retireError
|
|
478
|
+
}) : null,
|
|
479
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
480
|
+
rowHeight: TABLE_ROW_HEIGHT,
|
|
481
|
+
columns: columns,
|
|
482
|
+
noRowsLabel: showArchived ? 'No forms' : 'No forms yet',
|
|
483
|
+
noRowsDescription: "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.",
|
|
484
|
+
noRowsAction: /*#__PURE__*/ _jsx(Button, {
|
|
485
|
+
variant: "contained",
|
|
486
|
+
onClick: ()=>setCreateOpen(true),
|
|
487
|
+
children: 'Create your first form'
|
|
488
|
+
}),
|
|
489
|
+
rows: forms,
|
|
490
|
+
// The whole row opens the detail page; the action cluster stops
|
|
491
|
+
// propagation so a menu click never navigates underneath it.
|
|
492
|
+
onOpen: (id)=>router.push(formHref(String(id))),
|
|
493
|
+
// An empty table while the read is in flight reads as "you have none"
|
|
494
|
+
// rather than "these are on their way".
|
|
495
|
+
loading: status === 'loading',
|
|
496
|
+
// Paged by the footer below, so the grid must not also slice.
|
|
497
|
+
hideFooter: true
|
|
498
|
+
}),
|
|
499
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
500
|
+
page: page,
|
|
501
|
+
pageSize: pageSize,
|
|
502
|
+
rowCount: forms.length,
|
|
503
|
+
hasMore: hasMore,
|
|
504
|
+
onPageChange: setPage,
|
|
505
|
+
onPageSizeChange: setPageSize
|
|
506
|
+
}),
|
|
507
|
+
/*#__PURE__*/ _jsx(CreateArtifactDrawer, {
|
|
508
|
+
open: createOpen,
|
|
509
|
+
onClose: ()=>setCreateOpen(false),
|
|
510
|
+
title: "Create new form",
|
|
511
|
+
// A form document stores no description: `/api/hosts/resources` filters
|
|
512
|
+
// `data` through a per-kind allow-list, so one typed here is dropped
|
|
513
|
+
// without a word.
|
|
514
|
+
includeDescription: false,
|
|
515
|
+
onSubmit: handleCreate,
|
|
516
|
+
errorSlot: createError ? /*#__PURE__*/ _jsx(Alert, {
|
|
517
|
+
severity: "error",
|
|
518
|
+
sx: {
|
|
519
|
+
mt: 2,
|
|
520
|
+
mb: 1
|
|
521
|
+
},
|
|
522
|
+
children: createError
|
|
523
|
+
}) : null
|
|
524
|
+
})
|
|
525
|
+
]
|
|
526
|
+
})
|
|
527
|
+
]
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
HostFormsCard.displayName = 'HostFormsCard';
|
|
531
|
+
export default HostFormsCard;
|
|
532
|
+
|
|
533
|
+
//# sourceMappingURL=host-forms-card.component.js.map
|
|
@@ -0,0 +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} 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 { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListTable, {\n ListRowActions,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useLiveArtifactCount,\n usePagedCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { collectionPage } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\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, so both are real\n * numbers. `leads` is counted only for a form whose `routing.lead` is set, so\n * a form that does not route to leads carries no figure at all and renders as\n * a dash rather than as `0`. A zero would say this form has produced no leads,\n * which for an unrouted form is a measurement nobody took.\n */\nexport function HostFormsCard(props: HostFormsCardProps) {\n const { hostId, basePath, org } = props\n const router = useRouter()\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\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 })\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 PAGES, over an ordered walk.\n *\n * `collectionPage` holds the ordering decision and the reason it is the\n * document id rather than `displayName` — briefly, `orderBy` matches only\n * documents that HAVE the field, and the resources route stores an\n * allow-list it never checks for presence, so ordering on a name would hide\n * every form created without one rather than mis-sorting the list.\n *\n * The page is NOT re-sorted: sorting a server-ordered window in the browser\n * is what makes a pseudo-random sample look like the first page.\n */\n const {\n status,\n rows: formWindow,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'forms'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n /*\n * A retired form is a TOMBSTONE, not a row — unless the reader asks for the\n * tombstones (AGL-2671). Client-side because it has to be: Firestore cannot\n * ask for the ABSENCE of a field, so a form created through the resources\n * route (which carries no `archivedAt` at all) and one archived later are\n * not one value to filter on. The cost is that a tombstone spends a slot in\n * whichever page it falls in.\n *\n * The toggle is not decoration. Retiring a form removes it from this list,\n * and a retirement with no way to see what has been retired is a one-way\n * door: the row is the only route back to Restore.\n */\n const [showArchived, setShowArchived] = useState(false)\n const forms = showArchived\n ? formWindow\n : formWindow.filter((form: any) => !isFormArchived(form))\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 window stands in: a LOWER bound, never a\n // confident zero.\n const formsUsed = liveFormCount ?? forms.length\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 setCreateError('Could not create that form')\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 is absent.\n *\n * `stats.leads` is incremented only for a form whose `routing.lead` is\n * set, so a form that does not route to leads has no recorded figure.\n * Rendering `0` there would read as \"this form has produced no leads\",\n * which is a claim about a measurement 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 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.FORM_PREVIEW, {\n orgSlug,\n host,\n formId: 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.FORM_BESIGNER, {\n orgSlug,\n host,\n formId: 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 a retirement is actually in the loaded page: the\n line answers a question nobody is asking on a site that has never\n retired anything. It makes no numeric claim, because one page is a\n lower bound on how many there are.\n */}\n {formWindow.some((form: any) => isFormArchived(form)) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Retired forms keep their slot'}\n </Typography>\n ) : null}\n {/*\n The only route back to a retired form (AGL-2671). Retiring one\n removes its row, so without this the Restore action would exist on\n a row nobody could reach again.\n\n A plain toggle rather than a filter control: there are exactly two\n states, and the retired set is expected to be small and rarely\n looked at.\n */}\n <Button\n size=\"small\"\n variant={showArchived ? 'outlined' : 'text'}\n onClick={() => setShowArchived((shown) => !shown)}\n >\n {showArchived ? 'Hide retired' : 'Show retired'}\n </Button>\n <Stack direction=\"row\" spacing={1}>\n {/*\n Other ways to start a form, from plugins (AGL-3043): the\n `hostForms` zone, drawn through the shell's own gated slot. A\n plugin page cannot mount that slot itself, so the shell hands it\n down, and a widget here passes the gates a console page's would.\n */}\n {CreateZone ? (\n <CreateZone slot=\"hostForms\" 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: '#build-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 >\n {retireError ? (\n <Alert severity=\"error\" sx={{ mb: 2 }}>\n {retireError}\n </Alert>\n ) : null}\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={columns}\n noRowsLabel={showArchived ? 'No forms' : 'No forms yet'}\n noRowsDescription=\"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 <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create your first form'}\n </Button>\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 />\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","useConsoleWidgetSlot","ICON_VARIANT_SHOW_DETAIL","mdiArchiveArrowDownOutline","mdiArchiveArrowUpOutline","mdiContentCopy","mdiEyeOutline","mdiVectorSquare","AppLink","CardDisplay","MdiIcon","ListPagination","ListTable","ListRowActions","listActionsColumn","TABLE_ROW_HEIGHT","QuotaReadoutComponent","CreateArtifactDrawer","Alert","Button","Stack","Typography","collection","doc","updateDoc","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useLiveArtifactCount","usePagedCollection","collectionPage","useRouter","useCallback","useState","BUNDLE_ID","HostFormsCard","props","hostId","basePath","org","router","duplicate","onDuplicated","_kind","copy","push","formHref","id","orgSlug","subdomain","host","firestore","createHostResource","CreateZone","formId","FORM_DETAILS","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","retireError","setRetireError","setRetired","retired","archivedAt","Date","now","status","rows","formWindow","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","showArchived","setShowArchived","forms","filter","form","liveFormCount","formsUsed","length","handleCreate","values","createResourceUid","resource","data","displayName","slug","normalizeFormSlug","fields","rootId","CANVAS_ROOT_ELEMENT_ID","nodes","$id","componentId","formRoot","pluginId","parentId","formName","error","console","columns","field","headerName","minWidth","type","renderCell","value","href","flex","valueFormatter","align","headerAlign","valueGetter","_value","row","stats","submissions","toLocaleString","leads","lastSubmissionAtMs","toDate","versionId","label","quick","icon","path","to","FORM_PREVIEW","unavailableReason","items","key","size","FORM_BESIGNER","onClick","request","name","dialog","direction","spacing","sx","alignItems","ready","used","limit","checkQuota","noun","some","variant","color","shown","slot","orgId","disabled","header","help","anchor","excerpt","severity","mb","rowHeight","noRowsLabel","noRowsDescription","noRowsAction","onOpen","String","loading","hideFooter","rowCount","onPageChange","onPageSizeChange","open","onClose","title","includeDescription","onSubmit","errorSlot","mt"],"mappings":"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"}
|