feat: integrate files and portal surfaces
This commit is contained in:
@@ -112,6 +112,7 @@ const templateDefaults = (): TemplateDraft => ({
|
||||
address_pattern: "{template_slug}.{unit_slug}.{function_slug}",
|
||||
classification: "internal",
|
||||
allow_vacant_delivery: true,
|
||||
portal_visible: false,
|
||||
routing_policy: routingDefaults()
|
||||
});
|
||||
|
||||
@@ -121,7 +122,8 @@ const exactDefaults = (): ExactDraft => ({
|
||||
organization_unit_id: "",
|
||||
function_id: "",
|
||||
address_key: "",
|
||||
classification: "internal"
|
||||
classification: "internal",
|
||||
portal_visible: false
|
||||
});
|
||||
|
||||
export default function PostboxAdminPanel({
|
||||
@@ -270,6 +272,7 @@ export default function PostboxAdminPanel({
|
||||
address_pattern: revision.address_pattern,
|
||||
classification: revision.classification,
|
||||
allow_vacant_delivery: revision.allow_vacant_delivery,
|
||||
portal_visible: revision.portal_visible,
|
||||
routing_policy: revision.routing_policy ?? routingDefaults()
|
||||
};
|
||||
setTemplatePreview(null);
|
||||
@@ -1126,6 +1129,14 @@ function TemplateDialog({
|
||||
onChange={(checked) => onChange({ ...draft, allow_vacant_delivery: checked })}
|
||||
/>
|
||||
</div>
|
||||
<div className="postbox-toggle-field">
|
||||
<ToggleSwitch
|
||||
label="Show in Portal"
|
||||
help="Portal lists this Postbox only for users whose current function assignment already grants Postbox access."
|
||||
checked={draft.portal_visible}
|
||||
onChange={(checked) => onChange({ ...draft, portal_visible: checked })}
|
||||
/>
|
||||
</div>
|
||||
<div className="postbox-routing-section">
|
||||
<div className="postbox-routing-heading">
|
||||
<div>
|
||||
@@ -1472,6 +1483,14 @@ function ExactPostboxDialog({
|
||||
<FormField label="Description" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.description || ""} onChange={(event) => onChange({ ...draft, description: event.target.value })} />
|
||||
</FormField>
|
||||
<div className="postbox-toggle-field">
|
||||
<ToggleSwitch
|
||||
label="Show in Portal"
|
||||
help="Portal lists this Postbox only when the current user's function assignment grants access."
|
||||
checked={draft.portal_visible}
|
||||
onChange={(checked) => onChange({ ...draft, portal_visible: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -1578,6 +1597,7 @@ function revisionPayload(draft: TemplateDraft): PostboxTemplateRevisionPayload {
|
||||
address_pattern: draft.address_pattern,
|
||||
classification: draft.classification,
|
||||
allow_vacant_delivery: draft.allow_vacant_delivery,
|
||||
portal_visible: draft.portal_visible,
|
||||
routing_policy: draft.routing_policy
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Archive,
|
||||
Building2,
|
||||
CheckCheck,
|
||||
Download,
|
||||
Inbox,
|
||||
Layers3,
|
||||
MailOpen,
|
||||
@@ -45,15 +46,18 @@ import {
|
||||
createPostboxGrouping,
|
||||
createPostboxMessage,
|
||||
deletePostboxGrouping,
|
||||
downloadPostboxAttachment,
|
||||
getPostboxMessage,
|
||||
listPostboxGroupings,
|
||||
listPostboxMessages,
|
||||
listPostboxes,
|
||||
markPostboxMessage,
|
||||
resolvePostboxAttachments,
|
||||
replyToPostboxMessage,
|
||||
updatePostboxGrouping,
|
||||
type PostboxDirectoryItem,
|
||||
type PostboxGrouping,
|
||||
type PostboxAttachmentResolution,
|
||||
type PostboxMessage
|
||||
} from "../../api/postbox";
|
||||
import {
|
||||
@@ -106,6 +110,9 @@ export default function PostboxPage({
|
||||
const requestedMessageId = useRef(
|
||||
new URLSearchParams(window.location.search).get("message") ?? ""
|
||||
);
|
||||
const requestedPostboxId = useRef(
|
||||
new URLSearchParams(window.location.search).get("postbox") ?? ""
|
||||
);
|
||||
const requestedMessageLoaded = useRef(false);
|
||||
const [postboxes, setPostboxes] = useState<PostboxDirectoryItem[]>([]);
|
||||
const [groupings, setGroupings] = useState<PostboxGrouping[]>([]);
|
||||
@@ -114,6 +121,7 @@ export default function PostboxPage({
|
||||
const [messages, setMessages] = useState<PostboxMessage[]>([]);
|
||||
const [selectedMessageId, setSelectedMessageId] = useState("");
|
||||
const [selectedMessage, setSelectedMessage] = useState<PostboxMessage | null>(null);
|
||||
const [attachmentResolutions, setAttachmentResolutions] = useState<PostboxAttachmentResolution[]>([]);
|
||||
const [unavailableSelection, setUnavailableSelection] = useState("");
|
||||
const [total, setTotal] = useState(0);
|
||||
const [messageState, setMessageState] = useState<MessageStateFilter>("all");
|
||||
@@ -204,7 +212,9 @@ export default function PostboxPage({
|
||||
setSelectedPostboxId((current) =>
|
||||
current && nextPostboxes.some((postbox) => postbox.id === current)
|
||||
? current
|
||||
: ""
|
||||
: nextPostboxes.some((postbox) => postbox.id === requestedPostboxId.current)
|
||||
? requestedPostboxId.current
|
||||
: ""
|
||||
);
|
||||
} catch (loadError) {
|
||||
setError(errorMessage(loadError));
|
||||
@@ -316,14 +326,19 @@ export default function PostboxPage({
|
||||
if (message.availability === "available" && !message.read_at) {
|
||||
message = await markPostboxMessage(settings, selectedMessageId, "read");
|
||||
}
|
||||
const resolutions = message.attachments.length
|
||||
? await resolvePostboxAttachments(settings, message.id)
|
||||
: [];
|
||||
if (cancelled) return;
|
||||
setSelectedMessage(message);
|
||||
setAttachmentResolutions(resolutions);
|
||||
setMessages((items) =>
|
||||
items.map((item) => (item.id === message.id ? message : item))
|
||||
);
|
||||
} catch (loadError) {
|
||||
if (!cancelled && isApiError(loadError, 403, 404)) {
|
||||
setSelectedMessage(null);
|
||||
setAttachmentResolutions([]);
|
||||
setUnavailableSelection(
|
||||
"This message is no longer available or is outside your current Postbox assignments."
|
||||
);
|
||||
@@ -866,6 +881,12 @@ export default function PostboxPage({
|
||||
<MessageDetail
|
||||
message={selectedMessage}
|
||||
postbox={postboxes.find((item) => item.id === selectedMessage.postbox_id)}
|
||||
attachmentResolutions={attachmentResolutions}
|
||||
onDownload={(attachment) => {
|
||||
void downloadPostboxAttachment(settings, attachment).catch((downloadError) => {
|
||||
setError(errorMessage(downloadError));
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : unavailableSelection ? (
|
||||
<div className="postbox-empty postbox-unavailable-message">
|
||||
@@ -1099,10 +1120,14 @@ export default function PostboxPage({
|
||||
|
||||
function MessageDetail({
|
||||
message,
|
||||
postbox
|
||||
postbox,
|
||||
attachmentResolutions,
|
||||
onDownload
|
||||
}: {
|
||||
message: PostboxMessage;
|
||||
postbox?: PostboxDirectoryItem;
|
||||
attachmentResolutions: PostboxAttachmentResolution[];
|
||||
onDownload: (attachment: PostboxAttachmentResolution) => void;
|
||||
}) {
|
||||
const { language } = usePlatformLanguage();
|
||||
return (
|
||||
@@ -1166,15 +1191,30 @@ function MessageDetail({
|
||||
<section className="postbox-attachments">
|
||||
<h2>Evidence and attachments</h2>
|
||||
{!message.attachments.length ? <p>No attachment references.</p> : null}
|
||||
{message.attachments.map((attachment) => (
|
||||
{message.attachments.map((attachment) => {
|
||||
const resolution = attachmentResolutions.find(
|
||||
(item) => item.reference_type === attachment.reference_type
|
||||
&& item.reference_id === attachment.reference_id
|
||||
);
|
||||
return (
|
||||
<div key={`${attachment.reference_type}:${attachment.reference_id}`}>
|
||||
<Paperclip size={15} />
|
||||
<span>
|
||||
<strong>{attachment.name || attachment.reference_id}</strong>
|
||||
<strong>{resolution?.name || attachment.name || attachment.reference_id}</strong>
|
||||
<small>{attachment.reference_type}{attachment.media_type ? ` · ${attachment.media_type}` : ""}</small>
|
||||
{resolution && !resolution.available ? (
|
||||
<small>{attachmentResolutionExplanation(resolution.reason_code)}</small>
|
||||
) : null}
|
||||
</span>
|
||||
{resolution?.available ? (
|
||||
<IconButton
|
||||
icon={<Download size={15} />}
|
||||
label={`Download ${resolution.name || attachment.name || "attachment"}`}
|
||||
onClick={() => onDownload(resolution)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
)})}
|
||||
</section>
|
||||
{postbox?.access ? (
|
||||
<section className="postbox-access-explanation">
|
||||
@@ -1189,6 +1229,18 @@ function MessageDetail({
|
||||
);
|
||||
}
|
||||
|
||||
function attachmentResolutionExplanation(reasonCode: string): string {
|
||||
const explanations: Record<string, string> = {
|
||||
download_permission_missing: "Files download permission is required.",
|
||||
file_access_denied: "The referenced file is outside your current Files access.",
|
||||
file_not_found: "The referenced file or version no longer exists.",
|
||||
file_payload_missing: "The referenced file payload is unavailable.",
|
||||
files_provider_unavailable: "Files is not available in this installation.",
|
||||
reference_provider_unavailable: "No provider can open this evidence type."
|
||||
};
|
||||
return explanations[reasonCode] || "The referenced payload cannot currently be opened.";
|
||||
}
|
||||
|
||||
function sourceName(
|
||||
postboxes: PostboxDirectoryItem[],
|
||||
postboxId: string
|
||||
|
||||
Reference in New Issue
Block a user