Refactor file handling to improve URL safety and enhance file viewing capabilities across components

This commit is contained in:
Marshal
2026-06-28 08:19:21 +00:00
parent b424de8436
commit da533a488d
11 changed files with 100 additions and 28 deletions

View File

@@ -32,6 +32,7 @@ import { isViewable } from "@edr/ui-common";
import { SectionCard } from "./SectionCard";
import { bookingsService } from "@/services/bookings.service";
import { fileViewUrl } from "@/constants/apiConfig";
import { useFileViewer } from "@/hooks/useFileViewer";
export interface ClearanceReviewSectionProps {
@@ -240,7 +241,7 @@ export function ClearanceReviewSection({
<>
{isViewable({
name: doc.file.name,
url: doc.file.url,
url: fileViewUrl(doc.file.id),
}) && (
<Tooltip label="View">
<Box
@@ -249,7 +250,7 @@ export function ClearanceReviewSection({
onClick={() =>
view({
name: doc.file!.name,
url: doc.file!.url,
url: fileViewUrl(doc.file!.id),
})
}
c="edr-green"
@@ -267,9 +268,7 @@ export function ClearanceReviewSection({
<Tooltip label="Download">
<Box
component="a"
href={doc.file.url}
target="_blank"
rel="noreferrer"
href={fileViewUrl(doc.file.id, true)}
c="edr-green"
style={{ display: "flex" }}
>
@@ -456,7 +455,10 @@ function DocReviewCard({
{meta.label}
</Badge>
{hasFile &&
isViewable({ name: doc.file!.name, url: doc.file!.url }) && (
isViewable({
name: doc.file!.name,
url: fileViewUrl(doc.file!.id),
}) && (
<Tooltip label="Preview document">
<Button
size="compact-xs"
@@ -464,7 +466,10 @@ function DocReviewCard({
radius="md"
leftSection={<Eye size={13} />}
onClick={() =>
onView({ name: doc.file!.name, url: doc.file!.url })
onView({
name: doc.file!.name,
url: fileViewUrl(doc.file!.id),
})
}
>
View

View File

@@ -34,6 +34,7 @@ import { isViewable } from "@edr/ui-common";
import { SectionCard } from "@/components/bookings/detail/SectionCard";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { contractsService } from "@/services/contracts.service";
import { fileViewUrl } from "@/constants/apiConfig";
import { useContractClearanceMutations } from "@/hooks/contracts/useContracts";
import { useFileViewer } from "@/hooks/useFileViewer";
@@ -261,7 +262,7 @@ export function ContractClearanceReviewSection({
<>
{isViewable({
name: doc.file.name,
url: doc.file.url,
url: fileViewUrl(doc.file.id),
}) && (
<Tooltip label="View">
<Box
@@ -270,7 +271,7 @@ export function ContractClearanceReviewSection({
onClick={() =>
view({
name: doc.file!.name,
url: doc.file!.url,
url: fileViewUrl(doc.file!.id),
})
}
c="edr-green"
@@ -288,9 +289,7 @@ export function ContractClearanceReviewSection({
<Tooltip label="Download">
<Box
component="a"
href={doc.file.url}
target="_blank"
rel="noreferrer"
href={fileViewUrl(doc.file.id, true)}
c="edr-green"
style={{ display: "flex" }}
>
@@ -548,7 +547,10 @@ function DocReviewCard({
{meta.label}
</Badge>
{hasFile &&
isViewable({ name: doc.file!.name, url: doc.file!.url }) && (
isViewable({
name: doc.file!.name,
url: fileViewUrl(doc.file!.id),
}) && (
<Tooltip label="Preview document">
<Button
size="compact-xs"
@@ -556,7 +558,10 @@ function DocReviewCard({
radius="md"
leftSection={<Eye size={13} />}
onClick={() =>
onView({ name: doc.file!.name, url: doc.file!.url })
onView({
name: doc.file!.name,
url: fileViewUrl(doc.file!.id),
})
}
>
View

View File

@@ -1,3 +1,15 @@
export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
// export const API_BASE_URL = 'http://localhost:3001';
/**
* URL that streams an uploaded file through the API by its UUID. Routes the
* bytes through `GET /api/files/:id` (served from MinIO with backend
* credentials) instead of a presigned MinIO URL — the latter is not reachable
* from the browser and breaks on the minio-js port-443 signature quirk. Serves
* inline for preview by default; pass `download` to force a save dialog.
*/
export function fileViewUrl(fileId: string, download = false): string {
const base = `${API_BASE_URL}/api/files/${fileId}`;
return download ? `${base}?download=1` : base;
}

View File

@@ -42,6 +42,7 @@ import {
humanize,
} from "@/components/customers";
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import { fileViewUrl } from "@/constants/apiConfig";
import { api } from "@/services/api";
import type {
CompanyProfile,
@@ -289,7 +290,7 @@ export default function CustomerDetailPage() {
cell: ({ row }) => (
<ActionIcon
component="a"
href={row.original.url ?? "#"}
href={fileViewUrl(row.original.id, true)}
variant="subtle"
color="gray"
aria-label="Download"

View File

@@ -23,6 +23,7 @@ import { useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { api } from "@/services/api";
import { fileViewUrl } from "@/constants/apiConfig";
import type { SubmitBookingResponse } from "@/services/bookings.service";
import type { Freight } from "@edr/types";
@@ -288,14 +289,16 @@ export function DraftBookingView({
action={
isUploaded && !allowReplace ? (
<IconSquare
href={file?.signedUrl ?? file?.url}
href={file ? fileViewUrl(file.id, true) : undefined}
icon={<Download size={16} />}
/>
) : (
<>
{isUploaded && (
<IconSquare
href={file?.signedUrl ?? file?.url}
href={
file ? fileViewUrl(file.id, true) : undefined
}
icon={<Download size={16} />}
/>
)}

View File

@@ -6,6 +6,7 @@ import { useNavigate } from "react-router-dom";
import { isViewable } from "@edr/ui-common";
import { api } from "@/services/api";
import { fileViewUrl } from "@/constants/apiConfig";
import { useFileViewer } from "@/hooks/useFileViewer";
import { paymentsService, type PaymentMethod } from "@/services/payments.service";
import type { Freight } from "@edr/types";
@@ -165,7 +166,7 @@ export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking })
<Group gap={6} wrap="nowrap">
{isViewable({
name: file.name,
url: file.signedUrl ?? file.url,
url: fileViewUrl(file.id),
mimeType: file.mimeType,
}) && (
<IconSquare
@@ -173,14 +174,14 @@ export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking })
onClick={() =>
view({
name: file.name,
url: file.signedUrl ?? file.url,
url: fileViewUrl(file.id),
mimeType: file.mimeType,
})
}
/>
)}
<IconSquare
href={file.signedUrl ?? file.url}
href={fileViewUrl(file.id, true)}
icon={<Download size={16} />}
/>
</Group>

View File

@@ -1,4 +1,5 @@
import { api } from "@/services/api";
import { fileViewUrl } from "@/constants/apiConfig";
import type { CreateBookingPayload } from "@/services/bookings.service";
import type { Freight } from "@edr/types";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -948,7 +949,11 @@ export default function EditBookingPage() {
<Group gap={6} wrap="nowrap">
{isUploaded && !selected && (
<IconSquare
href={uploadedFile?.signedUrl ?? uploadedFile?.url}
href={
uploadedFile
? fileViewUrl(uploadedFile.id, true)
: undefined
}
icon={<Download size={16} />}
/>
)}

View File

@@ -23,6 +23,7 @@ import type { Freight } from "@edr/types";
import { isViewable } from "@edr/ui-common";
import { IconSquare } from "../BookingDetailPage/components/Documents";
import { fileViewUrl } from "@/constants/apiConfig";
import { useFileViewer } from "@/hooks/useFileViewer";
import { OperationDatePicker } from "./OperationDatePicker";
import type { ClearanceFlowController } from "./useClearanceFlow";
@@ -169,16 +170,25 @@ export function ClearanceFlow({ booking, flow, footer }: ClearanceFlowProps) {
<Group gap={10} wrap="nowrap">
<StatusPill doc={doc} />
{doc.file &&
isViewable({ name: doc.file.name, url: doc.file.url }) && (
isViewable({
name: doc.file.name,
url: fileViewUrl(doc.file.id),
}) && (
<IconSquare
icon={<Eye size={15} />}
onClick={() =>
view({ name: doc.file!.name, url: doc.file!.url })
view({
name: doc.file!.name,
url: fileViewUrl(doc.file!.id),
})
}
/>
)}
{doc.file && (
<IconSquare href={doc.file.url} icon={<Download size={15} />} />
<IconSquare
href={fileViewUrl(doc.file.id, true)}
icon={<Download size={15} />}
/>
)}
{canUpload && doc.reviewStatus !== "APPROVED" && (
<FileButton
@@ -233,7 +243,10 @@ export function ClearanceFlow({ booking, flow, footer }: ClearanceFlowProps) {
{doc.label}
</Text>
{doc.file ? (
<IconSquare href={doc.file.url} icon={<Download size={15} />} />
<IconSquare
href={fileViewUrl(doc.file.id, true)}
icon={<Download size={15} />}
/>
) : (
<Text fz="12px" c="#9AA8B5">
Pending

View File

@@ -3,6 +3,7 @@ import { SmartFileInput } from "@edr/ui-common";
import { CheckCircle2, Download, FileText } from "lucide-react";
import { IconSquare } from "../BookingDetailPage/components/Documents";
import { fileViewUrl } from "@/constants/apiConfig";
import { labelForDocCode } from "./resubmitDocs";
import type { ResubmitFlowController } from "./useResubmitFlow";
@@ -69,7 +70,7 @@ export function ResubmitDocuments({ flow }: { flow: ResubmitFlowController }) {
</Text>
</Group>
<IconSquare
href={file.signedUrl ?? file.url}
href={fileViewUrl(file.id, true)}
icon={<Download size={15} />}
/>
</Group>