feat: implement multi-segment PDF email attachments and add PWA mobile icons
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s
This commit is contained in:
@@ -200,7 +200,7 @@ export class InboxController {
|
|||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
filename?: string;
|
segments: { pages: number[]; filename: string }[];
|
||||||
},
|
},
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
type InboxSource,
|
type InboxSource,
|
||||||
} from '../database/entities/inbox-document.entity';
|
} from '../database/entities/inbox-document.entity';
|
||||||
import { MailService } from '../postprocessing/mail.service';
|
import { MailService } from '../postprocessing/mail.service';
|
||||||
import { applyEditsToTemp, cleanupTemp, buildSegmentBuffer } from '../inbox-postprocessor/edit-applier';
|
import { buildSegmentBuffer } from '../inbox-postprocessor/edit-applier';
|
||||||
|
|
||||||
export interface InboxFile {
|
export interface InboxFile {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -273,24 +273,27 @@ export class InboxService {
|
|||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
filename?: string;
|
segments: { pages: number[]; filename: string }[];
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { doc, pdfPath } = await this.resolveDocument(id, preferredUsername);
|
const { doc, pdfPath } = await this.resolveDocument(id, preferredUsername);
|
||||||
let tmpPath: string | null = null;
|
const deleted = new Set(doc.DeletedPages ?? []);
|
||||||
try {
|
|
||||||
tmpPath = await applyEditsToTemp(doc, pdfPath);
|
const attachments = await Promise.all(
|
||||||
const content = await fs.readFile(tmpPath);
|
opts.segments.map(async (seg) => {
|
||||||
const filename = opts.filename ? `${opts.filename}.pdf` : doc.OriginalName;
|
const safePages = seg.pages.filter((p) => p >= 1 && p <= doc.PageCount && !deleted.has(p));
|
||||||
await this.mailService.sendMail({
|
const content = await buildSegmentBuffer(doc, pdfPath, safePages);
|
||||||
to: opts.to,
|
const filename = seg.filename.endsWith('.pdf') ? seg.filename : `${seg.filename}.pdf`;
|
||||||
subject: opts.subject,
|
return { filename, content };
|
||||||
body: opts.body,
|
}),
|
||||||
html: opts.html,
|
);
|
||||||
attachments: [{ filename, content }],
|
|
||||||
});
|
await this.mailService.sendMail({
|
||||||
} finally {
|
to: opts.to,
|
||||||
await cleanupTemp(tmpPath);
|
subject: opts.subject,
|
||||||
}
|
body: opts.body,
|
||||||
|
html: opts.html,
|
||||||
|
attachments,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Paperless" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 308 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 308 KiB |
@@ -119,7 +119,7 @@ export const inboxApi = {
|
|||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
filename?: string;
|
segments: { pages: number[]; filename: string }[];
|
||||||
},
|
},
|
||||||
) =>
|
) =>
|
||||||
api.post(`/api/inbox/${encodeURIComponent(id)}/send-email`, body).then(() => {}),
|
api.post(`/api/inbox/${encodeURIComponent(id)}/send-email`, body).then(() => {}),
|
||||||
|
|||||||
@@ -660,13 +660,15 @@ function TiptapToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
|||||||
interface SendEmailDialogProps {
|
interface SendEmailDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
defaultFilename: string;
|
fileName: string;
|
||||||
|
documents: DocumentSegment[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SendEmailDialog({ open, fileId, defaultFilename, onClose }: SendEmailDialogProps) {
|
function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEmailDialogProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [filenames, setFilenames] = useState<string[]>([]);
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [StarterKit, Underline],
|
extensions: [StarterKit, Underline],
|
||||||
@@ -674,12 +676,16 @@ function SendEmailDialog({ open, fileId, defaultFilename, onClose }: SendEmailDi
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (!open) return;
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
form.setFieldsValue({ filename: defaultFilename });
|
editor?.commands.clearContent();
|
||||||
editor?.commands.clearContent();
|
const base = fileName.replace(/\.pdf$/i, '');
|
||||||
}
|
setFilenames(
|
||||||
}, [open, defaultFilename, form, editor]);
|
documents.map((doc) =>
|
||||||
|
doc.belegname || (documents.length === 1 ? base : `${base}_${doc.index + 1}`),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [open, documents, fileName, form, editor]);
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -690,7 +696,10 @@ function SendEmailDialog({ open, fileId, defaultFilename, onClose }: SendEmailDi
|
|||||||
subject: values.subject,
|
subject: values.subject,
|
||||||
body: editor?.getText() ?? '',
|
body: editor?.getText() ?? '',
|
||||||
html: editor?.getHTML(),
|
html: editor?.getHTML(),
|
||||||
filename: values.filename || undefined,
|
segments: documents.map((doc, i) => ({
|
||||||
|
pages: doc.pages,
|
||||||
|
filename: filenames[i] || fileName,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
message.success('E-Mail wurde gesendet');
|
message.success('E-Mail wurde gesendet');
|
||||||
onClose();
|
onClose();
|
||||||
@@ -727,8 +736,26 @@ function SendEmailDialog({ open, fileId, defaultFilename, onClose }: SendEmailDi
|
|||||||
<EditorContent editor={editor} style={{ minHeight: 120, outline: 'none' }} />
|
<EditorContent editor={editor} style={{ minHeight: 120, outline: 'none' }} />
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="filename" label="Dateiname des Anhangs (ohne .pdf)">
|
<Form.Item label="Anhänge">
|
||||||
<Input placeholder={defaultFilename} />
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{documents.map((doc, i) => {
|
||||||
|
const first = doc.pages[0];
|
||||||
|
const last = doc.pages[doc.pages.length - 1];
|
||||||
|
const range = first === last ? `Seite ${first}` : `Seiten ${first}–${last}`;
|
||||||
|
return (
|
||||||
|
<div key={doc.index} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
|
<span style={{ minWidth: 90, color: '#888', fontSize: 12 }}>{range}</span>
|
||||||
|
<Input
|
||||||
|
value={filenames[i] ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilenames((prev) => prev.map((f, j) => (j === i ? e.target.value : f)))
|
||||||
|
}
|
||||||
|
suffix=".pdf"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -1560,7 +1587,8 @@ export default function InboxDetailPage() {
|
|||||||
<SendEmailDialog
|
<SendEmailDialog
|
||||||
open={emailDialogOpen}
|
open={emailDialogOpen}
|
||||||
fileId={file.id}
|
fileId={file.id}
|
||||||
defaultFilename={file.name.replace(/\.pdf$/i, '')}
|
fileName={file.name}
|
||||||
|
documents={documents}
|
||||||
onClose={() => setEmailDialogOpen(false)}
|
onClose={() => setEmailDialogOpen(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user