Add PDF Preview and Viewer Components with Enhanced Styling
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
- Introduced PDFPreview and PDFViewer components to facilitate PDF rendering and interaction within the dashboard. - Implemented zoom and pan functionalities in PDFPreview for improved user experience. - Enhanced App.css with styles for a new overlay during PDF loading, ensuring a smooth visual transition. - Updated TemplatePreview to integrate PDF viewing capabilities, allowing users to preview PDF documents seamlessly. - Modified ApiServerContext to support asynchronous PDF downloads, improving document handling efficiency.
This commit is contained in:
parent
6dcc5759f4
commit
85273bea1b
@ -1684,3 +1684,26 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
.code-block-editor .simplebar-content-wrapper {
|
||||
background-color: var(--layout-modal-bg);
|
||||
}
|
||||
|
||||
.previewProgressOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
backdrop-filter: blur(8px);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.previewProgressOverlay.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.previewProgressOverlay.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -65,11 +65,13 @@
|
||||
"loglevel": "^1.9.2",
|
||||
"nanoid": "^5.1.14",
|
||||
"online-3d-viewer": "^0.16.0",
|
||||
"pdfjs-dist": "5.4.296",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.1",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-pdf": "^10.5.0",
|
||||
"react-responsive": "^10.0.1",
|
||||
"react-router-dom": "^7.8.2",
|
||||
"recharts": "^3.8.1",
|
||||
|
||||
186
pnpm-lock.yaml
generated
186
pnpm-lock.yaml
generated
@ -184,6 +184,9 @@ importers:
|
||||
online-3d-viewer:
|
||||
specifier: ^0.16.0
|
||||
version: 0.16.0
|
||||
pdfjs-dist:
|
||||
specifier: 5.4.296
|
||||
version: 5.4.296
|
||||
prop-types:
|
||||
specifier: ^15.8.1
|
||||
version: 15.8.1
|
||||
@ -199,6 +202,9 @@ importers:
|
||||
react-markdown:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0(@types/react@19.2.10)(react@19.2.4)
|
||||
react-pdf:
|
||||
specifier: ^10.5.0
|
||||
version: 10.5.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react-responsive:
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1(react@19.2.4)
|
||||
@ -1448,6 +1454,76 @@ packages:
|
||||
'@marijn/find-cluster-break@1.0.3':
|
||||
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -4434,6 +4510,12 @@ packages:
|
||||
magic-string@0.25.9:
|
||||
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||
|
||||
make-cancellable-promise@2.0.0:
|
||||
resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==}
|
||||
|
||||
make-event-props@2.0.0:
|
||||
resolution: {integrity: sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==}
|
||||
|
||||
make-fetch-happen@14.0.3:
|
||||
resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
@ -4526,6 +4608,14 @@ packages:
|
||||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
merge-refs@2.0.0:
|
||||
resolution: {integrity: sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
merge-stream@2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
|
||||
@ -5023,6 +5113,10 @@ packages:
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
pdfjs-dist@5.4.296:
|
||||
resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==}
|
||||
engines: {node: '>=20.16.0 || >=22.3.0'}
|
||||
|
||||
pe-library@0.4.1:
|
||||
resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
@ -5515,6 +5609,16 @@ packages:
|
||||
'@types/react': '>=18'
|
||||
react: '>=18'
|
||||
|
||||
react-pdf@10.5.0:
|
||||
resolution: {integrity: sha512-zGKb6Ph5AYn2OfO4TS8Gg0Go3eW5K/TKy0+0FTm8EwFNbpgbuaWBZoJdYkJt+cRBCkizE9dYvqtMtHRj522Q4w==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-redux@9.3.0:
|
||||
resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
|
||||
peerDependencies:
|
||||
@ -6472,6 +6576,9 @@ packages:
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
warning@4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
|
||||
wcwidth@1.0.1:
|
||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||
|
||||
@ -8017,6 +8124,54 @@ snapshots:
|
||||
|
||||
'@marijn/find-cluster-break@1.0.3': {}
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-x64': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.100
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-musl': 0.1.100
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 0.1.100
|
||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.100
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@ -11575,6 +11730,10 @@ snapshots:
|
||||
dependencies:
|
||||
sourcemap-codec: 1.4.8
|
||||
|
||||
make-cancellable-promise@2.0.0: {}
|
||||
|
||||
make-event-props@2.0.0: {}
|
||||
|
||||
make-fetch-happen@14.0.3:
|
||||
dependencies:
|
||||
'@npmcli/agent': 3.0.0
|
||||
@ -11780,6 +11939,10 @@ snapshots:
|
||||
|
||||
merge-descriptors@2.0.0: {}
|
||||
|
||||
merge-refs@2.0.0(@types/react@19.2.10):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.10
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
@ -12392,6 +12555,10 @@ snapshots:
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pdfjs-dist@5.4.296:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.100
|
||||
|
||||
pe-library@0.4.1: {}
|
||||
|
||||
pend@1.2.0: {}
|
||||
@ -13025,6 +13192,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react-pdf@10.5.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
dequal: 2.0.3
|
||||
make-cancellable-promise: 2.0.0
|
||||
make-event-props: 2.0.0
|
||||
merge-refs: 2.0.0(@types/react@19.2.10)
|
||||
pdfjs-dist: 5.4.296
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
tiny-invariant: 1.3.3
|
||||
warning: 4.0.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.10
|
||||
|
||||
react-redux@9.3.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
@ -14212,6 +14394,10 @@ snapshots:
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
warning@4.0.3:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
wcwidth@1.0.1:
|
||||
dependencies:
|
||||
defaults: 1.0.4
|
||||
|
||||
85
src/components/Dashboard/common/PDFPreview.jsx
Normal file
85
src/components/Dashboard/common/PDFPreview.jsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Flex, Button } from 'antd'
|
||||
import PlusIcon from '../../Icons/PlusIcon.jsx'
|
||||
import MinusIcon from '../../Icons/MinusIcon.jsx'
|
||||
import PanIcon from '../../Icons/PanIcon.jsx'
|
||||
import PanFilledIcon from '../../Icons/PanFilledIcon.jsx'
|
||||
import PDFViewer from './PDFViewer.jsx'
|
||||
|
||||
const PDFPreview = ({ file, loading = false, style }) => {
|
||||
const [previewScale, setPreviewScale] = useState(1)
|
||||
const [panMode, setPanMode] = useState(false)
|
||||
|
||||
return (
|
||||
<Flex vertical gap={'middle'} style={{ height: '100%', ...style }}>
|
||||
<Flex gap={'small'}>
|
||||
<Button
|
||||
icon={
|
||||
panMode ? (
|
||||
<PanFilledIcon style={{ color: 'var(--color-primary)' }} />
|
||||
) : (
|
||||
<PanIcon />
|
||||
)
|
||||
}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setPanMode((prev) => !prev)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<PlusIcon />}
|
||||
onClick={() => {
|
||||
setPreviewScale((prev) => prev + 0.05)
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
icon={<MinusIcon />}
|
||||
onClick={() => {
|
||||
setPreviewScale((prev) => Math.max(0.1, prev - 0.05))
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
readOnly={true}
|
||||
style={{ width: '65px' }}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setPreviewScale(1)
|
||||
}}
|
||||
>
|
||||
{previewScale.toFixed(2)}x
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0,
|
||||
border: '1px solid #85858541'
|
||||
}}
|
||||
>
|
||||
<PDFViewer
|
||||
file={file}
|
||||
scale={previewScale}
|
||||
panMode={panMode}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
PDFPreview.propTypes = {
|
||||
file: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.instanceOf(Blob),
|
||||
PropTypes.instanceOf(ArrayBuffer),
|
||||
PropTypes.object
|
||||
]),
|
||||
loading: PropTypes.bool,
|
||||
style: PropTypes.object
|
||||
}
|
||||
|
||||
export default PDFPreview
|
||||
241
src/components/Dashboard/common/PDFViewer.jsx
Normal file
241
src/components/Dashboard/common/PDFViewer.jsx
Normal file
@ -0,0 +1,241 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Document, Page, pdfjs } from 'react-pdf'
|
||||
import pdfWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css'
|
||||
import 'react-pdf/dist/Page/TextLayer.css'
|
||||
import LoadingPlaceholder from './LoadingPlaceholder.jsx'
|
||||
import ScrollBox from './ScrollBox.jsx'
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfWorker
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
const PDFViewer = ({
|
||||
file,
|
||||
scale = 1,
|
||||
panMode = false,
|
||||
loading = false,
|
||||
onLoadSuccess = noop,
|
||||
onLoadError = noop
|
||||
}) => {
|
||||
const scrollElementRef = useRef(null)
|
||||
const panStartRef = useRef({ x: 0, y: 0, scrollLeft: 0, scrollTop: 0 })
|
||||
const pdfDocumentRef = useRef(null)
|
||||
const prevFileRef = useRef(file)
|
||||
const documentKeyRef = useRef(0)
|
||||
const [numPages, setNumPages] = useState(0)
|
||||
const [isPanning, setIsPanning] = useState(false)
|
||||
const [hasLoadError, setHasLoadError] = useState(false)
|
||||
|
||||
const activeFile = loading == true ? null : file
|
||||
if (prevFileRef.current !== activeFile) {
|
||||
prevFileRef.current = activeFile
|
||||
documentKeyRef.current += 1
|
||||
}
|
||||
|
||||
const destroyPdfDocument = useCallback(() => {
|
||||
const pdf = pdfDocumentRef.current
|
||||
pdfDocumentRef.current = null
|
||||
if (pdf && typeof pdf.destroy === 'function') {
|
||||
pdf.destroy()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleDocumentLoadSuccess = useCallback(
|
||||
(pdf) => {
|
||||
pdfDocumentRef.current = pdf
|
||||
setHasLoadError(false)
|
||||
setNumPages(pdf.numPages)
|
||||
onLoadSuccess(pdf)
|
||||
},
|
||||
[onLoadSuccess]
|
||||
)
|
||||
|
||||
const handleDocumentLoadError = useCallback(
|
||||
(error) => {
|
||||
destroyPdfDocument()
|
||||
setHasLoadError(true)
|
||||
setNumPages(0)
|
||||
onLoadError(error)
|
||||
},
|
||||
[destroyPdfDocument, onLoadError]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setNumPages(0)
|
||||
setHasLoadError(false)
|
||||
return () => {
|
||||
destroyPdfDocument()
|
||||
}
|
||||
}, [activeFile, destroyPdfDocument])
|
||||
|
||||
useEffect(() => {
|
||||
if (!panMode) {
|
||||
setIsPanning(false)
|
||||
}
|
||||
}, [panMode])
|
||||
|
||||
useEffect(() => {
|
||||
const scrollEl = scrollElementRef.current
|
||||
if (!panMode || !scrollEl) {
|
||||
return
|
||||
}
|
||||
|
||||
const handlePointerDown = (event) => {
|
||||
if (event.pointerType === 'mouse' && event.button !== 0) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
panStartRef.current = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
scrollLeft: scrollEl.scrollLeft,
|
||||
scrollTop: scrollEl.scrollTop
|
||||
}
|
||||
setIsPanning(true)
|
||||
}
|
||||
|
||||
scrollEl.addEventListener('pointerdown', handlePointerDown)
|
||||
return () => {
|
||||
scrollEl.removeEventListener('pointerdown', handlePointerDown)
|
||||
}
|
||||
}, [panMode, activeFile])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPanning) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousCursor = document.body.style.cursor
|
||||
const previousUserSelect = document.body.style.userSelect
|
||||
document.body.style.cursor = 'grabbing'
|
||||
document.body.style.userSelect = 'none'
|
||||
|
||||
const handlePointerMove = (event) => {
|
||||
const scrollEl = scrollElementRef.current
|
||||
if (!scrollEl) {
|
||||
return
|
||||
}
|
||||
const { x, y, scrollLeft, scrollTop } = panStartRef.current
|
||||
scrollEl.scrollLeft = scrollLeft - (event.clientX - x)
|
||||
scrollEl.scrollTop = scrollTop - (event.clientY - y)
|
||||
}
|
||||
|
||||
const handlePointerUp = () => {
|
||||
setIsPanning(false)
|
||||
}
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove)
|
||||
window.addEventListener('pointerup', handlePointerUp)
|
||||
window.addEventListener('pointercancel', handlePointerUp)
|
||||
return () => {
|
||||
document.body.style.cursor = previousCursor
|
||||
document.body.style.userSelect = previousUserSelect
|
||||
window.removeEventListener('pointermove', handlePointerMove)
|
||||
window.removeEventListener('pointerup', handlePointerUp)
|
||||
window.removeEventListener('pointercancel', handlePointerUp)
|
||||
}
|
||||
}, [isPanning])
|
||||
|
||||
const isDocumentLoading =
|
||||
Boolean(activeFile) && numPages === 0 && hasLoadError != true
|
||||
const showLoadingPlaceholder = loading == true || isDocumentLoading
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
minHeight: 0,
|
||||
overflow: 'visible',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{showLoadingPlaceholder ? (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...(activeFile
|
||||
? { position: 'absolute', inset: 0, zIndex: 1 }
|
||||
: {})
|
||||
}}
|
||||
>
|
||||
<LoadingPlaceholder message={'Loading PDF...'} />
|
||||
</div>
|
||||
) : null}
|
||||
{activeFile ? (
|
||||
<ScrollBox
|
||||
scrollableNodeProps={{ ref: scrollElementRef }}
|
||||
style={{
|
||||
cursor: panMode ? (isPanning ? 'grabbing' : 'grab') : undefined,
|
||||
touchAction: panMode ? 'none' : undefined
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: 60,
|
||||
width: 'max-content',
|
||||
minWidth: '100%',
|
||||
cursor: panMode ? (isPanning ? 'grabbing' : 'grab') : undefined,
|
||||
pointerEvents: panMode ? 'none' : undefined
|
||||
}}
|
||||
>
|
||||
<Document
|
||||
key={documentKeyRef.current}
|
||||
file={activeFile}
|
||||
loading={null}
|
||||
error={
|
||||
<div style={{ color: 'var(--color-error, #ff4d4f)' }}>
|
||||
Failed to load PDF.
|
||||
</div>
|
||||
}
|
||||
onLoadSuccess={handleDocumentLoadSuccess}
|
||||
onLoadError={handleDocumentLoadError}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
gap: 32,
|
||||
width: 'max-content',
|
||||
minWidth: '100%'
|
||||
}}
|
||||
>
|
||||
{Array.from(new Array(numPages), (_el, index) => (
|
||||
<Page
|
||||
key={`page_${index + 1}`}
|
||||
pageNumber={index + 1}
|
||||
scale={scale}
|
||||
renderTextLayer={panMode != true}
|
||||
renderAnnotationLayer={panMode != true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Document>
|
||||
</div>
|
||||
</ScrollBox>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
PDFViewer.propTypes = {
|
||||
file: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.instanceOf(Blob),
|
||||
PropTypes.instanceOf(ArrayBuffer),
|
||||
PropTypes.object
|
||||
]),
|
||||
scale: PropTypes.number,
|
||||
panMode: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
onLoadSuccess: PropTypes.func,
|
||||
onLoadError: PropTypes.func
|
||||
}
|
||||
|
||||
export default PDFViewer
|
||||
@ -7,15 +7,19 @@ import {
|
||||
useCallback
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Flex, Button, Input, Select } from 'antd'
|
||||
import { Flex, Button, Input, Select, Typography, Card } from 'antd'
|
||||
import PlusIcon from '../../Icons/PlusIcon.jsx'
|
||||
import MinusIcon from '../../Icons/MinusIcon.jsx'
|
||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
|
||||
import PanIcon from '../../Icons/PanIcon.jsx'
|
||||
import PanFilledIcon from '../../Icons/PanFilledIcon.jsx'
|
||||
import ObjectProperty from '../common/ObjectProperty.jsx'
|
||||
import PDFViewer from './PDFViewer.jsx'
|
||||
import ScrollBox from './ScrollBox.jsx'
|
||||
import { ApiServerContext } from '../context/ApiServerContext.jsx'
|
||||
import HProgress from './HProgress.jsx'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
@ -34,11 +38,16 @@ const TemplatePreview = ({
|
||||
const savedScrollRef = useRef({ top: 0, left: 0 })
|
||||
const iframeSizeObserverRef = useRef(null)
|
||||
const previewRequestIdRef = useRef(0)
|
||||
const { fetchTemplatePreview, fetchTemplatePDF } =
|
||||
useContext(ApiServerContext)
|
||||
const renderUnsubscribeRef = useRef(null)
|
||||
const {
|
||||
fetchTemplatePreview,
|
||||
fetchTemplateDownload,
|
||||
subscribeToObjectEvent
|
||||
} = useContext(ApiServerContext)
|
||||
const [previewContentHTML, setPreviewContentHTML] = useState('')
|
||||
const [pdfBlob, setPDFBlob] = useState(null)
|
||||
const [reloadLoading, setReloadLoading] = useState(false)
|
||||
const [downloadProgress, setDownloadProgress] = useState(null)
|
||||
const [previewScale, setPreviewScale] = useState(1)
|
||||
const [previewType, setPreviewType] = useState('HTML')
|
||||
const [panMode, setPanMode] = useState(false)
|
||||
@ -162,6 +171,35 @@ const TemplatePreview = ({
|
||||
setPreviewContentHTML(html)
|
||||
}
|
||||
|
||||
const stopRenderProgressListener = useCallback(() => {
|
||||
if (typeof renderUnsubscribeRef.current === 'function') {
|
||||
renderUnsubscribeRef.current()
|
||||
}
|
||||
renderUnsubscribeRef.current = null
|
||||
}, [])
|
||||
|
||||
const applyPdfResult = useCallback(
|
||||
(result, requestId) => {
|
||||
if (requestId !== previewRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
setReloadLoading(false)
|
||||
setDownloadProgress(null)
|
||||
stopRenderProgressListener()
|
||||
if (result?.error) {
|
||||
onPreviewMessage(result.error, true)
|
||||
return
|
||||
}
|
||||
setPDFBlob(
|
||||
new Blob([result.buffer || result.pdf], {
|
||||
type: result.mime || 'application/pdf'
|
||||
})
|
||||
)
|
||||
onPreviewMessage('No issues found.', false)
|
||||
},
|
||||
[onPreviewMessage, stopRenderProgressListener]
|
||||
)
|
||||
|
||||
const reloadPreviewPDF = useCallback(
|
||||
(content, testObject = {}) => {
|
||||
const templateId = documentTemplate?._id
|
||||
@ -170,24 +208,117 @@ const TemplatePreview = ({
|
||||
}
|
||||
|
||||
const requestId = ++previewRequestIdRef.current
|
||||
stopRenderProgressListener()
|
||||
setPDFBlob(null)
|
||||
setReloadLoading(true)
|
||||
fetchTemplatePDF(templateId, content, testObject, (result) => {
|
||||
if (requestId !== previewRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
setReloadLoading(false)
|
||||
if (result?.error) {
|
||||
onPreviewMessage(result.error, true)
|
||||
} else {
|
||||
const pdfBlob = new Blob([result.pdf], { type: 'application/pdf' })
|
||||
const pdfUrl = URL.createObjectURL(pdfBlob)
|
||||
setDownloadProgress({ progress: 0, message: 'Starting render' })
|
||||
|
||||
setPDFBlob(pdfUrl)
|
||||
onPreviewMessage('No issues found.', false)
|
||||
const startDownload = async () => {
|
||||
try {
|
||||
const startResult = await fetchTemplateDownload(
|
||||
templateId,
|
||||
content,
|
||||
testObject,
|
||||
'pdf',
|
||||
{ async: true }
|
||||
)
|
||||
if (requestId !== previewRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
if (startResult?.error || !startResult?.renderRequestId) {
|
||||
applyPdfResult(startResult, requestId)
|
||||
return
|
||||
}
|
||||
|
||||
const renderRequestId = startResult.renderRequestId
|
||||
const eventType = `render:${renderRequestId}`
|
||||
let fetched = false
|
||||
|
||||
const fetchRenderedFile = async () => {
|
||||
if (fetched) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const result = await fetchTemplateDownload(
|
||||
templateId,
|
||||
content,
|
||||
testObject,
|
||||
'pdf',
|
||||
{ renderRequestId }
|
||||
)
|
||||
fetched = true
|
||||
applyPdfResult(result, requestId)
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err?.response?.status === 404) {
|
||||
return false
|
||||
}
|
||||
applyPdfResult(
|
||||
{
|
||||
error: err?.message || 'Failed to download rendered template.'
|
||||
},
|
||||
requestId
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = subscribeToObjectEvent(
|
||||
renderRequestId,
|
||||
'render',
|
||||
eventType,
|
||||
(event) => {
|
||||
if (requestId !== previewRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
setDownloadProgress({
|
||||
progress: Number(event?.progress) || 0,
|
||||
message: event?.message || ''
|
||||
})
|
||||
if (event?.error) {
|
||||
applyPdfResult({ error: event.error }, requestId)
|
||||
return
|
||||
}
|
||||
if (Number(event?.progress) >= 1) {
|
||||
fetchRenderedFile()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const pollId = window.setInterval(async () => {
|
||||
if (requestId !== previewRequestIdRef.current) {
|
||||
window.clearInterval(pollId)
|
||||
return
|
||||
}
|
||||
const didFetch = await fetchRenderedFile()
|
||||
if (didFetch) {
|
||||
window.clearInterval(pollId)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
renderUnsubscribeRef.current = () => {
|
||||
window.clearInterval(pollId)
|
||||
if (typeof unsubscribe === 'function') {
|
||||
unsubscribe()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
applyPdfResult(
|
||||
{ error: err?.message || 'Failed to start template download.' },
|
||||
requestId
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
startDownload()
|
||||
},
|
||||
[documentTemplate?._id, fetchTemplatePDF, onPreviewMessage]
|
||||
[
|
||||
applyPdfResult,
|
||||
documentTemplate?._id,
|
||||
fetchTemplateDownload,
|
||||
stopRenderProgressListener,
|
||||
subscribeToObjectEvent
|
||||
]
|
||||
)
|
||||
|
||||
const reloadPreview = useCallback(
|
||||
@ -218,18 +349,13 @@ const TemplatePreview = ({
|
||||
const templateContent = documentTemplate?.content
|
||||
const templateId = documentTemplate?._id
|
||||
|
||||
// Move useEffect to component level and use state to track objectData changes
|
||||
useEffect(() => {
|
||||
if (!templateId || !templateContent) {
|
||||
if (!templateId || !templateContent || previewType != 'HTML') {
|
||||
return
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
if (previewType == 'HTML') {
|
||||
reloadPreview(templateContent, objectData, previewScale)
|
||||
} else {
|
||||
reloadPreviewPDF(templateContent, objectData)
|
||||
}
|
||||
reloadPreview(templateContent, objectData, previewScale)
|
||||
}, 300)
|
||||
|
||||
return () => {
|
||||
@ -241,15 +367,31 @@ const TemplatePreview = ({
|
||||
templateId,
|
||||
previewScale,
|
||||
previewType,
|
||||
reloadPreview,
|
||||
reloadPreviewPDF
|
||||
reloadPreview
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!templateId || !templateContent || previewType != 'PDF') {
|
||||
return
|
||||
}
|
||||
|
||||
setPDFBlob(null)
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
reloadPreviewPDF(templateContent, objectData)
|
||||
}, 300)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}, [objectData, templateContent, templateId, previewType, reloadPreviewPDF])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
iframeSizeObserverRef.current?.disconnect()
|
||||
stopRenderProgressListener()
|
||||
}
|
||||
}, [])
|
||||
}, [stopRenderProgressListener])
|
||||
|
||||
useEffect(() => {
|
||||
if (!panMode) {
|
||||
@ -319,6 +461,8 @@ const TemplatePreview = ({
|
||||
}
|
||||
}, [isPanning])
|
||||
|
||||
const showProgressOverlay = previewType != 'HTML' && downloadProgress != null
|
||||
|
||||
return (
|
||||
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
|
||||
<Flex gap={'small'}>
|
||||
@ -329,8 +473,8 @@ const TemplatePreview = ({
|
||||
objectType={documentTemplate?.objectType}
|
||||
name={'testObject'}
|
||||
isEditing={true}
|
||||
objectData={objectData}
|
||||
disabled={!isEditing || objectData?.global}
|
||||
objectData={documentTemplate}
|
||||
disabled={!isEditing || documentTemplate?.global}
|
||||
type={'object'}
|
||||
style={{ minWidth: 0 }}
|
||||
/>
|
||||
@ -341,7 +485,7 @@ const TemplatePreview = ({
|
||||
)}
|
||||
<Button
|
||||
icon={<InfoCircleIcon />}
|
||||
disabled={objectData?.global}
|
||||
disabled={documentTemplate?.global}
|
||||
onClick={() => {
|
||||
onTestObjectOpen()
|
||||
}}
|
||||
@ -357,7 +501,7 @@ const TemplatePreview = ({
|
||||
<PanIcon />
|
||||
)
|
||||
}
|
||||
disabled={loading || reloadLoading || previewType == 'PDF'}
|
||||
disabled={loading || reloadLoading}
|
||||
onClick={() => {
|
||||
setPanMode((prev) => !prev)
|
||||
}}
|
||||
@ -367,19 +511,19 @@ const TemplatePreview = ({
|
||||
onClick={() => {
|
||||
setPreviewScale((prev) => prev + 0.05)
|
||||
}}
|
||||
disabled={loading || reloadLoading || previewType == 'PDF'}
|
||||
disabled={loading || reloadLoading}
|
||||
/>
|
||||
<Button
|
||||
icon={<MinusIcon />}
|
||||
onClick={() => {
|
||||
setPreviewScale((prev) => prev - 0.05)
|
||||
setPreviewScale((prev) => Math.max(0.1, prev - 0.05))
|
||||
}}
|
||||
disabled={loading || reloadLoading || previewType == 'PDF'}
|
||||
disabled={loading || reloadLoading}
|
||||
/>
|
||||
<Button
|
||||
readOnly={true}
|
||||
style={{ width: '65px' }}
|
||||
disabled={loading || reloadLoading || previewType == 'PDF'}
|
||||
disabled={loading || reloadLoading}
|
||||
onClick={() => {
|
||||
setPreviewScale(1)
|
||||
}}
|
||||
@ -392,11 +536,15 @@ const TemplatePreview = ({
|
||||
{ value: 'HTML', label: 'HTML' },
|
||||
{ value: 'PDF', label: 'PDF' }
|
||||
]}
|
||||
style
|
||||
loading={loading || reloadLoading}
|
||||
disabled={loading || reloadLoading}
|
||||
value={previewType}
|
||||
onChange={(value) => {
|
||||
setPreviewType(value)
|
||||
if (value == 'PDF') {
|
||||
setPDFBlob(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
@ -406,9 +554,36 @@ const TemplatePreview = ({
|
||||
style={{
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0,
|
||||
border: '1px solid #85858541'
|
||||
border: '1px solid #85858541',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`previewProgressOverlay ${showProgressOverlay ? 'visible' : 'hidden'}`}
|
||||
>
|
||||
{showProgressOverlay ? (
|
||||
<Flex
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
align='center'
|
||||
justify='center'
|
||||
>
|
||||
<Card style={{ minWidth: 360 }}>
|
||||
<Flex vertical gap={'small'} style={{ width: '100%' }}>
|
||||
<Text ellipsis style={{ width: '100%', minWidth: 0 }}>
|
||||
{downloadProgress.message || 'Rendering...'}
|
||||
</Text>
|
||||
<HProgress
|
||||
percent={Math.round(
|
||||
(Number(downloadProgress.progress) || 0) * 100
|
||||
)}
|
||||
status='active'
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Flex>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{previewType == 'HTML' ? (
|
||||
<ScrollBox
|
||||
scrollableNodeProps={{ ref: scrollElementRef }}
|
||||
@ -421,11 +596,7 @@ const TemplatePreview = ({
|
||||
style={{
|
||||
width: iframeSize.width,
|
||||
height: iframeSize.height,
|
||||
cursor: panMode
|
||||
? isPanning
|
||||
? 'grabbing'
|
||||
: 'grab'
|
||||
: undefined
|
||||
cursor: panMode ? (isPanning ? 'grabbing' : 'grab') : undefined
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
@ -446,14 +617,13 @@ const TemplatePreview = ({
|
||||
</div>
|
||||
</ScrollBox>
|
||||
) : (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={pdfBlob}
|
||||
frameBorder='0'
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 0
|
||||
<PDFViewer
|
||||
file={pdfBlob}
|
||||
scale={previewScale}
|
||||
panMode={panMode}
|
||||
loading={false}
|
||||
onLoadError={() => {
|
||||
onPreviewMessage('Failed to render PDF preview.', true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -464,7 +634,7 @@ const TemplatePreview = ({
|
||||
|
||||
TemplatePreview.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
objectData: PropTypes.object,
|
||||
objectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
documentTemplate: PropTypes.object,
|
||||
isEditing: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
|
||||
@ -1865,16 +1865,30 @@ const ApiServerProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTemplateDownload = async (id, content, object, type = 'pdf') => {
|
||||
const fetchTemplateDownload = async (
|
||||
id,
|
||||
content,
|
||||
object,
|
||||
type = 'pdf',
|
||||
extra = {}
|
||||
) => {
|
||||
logger.debug(`Fetching template download as ${type}...`)
|
||||
const { async: asyncMode, renderRequestId } = extra
|
||||
const response = await axios.post(
|
||||
`${config.backendUrl}/documenttemplates/${id}/download`,
|
||||
{
|
||||
content,
|
||||
object
|
||||
object,
|
||||
...(asyncMode === true ? { async: true } : {}),
|
||||
...(renderRequestId ? { renderRequestId } : {})
|
||||
},
|
||||
{
|
||||
params: { type, padding: true },
|
||||
params: {
|
||||
type,
|
||||
padding: true,
|
||||
...(asyncMode === true ? { async: true } : {}),
|
||||
...(renderRequestId ? { renderRequestId } : {})
|
||||
},
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
@ -1892,11 +1906,109 @@ const ApiServerProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTemplatePDF = async (id, content, testObject, callback) => {
|
||||
const requestAsyncTemplateDownload = async (
|
||||
id,
|
||||
content,
|
||||
object,
|
||||
type = 'pdf',
|
||||
onProgress
|
||||
) => {
|
||||
const startResult = await fetchTemplateDownload(id, content, object, type, {
|
||||
async: true
|
||||
})
|
||||
if (startResult?.error) {
|
||||
return startResult
|
||||
}
|
||||
if (!startResult?.renderRequestId) {
|
||||
return startResult
|
||||
}
|
||||
|
||||
const renderRequestId = startResult.renderRequestId
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress({ progress: 0, message: 'Starting render' })
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false
|
||||
let unsubscribe
|
||||
|
||||
const finish = (result, error) => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
window.clearTimeout(timeoutId)
|
||||
window.clearInterval(pollId)
|
||||
if (typeof unsubscribe === 'function') {
|
||||
unsubscribe()
|
||||
}
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
}
|
||||
|
||||
const tryFetchRenderedFile = async () => {
|
||||
try {
|
||||
const result = await fetchTemplateDownload(id, content, object, type, {
|
||||
renderRequestId
|
||||
})
|
||||
finish(result)
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err?.response?.status === 404) {
|
||||
return false
|
||||
}
|
||||
finish(null, err)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe = subscribeToObjectEvent(
|
||||
renderRequestId,
|
||||
'render',
|
||||
`render:${renderRequestId}`,
|
||||
(event) => {
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress(event)
|
||||
}
|
||||
if (event?.error) {
|
||||
finish({ error: event.error })
|
||||
return
|
||||
}
|
||||
if (Number(event?.progress) >= 1) {
|
||||
tryFetchRenderedFile()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const pollId = window.setInterval(tryFetchRenderedFile, 1000)
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
finish(null, new Error('Timed out waiting for template render.'))
|
||||
}, 120000)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchTemplatePDF = async (
|
||||
id,
|
||||
content,
|
||||
testObject,
|
||||
callback,
|
||||
onProgress
|
||||
) => {
|
||||
logger.debug('Fetching pdf template...')
|
||||
try {
|
||||
const result = await fetchTemplateDownload(id, content, testObject, 'pdf')
|
||||
const payload = result.error ? result : { pdf: result.buffer || result }
|
||||
const result = await requestAsyncTemplateDownload(
|
||||
id,
|
||||
content,
|
||||
testObject,
|
||||
'pdf',
|
||||
onProgress
|
||||
)
|
||||
const payload = result.error
|
||||
? result
|
||||
: { pdf: result.buffer || result, mime: result.mime }
|
||||
if (typeof callback === 'function') {
|
||||
callback(payload)
|
||||
}
|
||||
@ -1921,7 +2033,12 @@ const ApiServerProvider = ({ children }) => {
|
||||
) => {
|
||||
logger.debug(`Downloading template as ${type}...`)
|
||||
try {
|
||||
const result = await fetchTemplateDownload(id, content, object, type)
|
||||
const result = await requestAsyncTemplateDownload(
|
||||
id,
|
||||
content,
|
||||
object,
|
||||
type
|
||||
)
|
||||
if (result?.error) {
|
||||
if (callback) {
|
||||
callback(result.error)
|
||||
@ -2402,6 +2519,7 @@ const ApiServerProvider = ({ children }) => {
|
||||
fetchTemplatePreview,
|
||||
fetchTemplateFormat,
|
||||
fetchTemplatePDF,
|
||||
fetchTemplateDownload,
|
||||
fetchNotes,
|
||||
downloadTemplate,
|
||||
downloadTemplatePDF,
|
||||
|
||||
@ -245,8 +245,9 @@ export const DocumentTemplate = {
|
||||
name: 'testObject',
|
||||
label: 'Test Object',
|
||||
required: false,
|
||||
type: 'codeBlock',
|
||||
language: 'json'
|
||||
type: 'object',
|
||||
objectType: (objectData) => objectData?.objectType,
|
||||
showHyperlink: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user