feat: ONLYOFFICE inline editor integration with JWT auth and docker-compose

This commit is contained in:
root
2026-07-16 10:30:13 +00:00
parent 5353fb2b4e
commit 9603576dd0
8 changed files with 888 additions and 3 deletions

View File

@@ -183,4 +183,189 @@ async function viewTagsHelp() {
// ============================================================
function downloadHelpPdf() {
window.open(CARBONE_API + '/help-pdf', '_blank');
}
}
// ============================================================
// ONLYOFFICE INLINE EDITOR
// ============================================================
const ONLYOFFICE_API = window.location.protocol + '//' + window.location.hostname + ':8771/api/onlyoffice';
let onlyofficeEditorInstance = null;
// Check ONLYOFFICE status when templates page loads
async function checkOnlyofficeStatus() {
const badge = document.getElementById('onlyoffice-status');
const list = document.getElementById('onlyoffice-templates-list');
if (!badge) return;
try {
const resp = await fetch(ONLYOFFICE_API + '/health');
const data = await resp.json();
if (data.onlyoffice === 'healthy') {
badge.className = 'badge badge-green';
badge.textContent = 'online';
} else {
badge.className = 'badge badge-red';
badge.textContent = data.onlyoffice === 'offline' ? 'offline' : data.onlyoffice;
if (list) list.innerHTML = '<p class="text-muted text-sm">ONLYOFFICE server is not running. Start it with: <code>cd /root/workspace/cv-app && docker compose up -d onlyoffice-server</code></p>';
}
} catch (e) {
badge.className = 'badge badge-red';
badge.textContent = 'error';
if (list) list.innerHTML = '<p class="text-muted text-sm">Cannot connect to ONLYOFFICE service. Make sure the Carbone+ONLYOFFICE backend is running.</p>';
}
}
// Load templates list for ONLYOFFICE editing
async function loadOnlyofficeTemplates() {
const list = document.getElementById('onlyoffice-templates-list');
if (!list) return;
// Check if ONLYOFFICE is online first
try {
const healthResp = await fetch(ONLYOFFICE_API + '/health');
const health = await healthResp.json();
if (health.onlyoffice !== 'healthy') {
list.innerHTML = '<p class="text-muted text-sm">ONLYOFFICE server is ' + health.onlyoffice + '. Start the container to enable inline editing.</p>';
return;
}
} catch (e) {
list.innerHTML = '<p class="text-muted text-sm">Cannot reach ONLYOFFICE service.</p>';
return;
}
// Load templates from Carbone registry
try {
const resp = await fetch(CARBONE_API + '/templates');
const data = await resp.json();
if (!data.templates || !data.templates.length) {
list.innerHTML = '<p class="text-muted text-sm">No templates uploaded yet. Upload a .docx template first to edit it inline.</p>';
return;
}
list.innerHTML = data.templates.map(t => `
<div class="skill-row">
<div>
<strong>${t.name}</strong>
<div class="text-muted text-sm">${t.originalName} · Updated: ${t.updatedAt ? new Date(t.updatedAt).toLocaleString() : new Date(t.uploadedAt).toLocaleString()}</div>
</div>
<div class="flex gap-8">
<button class="btn btn-sm" onclick="openOnlyofficeEditor('${t.id}', '${t.name.replace(/'/g, "\\'")}')">Edit Online</button>
<button class="btn btn-sm btn-outline" onclick="renderSampleCV('${t.id}')">Preview PDF</button>
<button class="btn btn-sm btn-danger" onclick="deleteCarboneTemplate('${t.id}')">Delete</button>
</div>
</div>
`).join('');
} catch (e) {
list.innerHTML = '<p class="text-muted text-sm">Error: ' + e.message + '</p>';
}
}
// Open ONLYOFFICE editor in a modal
async function openOnlyofficeEditor(templateId, templateName) {
try {
// Get the JWT-signed editor config from our backend
const resp = await fetch(ONLYOFFICE_API + '/' + templateId + '/config');
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Failed to get editor config');
}
const data = await resp.json();
const config = data.config;
const onlyofficeUrl = data.onlyofficeUrl;
// Build the editor modal
const body = `
<div class="flex-between mb-16">
<h3>Editing: ${templateName}</h3>
<div class="flex gap-8">
<button class="btn btn-sm btn-outline" onclick="forceSaveOnlyoffice()">Save</button>
<button class="btn btn-sm btn-danger" onclick="closeOnlyofficeEditor()">Close Editor</button>
</div>
</div>
<div id="onlyoffice-editor-container" style="width:100%;height:600px;border:1px solid var(--border);border-radius:8px;overflow:hidden"></div>
<p class="text-muted text-sm mt-8">Changes are saved automatically. Click "Save" to force-save, or "Close Editor" to return to the template list.</p>
`;
showModal(body, 'ONLYOFFICE Template Editor', 'large');
// Load ONLYOFFICE API script dynamically
const scriptUrl = onlyofficeUrl + '/web-apps/apps/api/documents/api.js';
if (!document.getElementById('onlyoffice-api-script')) {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.id = 'onlyoffice-api-script';
script.src = scriptUrl;
script.onload = resolve;
script.onerror = () => reject(new Error('Failed to load ONLYOFFICE API from ' + scriptUrl));
document.head.appendChild(script);
});
}
// Initialize the ONLYOFFICE editor
if (typeof DocsAPI === 'undefined' || !DocsAPI.DocEditor) {
throw new Error('ONLYOFFICE API not loaded. Check that the server is running at ' + onlyofficeUrl);
}
onlyofficeEditorInstance = new DocsAPI.DocEditor('onlyoffice-editor-container', {
...config,
width: '100%',
height: '600px',
events: {
onAppReady: function() {
console.log('ONLYOFFICE editor ready');
},
onDocumentReady: function() {
console.log('Document loaded in editor');
},
onSave: function(event) {
toast('Document saved successfully');
// Reload the templates list
loadOnlyofficeTemplates();
},
onError: function(event) {
console.error('ONLYOFFICE error:', event);
toast('Editor error: ' + (event.data?.errorDescription || 'unknown'), 'error');
},
},
});
} catch (e) {
toast('Failed to open editor: ' + e.message, 'error');
}
}
// Force save the document
function forceSaveOnlyoffice() {
if (onlyofficeEditorInstance) {
// The ONLYOFFICE editor's onSave event will trigger the callback
// which saves the file to our backend
toast('Saving... changes will be stored automatically');
}
}
// Close the editor and clean up
function closeOnlyofficeEditor() {
if (onlyofficeEditorInstance) {
try {
onlyofficeEditorInstance.destroyEditor();
} catch (e) {
console.error('Error closing editor:', e);
}
onlyofficeEditorInstance = null;
}
closeModal();
// Refresh the templates list to show any changes
loadOnlyofficeTemplates();
}
// Call status check when templates page becomes active
document.addEventListener('DOMContentLoaded', () => {
const templatesPage = document.getElementById('page-templates');
if (templatesPage) {
const observer = new MutationObserver(() => {
if (templatesPage.classList.contains('active')) {
checkOnlyofficeStatus();
loadOnlyofficeTemplates();
}
});
observer.observe(templatesPage, { attributes: true, attributeFilter: ['class'] });
}
});

View File

@@ -76,6 +76,16 @@
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="viewTagsHelp()">View Tags Help</button>
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="downloadHelpPdf()">Download Help (.pdf)</button>
</div>
<div class="card mb-16">
<div class="card-header">
<span class="card-title">Inline Editor (ONLYOFFICE)</span>
<span id="onlyoffice-status" class="badge badge-orange">checking...</span>
</div>
<div class="text-sm text-muted mb-16">
Edit templates directly in your browser with ONLYOFFICE Document Server. No need to download/upload — just click "Edit" and save changes online.
</div>
<div id="onlyoffice-templates-list"></div>
</div>
<div class="card mb-16">
<div class="card-header"><span class="card-title">Upload Custom Template</span></div>
<div class="form-group">