Add download functionality for equipment attachments
- Introduced a new endpoint `GET /equipment/{id}/attachment/download` in the API for downloading equipment attachment files.
- Implemented the `downloadEquipmentAttachmentFile` function in the client to handle file downloads via the API, ensuring proper token management and blob handling.
- Updated the EquipmentAttachmentInput, EquipmentList, and EquipmentShow components to utilize the new download link, enhancing user experience by allowing direct downloads without exposing the MinIO URL.
- Added a new EquipmentAttachmentLink component to encapsulate the download link logic and improve code reusability.
This commit is contained in:
@@ -3,6 +3,7 @@ import { useNotify, useRecordContext, useRefresh } from 'react-admin';
|
||||
import { useCallback, useState, type ChangeEvent } from 'react';
|
||||
import { ensureFreshToken, getAccessToken } from '../../auth/keycloak';
|
||||
import { resolveApiBaseUrl } from '../../lib/apiBase';
|
||||
import { EquipmentAttachmentLink } from './EquipmentAttachmentLink';
|
||||
|
||||
export type FileAttachmentValue = {
|
||||
objectKey?: string;
|
||||
@@ -83,11 +84,12 @@ export function EquipmentAttachmentInput() {
|
||||
<Typography component="label" variant="body2">
|
||||
Вложение (файл в MinIO)
|
||||
</Typography>
|
||||
{att?.downloadUrl ? (
|
||||
<Typography variant="body2">
|
||||
<a href={att.downloadUrl} target="_blank" rel="noreferrer">
|
||||
{att.originalFileName || 'Скачать файл'}
|
||||
</a>
|
||||
{att?.objectKey ? (
|
||||
<Typography variant="body2" component="div">
|
||||
<EquipmentAttachmentLink
|
||||
equipmentId={String(record.id)}
|
||||
fileName={att.originalFileName || 'Скачать файл'}
|
||||
/>
|
||||
{att.sizeBytes != null ? ` (${Math.round(att.sizeBytes / 1024)} КБ)` : null}
|
||||
</Typography>
|
||||
) : (
|
||||
|
||||
31
client/src/resources/equipment/EquipmentAttachmentLink.tsx
Normal file
31
client/src/resources/equipment/EquipmentAttachmentLink.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Link } from '@mui/material';
|
||||
import { useNotify } from 'react-admin';
|
||||
import { downloadEquipmentAttachmentFile } from './attachmentDownload';
|
||||
|
||||
type Props = {
|
||||
equipmentId: string;
|
||||
/** Текст ссылки и имя при сохранении в браузере */
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
export function EquipmentAttachmentLink({ equipmentId, fileName }: Props) {
|
||||
const notify = useNotify();
|
||||
const label = fileName.trim() || 'Скачать';
|
||||
|
||||
return (
|
||||
<Link
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void downloadEquipmentAttachmentFile(equipmentId, label).catch(() =>
|
||||
notify('Не удалось скачать файл', { type: 'warning' }),
|
||||
);
|
||||
}}
|
||||
sx={{ cursor: 'pointer', textAlign: 'left', verticalAlign: 'inherit' }}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TextInput,
|
||||
TopToolbar,
|
||||
} from 'react-admin';
|
||||
import { EquipmentAttachmentLink } from './EquipmentAttachmentLink';
|
||||
import { equipmentStatusChoices } from './shared';
|
||||
|
||||
const equipmentFilters = [
|
||||
@@ -37,7 +38,19 @@ export function EquipmentList() {
|
||||
<SelectField source="status" choices={equipmentStatusChoices} />
|
||||
<FunctionField
|
||||
label="Файл"
|
||||
render={(record: { attachment?: { objectKey?: string } | null }) => (record?.attachment?.objectKey ? '✓' : '—')}
|
||||
render={(record: {
|
||||
id: string;
|
||||
attachment?: { objectKey?: string; originalFileName?: string | null } | null;
|
||||
}) =>
|
||||
record?.attachment?.objectKey ? (
|
||||
<EquipmentAttachmentLink
|
||||
equipmentId={record.id}
|
||||
fileName={record.attachment.originalFileName ?? 'файл'}
|
||||
/>
|
||||
) : (
|
||||
'—'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Datagrid>
|
||||
</List>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DateField, FunctionField, SelectField, Show, SimpleShowLayout, TextField } from 'react-admin';
|
||||
import { EquipmentAttachmentLink } from './EquipmentAttachmentLink';
|
||||
import { equipmentStatusChoices } from './shared';
|
||||
|
||||
export function EquipmentShow() {
|
||||
@@ -13,11 +14,15 @@ export function EquipmentShow() {
|
||||
<SelectField source="status" choices={equipmentStatusChoices} />
|
||||
<FunctionField
|
||||
label="Вложение"
|
||||
render={(record: { attachment?: { downloadUrl?: string | null; originalFileName?: string | null } | null }) =>
|
||||
record?.attachment?.downloadUrl ? (
|
||||
<a href={record.attachment.downloadUrl} target="_blank" rel="noreferrer">
|
||||
{record.attachment.originalFileName || 'Скачать'}
|
||||
</a>
|
||||
render={(record: {
|
||||
id: string;
|
||||
attachment?: { objectKey?: string | null; originalFileName?: string | null } | null;
|
||||
}) =>
|
||||
record?.attachment?.objectKey ? (
|
||||
<EquipmentAttachmentLink
|
||||
equipmentId={record.id}
|
||||
fileName={record.attachment.originalFileName ?? 'файл'}
|
||||
/>
|
||||
) : (
|
||||
'—'
|
||||
)
|
||||
|
||||
26
client/src/resources/equipment/attachmentDownload.ts
Normal file
26
client/src/resources/equipment/attachmentDownload.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ensureFreshToken, getAccessToken } from '../../auth/keycloak';
|
||||
import { resolveApiBaseUrl } from '../../lib/apiBase';
|
||||
|
||||
/** Скачивание через API (JWT + Content-Disposition), без открытия URL MinIO в браузере. */
|
||||
export async function downloadEquipmentAttachmentFile(equipmentId: string, suggestedFileName: string): Promise<void> {
|
||||
await ensureFreshToken();
|
||||
const token = getAccessToken();
|
||||
const url = `${resolveApiBaseUrl()}/equipment/${equipmentId}/attachment/download`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: token ? { Authorization: `Bearer ${token}`, Accept: '*/*' } : { Accept: '*/*' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = objectUrl;
|
||||
a.download = suggestedFileName || 'download';
|
||||
a.rel = 'noopener';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
Reference in New Issue
Block a user