@aglyn/plugins-inbox 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 +7 -0
- package/package.json +51 -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/contacts-card.component.d.ts +16 -0
- package/src/lib/components/contacts-card.component.js +351 -0
- package/src/lib/components/contacts-card.component.js.map +1 -0
- package/src/lib/components/inbox-attribution-zone.d.ts +52 -0
- package/src/lib/components/inbox-attribution-zone.js +53 -0
- package/src/lib/components/inbox-attribution-zone.js.map +1 -0
- package/src/lib/components/inbox-console-page.d.ts +28 -0
- package/src/lib/components/inbox-console-page.js +219 -0
- package/src/lib/components/inbox-console-page.js.map +1 -0
- package/src/lib/components/inbox-console-sections.d.ts +40 -0
- package/src/lib/components/inbox-console-sections.js +66 -0
- package/src/lib/components/inbox-console-sections.js.map +1 -0
- package/src/lib/components/inbox-glance-card.component.d.ts +26 -0
- package/src/lib/components/inbox-glance-card.component.js +215 -0
- package/src/lib/components/inbox-glance-card.component.js.map +1 -0
- package/src/lib/components/submission-list-assignment.component.d.ts +29 -0
- package/src/lib/components/submission-list-assignment.component.js +257 -0
- package/src/lib/components/submission-list-assignment.component.js.map +1 -0
- package/src/lib/components/submission-reply.component.d.ts +30 -0
- package/src/lib/components/submission-reply.component.js +249 -0
- package/src/lib/components/submission-reply.component.js.map +1 -0
- package/src/lib/components/submissions-card.component.d.ts +35 -0
- package/src/lib/components/submissions-card.component.js +577 -0
- package/src/lib/components/submissions-card.component.js.map +1 -0
- package/src/lib/components/use-record-route-context.d.ts +15 -0
- package/src/lib/components/use-record-route-context.js +41 -0
- package/src/lib/components/use-record-route-context.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/reply-policy.d.ts +134 -0
- package/src/lib/model/reply-policy.js +141 -0
- package/src/lib/model/reply-policy.js.map +1 -0
- package/src/lib/model/submission-presenter.d.ts +81 -0
- package/src/lib/model/submission-presenter.js +152 -0
- package/src/lib/model/submission-presenter.js.map +1 -0
- package/src/lib/plugin.d.ts +25 -0
- package/src/lib/plugin.js +102 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server.d.ts +179 -0
- package/src/lib/server.js +666 -0
- package/src/lib/server.js.map +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Submissions section of the Inbox (AGL-77/104/109 → AGL-395): the form
|
|
3
|
+
* messages a site collected, newest first, and the reader that opens one.
|
|
4
|
+
*
|
|
5
|
+
* Its own component since AGL-2501, when the Inbox's tabs became routes. The
|
|
6
|
+
* split is what makes "mount only the section being read" structural: hooks
|
|
7
|
+
* cannot be conditional, so a page holding every section's reads pays for all
|
|
8
|
+
* of them whichever one the URL names.
|
|
9
|
+
*/
|
|
10
|
+
export interface SubmissionsCardProps {
|
|
11
|
+
hostId: string;
|
|
12
|
+
/**
|
|
13
|
+
* Narrow this card to ONE form, permanently.
|
|
14
|
+
*
|
|
15
|
+
* What the forms plugin's detail surface renders instead of a second
|
|
16
|
+
* submissions table. The reader has already chosen the subject by being on
|
|
17
|
+
* that page, so scoping here does three things a copy would have had to
|
|
18
|
+
* re-derive: the form picker is not rendered (there is nothing to pick),
|
|
19
|
+
* the site's `forms` collection is not read at all (the picker was its only
|
|
20
|
+
* consumer), and the empty state names the form rather than the site.
|
|
21
|
+
*
|
|
22
|
+
* Everything else — the paged walk, the reader dialog, read/unread,
|
|
23
|
+
* delete, reply, list assignment, attribution — is the same code answering
|
|
24
|
+
* a narrower query. That is the reason this is a prop rather than a second
|
|
25
|
+
* component: a per-form table written separately would be a second reader
|
|
26
|
+
* to keep in step, and it would have been the one to reintroduce the
|
|
27
|
+
* unordered `limit()` this card's own comment exists to warn about.
|
|
28
|
+
*/
|
|
29
|
+
formId?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function SubmissionsCard({ hostId, formId }: SubmissionsCardProps): import("react").JSX.Element;
|
|
32
|
+
export declare namespace SubmissionsCard {
|
|
33
|
+
var displayName: string;
|
|
34
|
+
}
|
|
35
|
+
export default SubmissionsCard;
|
|
@@ -0,0 +1,577 @@
|
|
|
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 { FORMS_MAX_PER_HOST, INBOX_SUBMISSION_PARAM, pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
|
+
// A deep import, NOT the plugin barrel (AGL-1151): the barrel is the entry
|
|
21
|
+
// point the tenant's loader dynamically imports to activate the marketing
|
|
22
|
+
// plugin's SITE half, so a console card named there ships to every published
|
|
23
|
+
// page. The component path reaches the same module without crossing it.
|
|
24
|
+
import { InboxRecordAttributionZone } from "./inbox-attribution-zone.js";
|
|
25
|
+
// The CRM's route builder by its leaf path, for the reason above: the barrel
|
|
26
|
+
// is the plugin's site entry point.
|
|
27
|
+
import { pluginRecordByEmailHref, pluginRecordListHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
28
|
+
import { mdiAccountArrowRight, mdiDeleteOutline, mdiEmailOpenOutline, mdiEmailOutline } from "@aglyn/shared-data-mdi";
|
|
29
|
+
import { CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
30
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
31
|
+
import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
32
|
+
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
33
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
34
|
+
import { useFirestore, useFirestoreCollection, usePagedCollection } from "@aglyn/tenant-feature-instance";
|
|
35
|
+
import { Avatar, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
|
|
36
|
+
import { collection, deleteDoc, doc, getDoc, limit, orderBy, query, updateDoc, where } from "firebase/firestore";
|
|
37
|
+
import { useSearchParams } from "next/navigation";
|
|
38
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
39
|
+
import { relativeTime, routingChips, senderHue, submissionSender } from "../model/submission-presenter.js";
|
|
40
|
+
import SubmissionListAssignment from "./submission-list-assignment.component.js";
|
|
41
|
+
import SubmissionReply from "./submission-reply.component.js";
|
|
42
|
+
import { useRecordRouteContext } from "./use-record-route-context.js";
|
|
43
|
+
export function SubmissionsCard({ hostId, formId }) {
|
|
44
|
+
var _ref, _ref1, _ref2, _ref3, _ref4;
|
|
45
|
+
var _reader_createdAt_toDate, _reader_createdAt;
|
|
46
|
+
const firestore = useFirestore();
|
|
47
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
48
|
+
const { confirm } = useConfirmationContext();
|
|
49
|
+
/** Scoped to one form: the subject is fixed and nothing may widen it. */ const scoped = Boolean(formId);
|
|
50
|
+
/*
|
|
51
|
+
* Where the sender's CONTACT is (AGL-2612). A submission that carried an
|
|
52
|
+
* address updated a contact in the CRM at stage Lead; the row links to
|
|
53
|
+
* it by that address, and the Contacts list — which holds the id nothing
|
|
54
|
+
* here does — opens the record. The same hub path the Members & leads
|
|
55
|
+
* rows use to open a lead.
|
|
56
|
+
*/ const routeContext = useRecordRouteContext();
|
|
57
|
+
/*
|
|
58
|
+
* The site's forms, for the Submissions filter.
|
|
59
|
+
*
|
|
60
|
+
* `FORMS_MAX_PER_HOST` is a read WINDOW, not a cap on the collection — how
|
|
61
|
+
* many forms a site may hold is `formsPerHost`, enforced at the create in
|
|
62
|
+
* `/api/hosts/resources`, and a staff-set per-org override can raise it
|
|
63
|
+
* past this window. So the window can be smaller than the catalog, and one
|
|
64
|
+
* more document than fits is read on purpose: a filter that quietly listed
|
|
65
|
+
* the first N would report "this form does not exist" and "this form is
|
|
66
|
+
* past the window" as the same empty answer.
|
|
67
|
+
*
|
|
68
|
+
* Ordered by `__name__`. `displayName` would be the nicer order and is the
|
|
69
|
+
* wrong instrument: `orderBy` on a data field DROPS every document missing
|
|
70
|
+
* it, invisibly, so a form saved without a name would vanish from its own
|
|
71
|
+
* filter. The list is sorted for display below, where a missing name is
|
|
72
|
+
* merely ugly.
|
|
73
|
+
*/ const { data: formDocs } = useFirestoreCollection(()=>scoped ? null : query(collection(firestore, 'hosts', hostId, 'forms'), orderBy('__name__'), limit(FORMS_MAX_PER_HOST + 1)), [
|
|
74
|
+
firestore,
|
|
75
|
+
hostId,
|
|
76
|
+
scoped
|
|
77
|
+
], {
|
|
78
|
+
idField: '$id'
|
|
79
|
+
});
|
|
80
|
+
/** More forms exist than the window shows; the filter has to say so. */ const formsTruncated = ((_ref = formDocs == null ? void 0 : formDocs.length) != null ? _ref : 0) > FORMS_MAX_PER_HOST;
|
|
81
|
+
const forms = useMemo(()=>[
|
|
82
|
+
...formDocs != null ? formDocs : []
|
|
83
|
+
].slice(0, FORMS_MAX_PER_HOST).sort((left, right)=>{
|
|
84
|
+
var _left_displayName, _right_displayName;
|
|
85
|
+
return String((_left_displayName = left.displayName) != null ? _left_displayName : left.$id).localeCompare(String((_right_displayName = right.displayName) != null ? _right_displayName : right.$id));
|
|
86
|
+
}), [
|
|
87
|
+
formDocs
|
|
88
|
+
]);
|
|
89
|
+
/*
|
|
90
|
+
* `null` is "All forms"; a form id narrows to one form's submissions.
|
|
91
|
+
*
|
|
92
|
+
* A PRIMITIVE, deliberately: `usePagedCollection` reopens its listener when
|
|
93
|
+
* a dep changes, and an object identity would tear down and reopen on every
|
|
94
|
+
* render. It also resets the reader to page 1, which is what switching
|
|
95
|
+
* subjects should do.
|
|
96
|
+
*/ const [formFilter, setFormFilter] = useState(null);
|
|
97
|
+
/**
|
|
98
|
+
* The form the query is actually narrowed to.
|
|
99
|
+
*
|
|
100
|
+
* The scope wins over the picker rather than seeding it. A scoped card
|
|
101
|
+
* renders no picker, so a `formFilter` that could outrank `formId` would be
|
|
102
|
+
* a filter with no control — reachable only by a state change nothing on
|
|
103
|
+
* screen can cause, and unclearable if one ever could.
|
|
104
|
+
*/ const activeForm = formId != null ? formId : formFilter;
|
|
105
|
+
/*
|
|
106
|
+
* The inbox WALKS its submissions instead of sampling them (AGL-2501,
|
|
107
|
+
* AGL-2292).
|
|
108
|
+
*
|
|
109
|
+
* `limit(200)` carried no `orderBy`, so Firestore answered it in
|
|
110
|
+
* DOCUMENT-ID order over ids `add()` generates — an arbitrary two hundred
|
|
111
|
+
* of the site's messages, which the client sort then arranged newest-first
|
|
112
|
+
* so the result looked like a feed. A site past two hundred submissions
|
|
113
|
+
* could not reach the rest, and the messages missing left no gap: the row
|
|
114
|
+
* dates on screen simply skipped, which reads as a quiet week rather than
|
|
115
|
+
* as an unreachable inbox.
|
|
116
|
+
*
|
|
117
|
+
* `createdAt` is safe to order on, checked against the writer rather than
|
|
118
|
+
* assumed: `apps/tenant/app/api/forms/submit/route.ts` is the only path
|
|
119
|
+
* that creates one and stamps `createdAt: serverTimestamp()` on every add,
|
|
120
|
+
* the v1 API only ever reads and deletes, and `formSubmissions` is absent
|
|
121
|
+
* from `IMPORTABLE_FIELDS`, so no restore path can make one without it.
|
|
122
|
+
*/ const { rows: submissions, hasMore: hasMoreSubmissions, page: submissionPage, setPage: setSubmissionPage, pageSize: submissionPageSize, setPageSize: setSubmissionPageSize } = usePagedCollection((pageLimit)=>query(collection(firestore, 'hosts', hostId, 'formSubmissions'), // Served by the `formId ASC, createdAt DESC` composite index in
|
|
123
|
+
// `cloud/firebase-firestore.indexes.json`, which must be deployed
|
|
124
|
+
// before this ships — without it Firestore refuses the query rather
|
|
125
|
+
// than answering it slowly.
|
|
126
|
+
...activeForm ? [
|
|
127
|
+
where('formId', '==', activeForm)
|
|
128
|
+
] : [], orderBy('createdAt', 'desc'), limit(pageLimit)), [
|
|
129
|
+
firestore,
|
|
130
|
+
hostId,
|
|
131
|
+
activeForm
|
|
132
|
+
], {
|
|
133
|
+
idField: '$id'
|
|
134
|
+
});
|
|
135
|
+
// Mail reader (AGL-104): opening a submission shows the full message and
|
|
136
|
+
// marks it read.
|
|
137
|
+
const [reader, setReader] = useState(null);
|
|
138
|
+
const handleOpenReader = useCallback((submission)=>()=>{
|
|
139
|
+
setReader(submission);
|
|
140
|
+
if (!submission.read) {
|
|
141
|
+
void updateDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', submission.$id), {
|
|
142
|
+
read: true
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}, [
|
|
146
|
+
firestore,
|
|
147
|
+
hostId
|
|
148
|
+
]);
|
|
149
|
+
const handleToggleRead = useCallback((submission)=>()=>{
|
|
150
|
+
void updateDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', submission.$id), {
|
|
151
|
+
read: !submission.read
|
|
152
|
+
});
|
|
153
|
+
}, [
|
|
154
|
+
firestore,
|
|
155
|
+
hostId
|
|
156
|
+
]);
|
|
157
|
+
/*
|
|
158
|
+
* A submission named in the URL opens in the reader on arrival (AGL-2622).
|
|
159
|
+
*
|
|
160
|
+
* A contact's timeline names the submission that captured the person, and
|
|
161
|
+
* the address it links to is this tab with `?submission={id}`. The
|
|
162
|
+
* document is read once, by id, rather than looked for in the paged
|
|
163
|
+
* window: the window is the newest page of a walk that may not reach a
|
|
164
|
+
* submission from months back, and one document read is what the URL
|
|
165
|
+
* asked for. Opening it marks it read, as a click on the row would. A
|
|
166
|
+
* submission that is gone says so, because a reader that silently stays
|
|
167
|
+
* shut reads as the link having done nothing.
|
|
168
|
+
*
|
|
169
|
+
* Once per id: the ref keeps a re-render — and the same URL re-settling —
|
|
170
|
+
* from reopening a reader the person has since closed. The landing read
|
|
171
|
+
* is judged against that ref rather than an effect cleanup: a listener
|
|
172
|
+
* settling while the read is in flight re-renders this card, and a
|
|
173
|
+
* cleanup keyed on any dep would cancel the answer to the question the
|
|
174
|
+
* URL just asked.
|
|
175
|
+
*/ const searchParams = useSearchParams();
|
|
176
|
+
const seededSubmissionId = (_ref1 = searchParams == null ? void 0 : searchParams.get(INBOX_SUBMISSION_PARAM)) != null ? _ref1 : null;
|
|
177
|
+
const seededOpened = useRef(null);
|
|
178
|
+
useEffect(()=>{
|
|
179
|
+
if (!seededSubmissionId || seededOpened.current === seededSubmissionId) return;
|
|
180
|
+
seededOpened.current = seededSubmissionId;
|
|
181
|
+
void getDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', seededSubmissionId)).then((snapshot)=>{
|
|
182
|
+
if (seededOpened.current !== snapshot.id) return;
|
|
183
|
+
if (!snapshot.exists()) {
|
|
184
|
+
enqueueSnackbar('That submission is no longer in the Inbox.', {
|
|
185
|
+
variant: 'warning',
|
|
186
|
+
persist: false
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
handleOpenReader(_extends({}, snapshot.data(), {
|
|
191
|
+
$id: snapshot.id
|
|
192
|
+
}))();
|
|
193
|
+
}).catch(()=>undefined);
|
|
194
|
+
}, [
|
|
195
|
+
seededSubmissionId,
|
|
196
|
+
firestore,
|
|
197
|
+
hostId,
|
|
198
|
+
handleOpenReader,
|
|
199
|
+
enqueueSnackbar
|
|
200
|
+
]);
|
|
201
|
+
const handleDelete = useCallback((submission)=>async ()=>{
|
|
202
|
+
const confirmed = await confirm({
|
|
203
|
+
title: 'Delete this submission?',
|
|
204
|
+
description: 'The submission is removed permanently.',
|
|
205
|
+
confirmationText: 'Delete',
|
|
206
|
+
confirmationButtonProps: {
|
|
207
|
+
color: 'error'
|
|
208
|
+
}
|
|
209
|
+
}).then(()=>true).catch(()=>false);
|
|
210
|
+
if (!confirmed) return;
|
|
211
|
+
await deleteDoc(doc(firestore, 'hosts', hostId, 'formSubmissions', submission.$id));
|
|
212
|
+
enqueueSnackbar('Submission deleted', {
|
|
213
|
+
variant: 'success',
|
|
214
|
+
persist: false
|
|
215
|
+
});
|
|
216
|
+
}, [
|
|
217
|
+
confirm,
|
|
218
|
+
firestore,
|
|
219
|
+
hostId,
|
|
220
|
+
enqueueSnackbar
|
|
221
|
+
]);
|
|
222
|
+
/*
|
|
223
|
+
* One row per submission, newest first: who sent it, what it said and how
|
|
224
|
+
* long ago. The row opens the reader, which also marks it read.
|
|
225
|
+
*/ const submissionActions = (submission)=>{
|
|
226
|
+
var _pluginRecordByEmailHref;
|
|
227
|
+
const senderEmail = submissionSender(submission.fields).email;
|
|
228
|
+
// Where the sender's contact is read, asked of whichever plugin keeps
|
|
229
|
+
// people; with none loaded there is no such page, and no menu item.
|
|
230
|
+
const contactsHref = routeContext ? pluginRecordListHref('contact', routeContext) : null;
|
|
231
|
+
return [
|
|
232
|
+
...routeContext && contactsHref ? [
|
|
233
|
+
_extends({
|
|
234
|
+
key: 'crm',
|
|
235
|
+
label: 'Open contact in CRM',
|
|
236
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
237
|
+
path: mdiAccountArrowRight.path,
|
|
238
|
+
size: 0.8
|
|
239
|
+
})
|
|
240
|
+
}, senderEmail ? {
|
|
241
|
+
href: (_pluginRecordByEmailHref = pluginRecordByEmailHref('contact', routeContext, senderEmail)) != null ? _pluginRecordByEmailHref : contactsHref
|
|
242
|
+
} : {
|
|
243
|
+
disabled: true,
|
|
244
|
+
disabledReason: 'This submission carried no email address, so no contact was updated.'
|
|
245
|
+
})
|
|
246
|
+
] : [],
|
|
247
|
+
{
|
|
248
|
+
key: 'read',
|
|
249
|
+
label: submission.read ? 'Mark unread' : 'Mark read',
|
|
250
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
251
|
+
path: submission.read ? mdiEmailOutline.path : mdiEmailOpenOutline.path,
|
|
252
|
+
size: 0.8
|
|
253
|
+
}),
|
|
254
|
+
onClick: handleToggleRead(submission)
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
key: 'delete',
|
|
258
|
+
label: 'Delete',
|
|
259
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
260
|
+
path: mdiDeleteOutline.path,
|
|
261
|
+
size: 0.8
|
|
262
|
+
}),
|
|
263
|
+
destructive: true,
|
|
264
|
+
onClick: handleDelete(submission)
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
};
|
|
268
|
+
const submissionColumns = [
|
|
269
|
+
{
|
|
270
|
+
/*
|
|
271
|
+
The mockup's list is people, not forms (AGL-2168): an initials avatar,
|
|
272
|
+
the sender, and the form name beneath it. The unread DOT replaces the
|
|
273
|
+
`New` chip — the row is already bold, and a chip that says "New" beside
|
|
274
|
+
bold text is the same fact twice.
|
|
275
|
+
*/ field: 'from',
|
|
276
|
+
headerName: 'From',
|
|
277
|
+
flex: 1,
|
|
278
|
+
minWidth: 220,
|
|
279
|
+
valueGetter: (_value, submission)=>submissionSender(submission.fields).label,
|
|
280
|
+
renderCell: ({ row: submission })=>{
|
|
281
|
+
var _submission_formName;
|
|
282
|
+
const sender = submissionSender(submission.fields);
|
|
283
|
+
const hue = senderHue(sender.label);
|
|
284
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
285
|
+
direction: "row",
|
|
286
|
+
spacing: 1,
|
|
287
|
+
sx: {
|
|
288
|
+
alignItems: 'center',
|
|
289
|
+
minWidth: 0
|
|
290
|
+
},
|
|
291
|
+
children: [
|
|
292
|
+
!submission.read ? /*#__PURE__*/ _jsx(Box, {
|
|
293
|
+
"aria-label": "Unread",
|
|
294
|
+
sx: {
|
|
295
|
+
width: 8,
|
|
296
|
+
height: 8,
|
|
297
|
+
borderRadius: '50%',
|
|
298
|
+
bgcolor: 'primary.main',
|
|
299
|
+
flexShrink: 0
|
|
300
|
+
}
|
|
301
|
+
}) : /*#__PURE__*/ _jsx(Box, {
|
|
302
|
+
sx: {
|
|
303
|
+
width: 8,
|
|
304
|
+
flexShrink: 0
|
|
305
|
+
}
|
|
306
|
+
}),
|
|
307
|
+
/*#__PURE__*/ _jsx(Avatar, {
|
|
308
|
+
sx: {
|
|
309
|
+
width: 28,
|
|
310
|
+
height: 28,
|
|
311
|
+
fontSize: 13,
|
|
312
|
+
bgcolor: `hsl(${hue} 55% 45%)`
|
|
313
|
+
},
|
|
314
|
+
children: sender.initials
|
|
315
|
+
}),
|
|
316
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
317
|
+
sx: {
|
|
318
|
+
minWidth: 0
|
|
319
|
+
},
|
|
320
|
+
children: [
|
|
321
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
322
|
+
variant: "body2",
|
|
323
|
+
noWrap: true,
|
|
324
|
+
children: sender.label
|
|
325
|
+
}),
|
|
326
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
327
|
+
variant: "caption",
|
|
328
|
+
color: "text.secondary",
|
|
329
|
+
noWrap: true,
|
|
330
|
+
children: (_submission_formName = submission.formName) != null ? _submission_formName : 'Form'
|
|
331
|
+
})
|
|
332
|
+
]
|
|
333
|
+
})
|
|
334
|
+
]
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
field: 'message',
|
|
340
|
+
headerName: 'Message',
|
|
341
|
+
flex: 2,
|
|
342
|
+
minWidth: 260,
|
|
343
|
+
valueGetter: (_value, submission)=>{
|
|
344
|
+
var _submission_fields;
|
|
345
|
+
return Object.entries((_submission_fields = submission.fields) != null ? _submission_fields : {}).map(([key, value])=>`${key}: ${value}`).join(' · ');
|
|
346
|
+
},
|
|
347
|
+
renderCell: ({ value })=>/*#__PURE__*/ _jsx(Typography, {
|
|
348
|
+
variant: "body2",
|
|
349
|
+
noWrap: true,
|
|
350
|
+
children: value
|
|
351
|
+
})
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
/*
|
|
355
|
+
Relative, as the mockup shows it — an inbox is scanned for recency and a
|
|
356
|
+
locale timestamp makes the reader do the subtraction. The absolute time
|
|
357
|
+
stays on the detail dialog, where it is the fact you actually want.
|
|
358
|
+
*/ field: 'createdAt',
|
|
359
|
+
headerName: 'Received',
|
|
360
|
+
width: 130,
|
|
361
|
+
valueGetter: (_value, submission)=>{
|
|
362
|
+
var _ref;
|
|
363
|
+
var _submission_createdAt_toDate_getTime, _submission_createdAt_toDate, _submission_createdAt_toDate1, _submission_createdAt;
|
|
364
|
+
return (_ref = (_submission_createdAt = submission.createdAt) == null ? void 0 : (_submission_createdAt_toDate1 = _submission_createdAt.toDate) == null ? void 0 : (_submission_createdAt_toDate = _submission_createdAt_toDate1.call(_submission_createdAt)) == null ? void 0 : (_submission_createdAt_toDate_getTime = _submission_createdAt_toDate.getTime) == null ? void 0 : _submission_createdAt_toDate_getTime.call(_submission_createdAt_toDate)) != null ? _ref : 0;
|
|
365
|
+
},
|
|
366
|
+
renderCell: ({ row: submission })=>{
|
|
367
|
+
var _ref;
|
|
368
|
+
var _submission_createdAt_toDate, _submission_createdAt, _submission_createdAt_toDate1, _submission_createdAt1;
|
|
369
|
+
return /*#__PURE__*/ _jsx(Tooltip, {
|
|
370
|
+
title: (_ref = (_submission_createdAt = submission.createdAt) == null ? void 0 : (_submission_createdAt_toDate = _submission_createdAt.toDate) == null ? void 0 : _submission_createdAt_toDate.call(_submission_createdAt).toLocaleString()) != null ? _ref : '',
|
|
371
|
+
children: /*#__PURE__*/ _jsx("span", {
|
|
372
|
+
children: relativeTime((_submission_createdAt1 = submission.createdAt) == null ? void 0 : (_submission_createdAt_toDate1 = _submission_createdAt1.toDate) == null ? void 0 : _submission_createdAt_toDate1.call(_submission_createdAt1).getTime())
|
|
373
|
+
})
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
/*
|
|
378
|
+
One overflow menu, as the Members & leads rows have: two inline buttons
|
|
379
|
+
had no room for a third, and the contact link is the one a reader
|
|
380
|
+
reaches for after reading. Present but disabled for a submission with no
|
|
381
|
+
address, with the reason — a row that simply lacked the item would read
|
|
382
|
+
as the contact not existing.
|
|
383
|
+
*/ listActionsColumn((submission)=>{
|
|
384
|
+
var _submissionSender_email;
|
|
385
|
+
return /*#__PURE__*/ _jsx(ListRowActions, {
|
|
386
|
+
label: String((_submissionSender_email = submissionSender(submission.fields).email) != null ? _submissionSender_email : submission.$id),
|
|
387
|
+
items: submissionActions(submission)
|
|
388
|
+
});
|
|
389
|
+
}, {
|
|
390
|
+
width: 72
|
|
391
|
+
})
|
|
392
|
+
];
|
|
393
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
394
|
+
children: [
|
|
395
|
+
/*#__PURE__*/ _jsxs(CardDisplay, {
|
|
396
|
+
header: scoped ? 'Submissions to this form' : 'Form Submissions',
|
|
397
|
+
help: pluginDocsHelp('forms', {
|
|
398
|
+
anchor: '#the-inbox',
|
|
399
|
+
excerpt: 'Messages your forms collected, newest first, showing who sent ' + 'each one and where it was routed.'
|
|
400
|
+
}),
|
|
401
|
+
contentGutterX: true,
|
|
402
|
+
contentGutterY: true,
|
|
403
|
+
contentBordered: "all",
|
|
404
|
+
children: [
|
|
405
|
+
!scoped && forms.length > 0 ? /*#__PURE__*/ _jsxs(TextField, {
|
|
406
|
+
select: true,
|
|
407
|
+
size: "small",
|
|
408
|
+
label: 'Form',
|
|
409
|
+
value: formFilter != null ? formFilter : '',
|
|
410
|
+
onChange: (event)=>setFormFilter(event.target.value || null),
|
|
411
|
+
helperText: formsTruncated ? `Showing the first ${FORMS_MAX_PER_HOST.toLocaleString()} ` + 'forms. Narrow by form is incomplete; All forms still ' + 'covers every submission.' : undefined,
|
|
412
|
+
sx: {
|
|
413
|
+
mb: 2,
|
|
414
|
+
minWidth: 240
|
|
415
|
+
},
|
|
416
|
+
children: [
|
|
417
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
418
|
+
value: "",
|
|
419
|
+
children: 'All forms'
|
|
420
|
+
}),
|
|
421
|
+
forms.map((form)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
422
|
+
value: form.$id,
|
|
423
|
+
children: form.displayName || form.$id
|
|
424
|
+
}, form.$id))
|
|
425
|
+
]
|
|
426
|
+
}) : null,
|
|
427
|
+
submissions.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
428
|
+
variant: "body2",
|
|
429
|
+
color: "text.secondary",
|
|
430
|
+
children: scoped ? 'No submissions carry this form’s id yet. Messages this ' + 'form’s design collected before it became a form entity are ' + 'in the Inbox, filed under the name they were sent with.' : activeForm ? 'No submissions for this form yet. Submissions sent before ' + 'the form was created stay under All forms.' : 'No form submissions yet. Add a Contact Form element to a ' + 'screen — visitor messages arrive here.'
|
|
431
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
432
|
+
children: [
|
|
433
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
434
|
+
"aria-label": scoped ? 'Submissions to this form' : 'Form submissions',
|
|
435
|
+
rows: submissions,
|
|
436
|
+
columns: submissionColumns,
|
|
437
|
+
rowHeight: TABLE_ROW_HEIGHT,
|
|
438
|
+
onOpen: (_id, submission)=>handleOpenReader(submission)(),
|
|
439
|
+
// An unread submission reads bold across its row, which is why
|
|
440
|
+
// the list needs no `New` chip.
|
|
441
|
+
getRowClassName: ({ row })=>row.read ? '' : 'submission-unread',
|
|
442
|
+
sx: {
|
|
443
|
+
'& .submission-unread .MuiDataGrid-cell, & .submission-unread .MuiTypography-root': {
|
|
444
|
+
fontWeight: 'fontWeightBold'
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
// Paged by the footer below, so the grid must not also slice.
|
|
448
|
+
hideFooter: true
|
|
449
|
+
}),
|
|
450
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
451
|
+
page: submissionPage,
|
|
452
|
+
pageSize: submissionPageSize,
|
|
453
|
+
rowCount: submissions.length,
|
|
454
|
+
hasMore: hasMoreSubmissions,
|
|
455
|
+
onPageChange: setSubmissionPage,
|
|
456
|
+
onPageSizeChange: setSubmissionPageSize
|
|
457
|
+
})
|
|
458
|
+
]
|
|
459
|
+
})
|
|
460
|
+
]
|
|
461
|
+
}),
|
|
462
|
+
/*#__PURE__*/ _jsxs(Dialog, {
|
|
463
|
+
open: Boolean(reader),
|
|
464
|
+
onClose: ()=>setReader(null),
|
|
465
|
+
maxWidth: "sm",
|
|
466
|
+
fullWidth: true,
|
|
467
|
+
children: [
|
|
468
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
469
|
+
children: (_ref2 = reader == null ? void 0 : reader.formName) != null ? _ref2 : 'Form submission'
|
|
470
|
+
}),
|
|
471
|
+
/*#__PURE__*/ _jsxs(DialogContent, {
|
|
472
|
+
children: [
|
|
473
|
+
/*#__PURE__*/ _jsxs(Typography, {
|
|
474
|
+
variant: "caption",
|
|
475
|
+
color: "text.secondary",
|
|
476
|
+
children: [
|
|
477
|
+
`Received ${(_ref3 = reader == null ? void 0 : (_reader_createdAt = reader.createdAt) == null ? void 0 : (_reader_createdAt_toDate = _reader_createdAt.toDate) == null ? void 0 : _reader_createdAt_toDate.call(_reader_createdAt).toLocaleString()) != null ? _ref3 : ''}` + ((reader == null ? void 0 : reader.screenId) ? ` · screen ${reader.screenId}` : ''),
|
|
478
|
+
(reader == null ? void 0 : reader.path) ? ` · ${String(reader.path)}` : ''
|
|
479
|
+
]
|
|
480
|
+
}),
|
|
481
|
+
/*#__PURE__*/ _jsx(Divider, {
|
|
482
|
+
sx: {
|
|
483
|
+
my: 1.5
|
|
484
|
+
}
|
|
485
|
+
}),
|
|
486
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
487
|
+
spacing: 1.5,
|
|
488
|
+
children: Object.entries((_ref4 = reader == null ? void 0 : reader.fields) != null ? _ref4 : {}).map(([key, value])=>/*#__PURE__*/ _jsxs(Stack, {
|
|
489
|
+
spacing: 0.25,
|
|
490
|
+
children: [
|
|
491
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
492
|
+
variant: "caption",
|
|
493
|
+
color: "text.secondary",
|
|
494
|
+
children: key
|
|
495
|
+
}),
|
|
496
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
497
|
+
variant: "body2",
|
|
498
|
+
sx: {
|
|
499
|
+
whiteSpace: 'pre-wrap',
|
|
500
|
+
wordBreak: 'break-word'
|
|
501
|
+
},
|
|
502
|
+
children: String(value)
|
|
503
|
+
})
|
|
504
|
+
]
|
|
505
|
+
}, key))
|
|
506
|
+
}),
|
|
507
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
508
|
+
direction: "row",
|
|
509
|
+
spacing: 1,
|
|
510
|
+
sx: {
|
|
511
|
+
mt: 2,
|
|
512
|
+
flexWrap: 'wrap',
|
|
513
|
+
rowGap: 1
|
|
514
|
+
},
|
|
515
|
+
children: routingChips(reader == null ? void 0 : reader.routing).map((chip)=>/*#__PURE__*/ _jsx(Chip, {
|
|
516
|
+
size: "small",
|
|
517
|
+
label: chip.label,
|
|
518
|
+
color: chip.color,
|
|
519
|
+
variant: "outlined"
|
|
520
|
+
}, chip.label))
|
|
521
|
+
}),
|
|
522
|
+
(reader == null ? void 0 : reader.$id) ? /*#__PURE__*/ _jsx(Box, {
|
|
523
|
+
sx: {
|
|
524
|
+
mt: 2
|
|
525
|
+
},
|
|
526
|
+
children: /*#__PURE__*/ _jsx(InboxRecordAttributionZone, {
|
|
527
|
+
hostId: hostId,
|
|
528
|
+
recordKind: "form",
|
|
529
|
+
recordId: String(reader.$id)
|
|
530
|
+
})
|
|
531
|
+
}) : null,
|
|
532
|
+
reader ? /*#__PURE__*/ _jsx(SubmissionReply, {
|
|
533
|
+
hostId: hostId,
|
|
534
|
+
submission: reader
|
|
535
|
+
}) : null,
|
|
536
|
+
reader ? /*#__PURE__*/ _jsx(SubmissionListAssignment, {
|
|
537
|
+
hostId: hostId,
|
|
538
|
+
submission: reader
|
|
539
|
+
}) : null
|
|
540
|
+
]
|
|
541
|
+
}),
|
|
542
|
+
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
543
|
+
children: [
|
|
544
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
545
|
+
color: "error",
|
|
546
|
+
onClick: async ()=>{
|
|
547
|
+
const target = reader;
|
|
548
|
+
setReader(null);
|
|
549
|
+
if (target) await handleDelete(target)();
|
|
550
|
+
},
|
|
551
|
+
children: 'Delete'
|
|
552
|
+
}),
|
|
553
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
554
|
+
onClick: ()=>{
|
|
555
|
+
if (reader) void handleToggleRead(_extends({}, reader, {
|
|
556
|
+
read: true
|
|
557
|
+
}))();
|
|
558
|
+
setReader(null);
|
|
559
|
+
},
|
|
560
|
+
children: 'Mark unread'
|
|
561
|
+
}),
|
|
562
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
563
|
+
variant: "contained",
|
|
564
|
+
onClick: ()=>setReader(null),
|
|
565
|
+
children: 'Close'
|
|
566
|
+
})
|
|
567
|
+
]
|
|
568
|
+
})
|
|
569
|
+
]
|
|
570
|
+
})
|
|
571
|
+
]
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
SubmissionsCard.displayName = 'SubmissionsCard';
|
|
575
|
+
export default SubmissionsCard;
|
|
576
|
+
|
|
577
|
+
//# sourceMappingURL=submissions-card.component.js.map
|