fix preview brand device error code , solution
This commit is contained in:
@@ -5,9 +5,18 @@ const API_BASE_URL = import.meta.env.VITE_API_SERVER;
|
||||
|
||||
// Get file from uploads directory
|
||||
const getFile = async (folder, filename) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
throw new Error('No authentication token found');
|
||||
}
|
||||
|
||||
const response = await axios.get(`${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}`, {
|
||||
responseType: 'blob'
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.replace(/"/g, '')}`
|
||||
}
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { Typography, Card, Row, Col, Tag, Button, Space, Descriptions, Divider, Table, Steps, Collapse, Switch, Skeleton, Spin, Modal } from 'antd';
|
||||
import { ArrowLeftOutlined, EditOutlined, DeleteOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { Typography, Card, Row, Col, Tag, Button, Space, Descriptions, Divider, Steps, Collapse, Switch, Spin, Modal, Empty } from 'antd';
|
||||
import { ArrowLeftOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||
import { NotifConfirmDialog, NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
|
||||
import { getBrandById, deleteBrand } from '../../../api/master-brand';
|
||||
import TableList from '../../../components/Global/TableList';
|
||||
import { NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
|
||||
import { getBrandById } from '../../../api/master-brand';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Step } = Steps;
|
||||
@@ -19,9 +18,8 @@ const ViewBrandDevice = () => {
|
||||
const [brandData, setBrandData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [errorCodesTriger, setErrorCodesTriger] = useState(0);
|
||||
const [activeErrorKeys, setActiveErrorKeys] = useState([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBrandData = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -44,10 +42,8 @@ const ViewBrandDevice = () => {
|
||||
setLoading(true);
|
||||
const response = await getBrandById(id);
|
||||
|
||||
|
||||
if (response && response.statusCode === 200) {
|
||||
setBrandData(response.data);
|
||||
setErrorCodesTriger(prev => prev + 1);
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
@@ -73,264 +69,21 @@ const ViewBrandDevice = () => {
|
||||
fetchBrandData();
|
||||
}, [id, setBreadcrumbItems, navigate, location.state]);
|
||||
|
||||
// const handleEdit = () => {
|
||||
// navigate(`/master/brand-device/edit/${id}`);
|
||||
// };
|
||||
|
||||
// const handleDelete = () => {
|
||||
// NotifConfirmDialog({
|
||||
// icon: 'question',
|
||||
// title: 'Konfirmasi Hapus',
|
||||
// message: `Brand Device "${brandData?.brand_name}" akan dihapus?`,
|
||||
// onConfirm: async () => {
|
||||
// try {
|
||||
// const response = await deleteBrand(id);
|
||||
|
||||
// if (response && response.statusCode === 200) {
|
||||
// NotifOk({
|
||||
// icon: 'success',
|
||||
// title: 'Berhasil',
|
||||
// message: response.message || 'Brand Device berhasil dihapus.',
|
||||
// });
|
||||
// navigate('/master/brand-device');
|
||||
// } else {
|
||||
// NotifAlert({
|
||||
// icon: 'error',
|
||||
// title: 'Gagal',
|
||||
// message: response?.message || 'Gagal menghapus Brand Device',
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Delete Brand Device Error:', error);
|
||||
// NotifAlert({
|
||||
// icon: 'error',
|
||||
// title: 'Error',
|
||||
// message: error.message || 'Gagal menghapus Brand Device',
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// onCancel: () => {},
|
||||
// });
|
||||
// };
|
||||
|
||||
// Fungsi untuk membuka file viewer di halaman baru
|
||||
const handleFileView = (fileName, fileType) => {
|
||||
console.log('handleFileView called with:', { fileName, fileType });
|
||||
|
||||
// Save current phase before navigating to file viewer
|
||||
localStorage.setItem(`brand_device_${id}_last_phase`, currentStep.toString());
|
||||
|
||||
// Extract only the filename without folder prefix
|
||||
let actualFileName = fileName;
|
||||
if (fileName && fileName.includes('/')) {
|
||||
const parts = fileName.split('/');
|
||||
actualFileName = parts[parts.length - 1]; // Get the last part (actual filename)
|
||||
actualFileName = parts[parts.length - 1];
|
||||
}
|
||||
|
||||
console.log('Processed filename:', { original: fileName, actual: actualFileName });
|
||||
|
||||
const encodedFileName = encodeURIComponent(actualFileName);
|
||||
const fileTypeParam = fileType === 'image' ? 'image' : 'pdf';
|
||||
const navigationPath = `/master/brand-device/view/${id}/files/${fileTypeParam}/${encodedFileName}`;
|
||||
|
||||
console.log('Navigating to:', navigationPath);
|
||||
navigate(navigationPath);
|
||||
};
|
||||
|
||||
|
||||
// if (loading) {
|
||||
// return (
|
||||
// <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
// <Spin size="large" />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
if (!brandData && !loading) {
|
||||
return <div>Brand Device not found</div>;
|
||||
}
|
||||
|
||||
// Error code table columns configuration
|
||||
const errorCodeColumns = [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'Error Code',
|
||||
dataIndex: 'error_code',
|
||||
key: 'error_code',
|
||||
width: '15%',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
title: 'Error Code Name',
|
||||
dataIndex: 'error_code_name',
|
||||
key: 'error_code_name',
|
||||
width: '20%',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'error_code_description',
|
||||
key: 'error_code_description',
|
||||
width: '25%',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
title: 'Solutions',
|
||||
dataIndex: 'solution',
|
||||
key: 'solution',
|
||||
width: '20%',
|
||||
render: (solutions) => (
|
||||
<div>
|
||||
{solutions && solutions.length > 0 ? (
|
||||
<div>
|
||||
<Text type="secondary">{solutions.length} solution(s)</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{solutions.slice(0, 2).map((sol, index) => (
|
||||
<div key={index} style={{ fontSize: '12px', color: '#666' }}>
|
||||
{sol.type_solution === 'text' ? (
|
||||
<span>• {sol.solution_name}</span>
|
||||
) : (
|
||||
<span>• {sol.solution_name} ({sol.type_solution})</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{solutions.length > 2 && (
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
...and {solutions.length - 2} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary">No solutions</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'is_active',
|
||||
key: 'is_active',
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
render: (_, { is_active }) => (
|
||||
<Tag color={is_active ? 'green' : 'red'}>
|
||||
{is_active ? 'Active' : 'Inactive'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: '5%',
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => {
|
||||
// Show detailed view for this error code
|
||||
Modal.info({
|
||||
title: 'Error Code Details',
|
||||
width: 800,
|
||||
content: (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Descriptions bordered column={1} size="small">
|
||||
<Descriptions.Item label="Error Code">{record.error_code}</Descriptions.Item>
|
||||
<Descriptions.Item label="Error Code Name">{record.error_code_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="Description">{record.error_code_description}</Descriptions.Item>
|
||||
<Descriptions.Item label="Status">
|
||||
<Tag color={record.is_active ? 'green' : 'red'}>
|
||||
{record.is_active ? 'Active' : 'Inactive'}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Title level={5} style={{ marginTop: 24, marginBottom: 16 }}>
|
||||
Solutions ({record.solution?.length || 0})
|
||||
</Title>
|
||||
{record.solution && record.solution.length > 0 ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{record.solution.map((solution) => (
|
||||
<Col span={24} key={solution.brand_code_solution_id}>
|
||||
<Card size="small">
|
||||
<Row justify="space-between" align="middle">
|
||||
<Col>
|
||||
<Space>
|
||||
{solution.type_solution === 'pdf' ? (
|
||||
<FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />
|
||||
) : (
|
||||
<FileTextOutlined style={{ color: '#1890ff', fontSize: '16px' }} />
|
||||
)}
|
||||
<Text strong>{solution.solution_name}</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>
|
||||
<Tag color={solution.type_solution === 'pdf' ? 'red' : 'blue'}>
|
||||
{solution.type_solution.toUpperCase()}
|
||||
</Tag>
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{solution.type_solution === 'text' ? (
|
||||
<Text>{solution.text_solution}</Text>
|
||||
) : (
|
||||
<Space>
|
||||
<Text type="secondary">File: {solution.path_document || solution.path_solution}</Text>
|
||||
{solution.path_document && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
handleFileView(
|
||||
(solution.path_document || solution.path_solution || solution.file_upload_name || solution.solution_name || 'Document')?.toString(),
|
||||
solution.type_solution || 'pdf'
|
||||
);
|
||||
}}
|
||||
>
|
||||
View Document
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Text type="secondary">No solutions available</Text>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
style={{ color: '#1890ff' }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Mock data function for error codes
|
||||
const getErrorCodesData = async () => {
|
||||
const errorCodes = brandData?.error_code || [];
|
||||
return {
|
||||
data: errorCodes,
|
||||
paging: {
|
||||
current_page: 1,
|
||||
current_limit: 10,
|
||||
total_limit: errorCodes.length,
|
||||
total_page: 1,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
if (currentStep === 0) {
|
||||
return (
|
||||
@@ -444,26 +197,140 @@ const ViewBrandDevice = () => {
|
||||
}
|
||||
|
||||
if (currentStep === 1) {
|
||||
const errorCodesCount = loading ? 3 : (brandData?.error_code?.length || 0);
|
||||
const errorCodes = brandData?.error_code || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={5} style={{ marginBottom: 16 }}>
|
||||
Error Codes ({errorCodesCount})
|
||||
Error Codes ({errorCodes.length})
|
||||
</Title>
|
||||
{errorCodesCount > 0 ? (
|
||||
<TableList
|
||||
mobile={false}
|
||||
cardColor={'#42AAFF'}
|
||||
header={'error_code'}
|
||||
getData={getErrorCodesData}
|
||||
queryParams={{}}
|
||||
columns={errorCodeColumns}
|
||||
triger={errorCodesTriger}
|
||||
firstLoad={false}
|
||||
/>
|
||||
|
||||
{errorCodes.length > 0 ? (
|
||||
<Collapse
|
||||
activeKey={activeErrorKeys}
|
||||
onChange={setActiveErrorKeys}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
{errorCodes.map((errorCode, index) => (
|
||||
<Panel
|
||||
key={index}
|
||||
header={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<div>
|
||||
<Text strong style={{ fontSize: '14px' }}>{errorCode.error_code}</Text>
|
||||
<Text style={{ marginLeft: 8, fontSize: '12px', color: '#666' }}>
|
||||
- {errorCode.error_code_name}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Tag color={errorCode.is_active ? 'green' : 'red'} style={{ margin: 0 }}>
|
||||
{errorCode.is_active ? 'Active' : 'Inactive'}
|
||||
</Tag>
|
||||
<Text style={{ fontSize: '12px', color: '#999' }}>
|
||||
{errorCode.solution?.length || 0} solution(s)
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ padding: '12px 0' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text type="secondary">Description:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Text>{errorCode.error_code_description || 'No description'}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong>Solutions:</Text>
|
||||
{errorCode.solution && errorCode.solution.length > 0 ? (
|
||||
<div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{errorCode.solution.map((solution) => (
|
||||
<Card
|
||||
key={solution.brand_code_solution_id}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#fafafa',
|
||||
border: '1px solid #f0f0f0'
|
||||
}}
|
||||
>
|
||||
<Row justify="space-between" align="middle">
|
||||
<Col>
|
||||
<Space>
|
||||
{solution.type_solution === 'pdf' ? (
|
||||
<FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />
|
||||
) : solution.type_solution === 'image' ? (
|
||||
<EyeOutlined style={{ color: '#1890ff', fontSize: '16px' }} />
|
||||
) : (
|
||||
<FileTextOutlined style={{ color: '#1890ff', fontSize: '16px' }} />
|
||||
)}
|
||||
<Text strong style={{ fontSize: '13px' }}>{solution.solution_name}</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>
|
||||
<Tag
|
||||
color={
|
||||
solution.type_solution === 'pdf' ? 'red' :
|
||||
solution.type_solution === 'image' ? 'blue' :
|
||||
'default'
|
||||
}
|
||||
style={{ fontSize: '11px' }}
|
||||
>
|
||||
{solution.type_solution ? solution.type_solution.toUpperCase() : 'TEXT'}
|
||||
</Tag>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{solution.type_solution === 'text' ? (
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>
|
||||
{solution.text_solution}
|
||||
</Text>
|
||||
) : (
|
||||
<div>
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>
|
||||
File: {solution.path_document || solution.path_solution || 'Document'}
|
||||
</Text>
|
||||
{(solution.path_document || solution.path_solution) && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
handleFileView(
|
||||
(solution.path_document || solution.path_solution || solution.file_upload_name || solution.solution_name || 'Document')?.toString(),
|
||||
solution.type_solution || 'pdf'
|
||||
);
|
||||
}}
|
||||
style={{ padding: 0, height: 'auto', fontSize: '12px', marginLeft: 8 }}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>No solutions available</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
) : (
|
||||
!loading && <Text type="secondary">No error codes available</Text>
|
||||
!loading && (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={
|
||||
<Text type="secondary">No error codes available</Text>
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -483,7 +350,7 @@ const ViewBrandDevice = () => {
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/master/brand-device')}
|
||||
onClick={() => navigate('/master/brand-device')}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
@@ -496,9 +363,7 @@ const ViewBrandDevice = () => {
|
||||
<Step title="Error Codes & Solutions" />
|
||||
</Steps>
|
||||
|
||||
{/* Content area with blur overlay during loading */}
|
||||
<div style={{ position: 'relative', marginTop: 24 }}>
|
||||
{/* Overlay with blur effect during loading - only on content area */}
|
||||
{loading && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -544,7 +409,7 @@ const ViewBrandDevice = () => {
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -94,13 +94,15 @@ const ViewFilePage = () => {
|
||||
setPdfLoading(true);
|
||||
const folder = getFolderFromFileType('pdf');
|
||||
try {
|
||||
const response = await getFile(folder, decodedFileName);
|
||||
const blobUrl = window.URL.createObjectURL(response.data);
|
||||
const blobData = await getFile(folder, decodedFileName);
|
||||
console.log('PDF blob data received:', blobData);
|
||||
const blobUrl = window.URL.createObjectURL(blobData);
|
||||
setPdfBlobUrl(blobUrl);
|
||||
console.log('PDF blob URL created successfully:', blobUrl);
|
||||
} catch (pdfError) {
|
||||
console.error('Error loading PDF:', pdfError);
|
||||
setError('Failed to load PDF file');
|
||||
setError('Failed to load PDF file: ' + (pdfError.message || pdfError));
|
||||
setPdfBlobUrl(null);
|
||||
} finally {
|
||||
setPdfLoading(false);
|
||||
}
|
||||
@@ -194,7 +196,7 @@ const ViewFilePage = () => {
|
||||
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
|
||||
const isPdf = fileExtension === 'pdf';
|
||||
|
||||
const fileUrl = loading ? null : getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
|
||||
// const fileUrl = loading ? null : getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
|
||||
|
||||
// Show placeholder when loading
|
||||
if (loading) {
|
||||
@@ -260,7 +262,7 @@ const ViewFilePage = () => {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<img
|
||||
src={fileUrl}
|
||||
src={getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName)}
|
||||
alt={actualFileName}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
@@ -276,7 +278,7 @@ const ViewFilePage = () => {
|
||||
}
|
||||
|
||||
if (isPdf) {
|
||||
const displayUrl = pdfBlobUrl || fileUrl;
|
||||
const displayUrl = pdfBlobUrl || getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
|
||||
|
||||
return (
|
||||
<div style={{ height: '75vh', width: '100%', border: '1px solid #d9d9d9', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
@@ -342,13 +344,15 @@ const ViewFilePage = () => {
|
||||
setPdfLoading(true);
|
||||
const folder = getFolderFromFileType('pdf');
|
||||
getFile(folder, actualFileName)
|
||||
.then(response => {
|
||||
const blobUrl = window.URL.createObjectURL(response.data);
|
||||
.then(blobData => {
|
||||
console.log('Retry PDF blob data:', blobData);
|
||||
const blobUrl = window.URL.createObjectURL(blobData);
|
||||
setPdfBlobUrl(blobUrl);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error retrying PDF load:', error);
|
||||
setError('Failed to load PDF file');
|
||||
setError('Failed to load PDF file: ' + (error.message || error));
|
||||
setPdfBlobUrl(null);
|
||||
})
|
||||
.finally(() => {
|
||||
setPdfLoading(false);
|
||||
@@ -370,7 +374,7 @@ const ViewFilePage = () => {
|
||||
<div style={{ fontSize: '16px', marginBottom: '8px' }}>Preview tidak tersedia untuk jenis file ini</div>
|
||||
<div style={{ color: '#666', marginBottom: '16px' }}>{actualFileName}</div>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Button type="primary" href={fileUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Button type="primary" href={getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName)} target="_blank" rel="noopener noreferrer">
|
||||
Buka di Tab Baru
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user