EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
DocumentForm.jsx
Go to the documentation of this file.
1import { useState, useEffect, useRef } from 'react';
2import Quill from 'quill';
3import { useNavigate, useParams } from 'react-router-dom';
4import MainLayout from '../components/Layout/MainLayout';
5// import removed: TenantSelector
6import AIRewordButton from '../components/AIRewordButton';
7import './DocumentForm.css';
8import { apiFetch } from '../lib/api';
9
10function DocumentForm() {
11 const navigate = useNavigate();
12 const { id } = useParams();
13 const [selectedTenantId, setSelectedTenantId] = useState('');
14 const [form, setForm] = useState({ title: '', content: '', category: '' });
15 const [loading, setLoading] = useState(false);
16 const [loadingData, setLoadingData] = useState(false);
17 const [error, setError] = useState('');
18 const [attachments, setAttachments] = useState([]);
19 const quillRefGlobal = useRef(null);
20
21 useEffect(() => {
22 if (id) {
23 fetchDocument();
24 }
25 }, [id]);
26
27 // Initialize Quill editor once on mount
28 useEffect(() => {
29 if (!quillRefGlobal.current) {
30 const editor = document.getElementById('quill-editor');
31 if (editor) {
32 const quill = new Quill(editor, { theme: 'snow' });
33 quill.enable(false); // disable until data is loaded
34 quill.on('text-change', () => {
35 setForm(f => ({ ...f, content: quill.root.innerHTML }));
36 });
37 quillRefGlobal.current = quill;
38 console.log('[Quill] Editor initialized');
39 } else {
40 console.error('[Quill] #quill-editor not found in DOM!');
41 }
42 }
43 }, []);
44
45 const getTenantHeaders = async (baseHeaders = {}) => {
46 const headers = { ...baseHeaders };
47 if (selectedTenantId) {
48 const token = localStorage.getItem('token');
49 const tenantRes = await apiFetch('/tenants', {
50 headers: {
51 'Authorization': `Bearer ${token}`,
52 'X-Tenant-Subdomain': 'admin'
53 }
54 });
55 if (tenantRes.ok) {
56 const data = await tenantRes.json();
57 const tenants = Array.isArray(data) ? data : (data.tenants || []);
58 const tenant = tenants.find(t => t.tenant_id === parseInt(selectedTenantId));
59 if (tenant) {
60 headers['X-Tenant-Subdomain'] = tenant.subdomain;
61 }
62 }
63 }
64 return headers;
65 };
66
67 // Sync Quill with fetched content
68 useEffect(() => {
69 if (quillRefGlobal.current && form.content !== undefined) {
70 const quill = quillRefGlobal.current;
71 const currentHtml = quill.root.innerHTML;
72 if ((form.content || '') !== currentHtml) {
73 console.log('[Quill] Syncing fetched content');
74 quill.setContents([]);
75 quill.clipboard.dangerouslyPasteHTML(form.content || '');
76 quill.enable(true);
77 }
78 }
79 }, [form.content]);
80
81 const fetchDocument = async () => {
82 setLoadingData(true);
83 try {
84 const token = localStorage.getItem('token');
85 const res = await apiFetch(`/documents/${id}`, {
86 headers: { Authorization: `Bearer ${token}` }
87 });
88 if (!res.ok) throw new Error('Failed to load document');
89 const data = await res.json();
90
91 if (data.tenant_id && !selectedTenantId) {
92 setSelectedTenantId(String(data.tenant_id));
93 }
94
95 setForm({
96 title: data.title || '',
97 content: data.content || '',
98 category: data.category || '',
99 attachments: data.attachments || []
100 });
101
102 // Apply content to Quill once loaded
103 if (quillRefGlobal.current && data.content) {
104 quillRefGlobal.current.clipboard.dangerouslyPasteHTML(data.content);
105 quillRefGlobal.current.enable(true);
106 }
107 } catch (err) {
108 setError(err.message || 'Error loading document');
109 } finally {
110 setLoadingData(false);
111 }
112 };
113
114 const handleSubmit = async (e) => {
115 e.preventDefault();
116 setLoading(true);
117 setError('');
118 try {
119 const token = localStorage.getItem('token');
120 const payload = { title: form.title, content: form.content, category: form.category };
121 const url = id ? `/documents/${id}` : '/documents';
122 const method = id ? 'PUT' : 'POST';
123 const headers = await getTenantHeaders({
124 'Content-Type': 'application/json',
125 Authorization: `Bearer ${token}`
126 });
127
128 if (!id && attachments.length) {
129 payload.attachments = attachments;
130 }
131
132 const res = await apiFetch(url, {
133 method,
134 headers,
135 body: JSON.stringify(payload),
136 });
137
138 if (!res.ok) throw new Error(`Failed to ${id ? 'update' : 'create'} document`);
139 navigate('/knowledge-base');
140 } catch (err) {
141 console.error(err);
142 setError(err.message || 'Save failed');
143 } finally {
144 setLoading(false);
145 }
146 };
147
148 const handleFileChange = (ev) => {
149 const file = ev.target.files && ev.target.files[0];
150 if (!file) return;
151 const reader = new FileReader();
152 reader.onload = () => {
153 const data = reader.result.split(',')[1];
154 setAttachments([{ filename: file.name, data }]);
155 };
156 reader.readAsDataURL(file);
157 };
158
159 const handleUploadAttachment = async () => {
160 if (!id || attachments.length === 0) {
161 setError('Please select a file to upload');
162 return;
163 }
164 setLoading(true);
165 setError('');
166 try {
167 const token = localStorage.getItem('token');
168 const headers = await getTenantHeaders({
169 'Content-Type': 'application/json',
170 Authorization: `Bearer ${token}`
171 });
172
173 const res = await apiFetch(`/documents/${id}/attachments`, {
174 method: 'POST',
175 headers,
176 body: JSON.stringify({ attachments }),
177 });
178
179 if (!res.ok) {
180 const errData = await res.json().catch(() => ({}));
181 throw new Error(errData.error || 'Failed to upload attachment');
182 }
183
184 await fetchDocument();
185 setAttachments([]);
186 const fileInput = document.getElementById('attachment');
187 if (fileInput) fileInput.value = '';
188 } catch (err) {
189 console.error(err);
190 setError(err.message || 'Upload failed');
191 } finally {
192 setLoading(false);
193 }
194 };
195
196 return (
197 <MainLayout>
198 <div className="page-content">
199 <div className="page-header">
200 <h2>{id ? 'Edit Document' : 'Create Document'}</h2>
201 <div className="header-actions">
202 <button className="btn btn-secondary" onClick={() => navigate('/knowledge-base')}>
203 <span className="material-symbols-outlined">arrow_back</span>
204 Back
205 </button>
206 </div>
207 </div>
208
209 {error && <div className="alert alert-error">{error}</div>}
210
211
212 {loadingData ? (
213 <div className="loading-state">
214 <span className="material-symbols-outlined spinning">progress_activity</span>
215 <p>Loading document...</p>
216 </div>
217 ) : (
218 <form onSubmit={handleSubmit} className="document-form-container">
219 <div className="form-card">
220 <div className="card-header">
221 <h3>
222 <span className="material-symbols-outlined">description</span>
223 Document Information
224 </h3>
225 </div>
226 <div className="card-body">
227 <div className="form-group" style={{ position: 'relative' }}>
228 <label htmlFor="title">Title *</label>
229 <input
230 id="title"
231 type="text"
232 placeholder="Enter document title"
233 value={form.title}
234 onChange={(e) => setForm({ ...form, title: e.target.value })}
235 required
236 style={{ paddingRight: 40 }}
237 />
238 <AIRewordButton
239 value={form.title}
240 onReword={(reworded) => setForm(f => ({ ...f, title: reworded }))}
241 disabled={!form.title}
242 style={{ position: 'absolute', right: 8, top: 8 }}
243 />
244 </div>
245 <div className="form-group">
246 <label htmlFor="category">Category</label>
247 <input
248 id="category"
249 type="text"
250 placeholder="e.g., Policy, Guide, Report"
251 value={form.category}
252 onChange={(e) => setForm({ ...form, category: e.target.value })}
253 />
254 </div>
255 </div>
256 </div>
257
258 <div className="form-card">
259 <div className="card-header">
260 <h3>
261 <span className="material-symbols-outlined">edit_note</span>
262 Content
263 </h3>
264 </div>
265 <div className="card-body">
266 {loadingData ? (
267 <div>Loading content...</div>
268 ) : (
269 <div className="quill-wrapper" style={{ position: 'relative' }}>
270 <div id="quill-editor" />
271 <AIRewordButton
272 value={form.content}
273 onReword={(reworded) => {
274 setForm(f => ({ ...f, content: reworded }));
275 if (quillRefGlobal.current) {
276 quillRefGlobal.current.clipboard.dangerouslyPasteHTML(reworded);
277 }
278 }}
279 disabled={!form.content}
280 style={{ position: 'absolute', right: 8, top: 8, zIndex: 10 }}
281 />
282 </div>
283 )}
284 </div>
285 </div>
286
287 <div className="form-card">
288 <div className="card-header">
289 <h3>
290 <span className="material-symbols-outlined">attach_file</span>
291 Attachments
292 </h3>
293 </div>
294 <div className="card-body">
295 {Array.isArray(form.attachments) && form.attachments.length > 0 && (
296 <div className="existing-attachments">
297 <label>Existing Files</label>
298 <ul className="attachment-list" style={{ listStyle: 'none', padding: 0 }}>
299 {form.attachments.map(a => (
300 <li key={a.id} className="attachment-item" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem', borderBottom: '1px solid #e0e0e0' }}>
301 <span className="material-symbols-outlined" style={{ fontSize: '20px', color: '#666' }}>insert_drive_file</span>
302 <span style={{ flex: 1 }}>{a.original_filename || a.filename}</span>
303 <a
304 className="btn-icon"
305 style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', borderRadius: '4px', border: '1px solid #ccc', backgroundColor: '#fff', cursor: 'pointer', textDecoration: 'none', color: '#333' }}
306 href={`/documents/${id}/attachments/${a.id}/download`}
307 download
308 title="Download"
309 >
310 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>download</span>
311 </a>
312 </li>
313 ))}
314 </ul>
315 </div>
316 )}
317
318 <div className="form-group" style={{ marginTop: form.attachments?.length ? '1rem' : 0 }}>
319 <label htmlFor="attachment">{id ? 'Add Another File' : 'Upload File (optional)'}</label>
320 <input id="attachment" type="file" onChange={handleFileChange} className="file-input" />
321 {attachments.length > 0 && (
322 <div className="file-selected" style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
323 <span className="material-symbols-outlined" style={{ color: 'green' }}>check_circle</span>
324 <span>Selected: {attachments[0].filename}</span>
325 </div>
326 )}
327 {id && attachments.length > 0 && (
328 <button type="button" className="btn btn-primary" style={{ marginTop: '0.75rem' }} onClick={handleUploadAttachment} disabled={loading}>
329 <span className="material-symbols-outlined">upload</span>
330 {loading ? 'Uploading...' : 'Upload Document'}
331 </button>
332 )}
333 </div>
334 </div>
335 </div>
336
337 <div className="form-actions">
338 <button type="submit" className="btn btn-primary" disabled={loading || !form.title}>
339 {loading ? (
340 <>
341 <span className="material-symbols-outlined spinning">progress_activity</span>
342 Saving...
343 </>
344 ) : (
345 <>
346 <span className="material-symbols-outlined">save</span>
347 {id ? 'Update Document' : 'Create Document'}
348 </>
349 )}
350 </button>
351 <button type="button" className="btn btn-secondary" onClick={() => navigate('/knowledge-base')} disabled={loading}>
352 <span className="material-symbols-outlined">close</span>
353 Cancel
354 </button>
355 </div>
356 </form>
357 )}
358 </div>
359 </MainLayout>
360 );
361}
362
363export default DocumentForm;