feat: extract WysiwygEditor into reusable component and add advanced formatting options
Build and Push Multi-Platform Images / build-and-push (push) Successful in 23s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 23s
This commit is contained in:
Generated
+14
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.1.1",
|
"@ant-design/icons": "^6.1.1",
|
||||||
|
"@tiptap/extension-text-align": "^3.22.5",
|
||||||
"@tiptap/extension-underline": "^3.22.5",
|
"@tiptap/extension-underline": "^3.22.5",
|
||||||
"@tiptap/react": "^3.22.5",
|
"@tiptap/react": "^3.22.5",
|
||||||
"@tiptap/starter-kit": "^3.22.5",
|
"@tiptap/starter-kit": "^3.22.5",
|
||||||
@@ -2281,6 +2282,19 @@
|
|||||||
"@tiptap/core": "3.22.5"
|
"@tiptap/core": "3.22.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-align": {
|
||||||
|
"version": "3.22.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.22.5.tgz",
|
||||||
|
"integrity": "sha512-LNUinhsZJC+/Vm7ugtghSjqlO64FQuww8oJkHq54Oh135fJ+kY2yBRakYFeH0P0gl4VPJH13AUetLF696CM2UQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extension-underline": {
|
"node_modules/@tiptap/extension-underline": {
|
||||||
"version": "3.22.5",
|
"version": "3.22.5",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.5.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.5.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.1.1",
|
"@ant-design/icons": "^6.1.1",
|
||||||
|
"@tiptap/extension-text-align": "^3.22.5",
|
||||||
"@tiptap/extension-underline": "^3.22.5",
|
"@tiptap/extension-underline": "^3.22.5",
|
||||||
"@tiptap/react": "^3.22.5",
|
"@tiptap/react": "^3.22.5",
|
||||||
"@tiptap/starter-kit": "^3.22.5",
|
"@tiptap/starter-kit": "^3.22.5",
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import { forwardRef, useImperativeHandle } from 'react';
|
||||||
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import Underline from '@tiptap/extension-underline';
|
||||||
|
import TextAlign from '@tiptap/extension-text-align';
|
||||||
|
import { Select } from 'antd';
|
||||||
|
import {
|
||||||
|
AlignCenterOutlined,
|
||||||
|
AlignLeftOutlined,
|
||||||
|
AlignRightOutlined,
|
||||||
|
BoldOutlined,
|
||||||
|
ItalicOutlined,
|
||||||
|
OrderedListOutlined,
|
||||||
|
RedoOutlined,
|
||||||
|
StrikethroughOutlined,
|
||||||
|
UnderlineOutlined,
|
||||||
|
UndoOutlined,
|
||||||
|
UnorderedListOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
export interface WysiwygEditorHandle {
|
||||||
|
getHTML: () => string;
|
||||||
|
getText: () => string;
|
||||||
|
setContent: (html: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WysiwygEditorProps {
|
||||||
|
minHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolBtn({
|
||||||
|
active,
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
active?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
title?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title={title}
|
||||||
|
disabled={disabled}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!disabled) onClick();
|
||||||
|
}}
|
||||||
|
className={`pm-tool-btn${active ? ' pm-tool-btn--active' : ''}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Divider() {
|
||||||
|
return <span className="pm-tool-divider" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEADING_OPTIONS = [
|
||||||
|
{ value: 'p', label: 'Normal' },
|
||||||
|
{ value: '1', label: 'Überschrift 1' },
|
||||||
|
{ value: '2', label: 'Überschrift 2' },
|
||||||
|
{ value: '3', label: 'Überschrift 3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const WysiwygEditor = forwardRef<WysiwygEditorHandle, WysiwygEditorProps>(
|
||||||
|
({ minHeight = 200 }, ref) => {
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
Underline,
|
||||||
|
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
||||||
|
],
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
getHTML: () => editor?.getHTML() ?? '',
|
||||||
|
getText: () => editor?.getText() ?? '',
|
||||||
|
setContent: (html: string) => {
|
||||||
|
editor?.commands.setContent(html);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[editor],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!editor) return null;
|
||||||
|
|
||||||
|
const headingLevel = editor.isActive('heading', { level: 1 })
|
||||||
|
? '1'
|
||||||
|
: editor.isActive('heading', { level: 2 })
|
||||||
|
? '2'
|
||||||
|
: editor.isActive('heading', { level: 3 })
|
||||||
|
? '3'
|
||||||
|
: 'p';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pm-editor-wrap">
|
||||||
|
<div className="pm-toolbar">
|
||||||
|
<ToolBtn
|
||||||
|
title="Rückgängig (Strg+Z)"
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
>
|
||||||
|
<UndoOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
title="Wiederholen (Strg+Y)"
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
>
|
||||||
|
<RedoOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={headingLevel}
|
||||||
|
style={{ width: 128 }}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (v === 'p') {
|
||||||
|
editor.chain().focus().setParagraph().run();
|
||||||
|
} else {
|
||||||
|
editor.chain().focus().setHeading({ level: parseInt(v) as 1 | 2 | 3 }).run();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={HEADING_OPTIONS}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<ToolBtn
|
||||||
|
active={editor.isActive('bold')}
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
title="Fett (Strg+B)"
|
||||||
|
>
|
||||||
|
<BoldOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
active={editor.isActive('italic')}
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
title="Kursiv (Strg+I)"
|
||||||
|
>
|
||||||
|
<ItalicOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
active={editor.isActive('underline')}
|
||||||
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
|
title="Unterstrichen (Strg+U)"
|
||||||
|
>
|
||||||
|
<UnderlineOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
active={editor.isActive('strike')}
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
title="Durchgestrichen"
|
||||||
|
>
|
||||||
|
<StrikethroughOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<ToolBtn
|
||||||
|
active={editor.isActive('bulletList')}
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
title="Aufzählungsliste"
|
||||||
|
>
|
||||||
|
<UnorderedListOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
active={editor.isActive('orderedList')}
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
title="Nummerierte Liste"
|
||||||
|
>
|
||||||
|
<OrderedListOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<ToolBtn
|
||||||
|
active={editor.isActive({ textAlign: 'left' })}
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||||
|
title="Linksbündig"
|
||||||
|
>
|
||||||
|
<AlignLeftOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
active={editor.isActive({ textAlign: 'center' })}
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||||
|
title="Zentriert"
|
||||||
|
>
|
||||||
|
<AlignCenterOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
active={editor.isActive({ textAlign: 'right' })}
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||||
|
title="Rechtsbündig"
|
||||||
|
>
|
||||||
|
<AlignRightOutlined />
|
||||||
|
</ToolBtn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pm-document-surround" style={{ minHeight: minHeight + 32 }}>
|
||||||
|
<div className="pm-document-page" style={{ minHeight }}>
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -24,6 +24,112 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for font size in Ant Design components */
|
/* Fix for font size in Ant Design components */
|
||||||
|
/* ── Tiptap / ProseMirror Word-style editor ──────────────────── */
|
||||||
|
.pm-editor-wrap {
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-bottom: 1px solid #d9d9d9;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tool-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.1s, border-color 0.1s, color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tool-btn:hover:not(:disabled) {
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tool-btn--active {
|
||||||
|
background: #d6e4ff !important;
|
||||||
|
border-color: #91caff !important;
|
||||||
|
color: #1677ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tool-btn:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-tool-divider {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
background: #d0d0d0;
|
||||||
|
margin: 0 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-document-surround {
|
||||||
|
background: #e0e0e0;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-document-page {
|
||||||
|
background: #fff;
|
||||||
|
padding: 28px 36px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||||
|
font-family: 'Calibri', 'Segoe UI', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
|
outline: none;
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror > * + * {
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h1 { font-size: 2em; font-weight: 700; line-height: 1.2; }
|
||||||
|
.ProseMirror h2 { font-size: 1.5em; font-weight: 700; line-height: 1.25; }
|
||||||
|
.ProseMirror h3 { font-size: 1.17em; font-weight: 600; line-height: 1.3; }
|
||||||
|
|
||||||
|
.ProseMirror ul,
|
||||||
|
.ProseMirror ol {
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror ul { list-style-type: disc; }
|
||||||
|
.ProseMirror ol { list-style-type: decimal; }
|
||||||
|
|
||||||
|
.ProseMirror blockquote {
|
||||||
|
border-left: 3px solid #ccc;
|
||||||
|
padding-left: 1em;
|
||||||
|
color: #555;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.ant-select,
|
.ant-select,
|
||||||
.ant-select-selection-search-input,
|
.ant-select-selection-search-input,
|
||||||
.ant-select-item,
|
.ant-select-item,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||||
|
import { WysiwygEditor, type WysiwygEditorHandle } from '../components/WysiwygEditor';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Button, Dropdown, Empty, Form, Input, Modal, Popconfirm, Select, Space, Spin, Steps, Tag, Tooltip, Typography, message } from 'antd';
|
import { Button, Dropdown, Empty, Form, Input, Modal, Popconfirm, Select, Space, Spin, Steps, Tag, Tooltip, Typography, message } from 'antd';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
@@ -21,9 +22,6 @@ import {
|
|||||||
ZoomInOutlined,
|
ZoomInOutlined,
|
||||||
ZoomOutOutlined,
|
ZoomOutOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
|
||||||
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 } from '../api/paperless';
|
import { paperlessApi } from '../api/paperless';
|
||||||
import { userSettingsApi, type SenderOption } from '../api/userSettings';
|
import { userSettingsApi, type SenderOption } from '../api/userSettings';
|
||||||
@@ -637,27 +635,6 @@ function DownloadSegmentsDialog({ open, fileId, fileName, documents, onClose }:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TiptapToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
|
||||||
if (!editor) return null;
|
|
||||||
const btnStyle = (active: boolean): React.CSSProperties => ({
|
|
||||||
padding: '2px 8px',
|
|
||||||
border: '1px solid #d9d9d9',
|
|
||||||
borderRadius: 4,
|
|
||||||
cursor: 'pointer',
|
|
||||||
background: active ? '#e6f4ff' : '#fff',
|
|
||||||
fontWeight: active ? 600 : 400,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', gap: 4, padding: '4px 0', borderBottom: '1px solid #f0f0f0', marginBottom: 6, flexWrap: 'wrap' }}>
|
|
||||||
<button style={btnStyle(editor.isActive('bold'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleBold().run(); }}>F</button>
|
|
||||||
<button style={{ ...btnStyle(editor.isActive('italic')), fontStyle: 'italic' }} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleItalic().run(); }}>K</button>
|
|
||||||
<button style={{ ...btnStyle(editor.isActive('underline')), textDecoration: 'underline' }} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleUnderline().run(); }}>U</button>
|
|
||||||
<button style={btnStyle(editor.isActive('bulletList'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleBulletList().run(); }}>• Liste</button>
|
|
||||||
<button style={btnStyle(editor.isActive('orderedList'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleOrderedList().run(); }}>1. Liste</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SendEmailDialogProps {
|
interface SendEmailDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
@@ -672,11 +649,7 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma
|
|||||||
const [filenames, setFilenames] = useState<string[]>([]);
|
const [filenames, setFilenames] = useState<string[]>([]);
|
||||||
const [senders, setSenders] = useState<SenderOption[]>([]);
|
const [senders, setSenders] = useState<SenderOption[]>([]);
|
||||||
const [selectedSender, setSelectedSender] = useState<string>('default');
|
const [selectedSender, setSelectedSender] = useState<string>('default');
|
||||||
|
const editorRef = useRef<WysiwygEditorHandle>(null);
|
||||||
const editor = useEditor({
|
|
||||||
extensions: [StarterKit, Underline],
|
|
||||||
content: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -688,15 +661,13 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
userSettingsApi.get().then((settings) => {
|
userSettingsApi.get().then((settings) => {
|
||||||
editor?.commands.setContent(settings.mailSignatureHtml ?? '');
|
editorRef.current?.setContent(settings.mailSignatureHtml ?? '');
|
||||||
}).catch(() => {
|
}).catch(() => {});
|
||||||
editor?.commands.clearContent();
|
|
||||||
});
|
|
||||||
userSettingsApi.getSenders().then((s) => {
|
userSettingsApi.getSenders().then((s) => {
|
||||||
setSenders(s);
|
setSenders(s);
|
||||||
setSelectedSender(s.length > 1 ? 'user' : 'default');
|
setSelectedSender(s.length > 1 ? 'user' : 'default');
|
||||||
}).catch(() => setSenders([]));
|
}).catch(() => setSenders([]));
|
||||||
}, [open, documents, fileName, form, editor]);
|
}, [open, documents, fileName, form]);
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -705,8 +676,8 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma
|
|||||||
await inboxApi.sendEmail(fileId, {
|
await inboxApi.sendEmail(fileId, {
|
||||||
to: values.to,
|
to: values.to,
|
||||||
subject: values.subject,
|
subject: values.subject,
|
||||||
body: editor?.getText() ?? '',
|
body: editorRef.current?.getText() ?? '',
|
||||||
html: editor?.getHTML(),
|
html: editorRef.current?.getHTML(),
|
||||||
segments: documents.map((doc, i) => ({
|
segments: documents.map((doc, i) => ({
|
||||||
pages: doc.pages,
|
pages: doc.pages,
|
||||||
filename: filenames[i] || fileName,
|
filename: filenames[i] || fileName,
|
||||||
@@ -752,10 +723,7 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Nachricht">
|
<Form.Item label="Nachricht">
|
||||||
<div style={{ border: '1px solid #d9d9d9', borderRadius: 6, padding: '6px 10px', minHeight: 160 }}>
|
<WysiwygEditor ref={editorRef} minHeight={180} />
|
||||||
<TiptapToolbar editor={editor} />
|
|
||||||
<EditorContent editor={editor} style={{ minHeight: 120, outline: 'none' }} />
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Anhänge">
|
<Form.Item label="Anhänge">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
|||||||
@@ -1,34 +1,11 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Button, Card, Form, Input, InputNumber, Space, Switch, Tabs, Typography, message } from 'antd';
|
import { Button, Card, Form, Input, InputNumber, Space, Switch, Tabs, Typography, message } from 'antd';
|
||||||
import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { WysiwygEditor, type WysiwygEditorHandle } from '../components/WysiwygEditor';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
|
||||||
import Underline from '@tiptap/extension-underline';
|
|
||||||
import { userSettingsApi } from '../api/userSettings';
|
import { userSettingsApi } from '../api/userSettings';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
function TiptapToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
|
||||||
if (!editor) return null;
|
|
||||||
const btn = (active: boolean): React.CSSProperties => ({
|
|
||||||
padding: '2px 8px',
|
|
||||||
border: '1px solid #d9d9d9',
|
|
||||||
borderRadius: 4,
|
|
||||||
cursor: 'pointer',
|
|
||||||
background: active ? '#e6f4ff' : '#fff',
|
|
||||||
fontWeight: active ? 600 : 400,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', gap: 4, padding: '4px 0', borderBottom: '1px solid #f0f0f0', marginBottom: 6, flexWrap: 'wrap' }}>
|
|
||||||
<button style={btn(editor.isActive('bold'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleBold().run(); }}>F</button>
|
|
||||||
<button style={{ ...btn(editor.isActive('italic')), fontStyle: 'italic' }} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleItalic().run(); }}>K</button>
|
|
||||||
<button style={{ ...btn(editor.isActive('underline')), textDecoration: 'underline' }} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleUnderline().run(); }}>U</button>
|
|
||||||
<button style={btn(editor.isActive('bulletList'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleBulletList().run(); }}>• Liste</button>
|
|
||||||
<button style={btn(editor.isActive('orderedList'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleOrderedList().run(); }}>1. Liste</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MailSettingsTab() {
|
function MailSettingsTab() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -36,11 +13,7 @@ function MailSettingsTab() {
|
|||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
const [testResult, setTestResult] = useState<{ ok: boolean; error?: string } | null>(null);
|
const [testResult, setTestResult] = useState<{ ok: boolean; error?: string } | null>(null);
|
||||||
const [passSet, setPassSet] = useState(false);
|
const [passSet, setPassSet] = useState(false);
|
||||||
|
const editorRef = useRef<WysiwygEditorHandle>(null);
|
||||||
const editor = useEditor({
|
|
||||||
extensions: [StarterKit, Underline],
|
|
||||||
content: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userSettingsApi.get().then((data) => {
|
userSettingsApi.get().then((data) => {
|
||||||
@@ -54,7 +27,7 @@ function MailSettingsTab() {
|
|||||||
});
|
});
|
||||||
setPassSet(data.smtpPassSet);
|
setPassSet(data.smtpPassSet);
|
||||||
if (data.mailSignatureHtml) {
|
if (data.mailSignatureHtml) {
|
||||||
editor?.commands.setContent(data.mailSignatureHtml);
|
editorRef.current?.setContent(data.mailSignatureHtml);
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
message.error('Einstellungen konnten nicht geladen werden');
|
message.error('Einstellungen konnten nicht geladen werden');
|
||||||
@@ -74,7 +47,7 @@ function MailSettingsTab() {
|
|||||||
smtpPass: values.smtpPass || undefined,
|
smtpPass: values.smtpPass || undefined,
|
||||||
smtpFrom: values.smtpFrom || null,
|
smtpFrom: values.smtpFrom || null,
|
||||||
smtpFromName: values.smtpFromName || null,
|
smtpFromName: values.smtpFromName || null,
|
||||||
mailSignatureHtml: editor?.getHTML() ?? null,
|
mailSignatureHtml: editorRef.current?.getHTML() ?? null,
|
||||||
});
|
});
|
||||||
setPassSet(updated.smtpPassSet);
|
setPassSet(updated.smtpPassSet);
|
||||||
form.setFieldValue('smtpPass', '');
|
form.setFieldValue('smtpPass', '');
|
||||||
@@ -126,7 +99,10 @@ function MailSettingsTab() {
|
|||||||
<Input autoComplete="off" />
|
<Input autoComplete="off" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="smtpPass" label="Passwort">
|
<Form.Item name="smtpPass" label="Passwort">
|
||||||
<Input.Password placeholder={passSet ? '(bereits gesetzt — leer lassen zum Beibehalten)' : ''} autoComplete="new-password" />
|
<Input.Password
|
||||||
|
placeholder={passSet ? '(bereits gesetzt — leer lassen zum Beibehalten)' : ''}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="smtpFrom" label="Absender-Adresse (From)">
|
<Form.Item name="smtpFrom" label="Absender-Adresse (From)">
|
||||||
<Input placeholder="mein.name@beispiel.de" />
|
<Input placeholder="mein.name@beispiel.de" />
|
||||||
@@ -136,10 +112,7 @@ function MailSettingsTab() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="Signatur">
|
<Form.Item label="Signatur">
|
||||||
<div style={{ border: '1px solid #d9d9d9', borderRadius: 6, padding: '6px 10px', minHeight: 160 }}>
|
<WysiwygEditor ref={editorRef} minHeight={180} />
|
||||||
<TiptapToolbar editor={editor} />
|
|
||||||
<EditorContent editor={editor} style={{ minHeight: 120, outline: 'none' }} />
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
|
|||||||
Reference in New Issue
Block a user