refactor: replace direct Paperless upload with client-side document download functionality
Build and Push Multi-Platform Images / build-and-push (push) Successful in 36s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 36s
This commit is contained in:
@@ -176,21 +176,18 @@ export class InboxController {
|
|||||||
await this.inboxService.updateSource(id, body.source, preferredUsername);
|
await this.inboxService.updateSource(id, body.source, preferredUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/save-to-paperless')
|
@Get(':id/download')
|
||||||
@HttpCode(204)
|
async download(
|
||||||
async saveToPaperless(
|
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() body: {
|
|
||||||
title: string;
|
|
||||||
date?: string;
|
|
||||||
documentTypeId?: number;
|
|
||||||
correspondentId?: number;
|
|
||||||
tagIds?: number[];
|
|
||||||
},
|
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
): Promise<void> {
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
): Promise<StreamableFile> {
|
||||||
const preferredUsername: string | null = req.user?.preferredUsername ?? null;
|
const preferredUsername: string | null = req.user?.preferredUsername ?? null;
|
||||||
await this.inboxService.saveToPaperless(id, preferredUsername, body);
|
const { buffer, filename } = await this.inboxService.getEditedPdfBuffer(id, preferredUsername);
|
||||||
|
const { Readable } = await import('stream');
|
||||||
|
res.setHeader('Content-Type', 'application/pdf');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
|
||||||
|
return new StreamableFile(Readable.from(buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/send-email')
|
@Post(':id/send-email')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Module, forwardRef } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Client } from '../database/entities/client.entity';
|
import { Client } from '../database/entities/client.entity';
|
||||||
import { UserClient } from '../database/entities/user-client.entity';
|
import { UserClient } from '../database/entities/user-client.entity';
|
||||||
@@ -9,7 +9,6 @@ import { InboxService } from './inbox.service';
|
|||||||
import { InboxMigrationService } from './inbox-migration.service';
|
import { InboxMigrationService } from './inbox-migration.service';
|
||||||
import { BarcodeModule } from '../barcode/barcode.module';
|
import { BarcodeModule } from '../barcode/barcode.module';
|
||||||
import { InboxPostprocessorModule } from '../inbox-postprocessor/inbox-postprocessor.module';
|
import { InboxPostprocessorModule } from '../inbox-postprocessor/inbox-postprocessor.module';
|
||||||
import { PaperlessModule } from '../paperless/paperless.module';
|
|
||||||
import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -17,7 +16,6 @@ import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
|||||||
TypeOrmModule.forFeature([Client, UserClient, InboxDocument]),
|
TypeOrmModule.forFeature([Client, UserClient, InboxDocument]),
|
||||||
BarcodeModule,
|
BarcodeModule,
|
||||||
InboxPostprocessorModule,
|
InboxPostprocessorModule,
|
||||||
forwardRef(() => PaperlessModule),
|
|
||||||
PostprocessingModule,
|
PostprocessingModule,
|
||||||
],
|
],
|
||||||
controllers: [InboxController, ClientsController],
|
controllers: [InboxController, ClientsController],
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
InboxDocument,
|
InboxDocument,
|
||||||
type InboxSource,
|
type InboxSource,
|
||||||
} from '../database/entities/inbox-document.entity';
|
} from '../database/entities/inbox-document.entity';
|
||||||
import { PaperlessService } from '../paperless/paperless.service';
|
|
||||||
import { MailService } from '../postprocessing/mail.service';
|
import { MailService } from '../postprocessing/mail.service';
|
||||||
import { applyEditsToTemp, cleanupTemp } from '../inbox-postprocessor/edit-applier';
|
import { applyEditsToTemp, cleanupTemp } from '../inbox-postprocessor/edit-applier';
|
||||||
|
|
||||||
@@ -44,7 +43,6 @@ export class InboxService {
|
|||||||
private readonly pageCache: PageCacheService,
|
private readonly pageCache: PageCacheService,
|
||||||
@InjectRepository(InboxDocument)
|
@InjectRepository(InboxDocument)
|
||||||
private readonly documentRepo: Repository<InboxDocument>,
|
private readonly documentRepo: Repository<InboxDocument>,
|
||||||
private readonly paperlessService: PaperlessService,
|
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -255,28 +253,16 @@ export class InboxService {
|
|||||||
return this.barcodeScanner.scanRegion(doc, pdfPath, page, x, y, w, h);
|
return this.barcodeScanner.scanRegion(doc, pdfPath, page, x, y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveToPaperless(
|
async getEditedPdfBuffer(
|
||||||
id: string,
|
id: string,
|
||||||
preferredUsername: string | null,
|
preferredUsername: string | null,
|
||||||
opts: {
|
): Promise<{ buffer: Buffer; filename: string }> {
|
||||||
title: string;
|
|
||||||
date?: string;
|
|
||||||
documentTypeId?: number;
|
|
||||||
correspondentId?: number;
|
|
||||||
tagIds?: number[];
|
|
||||||
},
|
|
||||||
): Promise<void> {
|
|
||||||
const { doc, pdfPath } = await this.resolveDocument(id, preferredUsername);
|
const { doc, pdfPath } = await this.resolveDocument(id, preferredUsername);
|
||||||
let tmpPath: string | null = null;
|
let tmpPath: string | null = null;
|
||||||
try {
|
try {
|
||||||
tmpPath = await applyEditsToTemp(doc, pdfPath);
|
tmpPath = await applyEditsToTemp(doc, pdfPath);
|
||||||
await this.paperlessService.uploadDocument(tmpPath, {
|
const buffer = await fs.readFile(tmpPath);
|
||||||
title: opts.title,
|
return { buffer, filename: doc.OriginalName };
|
||||||
created: opts.date,
|
|
||||||
documentType: opts.documentTypeId,
|
|
||||||
correspondent: opts.correspondentId,
|
|
||||||
tags: opts.tagIds,
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
await cleanupTemp(tmpPath);
|
await cleanupTemp(tmpPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,17 +107,8 @@ export const inboxApi = {
|
|||||||
)
|
)
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
|
|
||||||
saveToPaperless: (
|
downloadBlob: (id: string) =>
|
||||||
id: string,
|
api.get<Blob>(`/api/inbox/${encodeURIComponent(id)}/download`, { responseType: 'blob' }).then((r) => r.data),
|
||||||
body: {
|
|
||||||
title: string;
|
|
||||||
date?: string;
|
|
||||||
documentTypeId?: number;
|
|
||||||
correspondentId?: number;
|
|
||||||
tagIds?: number[];
|
|
||||||
},
|
|
||||||
) =>
|
|
||||||
api.post(`/api/inbox/${encodeURIComponent(id)}/save-to-paperless`, body).then(() => {}),
|
|
||||||
|
|
||||||
sendEmail: (
|
sendEmail: (
|
||||||
id: string,
|
id: string,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Button, DatePicker, Dropdown, Empty, Form, Input, Modal, Popconfirm, Select, Space, Spin, Steps, Tag, Tooltip, Typography, message } from 'antd';
|
import { Button, Dropdown, Empty, Form, Input, Modal, Popconfirm, Space, Spin, Steps, Tag, Tooltip, Typography, message } from 'antd';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
@@ -25,7 +25,7 @@ import { useEditor, EditorContent } from '@tiptap/react';
|
|||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
import Underline from '@tiptap/extension-underline';
|
import Underline from '@tiptap/extension-underline';
|
||||||
import { inboxApi, type InboxBarcode, type InboxFile, type PostprocessActionResult } from '../api/inbox';
|
import { inboxApi, type InboxBarcode, type InboxFile, type PostprocessActionResult } from '../api/inbox';
|
||||||
import { paperlessApi, type PaperlessDocType, type PaperlessCorrespondent, type PaperlessTag } from '../api/paperless';
|
import { paperlessApi } from '../api/paperless';
|
||||||
|
|
||||||
const ZOOM_MIN = 0.5;
|
const ZOOM_MIN = 0.5;
|
||||||
const ZOOM_MAX = 3;
|
const ZOOM_MAX = 3;
|
||||||
@@ -528,88 +528,6 @@ function PostprocessWizardModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SaveToPaperlessDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
fileId: string;
|
|
||||||
defaultTitle: string;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SaveToPaperlessDialog({ open, fileId, defaultTitle, onClose }: SaveToPaperlessDialogProps) {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
const [docTypes, setDocTypes] = useState<PaperlessDocType[]>([]);
|
|
||||||
const [correspondents, setCorrespondents] = useState<PaperlessCorrespondent[]>([]);
|
|
||||||
const [tags, setTags] = useState<PaperlessTag[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
form.resetFields();
|
|
||||||
form.setFieldsValue({ title: defaultTitle });
|
|
||||||
Promise.all([
|
|
||||||
paperlessApi.getDocumentTypes(),
|
|
||||||
paperlessApi.getCorrespondents(),
|
|
||||||
paperlessApi.getTags(),
|
|
||||||
]).then(([dt, co, tg]) => {
|
|
||||||
setDocTypes(dt);
|
|
||||||
setCorrespondents(co);
|
|
||||||
setTags(tg);
|
|
||||||
}).catch(() => {});
|
|
||||||
}, [open, defaultTitle, form]);
|
|
||||||
|
|
||||||
const handleOk = async () => {
|
|
||||||
try {
|
|
||||||
const values = await form.validateFields();
|
|
||||||
setSubmitting(true);
|
|
||||||
await inboxApi.saveToPaperless(fileId, {
|
|
||||||
title: values.title,
|
|
||||||
date: values.date ? values.date.format('YYYY-MM-DD') : undefined,
|
|
||||||
documentTypeId: values.documentTypeId,
|
|
||||||
correspondentId: values.correspondentId,
|
|
||||||
tagIds: values.tagIds,
|
|
||||||
});
|
|
||||||
message.success('Dokument wurde an Paperless übertragen');
|
|
||||||
onClose();
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err?.errorFields) return;
|
|
||||||
message.error('Übertragung fehlgeschlagen');
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={open}
|
|
||||||
title="Dokument in Paperless speichern"
|
|
||||||
onCancel={onClose}
|
|
||||||
onOk={handleOk}
|
|
||||||
okText="Speichern"
|
|
||||||
cancelText="Abbrechen"
|
|
||||||
confirmLoading={submitting}
|
|
||||||
destroyOnClose
|
|
||||||
width={520}
|
|
||||||
>
|
|
||||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
|
||||||
<Form.Item name="title" label="Titel" rules={[{ required: true, message: 'Bitte Titel angeben' }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="date" label="Datum">
|
|
||||||
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="documentTypeId" label="Dokumenttyp">
|
|
||||||
<Select allowClear placeholder="Kein Typ" options={docTypes.map((d) => ({ value: d.id, label: d.name }))} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="correspondentId" label="Korrespondent">
|
|
||||||
<Select allowClear placeholder="Kein Korrespondent" options={correspondents.map((c) => ({ value: c.id, label: c.name }))} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="tagIds" label="Tags">
|
|
||||||
<Select mode="multiple" allowClear placeholder="Keine Tags" options={tags.map((t) => ({ value: t.id, label: t.name }))} />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TiptapToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
function TiptapToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
@@ -720,7 +638,7 @@ export default function InboxDetailPage() {
|
|||||||
const [selectedPage, setSelectedPage] = useState(1);
|
const [selectedPage, setSelectedPage] = useState(1);
|
||||||
const [zoom, setZoom] = useState(1);
|
const [zoom, setZoom] = useState(1);
|
||||||
const [wizardOpen, setWizardOpen] = useState(false);
|
const [wizardOpen, setWizardOpen] = useState(false);
|
||||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
const [emailDialogOpen, setEmailDialogOpen] = useState(false);
|
const [emailDialogOpen, setEmailDialogOpen] = useState(false);
|
||||||
const [scanMode, setScanMode] = useState(false);
|
const [scanMode, setScanMode] = useState(false);
|
||||||
const [scanning, setScanning] = useState(false);
|
const [scanning, setScanning] = useState(false);
|
||||||
@@ -1087,7 +1005,8 @@ export default function InboxDetailPage() {
|
|||||||
<Dropdown.Button
|
<Dropdown.Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<DownOutlined />}
|
icon={<DownOutlined />}
|
||||||
disabled={documents.length === 0}
|
disabled={documents.length === 0 || downloading}
|
||||||
|
loading={downloading}
|
||||||
onClick={() => setWizardOpen(true)}
|
onClick={() => setWizardOpen(true)}
|
||||||
menu={{
|
menu={{
|
||||||
items: [
|
items: [
|
||||||
@@ -1095,7 +1014,19 @@ export default function InboxDetailPage() {
|
|||||||
{ key: 'email', label: 'Als E-Mail-Anhang versenden', icon: <MailOutlined /> },
|
{ key: 'email', label: 'Als E-Mail-Anhang versenden', icon: <MailOutlined /> },
|
||||||
] as MenuProps['items'],
|
] as MenuProps['items'],
|
||||||
onClick: ({ key }) => {
|
onClick: ({ key }) => {
|
||||||
if (key === 'save') setSaveDialogOpen(true);
|
if (key === 'save') {
|
||||||
|
setDownloading(true);
|
||||||
|
inboxApi.downloadBlob(file.id).then((blob) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = file.name;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}).catch(() => {
|
||||||
|
message.error('Download fehlgeschlagen');
|
||||||
|
}).finally(() => setDownloading(false));
|
||||||
|
}
|
||||||
if (key === 'email') setEmailDialogOpen(true);
|
if (key === 'email') setEmailDialogOpen(true);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -1523,12 +1454,6 @@ export default function InboxDetailPage() {
|
|||||||
onClose={() => setWizardOpen(false)}
|
onClose={() => setWizardOpen(false)}
|
||||||
onDeleted={() => navigate('/inbox')}
|
onDeleted={() => navigate('/inbox')}
|
||||||
/>
|
/>
|
||||||
<SaveToPaperlessDialog
|
|
||||||
open={saveDialogOpen}
|
|
||||||
fileId={file.id}
|
|
||||||
defaultTitle={file.name}
|
|
||||||
onClose={() => setSaveDialogOpen(false)}
|
|
||||||
/>
|
|
||||||
<SendEmailDialog
|
<SendEmailDialog
|
||||||
open={emailDialogOpen}
|
open={emailDialogOpen}
|
||||||
fileId={file.id}
|
fileId={file.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user