feat(vessel-transfer): add document upload functionality to transfer requests

This commit is contained in:
estifanos
2026-07-24 08:29:37 +00:00
parent abbdc7668e
commit 47566dfe16
4 changed files with 111 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
export * from './lib/base-api';
export * from './lib/query-and-mutation';
export * from './lib/session';
export * from './lib/file-upload';
export { baseQueryWithReauth, configureTokenRefresh } from './lib/base-api/base-query-with-reauth';

View File

@@ -0,0 +1,87 @@
import { useCallback, useState } from 'react';
import { baseApi } from '../base-api';
import { resolveTokenFromStorage } from '../session';
export interface UploadKeyResponse<TFileInfo = unknown> {
presigned: string;
fileInfo: TFileInfo;
}
const fileUploadApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getFileUploadKey: builder.mutation<UploadKeyResponse, { endpoint: string; file: File }>({
query: ({ endpoint, file }) => ({
url: endpoint,
method: 'POST',
body: {
fileName: file.name,
contentType: file.type || 'application/octet-stream',
size: file.size,
originalname: file.name,
},
}),
}),
}),
overrideExisting: false,
});
export const { useGetFileUploadKeyMutation } = fileUploadApi;
export const STORAGE_UPLOAD_ERROR = 'STORAGE_UPLOAD_ERROR';
export const STORAGE_UPLOAD_TOO_LARGE = 'STORAGE_UPLOAD_TOO_LARGE';
export function isStorageUploadError(
error: unknown,
): error is Error & { message: typeof STORAGE_UPLOAD_ERROR | typeof STORAGE_UPLOAD_TOO_LARGE } {
return (
error instanceof Error &&
(error.message === STORAGE_UPLOAD_ERROR || error.message === STORAGE_UPLOAD_TOO_LARGE)
);
}
// Bare fetch on purpose: this PUT targets the presigned storage URL directly, not
// VITE_BASE_API_URL, so it must not go through baseApi/fetchBaseQuery.
async function uploadToPresigned(file: File, presignedUrl: string): Promise<void> {
if (!presignedUrl) throw new Error(STORAGE_UPLOAD_ERROR);
const token = resolveTokenFromStorage();
const res = await fetch(presignedUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type || 'application/octet-stream',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
if (res.status === 413) throw new Error(STORAGE_UPLOAD_TOO_LARGE);
if (!res.ok) throw new Error(STORAGE_UPLOAD_ERROR);
}
export function useDocumentUpload() {
const [getFileUploadKey, mutationState] = useGetFileUploadKeyMutation();
// Spans both steps (key request + presigned PUT) — mutationState.isLoading alone
// would drop to false once the key request resolves, before the file PUT finishes.
const [isUploading, setIsUploading] = useState(false);
const upload = useCallback(
async <TFileInfo = unknown>(file: File, endpoint: string): Promise<UploadKeyResponse<TFileInfo>> => {
setIsUploading(true);
try {
const { presigned, fileInfo } = await getFileUploadKey({ endpoint, file }).unwrap();
await uploadToPresigned(file, presigned);
return { presigned, fileInfo: fileInfo as TFileInfo };
} finally {
setIsUploading(false);
}
},
[getFileUploadKey],
);
return {
upload,
isUploading,
error: mutationState.error,
reset: mutationState.reset,
};
}