Add Postbox message authoring and concurrency
This commit is contained in:
+78
-20
@@ -40,6 +40,8 @@ export type PostboxDirectoryItem = {
|
||||
holder_count: number;
|
||||
vacant: boolean;
|
||||
access?: PostboxAccessDecision | null;
|
||||
resource_revision: number;
|
||||
etag: string;
|
||||
};
|
||||
|
||||
export type PostboxParticipant = {
|
||||
@@ -78,6 +80,8 @@ export type PostboxMessage = {
|
||||
producer_module?: string | null;
|
||||
producer_resource_type?: string | null;
|
||||
producer_resource_id?: string | null;
|
||||
in_reply_to_message_id?: string | null;
|
||||
replaces_message_id?: string | null;
|
||||
encryption_profile: string;
|
||||
key_epoch: number;
|
||||
ciphertext_ref?: string | null;
|
||||
@@ -92,6 +96,8 @@ export type PostboxGrouping = {
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
postbox_ids: string[];
|
||||
resource_revision: number;
|
||||
etag: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
@@ -188,6 +194,8 @@ export type PostboxTemplate = {
|
||||
description?: string | null;
|
||||
status: string;
|
||||
current_revision: number;
|
||||
resource_revision: number;
|
||||
etag: string;
|
||||
published_revision_id?: string | null;
|
||||
revisions: PostboxTemplateRevision[];
|
||||
created_at: string;
|
||||
@@ -221,6 +229,21 @@ export type PostboxExactCreatePayload = {
|
||||
classification: string;
|
||||
};
|
||||
|
||||
export type PostboxMessageAuthoringPayload = {
|
||||
idempotency_key: string;
|
||||
subject: string;
|
||||
body_text?: string | null;
|
||||
classification: string;
|
||||
participants: PostboxParticipant[];
|
||||
attachments: PostboxAttachment[];
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type PostboxGroupingPayload = Pick<
|
||||
PostboxGrouping,
|
||||
"name" | "is_default" | "postbox_ids"
|
||||
>;
|
||||
|
||||
export async function listPostboxes(settings: ApiSettings): Promise<PostboxDirectoryItem[]> {
|
||||
const response = await apiFetch<{ postboxes: PostboxDirectoryItem[] }>(
|
||||
settings,
|
||||
@@ -271,6 +294,29 @@ export function markPostboxMessage(
|
||||
);
|
||||
}
|
||||
|
||||
export function createPostboxMessage(
|
||||
settings: ApiSettings,
|
||||
postboxId: string,
|
||||
payload: PostboxMessageAuthoringPayload
|
||||
): Promise<PostboxMessage> {
|
||||
return apiPostJson(settings, "/api/v1/postbox/messages", {
|
||||
...payload,
|
||||
postbox_id: postboxId
|
||||
});
|
||||
}
|
||||
|
||||
export function replyToPostboxMessage(
|
||||
settings: ApiSettings,
|
||||
messageId: string,
|
||||
payload: PostboxMessageAuthoringPayload
|
||||
): Promise<PostboxMessage> {
|
||||
return apiPostJson(
|
||||
settings,
|
||||
`/api/v1/postbox/messages/${encodeURIComponent(messageId)}/replies`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
export async function listPostboxGroupings(settings: ApiSettings): Promise<PostboxGrouping[]> {
|
||||
const response = await apiFetch<{ groupings: PostboxGrouping[] }>(
|
||||
settings,
|
||||
@@ -281,34 +327,39 @@ export async function listPostboxGroupings(settings: ApiSettings): Promise<Postb
|
||||
|
||||
export function createPostboxGrouping(
|
||||
settings: ApiSettings,
|
||||
payload: Omit<PostboxGrouping, "id" | "created_at" | "updated_at">
|
||||
payload: PostboxGroupingPayload
|
||||
): Promise<PostboxGrouping> {
|
||||
return apiPostJson(settings, "/api/v1/postbox/groupings", payload);
|
||||
}
|
||||
|
||||
export function updatePostboxGrouping(
|
||||
settings: ApiSettings,
|
||||
groupingId: string,
|
||||
payload: Omit<PostboxGrouping, "id" | "created_at" | "updated_at">
|
||||
grouping: PostboxGrouping,
|
||||
payload: PostboxGroupingPayload
|
||||
): Promise<PostboxGrouping> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/postbox/groupings/${encodeURIComponent(groupingId)}`,
|
||||
`/api/v1/postbox/groupings/${encodeURIComponent(grouping.id)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload)
|
||||
headers: { "If-Match": grouping.etag },
|
||||
body: JSON.stringify({ ...payload, base_revision: grouping.resource_revision })
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function deletePostboxGrouping(
|
||||
settings: ApiSettings,
|
||||
groupingId: string
|
||||
grouping: PostboxGrouping
|
||||
): Promise<void> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/postbox/groupings/${encodeURIComponent(groupingId)}`,
|
||||
{ method: "DELETE" }
|
||||
`/api/v1/postbox/groupings/${encodeURIComponent(grouping.id)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { "If-Match": grouping.etag },
|
||||
body: JSON.stringify({ base_revision: grouping.resource_revision })
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -338,12 +389,16 @@ export function createExactPostbox(
|
||||
|
||||
export function archivePostbox(
|
||||
settings: ApiSettings,
|
||||
postboxId: string
|
||||
postbox: PostboxDirectoryItem
|
||||
): Promise<PostboxDirectoryItem> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/postbox/admin/postboxes/${encodeURIComponent(postboxId)}`,
|
||||
{ method: "DELETE" }
|
||||
`/api/v1/postbox/admin/postboxes/${encodeURIComponent(postbox.id)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { "If-Match": postbox.etag },
|
||||
body: JSON.stringify({ base_revision: postbox.resource_revision })
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -364,36 +419,39 @@ export function createPostboxTemplate(
|
||||
|
||||
export function revisePostboxTemplate(
|
||||
settings: ApiSettings,
|
||||
templateId: string,
|
||||
template: PostboxTemplate,
|
||||
payload: PostboxTemplateRevisionPayload
|
||||
): Promise<PostboxTemplate> {
|
||||
return apiPostJson(
|
||||
settings,
|
||||
`/api/v1/postbox/admin/templates/${encodeURIComponent(templateId)}/revisions`,
|
||||
payload
|
||||
`/api/v1/postbox/admin/templates/${encodeURIComponent(template.id)}/revisions`,
|
||||
{ ...payload, base_revision: template.resource_revision },
|
||||
{ headers: { "If-Match": template.etag } }
|
||||
);
|
||||
}
|
||||
|
||||
export function publishPostboxTemplate(
|
||||
settings: ApiSettings,
|
||||
templateId: string,
|
||||
template: PostboxTemplate,
|
||||
revision?: number
|
||||
): Promise<PostboxTemplate> {
|
||||
return apiPostJson(
|
||||
settings,
|
||||
`/api/v1/postbox/admin/templates/${encodeURIComponent(templateId)}/publish`,
|
||||
{ revision: revision ?? null }
|
||||
`/api/v1/postbox/admin/templates/${encodeURIComponent(template.id)}/publish`,
|
||||
{ revision: revision ?? null, base_revision: template.resource_revision },
|
||||
{ headers: { "If-Match": template.etag } }
|
||||
);
|
||||
}
|
||||
|
||||
export function retirePostboxTemplate(
|
||||
settings: ApiSettings,
|
||||
templateId: string
|
||||
template: PostboxTemplate
|
||||
): Promise<PostboxTemplate> {
|
||||
return apiPostJson(
|
||||
settings,
|
||||
`/api/v1/postbox/admin/templates/${encodeURIComponent(templateId)}/retire`,
|
||||
{}
|
||||
`/api/v1/postbox/admin/templates/${encodeURIComponent(template.id)}/retire`,
|
||||
{ base_revision: template.resource_revision },
|
||||
{ headers: { "If-Match": template.etag } }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -236,9 +236,13 @@ export default function PostboxAdminPanel({
|
||||
setSuccess("");
|
||||
try {
|
||||
if (templateDraft.templateId) {
|
||||
const currentTemplate = templates.find(
|
||||
(item) => item.id === templateDraft.templateId
|
||||
);
|
||||
if (!currentTemplate) throw new Error("The template is no longer available.");
|
||||
await revisePostboxTemplate(
|
||||
settings,
|
||||
templateDraft.templateId,
|
||||
currentTemplate,
|
||||
revisionPayload(templateDraft)
|
||||
);
|
||||
setSuccess("A new immutable template revision was created.");
|
||||
@@ -268,7 +272,7 @@ export default function PostboxAdminPanel({
|
||||
try {
|
||||
await publishPostboxTemplate(
|
||||
settings,
|
||||
selectedTemplate.id,
|
||||
selectedTemplate,
|
||||
selectedTemplate.current_revision
|
||||
);
|
||||
setSuccess(`Published revision ${selectedTemplate.current_revision}.`);
|
||||
@@ -286,7 +290,7 @@ export default function PostboxAdminPanel({
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
await retirePostboxTemplate(settings, selectedTemplate.id);
|
||||
await retirePostboxTemplate(settings, selectedTemplate);
|
||||
setSuccess("Postbox template retired. Existing addresses remain durable.");
|
||||
await load();
|
||||
} catch (actionError) {
|
||||
@@ -365,7 +369,7 @@ export default function PostboxAdminPanel({
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
await archivePostbox(settings, archiveTarget.id);
|
||||
await archivePostbox(settings, archiveTarget);
|
||||
setSuccess("Postbox archived. Messages and delivery evidence were retained.");
|
||||
setArchiveTarget(null);
|
||||
await load();
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Reply,
|
||||
Search,
|
||||
Send,
|
||||
Trash2,
|
||||
UserRoundCheck,
|
||||
X
|
||||
@@ -34,12 +36,14 @@ import {
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
createPostboxGrouping,
|
||||
createPostboxMessage,
|
||||
deletePostboxGrouping,
|
||||
getPostboxMessage,
|
||||
listPostboxGroupings,
|
||||
listPostboxMessages,
|
||||
listPostboxes,
|
||||
markPostboxMessage,
|
||||
replyToPostboxMessage,
|
||||
updatePostboxGrouping,
|
||||
type PostboxDirectoryItem,
|
||||
type PostboxGrouping,
|
||||
@@ -56,6 +60,14 @@ type GroupingDraft = {
|
||||
|
||||
type MessageStateFilter = "all" | "unread" | "read" | "acknowledged";
|
||||
|
||||
type MessageDraft = {
|
||||
postbox_id: string;
|
||||
subject: string;
|
||||
body_text: string;
|
||||
classification: string;
|
||||
recipients: string;
|
||||
};
|
||||
|
||||
const emptyGrouping = (): GroupingDraft => ({
|
||||
id: "",
|
||||
name: "",
|
||||
@@ -63,6 +75,14 @@ const emptyGrouping = (): GroupingDraft => ({
|
||||
postbox_ids: []
|
||||
});
|
||||
|
||||
const emptyMessageDraft = (): MessageDraft => ({
|
||||
postbox_id: "",
|
||||
subject: "",
|
||||
body_text: "",
|
||||
classification: "internal",
|
||||
recipients: ""
|
||||
});
|
||||
|
||||
export default function PostboxPage({
|
||||
settings,
|
||||
auth
|
||||
@@ -94,8 +114,13 @@ export default function PostboxPage({
|
||||
const [error, setError] = useState("");
|
||||
const [groupingDialogOpen, setGroupingDialogOpen] = useState(false);
|
||||
const [groupingDraft, setGroupingDraft] = useState<GroupingDraft>(emptyGrouping);
|
||||
const [messageDialogOpen, setMessageDialogOpen] = useState(false);
|
||||
const [replyParent, setReplyParent] = useState<PostboxMessage | null>(null);
|
||||
const [messageDraft, setMessageDraft] = useState<MessageDraft>(emptyMessageDraft);
|
||||
|
||||
const canAcknowledge = hasScope(auth, "postbox:message:acknowledge");
|
||||
const canSend = hasScope(auth, "postbox:message:write");
|
||||
const canReply = hasScope(auth, "postbox:message:reply");
|
||||
const selectedPostbox = useMemo(
|
||||
() => postboxes.find((postbox) => postbox.id === selectedPostboxId) ?? null,
|
||||
[postboxes, selectedPostboxId]
|
||||
@@ -335,8 +360,9 @@ export default function PostboxPage({
|
||||
postbox_ids: groupingDraft.postbox_ids
|
||||
};
|
||||
try {
|
||||
const saved = groupingDraft.id
|
||||
? await updatePostboxGrouping(settings, groupingDraft.id, payload)
|
||||
const existing = groupings.find((item) => item.id === groupingDraft.id);
|
||||
const saved = existing
|
||||
? await updatePostboxGrouping(settings, existing, payload)
|
||||
: await createPostboxGrouping(settings, payload);
|
||||
await loadDirectory();
|
||||
setSelectedScope(saved.id);
|
||||
@@ -354,7 +380,9 @@ export default function PostboxPage({
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await deletePostboxGrouping(settings, groupingDraft.id);
|
||||
const existing = groupings.find((item) => item.id === groupingDraft.id);
|
||||
if (!existing) throw new Error("The grouping is no longer available.");
|
||||
await deletePostboxGrouping(settings, existing);
|
||||
setSelectedScope("all");
|
||||
setSelectedPostboxId("");
|
||||
setGroupingDialogOpen(false);
|
||||
@@ -366,6 +394,86 @@ export default function PostboxPage({
|
||||
}
|
||||
}
|
||||
|
||||
function openCompose() {
|
||||
const postbox = selectedPostbox ?? postboxes[0] ?? null;
|
||||
if (!postbox) return;
|
||||
setReplyParent(null);
|
||||
setMessageDraft({
|
||||
...emptyMessageDraft(),
|
||||
postbox_id: postbox.id,
|
||||
classification: postbox.classification
|
||||
});
|
||||
setMessageDialogOpen(true);
|
||||
}
|
||||
|
||||
function openReply() {
|
||||
if (!selectedMessage) return;
|
||||
setReplyParent(selectedMessage);
|
||||
setMessageDraft({
|
||||
...emptyMessageDraft(),
|
||||
postbox_id: selectedMessage.postbox_id,
|
||||
subject: selectedMessage.subject.toLowerCase().startsWith("re:")
|
||||
? selectedMessage.subject
|
||||
: `Re: ${selectedMessage.subject}`,
|
||||
classification: selectedMessage.classification
|
||||
});
|
||||
setMessageDialogOpen(true);
|
||||
}
|
||||
|
||||
async function submitMessage() {
|
||||
if (!messageDraft.postbox_id || !messageDraft.subject.trim()) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
const participants = messageDraft.recipients
|
||||
.split(/[;,\n]/)
|
||||
.map((address) => address.trim())
|
||||
.filter(Boolean)
|
||||
.map((address) => ({
|
||||
kind: "to",
|
||||
reference_type: "address",
|
||||
address
|
||||
}));
|
||||
const payload = {
|
||||
idempotency_key: crypto.randomUUID(),
|
||||
subject: messageDraft.subject.trim(),
|
||||
body_text: messageDraft.body_text || null,
|
||||
classification: messageDraft.classification,
|
||||
participants,
|
||||
attachments: [],
|
||||
metadata: {}
|
||||
};
|
||||
try {
|
||||
const message = replyParent
|
||||
? await replyToPostboxMessage(settings, replyParent.id, payload)
|
||||
: await createPostboxMessage(
|
||||
settings,
|
||||
messageDraft.postbox_id,
|
||||
payload
|
||||
);
|
||||
setMessageDialogOpen(false);
|
||||
setReplyParent(null);
|
||||
setSelectedPostboxId(message.postbox_id);
|
||||
setSelectedScope("all");
|
||||
setSelectedMessageId(message.id);
|
||||
setSelectedMessage(message);
|
||||
setPage(1);
|
||||
const refreshed = await listPostboxMessages(
|
||||
settings,
|
||||
[message.postbox_id],
|
||||
pageSize,
|
||||
0,
|
||||
messageQuery,
|
||||
messageState
|
||||
);
|
||||
setMessages(refreshed.messages);
|
||||
setTotal(refreshed.total);
|
||||
} catch (actionError) {
|
||||
setError(errorMessage(actionError));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="workspace-data-page module-entry-page postbox-page">
|
||||
<div className="postbox-shell">
|
||||
@@ -472,12 +580,20 @@ export default function PostboxPage({
|
||||
)}
|
||||
<span className="postbox-total">{total}</span>
|
||||
</div>
|
||||
<IconButton
|
||||
label="Refresh messages"
|
||||
icon={<RefreshCw size={16} />}
|
||||
onClick={() => void loadMessages()}
|
||||
disabled={loadingMessages || busy}
|
||||
/>
|
||||
<div className="postbox-icon-actions">
|
||||
<IconButton
|
||||
label="New message"
|
||||
icon={<Send size={16} />}
|
||||
onClick={openCompose}
|
||||
disabled={!canSend || !postboxes.length || busy}
|
||||
/>
|
||||
<IconButton
|
||||
label="Refresh messages"
|
||||
icon={<RefreshCw size={16} />}
|
||||
onClick={() => void loadMessages()}
|
||||
disabled={loadingMessages || busy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="postbox-message-filters">
|
||||
<div className="postbox-search-row">
|
||||
@@ -615,25 +731,38 @@ export default function PostboxPage({
|
||||
<MailOpen size={17} aria-hidden="true" />
|
||||
<strong>{selectedMessage?.subject || "Message"}</strong>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => void acknowledgeSelected()}
|
||||
disabled={
|
||||
!selectedMessage ||
|
||||
selectedMessage.availability !== "available" ||
|
||||
Boolean(selectedMessage.acknowledged_at) ||
|
||||
busy
|
||||
}
|
||||
disabledReason={
|
||||
!canAcknowledge
|
||||
? "You cannot acknowledge Postbox messages."
|
||||
: selectedMessage &&
|
||||
selectedMessage.availability !== "available"
|
||||
? "Withdrawn or expired messages cannot be acknowledged."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<CheckCheck size={16} /> Acknowledge
|
||||
</Button>
|
||||
<div className="button-row compact-actions">
|
||||
<Button
|
||||
onClick={openReply}
|
||||
disabled={
|
||||
!canReply ||
|
||||
!selectedMessage ||
|
||||
selectedMessage.availability !== "available" ||
|
||||
busy
|
||||
}
|
||||
>
|
||||
<Reply size={16} /> Reply
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void acknowledgeSelected()}
|
||||
disabled={
|
||||
!selectedMessage ||
|
||||
selectedMessage.availability !== "available" ||
|
||||
Boolean(selectedMessage.acknowledged_at) ||
|
||||
busy
|
||||
}
|
||||
disabledReason={
|
||||
!canAcknowledge
|
||||
? "You cannot acknowledge Postbox messages."
|
||||
: selectedMessage &&
|
||||
selectedMessage.availability !== "available"
|
||||
? "Withdrawn or expired messages cannot be acknowledged."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<CheckCheck size={16} /> Acknowledge
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{selectedMessage ? (
|
||||
<MessageDetail
|
||||
@@ -737,6 +866,119 @@ export default function PostboxPage({
|
||||
))}
|
||||
</fieldset>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={messageDialogOpen}
|
||||
title={replyParent ? "Reply" : "New message"}
|
||||
className="postbox-dialog postbox-message-dialog"
|
||||
onClose={() => setMessageDialogOpen(false)}
|
||||
closeDisabled={busy}
|
||||
footer={
|
||||
<div className="postbox-dialog-actions end">
|
||||
<Button onClick={() => setMessageDialogOpen(false)} disabled={busy}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void submitMessage()}
|
||||
disabled={
|
||||
busy ||
|
||||
!messageDraft.postbox_id ||
|
||||
!messageDraft.subject.trim()
|
||||
}
|
||||
>
|
||||
<Send size={16} /> Send
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="postbox-compose-grid">
|
||||
<FormField label="Postbox">
|
||||
<select
|
||||
value={messageDraft.postbox_id}
|
||||
disabled={Boolean(replyParent)}
|
||||
onChange={(event) => {
|
||||
const postbox = postboxes.find(
|
||||
(item) => item.id === event.target.value
|
||||
);
|
||||
setMessageDraft((current) => ({
|
||||
...current,
|
||||
postbox_id: event.target.value,
|
||||
classification: postbox?.classification ?? "internal"
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{postboxes.map((postbox) => (
|
||||
<option key={postbox.id} value={postbox.id}>
|
||||
{postbox.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Classification">
|
||||
<select
|
||||
value={messageDraft.classification}
|
||||
onChange={(event) =>
|
||||
setMessageDraft((current) => ({
|
||||
...current,
|
||||
classification: event.target.value
|
||||
}))
|
||||
}
|
||||
>
|
||||
{classificationOptions(
|
||||
postboxes.find((item) => item.id === messageDraft.postbox_id)
|
||||
?.classification ?? "internal",
|
||||
replyParent?.classification
|
||||
).map((classification) => (
|
||||
<option key={classification} value={classification}>
|
||||
{classificationLabel(classification)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<div className="postbox-compose-wide">
|
||||
<FormField label="Recipients">
|
||||
<input
|
||||
value={messageDraft.recipients}
|
||||
placeholder="Separate addresses with commas"
|
||||
onChange={(event) =>
|
||||
setMessageDraft((current) => ({
|
||||
...current,
|
||||
recipients: event.target.value
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="postbox-compose-wide">
|
||||
<FormField label="Subject">
|
||||
<input
|
||||
value={messageDraft.subject}
|
||||
onChange={(event) =>
|
||||
setMessageDraft((current) => ({
|
||||
...current,
|
||||
subject: event.target.value
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="postbox-compose-wide">
|
||||
<FormField label="Message">
|
||||
<textarea
|
||||
rows={10}
|
||||
value={messageDraft.body_text}
|
||||
onChange={(event) =>
|
||||
setMessageDraft((current) => ({
|
||||
...current,
|
||||
body_text: event.target.value
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -842,6 +1084,38 @@ function producerLabel(message: PostboxMessage): string {
|
||||
return resource.length ? resource.join(" / ") : "Platform-native message";
|
||||
}
|
||||
|
||||
const CLASSIFICATIONS = [
|
||||
"public",
|
||||
"internal",
|
||||
"confidential",
|
||||
"restricted"
|
||||
] as const;
|
||||
|
||||
function classificationOptions(
|
||||
postboxClassification: string,
|
||||
minimumClassification?: string
|
||||
): string[] {
|
||||
const maximum = Math.max(
|
||||
0,
|
||||
CLASSIFICATIONS.indexOf(
|
||||
postboxClassification as (typeof CLASSIFICATIONS)[number]
|
||||
)
|
||||
);
|
||||
const minimum = minimumClassification
|
||||
? Math.max(
|
||||
0,
|
||||
CLASSIFICATIONS.indexOf(
|
||||
minimumClassification as (typeof CLASSIFICATIONS)[number]
|
||||
)
|
||||
)
|
||||
: 0;
|
||||
return CLASSIFICATIONS.slice(minimum, maximum + 1);
|
||||
}
|
||||
|
||||
function classificationLabel(value: string): string {
|
||||
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
||||
}
|
||||
|
||||
function formatDate(value: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
|
||||
@@ -518,6 +518,31 @@
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.postbox-message-dialog {
|
||||
width: min(720px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.postbox-compose-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(160px, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.postbox-compose-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.postbox-compose-grid textarea {
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.postbox-dialog-actions.end {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.postbox-admin-page > .segmented-control {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@@ -667,4 +692,8 @@
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.postbox-compose-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user