@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,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
export interface SubmissionListAssignmentProps {
|
|
18
|
+
hostId: string;
|
|
19
|
+
/** The submission being read. Only its id is sent; the address is resolved
|
|
20
|
+
* server-side from the stored record. */
|
|
21
|
+
submission: {
|
|
22
|
+
$id: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare function SubmissionListAssignment(props: SubmissionListAssignmentProps): import("react").JSX.Element;
|
|
26
|
+
export declare namespace SubmissionListAssignment {
|
|
27
|
+
var displayName: string;
|
|
28
|
+
}
|
|
29
|
+
export default SubmissionListAssignment;
|
|
@@ -0,0 +1,257 @@
|
|
|
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
|
+
/**
|
|
20
|
+
* "Add to a marketing list", inside the submission reader.
|
|
21
|
+
*
|
|
22
|
+
* ## Its own card, below the reply, and never part of it
|
|
23
|
+
*
|
|
24
|
+
* A reply is transactional and a list is marketing. Two cards rather than a
|
|
25
|
+
* checkbox on the composer, because a checkbox beside Send is a control a
|
|
26
|
+
* merchant ticks while thinking about the message — and the thing it would do
|
|
27
|
+
* is put a person into an audience that will be mailed for as long as the
|
|
28
|
+
* list exists. The separation on screen is the same one the routes and the
|
|
29
|
+
* stored records keep.
|
|
30
|
+
*
|
|
31
|
+
* ## It states the consent facts before offering the control
|
|
32
|
+
*
|
|
33
|
+
* Every sentence here comes from the server, which read the person's actual
|
|
34
|
+
* record. Nothing is computed locally: a component that decided for itself
|
|
35
|
+
* whether somebody may be added would be a second copy of the rule, on the
|
|
36
|
+
* one surface whose job is to tell the merchant the truth about what is about
|
|
37
|
+
* to happen. That is the defect this feature was held back to avoid — a
|
|
38
|
+
* screen claiming a check that did not happen.
|
|
39
|
+
*
|
|
40
|
+
* ## Nothing is read until the merchant asks
|
|
41
|
+
*
|
|
42
|
+
* The options are fetched by pressing the button, never on mount. The reader
|
|
43
|
+
* dialog opens on every submission a merchant reads; charging all of them
|
|
44
|
+
* three reads and a query for a feature most will not use is the
|
|
45
|
+
* read-on-mount shape this codebase refuses.
|
|
46
|
+
*/ import { pluginDocsHelp } from "@aglyn/aglyn/app-utils/docs-help";
|
|
47
|
+
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
48
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
49
|
+
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
50
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
51
|
+
import { Alert, Box, Button, Checkbox, FormControlLabel, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
|
52
|
+
import { useCallback, useEffect, useState } from "react";
|
|
53
|
+
export function SubmissionListAssignment(props) {
|
|
54
|
+
const { hostId, submission } = props;
|
|
55
|
+
const { data: user } = useUser();
|
|
56
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
57
|
+
const [options, setOptions] = useState(null);
|
|
58
|
+
const [loadError, setLoadError] = useState('');
|
|
59
|
+
const [listId, setListId] = useState('');
|
|
60
|
+
const [attested, setAttested] = useState(false);
|
|
61
|
+
const [busy, setBusy] = useState(false);
|
|
62
|
+
const [added, setAdded] = useState('');
|
|
63
|
+
/*
|
|
64
|
+
* Every answer belongs to the submission that produced it. The reader keeps
|
|
65
|
+
* this component mounted while the merchant moves between rows, so state
|
|
66
|
+
* left over from the previous sender would offer one person's lists — and
|
|
67
|
+
* one person's attestation — for the next.
|
|
68
|
+
*/ useEffect(()=>{
|
|
69
|
+
setOptions(null);
|
|
70
|
+
setLoadError('');
|
|
71
|
+
setListId('');
|
|
72
|
+
setAttested(false);
|
|
73
|
+
setAdded('');
|
|
74
|
+
}, [
|
|
75
|
+
submission == null ? void 0 : submission.$id
|
|
76
|
+
]);
|
|
77
|
+
const post = useCallback(async (route, body)=>{
|
|
78
|
+
const response = await authorizedFetch(user, `/api/inbox/${route}`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json'
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(_extends({
|
|
84
|
+
hostId,
|
|
85
|
+
submissionId: submission.$id
|
|
86
|
+
}, body))
|
|
87
|
+
});
|
|
88
|
+
const payload = await response.json().catch(()=>({}));
|
|
89
|
+
return {
|
|
90
|
+
ok: response.ok,
|
|
91
|
+
payload
|
|
92
|
+
};
|
|
93
|
+
}, [
|
|
94
|
+
user,
|
|
95
|
+
hostId,
|
|
96
|
+
submission == null ? void 0 : submission.$id
|
|
97
|
+
]);
|
|
98
|
+
const handleLoad = useCallback(async ()=>{
|
|
99
|
+
if (busy) return;
|
|
100
|
+
setBusy(true);
|
|
101
|
+
try {
|
|
102
|
+
const { ok, payload } = await post('list-options', {});
|
|
103
|
+
if (!ok) {
|
|
104
|
+
var _ref;
|
|
105
|
+
return setLoadError((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The lists could not be read.');
|
|
106
|
+
}
|
|
107
|
+
setOptions(payload);
|
|
108
|
+
} catch (unused) {
|
|
109
|
+
setLoadError('The lists could not be read.');
|
|
110
|
+
} finally{
|
|
111
|
+
setBusy(false);
|
|
112
|
+
}
|
|
113
|
+
}, [
|
|
114
|
+
busy,
|
|
115
|
+
post
|
|
116
|
+
]);
|
|
117
|
+
const handleAdd = useCallback(async ()=>{
|
|
118
|
+
if (busy || !listId) return;
|
|
119
|
+
setBusy(true);
|
|
120
|
+
try {
|
|
121
|
+
var _ref, _ref1;
|
|
122
|
+
const { ok, payload } = await post('assign-list', {
|
|
123
|
+
listId,
|
|
124
|
+
// Sent as the merchant's assertion, not as a basis they named: the
|
|
125
|
+
// server derives a pass-through from the person's own record and this
|
|
126
|
+
// flag can only ever add the attributable kind.
|
|
127
|
+
attestConsent: attested
|
|
128
|
+
});
|
|
129
|
+
if (!ok) {
|
|
130
|
+
var _ref2;
|
|
131
|
+
return void enqueueSnackbar((_ref2 = payload == null ? void 0 : payload.error) != null ? _ref2 : 'They were not added to the list', {
|
|
132
|
+
variant: 'warning',
|
|
133
|
+
allowDuplicate: true
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
setAdded(String((_ref = payload == null ? void 0 : payload.listName) != null ? _ref : 'the list'));
|
|
137
|
+
enqueueSnackbar(`Added to ${(_ref1 = payload == null ? void 0 : payload.listName) != null ? _ref1 : 'the list'}`, {
|
|
138
|
+
variant: 'success',
|
|
139
|
+
persist: false
|
|
140
|
+
});
|
|
141
|
+
} catch (unused) {
|
|
142
|
+
enqueueSnackbar('They were not added to the list', {
|
|
143
|
+
variant: 'warning',
|
|
144
|
+
allowDuplicate: true
|
|
145
|
+
});
|
|
146
|
+
} finally{
|
|
147
|
+
setBusy(false);
|
|
148
|
+
}
|
|
149
|
+
}, [
|
|
150
|
+
busy,
|
|
151
|
+
listId,
|
|
152
|
+
attested,
|
|
153
|
+
post,
|
|
154
|
+
enqueueSnackbar
|
|
155
|
+
]);
|
|
156
|
+
return(// `header`, outlined, and with its gutters on — see the same block in
|
|
157
|
+
// `submission-reply.component`, which sits directly above this one in the
|
|
158
|
+
// submission reader and had the identical defect.
|
|
159
|
+
/*#__PURE__*/ _jsx(CardDisplay, {
|
|
160
|
+
header: 'Add to a marketing list',
|
|
161
|
+
// A headered card carries help (AGL-2213). The topic is campaigns, not
|
|
162
|
+
// forms: what this card does to somebody is put them in an audience, and
|
|
163
|
+
// the rules about that live with the audience.
|
|
164
|
+
help: pluginDocsHelp('emailCampaigns', {
|
|
165
|
+
anchor: '#add-to-a-list',
|
|
166
|
+
excerpt: 'Put this sender on a marketing list. A list is what a campaign ' + 'sends to; adding somebody sends them nothing now.'
|
|
167
|
+
}),
|
|
168
|
+
variant: "outlined",
|
|
169
|
+
contentGutterX: true,
|
|
170
|
+
contentGutterY: true,
|
|
171
|
+
contentBordered: "top",
|
|
172
|
+
sx: {
|
|
173
|
+
mt: 2
|
|
174
|
+
},
|
|
175
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
176
|
+
spacing: 2,
|
|
177
|
+
children: [
|
|
178
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
179
|
+
variant: "body2",
|
|
180
|
+
color: "text.secondary",
|
|
181
|
+
children: 'A list is used by campaigns. Adding someone here sends them ' + 'nothing now, and replying to them never adds them.'
|
|
182
|
+
}),
|
|
183
|
+
added ? /*#__PURE__*/ _jsx(Alert, {
|
|
184
|
+
severity: "success",
|
|
185
|
+
children: `Added to ${added}.`
|
|
186
|
+
}) : null,
|
|
187
|
+
!options && !loadError ? /*#__PURE__*/ _jsx(Box, {
|
|
188
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
189
|
+
variant: "outlined",
|
|
190
|
+
disabled: busy,
|
|
191
|
+
onClick: handleLoad,
|
|
192
|
+
children: busy ? 'Checking…' : 'Add to a list'
|
|
193
|
+
})
|
|
194
|
+
}) : null,
|
|
195
|
+
loadError ? /*#__PURE__*/ _jsx(Alert, {
|
|
196
|
+
severity: "warning",
|
|
197
|
+
children: loadError
|
|
198
|
+
}) : null,
|
|
199
|
+
options ? /*#__PURE__*/ _jsxs(Stack, {
|
|
200
|
+
spacing: 2,
|
|
201
|
+
children: [
|
|
202
|
+
/*#__PURE__*/ _jsx(Alert, {
|
|
203
|
+
severity: options.enrollable ? 'info' : 'warning',
|
|
204
|
+
children: options.summary
|
|
205
|
+
}),
|
|
206
|
+
options.enrollable ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
207
|
+
children: [
|
|
208
|
+
options.lists.length ? /*#__PURE__*/ _jsx(TextField, {
|
|
209
|
+
select: true,
|
|
210
|
+
label: "List",
|
|
211
|
+
value: listId,
|
|
212
|
+
onChange: (event)=>setListId(event.target.value),
|
|
213
|
+
fullWidth: true,
|
|
214
|
+
size: "small",
|
|
215
|
+
children: options.lists.map((list)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
216
|
+
value: list.id,
|
|
217
|
+
children: list.name
|
|
218
|
+
}, list.id))
|
|
219
|
+
}) : /*#__PURE__*/ _jsx(Alert, {
|
|
220
|
+
severity: "info",
|
|
221
|
+
children: 'This organization has no marketing lists yet.'
|
|
222
|
+
}),
|
|
223
|
+
options.listsTruncated ? /*#__PURE__*/ _jsx(Typography, {
|
|
224
|
+
variant: "caption",
|
|
225
|
+
color: "text.secondary",
|
|
226
|
+
children: 'Showing the first lists only. This organization has more.'
|
|
227
|
+
}) : null,
|
|
228
|
+
options.requiresAttestation ? /*#__PURE__*/ _jsx(FormControlLabel, {
|
|
229
|
+
control: /*#__PURE__*/ _jsx(Checkbox, {
|
|
230
|
+
checked: attested,
|
|
231
|
+
onChange: (event)=>setAttested(event.target.checked)
|
|
232
|
+
}),
|
|
233
|
+
label: /*#__PURE__*/ _jsx(Typography, {
|
|
234
|
+
variant: "body2",
|
|
235
|
+
children: `I have ${options.to}'s permission to send them ` + 'marketing email. This is recorded against my ' + 'account, with the date.'
|
|
236
|
+
})
|
|
237
|
+
}) : null,
|
|
238
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
239
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
240
|
+
variant: "contained",
|
|
241
|
+
disabled: busy || !listId || options.requiresAttestation && !attested,
|
|
242
|
+
onClick: handleAdd,
|
|
243
|
+
children: busy ? 'Adding…' : 'Add to list'
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
]
|
|
247
|
+
}) : null
|
|
248
|
+
]
|
|
249
|
+
}) : null
|
|
250
|
+
]
|
|
251
|
+
})
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
SubmissionListAssignment.displayName = 'SubmissionListAssignment';
|
|
255
|
+
export default SubmissionListAssignment;
|
|
256
|
+
|
|
257
|
+
//# sourceMappingURL=submission-list-assignment.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/submission-list-assignment.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\n'use client'\n\n/**\n * \"Add to a marketing list\", inside the submission reader.\n *\n * ## Its own card, below the reply, and never part of it\n *\n * A reply is transactional and a list is marketing. Two cards rather than a\n * checkbox on the composer, because a checkbox beside Send is a control a\n * merchant ticks while thinking about the message — and the thing it would do\n * is put a person into an audience that will be mailed for as long as the\n * list exists. The separation on screen is the same one the routes and the\n * stored records keep.\n *\n * ## It states the consent facts before offering the control\n *\n * Every sentence here comes from the server, which read the person's actual\n * record. Nothing is computed locally: a component that decided for itself\n * whether somebody may be added would be a second copy of the rule, on the\n * one surface whose job is to tell the merchant the truth about what is about\n * to happen. That is the defect this feature was held back to avoid — a\n * screen claiming a check that did not happen.\n *\n * ## Nothing is read until the merchant asks\n *\n * The options are fetched by pressing the button, never on mount. The reader\n * dialog opens on every submission a merchant reads; charging all of them\n * three reads and a query for a feature most will not use is the\n * read-on-mount shape this codebase refuses.\n */\n\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n Alert,\n Box,\n Button,\n Checkbox,\n FormControlLabel,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\n\nexport interface SubmissionListAssignmentProps {\n hostId: string\n /** The submission being read. Only its id is sent; the address is resolved\n * server-side from the stored record. */\n submission: { $id: string }\n}\n\n/** What `inbox/list-options` answers. Mirrors the handler's payload. */\ninterface ListOptions {\n to: string\n lists: Array<{ id: string; name: string }>\n listsTruncated?: boolean\n enrollable: boolean\n requiresAttestation: boolean\n summary: string\n}\n\nexport function SubmissionListAssignment(props: SubmissionListAssignmentProps) {\n const { hostId, submission } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n\n const [options, setOptions] = useState<ListOptions | null>(null)\n const [loadError, setLoadError] = useState('')\n const [listId, setListId] = useState('')\n const [attested, setAttested] = useState(false)\n const [busy, setBusy] = useState(false)\n const [added, setAdded] = useState('')\n\n /*\n * Every answer belongs to the submission that produced it. The reader keeps\n * this component mounted while the merchant moves between rows, so state\n * left over from the previous sender would offer one person's lists — and\n * one person's attestation — for the next.\n */\n useEffect(() => {\n setOptions(null)\n setLoadError('')\n setListId('')\n setAttested(false)\n setAdded('')\n }, [submission?.$id])\n\n const post = useCallback(\n async (route: string, body: Record<string, unknown>) => {\n const response = await authorizedFetch(user, `/api/inbox/${route}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, submissionId: submission.$id, ...body }),\n })\n const payload = await response.json().catch(() => ({}))\n return { ok: response.ok, payload }\n },\n [user, hostId, submission?.$id],\n )\n\n const handleLoad = useCallback(async () => {\n if (busy) return\n setBusy(true)\n try {\n const { ok, payload } = await post('list-options', {})\n if (!ok) {\n return setLoadError(payload?.error ?? 'The lists could not be read.')\n }\n setOptions(payload as ListOptions)\n } catch {\n setLoadError('The lists could not be read.')\n } finally {\n setBusy(false)\n }\n }, [busy, post])\n\n const handleAdd = useCallback(async () => {\n if (busy || !listId) return\n setBusy(true)\n try {\n const { ok, payload } = await post('assign-list', {\n listId,\n // Sent as the merchant's assertion, not as a basis they named: the\n // server derives a pass-through from the person's own record and this\n // flag can only ever add the attributable kind.\n attestConsent: attested,\n })\n if (!ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'They were not added to the list',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setAdded(String(payload?.listName ?? 'the list'))\n enqueueSnackbar(`Added to ${payload?.listName ?? 'the list'}`, {\n variant: 'success',\n persist: false,\n })\n } catch {\n enqueueSnackbar('They were not added to the list', {\n variant: 'warning',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, listId, attested, post, enqueueSnackbar])\n\n return (\n // `header`, outlined, and with its gutters on — see the same block in\n // `submission-reply.component`, which sits directly above this one in the\n // submission reader and had the identical defect.\n <CardDisplay\n header={'Add to a marketing list'}\n // A headered card carries help (AGL-2213). The topic is campaigns, not\n // forms: what this card does to somebody is put them in an audience, and\n // the rules about that live with the audience.\n help={pluginDocsHelp('emailCampaigns', {\n anchor: '#add-to-a-list',\n excerpt:\n 'Put this sender on a marketing list. A list is what a campaign ' +\n 'sends to; adding somebody sends them nothing now.',\n })}\n variant=\"outlined\"\n contentGutterX\n contentGutterY\n contentBordered=\"top\"\n sx={{ mt: 2 }}\n >\n <Stack spacing={2}>\n {/*\n Said before anything is loaded, because it is the sentence that\n keeps the two acts apart in the merchant's head as well as in the\n data. Replying to somebody does not put them on a list, and this\n does not send them anything.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'A list is used by campaigns. Adding someone here sends them ' +\n 'nothing now, and replying to them never adds them.'}\n </Typography>\n\n {added ? (\n <Alert severity=\"success\">{`Added to ${added}.`}</Alert>\n ) : null}\n\n {!options && !loadError ? (\n <Box>\n <Button variant=\"outlined\" disabled={busy} onClick={handleLoad}>\n {busy ? 'Checking…' : 'Add to a list'}\n </Button>\n </Box>\n ) : null}\n\n {loadError ? <Alert severity=\"warning\">{loadError}</Alert> : null}\n\n {options ? (\n <Stack spacing={2}>\n {/*\n The consent facts, always, and above the control. A merchant who\n is about to add somebody to a marketing audience is entitled to\n know what the product actually knows about that person's\n permission — including, and especially, that it knows nothing.\n */}\n <Alert severity={options.enrollable ? 'info' : 'warning'}>\n {options.summary}\n </Alert>\n\n {options.enrollable ? (\n <>\n {options.lists.length ? (\n <TextField\n select\n label=\"List\"\n value={listId}\n onChange={(event) => setListId(event.target.value)}\n fullWidth\n size=\"small\"\n >\n {options.lists.map((list) => (\n <MenuItem key={list.id} value={list.id}>\n {list.name}\n </MenuItem>\n ))}\n </TextField>\n ) : (\n <Alert severity=\"info\">\n {'This organization has no marketing lists yet.'}\n </Alert>\n )}\n {options.listsTruncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Showing the first lists only. This organization has more.'}\n </Typography>\n ) : null}\n\n {/*\n Shown on exactly one state: no record either way. A stored\n opt-in needs no assertion, and a stored refusal is not\n something an assertion may override — offering the control\n there would present a door that is not there.\n */}\n {options.requiresAttestation ? (\n <FormControlLabel\n control={\n <Checkbox\n checked={attested}\n onChange={(event) => setAttested(event.target.checked)}\n />\n }\n label={\n <Typography variant=\"body2\">\n {`I have ${options.to}'s permission to send them ` +\n 'marketing email. This is recorded against my ' +\n 'account, with the date.'}\n </Typography>\n }\n />\n ) : null}\n\n <Box>\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n !listId ||\n (options.requiresAttestation && !attested)\n }\n onClick={handleAdd}\n >\n {busy ? 'Adding…' : 'Add to list'}\n </Button>\n </Box>\n </>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nSubmissionListAssignment.displayName = 'SubmissionListAssignment'\n\nexport default SubmissionListAssignment\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","useUser","authorizedFetch","Alert","Box","Button","Checkbox","FormControlLabel","MenuItem","Stack","TextField","Typography","useCallback","useEffect","useState","SubmissionListAssignment","props","hostId","submission","data","user","enqueueSnackbar","options","setOptions","loadError","setLoadError","listId","setListId","attested","setAttested","busy","setBusy","added","setAdded","$id","post","route","body","response","method","headers","JSON","stringify","submissionId","payload","json","catch","ok","handleLoad","error","handleAdd","attestConsent","variant","allowDuplicate","String","listName","persist","header","help","anchor","excerpt","contentGutterX","contentGutterY","contentBordered","sx","mt","spacing","color","severity","disabled","onClick","enrollable","summary","lists","length","select","label","value","onChange","event","target","fullWidth","size","map","list","id","name","listsTruncated","requiresAttestation","control","checked","to","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
export interface SubmissionReplyProps {
|
|
18
|
+
hostId: string;
|
|
19
|
+
/** The submission being read, with `$id` and `fields`. */
|
|
20
|
+
submission: {
|
|
21
|
+
$id: string;
|
|
22
|
+
fields?: Record<string, unknown>;
|
|
23
|
+
formName?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function SubmissionReply(props: SubmissionReplyProps): import("react").JSX.Element;
|
|
27
|
+
export declare namespace SubmissionReply {
|
|
28
|
+
var displayName: string;
|
|
29
|
+
}
|
|
30
|
+
export default SubmissionReply;
|
|
@@ -0,0 +1,249 @@
|
|
|
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
/**
|
|
19
|
+
* The reply composer, inside the submission reader.
|
|
20
|
+
*
|
|
21
|
+
* ## The one sentence this component exists to say
|
|
22
|
+
*
|
|
23
|
+
* "Replies come back to your email, not here." Nothing in this platform
|
|
24
|
+
* receives mail, so a customer who answers reaches the merchant's own
|
|
25
|
+
* mailbox and their answer never appears in the Inbox. A composer that
|
|
26
|
+
* looked like a mail client without saying so would teach a merchant to wait
|
|
27
|
+
* here for a reply that is already sitting somewhere else — which is a worse
|
|
28
|
+
* outcome than not shipping the button.
|
|
29
|
+
*
|
|
30
|
+
* That is also why the sent replies below are labelled as what was sent
|
|
31
|
+
* rather than as a conversation: this list is one-sided by construction.
|
|
32
|
+
*/ import { pluginDocsHelp } from "@aglyn/aglyn/app-utils/docs-help";
|
|
33
|
+
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
34
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
35
|
+
import { useFirestore, useFirestoreCollection, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
|
|
36
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
37
|
+
import { Alert, Box, Button, Stack, TextField, Typography } from "@mui/material";
|
|
38
|
+
import { collection, doc, limit, orderBy, query } from "firebase/firestore";
|
|
39
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
40
|
+
import { REPLY_BODY_MAX, REPLY_SUBJECT_MAX, defaultReplySubject, replyRecipient } from "../model/reply-policy.js";
|
|
41
|
+
/** How many past replies the panel lists. A submission answered more than a
|
|
42
|
+
* handful of times is a conversation that belongs in a real mailbox. */ const SENT_REPLIES_LIMIT = 10;
|
|
43
|
+
export function SubmissionReply(props) {
|
|
44
|
+
var _ref, _ref1, _ref2;
|
|
45
|
+
const { hostId, submission } = props;
|
|
46
|
+
const firestore = useFirestore();
|
|
47
|
+
const { data: user } = useUser();
|
|
48
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
49
|
+
/*
|
|
50
|
+
* The site's name leads the default subject, and reading it here rather
|
|
51
|
+
* than on the Inbox page is what keeps it off the mount path: this
|
|
52
|
+
* component exists only while a submission is open, so the read is paid
|
|
53
|
+
* when a merchant asks to reply and not once per visit to the page.
|
|
54
|
+
*/ const { data: host } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId), [
|
|
55
|
+
firestore,
|
|
56
|
+
hostId
|
|
57
|
+
]);
|
|
58
|
+
const siteName = String((_ref = (_ref1 = host == null ? void 0 : host.displayName) != null ? _ref1 : host == null ? void 0 : host.subdomain) != null ? _ref : '');
|
|
59
|
+
const recipient = useMemo(()=>replyRecipient(submission == null ? void 0 : submission.fields), [
|
|
60
|
+
submission == null ? void 0 : submission.fields
|
|
61
|
+
]);
|
|
62
|
+
const canReply = 'email' in recipient;
|
|
63
|
+
const [subject, setSubject] = useState('');
|
|
64
|
+
const [message, setMessage] = useState('');
|
|
65
|
+
const [busy, setBusy] = useState(false);
|
|
66
|
+
// Re-seeded per submission rather than once: the reader dialog keeps this
|
|
67
|
+
// component mounted while the merchant moves between rows, so a subject
|
|
68
|
+
// computed on first mount would carry the previous sender's site into the
|
|
69
|
+
// next reply.
|
|
70
|
+
useEffect(()=>{
|
|
71
|
+
setSubject(defaultReplySubject(siteName, submission == null ? void 0 : submission.formName));
|
|
72
|
+
setMessage('');
|
|
73
|
+
}, [
|
|
74
|
+
siteName,
|
|
75
|
+
submission == null ? void 0 : submission.$id,
|
|
76
|
+
submission == null ? void 0 : submission.formName
|
|
77
|
+
]);
|
|
78
|
+
const { data: sentReplies } = useFirestoreCollection(()=>(submission == null ? void 0 : submission.$id) ? query(collection(firestore, 'hosts', hostId, 'formSubmissions', submission.$id, 'replies'), orderBy('sentAtMs', 'desc'), // One past the window, so a thread longer than the cap can say
|
|
79
|
+
// so rather than silently dropping its oldest replies. The probe row
|
|
80
|
+
// is never rendered.
|
|
81
|
+
limit(SENT_REPLIES_LIMIT + 1)) : null, [
|
|
82
|
+
firestore,
|
|
83
|
+
hostId,
|
|
84
|
+
submission == null ? void 0 : submission.$id
|
|
85
|
+
], {
|
|
86
|
+
idField: '$id'
|
|
87
|
+
});
|
|
88
|
+
const handleSend = useCallback(async ()=>{
|
|
89
|
+
if (busy) return;
|
|
90
|
+
setBusy(true);
|
|
91
|
+
try {
|
|
92
|
+
var _ref;
|
|
93
|
+
const response = await authorizedFetch(user, '/api/inbox/reply', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/json'
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
hostId,
|
|
100
|
+
submissionId: submission.$id,
|
|
101
|
+
subject: subject.trim(),
|
|
102
|
+
message: message.trim()
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
const payload = await response.json().catch(()=>({}));
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
var _ref1;
|
|
108
|
+
return void enqueueSnackbar((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : 'The reply was not sent', {
|
|
109
|
+
variant: 'warning',
|
|
110
|
+
allowDuplicate: true
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
setMessage('');
|
|
114
|
+
enqueueSnackbar(`Reply sent to ${(_ref = payload == null ? void 0 : payload.to) != null ? _ref : 'the sender'}`, {
|
|
115
|
+
variant: 'success',
|
|
116
|
+
persist: false
|
|
117
|
+
});
|
|
118
|
+
} catch (unused) {
|
|
119
|
+
enqueueSnackbar('The reply was not sent', {
|
|
120
|
+
variant: 'warning',
|
|
121
|
+
allowDuplicate: true
|
|
122
|
+
});
|
|
123
|
+
} finally{
|
|
124
|
+
setBusy(false);
|
|
125
|
+
}
|
|
126
|
+
}, [
|
|
127
|
+
busy,
|
|
128
|
+
user,
|
|
129
|
+
hostId,
|
|
130
|
+
submission == null ? void 0 : submission.$id,
|
|
131
|
+
subject,
|
|
132
|
+
message,
|
|
133
|
+
enqueueSnackbar
|
|
134
|
+
]);
|
|
135
|
+
if (!canReply) {
|
|
136
|
+
return /*#__PURE__*/ _jsx(Alert, {
|
|
137
|
+
severity: "info",
|
|
138
|
+
sx: {
|
|
139
|
+
mt: 2
|
|
140
|
+
},
|
|
141
|
+
children: 'This submission has no email field, so there is nobody to reply to.'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return(/*
|
|
145
|
+
* `header`, NOT `title` — the bug class AGL-1140 named and
|
|
146
|
+
* `card-display-header.spec` guards, which this file escaped because that
|
|
147
|
+
* sweep read `apps/console` only. `title` is a valid DOM attribute, so it
|
|
148
|
+
* rides in on `CardProps`, compiles clean and renders a browser tooltip:
|
|
149
|
+
* the card drew with no heading at all, and its content sat against the
|
|
150
|
+
* card's edges because the gutters are opt-in, so inside the submission
|
|
151
|
+
* reader it read as an unexplained lighter slab rather than a section.
|
|
152
|
+
*
|
|
153
|
+
* Outlined for the same reason: this card is nested inside a Dialog, whose
|
|
154
|
+
* own surface is already raised, and a filled card on top of it is the
|
|
155
|
+
* paper-on-paper that made the slab look like a rendering fault. A border
|
|
156
|
+
* says "section" without a second background.
|
|
157
|
+
*/ /*#__PURE__*/ _jsx(CardDisplay, {
|
|
158
|
+
header: 'Reply',
|
|
159
|
+
// A headered card carries help (AGL-2213), and the guard that says so
|
|
160
|
+
// reads `libs/plugins` — which is how a card with the wrong prop, and
|
|
161
|
+
// therefore no header at all, went un-flagged here for so long.
|
|
162
|
+
help: pluginDocsHelp('forms', {
|
|
163
|
+
anchor: '#replying-to-a-submission',
|
|
164
|
+
excerpt: 'Answer a submission by email. Replies are transactional — they ' + 'add nobody to a list and never touch your campaign allowance.'
|
|
165
|
+
}),
|
|
166
|
+
variant: "outlined",
|
|
167
|
+
contentGutterX: true,
|
|
168
|
+
contentGutterY: true,
|
|
169
|
+
contentBordered: "top",
|
|
170
|
+
sx: {
|
|
171
|
+
mt: 2
|
|
172
|
+
},
|
|
173
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
174
|
+
spacing: 2,
|
|
175
|
+
children: [
|
|
176
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
177
|
+
variant: "body2",
|
|
178
|
+
color: "text.secondary",
|
|
179
|
+
children: `To ${recipient.email}`
|
|
180
|
+
}),
|
|
181
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
182
|
+
label: "Subject",
|
|
183
|
+
value: subject,
|
|
184
|
+
onChange: (event)=>setSubject(event.target.value.slice(0, REPLY_SUBJECT_MAX)),
|
|
185
|
+
fullWidth: true,
|
|
186
|
+
size: "small"
|
|
187
|
+
}),
|
|
188
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
189
|
+
label: "Message",
|
|
190
|
+
value: message,
|
|
191
|
+
onChange: (event)=>setMessage(event.target.value.slice(0, REPLY_BODY_MAX)),
|
|
192
|
+
fullWidth: true,
|
|
193
|
+
multiline: true,
|
|
194
|
+
minRows: 4,
|
|
195
|
+
placeholder: "Write your reply. The original message is quoted underneath it."
|
|
196
|
+
}),
|
|
197
|
+
/*#__PURE__*/ _jsx(Alert, {
|
|
198
|
+
severity: "info",
|
|
199
|
+
children: `Sent from your site's name at this platform's address, with replies ` + `directed to ${(_ref2 = user == null ? void 0 : user.email) != null ? _ref2 : 'your account email'}. Answers arrive ` + 'in your email, not in this Inbox.'
|
|
200
|
+
}),
|
|
201
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
202
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
203
|
+
variant: "contained",
|
|
204
|
+
disabled: busy || !subject.trim() || !message.trim(),
|
|
205
|
+
onClick: handleSend,
|
|
206
|
+
children: busy ? 'Sending…' : 'Send reply'
|
|
207
|
+
})
|
|
208
|
+
}),
|
|
209
|
+
(sentReplies == null ? void 0 : sentReplies.length) ? /*#__PURE__*/ _jsxs(Stack, {
|
|
210
|
+
spacing: 1,
|
|
211
|
+
children: [
|
|
212
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
213
|
+
variant: "overline",
|
|
214
|
+
color: "text.secondary",
|
|
215
|
+
children: 'Replies sent'
|
|
216
|
+
}),
|
|
217
|
+
sentReplies.length > SENT_REPLIES_LIMIT ? /*#__PURE__*/ _jsx(Typography, {
|
|
218
|
+
variant: "caption",
|
|
219
|
+
color: "text.secondary",
|
|
220
|
+
children: `Showing the ${SENT_REPLIES_LIMIT} most recent. This thread has more.`
|
|
221
|
+
}) : null,
|
|
222
|
+
sentReplies.slice(0, SENT_REPLIES_LIMIT).map((reply)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
223
|
+
spacing: 0.25,
|
|
224
|
+
children: [
|
|
225
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
226
|
+
variant: "caption",
|
|
227
|
+
color: "text.secondary",
|
|
228
|
+
children: `${new Date(reply.sentAtMs).toLocaleString()} · to ${reply.to}`
|
|
229
|
+
}),
|
|
230
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
231
|
+
variant: "body2",
|
|
232
|
+
sx: {
|
|
233
|
+
whiteSpace: 'pre-wrap',
|
|
234
|
+
wordBreak: 'break-word'
|
|
235
|
+
},
|
|
236
|
+
children: reply.message
|
|
237
|
+
})
|
|
238
|
+
]
|
|
239
|
+
}, reply.$id))
|
|
240
|
+
]
|
|
241
|
+
}) : null
|
|
242
|
+
]
|
|
243
|
+
})
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
SubmissionReply.displayName = 'SubmissionReply';
|
|
247
|
+
export default SubmissionReply;
|
|
248
|
+
|
|
249
|
+
//# sourceMappingURL=submission-reply.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/submission-reply.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\n'use client'\n\n/**\n * The reply composer, inside the submission reader.\n *\n * ## The one sentence this component exists to say\n *\n * \"Replies come back to your email, not here.\" Nothing in this platform\n * receives mail, so a customer who answers reaches the merchant's own\n * mailbox and their answer never appears in the Inbox. A composer that\n * looked like a mail client without saying so would teach a merchant to wait\n * here for a reply that is already sitting somewhere else — which is a worse\n * outcome than not shipping the button.\n *\n * That is also why the sent replies below are labelled as what was sent\n * rather than as a conversation: this list is one-sided by construction.\n */\n\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { Alert, Box, Button, Stack, TextField, Typography } from '@mui/material'\nimport { collection, doc, limit, orderBy, query } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport {\n REPLY_BODY_MAX,\n REPLY_SUBJECT_MAX,\n defaultReplySubject,\n replyRecipient,\n} from '../model/reply-policy'\n\n/** How many past replies the panel lists. A submission answered more than a\n * handful of times is a conversation that belongs in a real mailbox. */\nconst SENT_REPLIES_LIMIT = 10\n\nexport interface SubmissionReplyProps {\n hostId: string\n /** The submission being read, with `$id` and `fields`. */\n submission: { $id: string; fields?: Record<string, unknown>; formName?: string }\n}\n\nexport function SubmissionReply(props: SubmissionReplyProps) {\n const { hostId, submission } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n\n /*\n * The site's name leads the default subject, and reading it here rather\n * than on the Inbox page is what keeps it off the mount path: this\n * component exists only while a submission is open, so the read is paid\n * when a merchant asks to reply and not once per visit to the page.\n */\n const { data: host } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId),\n [firestore, hostId],\n )\n const siteName = String(host?.displayName ?? host?.subdomain ?? '')\n\n const recipient = useMemo(\n () => replyRecipient(submission?.fields),\n [submission?.fields],\n )\n const canReply = 'email' in recipient\n\n const [subject, setSubject] = useState('')\n const [message, setMessage] = useState('')\n const [busy, setBusy] = useState(false)\n\n // Re-seeded per submission rather than once: the reader dialog keeps this\n // component mounted while the merchant moves between rows, so a subject\n // computed on first mount would carry the previous sender's site into the\n // next reply.\n useEffect(() => {\n setSubject(defaultReplySubject(siteName, submission?.formName))\n setMessage('')\n }, [siteName, submission?.$id, submission?.formName])\n\n const { data: sentReplies } = useFirestoreCollection<any>(\n () =>\n submission?.$id\n ? query(\n collection(\n firestore,\n 'hosts',\n hostId,\n 'formSubmissions',\n submission.$id,\n 'replies',\n ),\n orderBy('sentAtMs', 'desc'),\n // One past the window, so a thread longer than the cap can say\n // so rather than silently dropping its oldest replies. The probe row\n // is never rendered.\n limit(SENT_REPLIES_LIMIT + 1),\n )\n : null,\n [firestore, hostId, submission?.$id],\n { idField: '$id' },\n )\n\n const handleSend = useCallback(async () => {\n if (busy) return\n setBusy(true)\n try {\n const response = await authorizedFetch(user, '/api/inbox/reply', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n submissionId: submission.$id,\n subject: subject.trim(),\n message: message.trim(),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(payload?.error ?? 'The reply was not sent', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n setMessage('')\n enqueueSnackbar(`Reply sent to ${payload?.to ?? 'the sender'}`, {\n variant: 'success',\n persist: false,\n })\n } catch {\n enqueueSnackbar('The reply was not sent', {\n variant: 'warning',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, user, hostId, submission?.$id, subject, message, enqueueSnackbar])\n\n if (!canReply) {\n return (\n <Alert severity=\"info\" sx={{ mt: 2 }}>\n {'This submission has no email field, so there is nobody to reply to.'}\n </Alert>\n )\n }\n\n return (\n /*\n * `header`, NOT `title` — the bug class AGL-1140 named and\n * `card-display-header.spec` guards, which this file escaped because that\n * sweep read `apps/console` only. `title` is a valid DOM attribute, so it\n * rides in on `CardProps`, compiles clean and renders a browser tooltip:\n * the card drew with no heading at all, and its content sat against the\n * card's edges because the gutters are opt-in, so inside the submission\n * reader it read as an unexplained lighter slab rather than a section.\n *\n * Outlined for the same reason: this card is nested inside a Dialog, whose\n * own surface is already raised, and a filled card on top of it is the\n * paper-on-paper that made the slab look like a rendering fault. A border\n * says \"section\" without a second background.\n */\n <CardDisplay\n header={'Reply'}\n // A headered card carries help (AGL-2213), and the guard that says so\n // reads `libs/plugins` — which is how a card with the wrong prop, and\n // therefore no header at all, went un-flagged here for so long.\n help={pluginDocsHelp('forms', {\n anchor: '#replying-to-a-submission',\n excerpt:\n 'Answer a submission by email. Replies are transactional — they ' +\n 'add nobody to a list and never touch your campaign allowance.',\n })}\n variant=\"outlined\"\n contentGutterX\n contentGutterY\n contentBordered=\"top\"\n sx={{ mt: 2 }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`To ${(recipient as { email: string }).email}`}\n </Typography>\n <TextField\n label=\"Subject\"\n value={subject}\n onChange={(event) => setSubject(event.target.value.slice(0, REPLY_SUBJECT_MAX))}\n fullWidth\n size=\"small\"\n />\n <TextField\n label=\"Message\"\n value={message}\n onChange={(event) => setMessage(event.target.value.slice(0, REPLY_BODY_MAX))}\n fullWidth\n multiline\n minRows={4}\n placeholder=\"Write your reply. The original message is quoted underneath it.\"\n />\n {/*\n The boundary, stated where the decision is made rather than in a\n docs page nobody opens mid-task. Both halves matter: the address the\n message LEAVES from is the platform's, because per-org sending\n domains do not exist yet, and the address an answer COMES BACK to is\n the merchant's own, which is the only reason the first half is\n tolerable.\n */}\n <Alert severity=\"info\">\n {`Sent from your site's name at this platform's address, with replies ` +\n `directed to ${user?.email ?? 'your account email'}. Answers arrive ` +\n 'in your email, not in this Inbox.'}\n </Alert>\n <Box>\n <Button\n variant=\"contained\"\n disabled={busy || !subject.trim() || !message.trim()}\n onClick={handleSend}\n >\n {busy ? 'Sending…' : 'Send reply'}\n </Button>\n </Box>\n {sentReplies?.length ? (\n <Stack spacing={1}>\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'Replies sent'}\n </Typography>\n {sentReplies.length > SENT_REPLIES_LIMIT ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Showing the ${SENT_REPLIES_LIMIT} most recent. This thread has more.`}\n </Typography>\n ) : null}\n {sentReplies.slice(0, SENT_REPLIES_LIMIT).map((reply: any) => (\n <Stack key={reply.$id} spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${new Date(reply.sentAtMs).toLocaleString()} · to ${reply.to}`}\n </Typography>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n >\n {reply.message}\n </Typography>\n </Stack>\n ))}\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nSubmissionReply.displayName = 'SubmissionReply'\n\nexport default SubmissionReply\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","useFirestore","useFirestoreCollection","useFirestoreDoc","useUser","authorizedFetch","Alert","Box","Button","Stack","TextField","Typography","collection","doc","limit","orderBy","query","useCallback","useEffect","useMemo","useState","REPLY_BODY_MAX","REPLY_SUBJECT_MAX","defaultReplySubject","replyRecipient","SENT_REPLIES_LIMIT","SubmissionReply","props","host","hostId","submission","firestore","data","user","enqueueSnackbar","siteName","String","displayName","subdomain","recipient","fields","canReply","subject","setSubject","message","setMessage","busy","setBusy","formName","$id","sentReplies","idField","handleSend","response","method","headers","body","JSON","stringify","submissionId","trim","payload","json","catch","ok","error","variant","allowDuplicate","to","persist","severity","sx","mt","header","help","anchor","excerpt","contentGutterX","contentGutterY","contentBordered","spacing","color","email","label","value","onChange","event","target","slice","fullWidth","size","multiline","minRows","placeholder","disabled","onClick","length","map","reply","Date","sentAtMs","toLocaleString","whiteSpace","wordBreak"],"mappings":"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"}
|