Initial onlyoffice integration, sidebar changes
Some checks failed
Automated Container Build / build-and-push (push) Failing after 28s

This commit is contained in:
Elijah 2026-05-24 10:46:27 -07:00
parent 4110657557
commit d80e31e7f5
7 changed files with 504 additions and 12 deletions

View file

@ -0,0 +1,129 @@
package handlers
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"git.elijahkuntz.com/Elijah/drive/config"
"git.elijahkuntz.com/Elijah/drive/middleware"
"github.com/gofiber/fiber/v2"
)
type OnlyOfficeHandler struct {
Config *config.Config
}
type CallbackRequest struct {
Status int `json:"status"`
Url string `json:"url"`
Key string `json:"key"`
}
func (h *OnlyOfficeHandler) Callback(c *fiber.Ctx) error {
path := c.Query("path")
token := c.Query("token")
if path == "" {
return c.Status(400).SendString("path required")
}
// Validate the token to ensure the callback is authorized
if err := middleware.ValidateDownloadToken(token, h.Config.JWTSecret, ""); err != nil {
return c.Status(401).SendString("unauthorized")
}
var req CallbackRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "invalid json"})
}
// Status 2: Document is ready for saving
// Status 6: Document is being edited, force save was triggered
if req.Status == 2 || req.Status == 6 {
if req.Url == "" {
return c.Status(400).JSON(fiber.Map{"error": "url required"})
}
// Download modified file from Document Server
resp, err := http.Get(req.Url)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "failed to download file"})
}
defer resp.Body.Close()
targetPath := filepath.Join(h.Config.StorageDir, path)
// Save file
out, err := os.Create(targetPath)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "failed to open file for writing"})
}
defer out.Close()
if _, err := io.Copy(out, resp.Body); err != nil {
return c.Status(500).JSON(fiber.Map{"error": "failed to write file"})
}
}
// Send successful response according to OnlyOffice API
return c.JSON(fiber.Map{"error": 0})
}
// CreateBlank creates an empty document for Docs or Slides mode
func (h *OnlyOfficeHandler) CreateBlank(c *fiber.Ctx) error {
req := new(struct {
Type string `json:"type"`
Name string `json:"name"`
})
if err := c.BodyParser(req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "invalid request"})
}
if req.Name == "" {
req.Name = "Untitled Document"
if req.Type == "slide" {
req.Name = "Untitled Presentation"
}
}
ext := ".docx"
if req.Type == "slide" {
ext = ".pptx"
}
// Add extension if missing
if filepath.Ext(req.Name) != ext {
req.Name += ext
}
// Make sure the file name doesn't collide
basePath := filepath.Join(h.Config.StorageDir, req.Name)
finalPath := basePath
counter := 1
for {
if _, err := os.Stat(finalPath); os.IsNotExist(err) {
break
}
finalPath = filepath.Join(h.Config.StorageDir, fmt.Sprintf("%s (%d)%s", req.Name[:len(req.Name)-len(ext)], counter, ext))
counter++
}
// Create an empty file. OnlyOffice will initialize it when opened.
f, err := os.Create(finalPath)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "failed to create file"})
}
f.Close()
// Return the relative path
relativePath, _ := filepath.Rel(h.Config.StorageDir, finalPath)
return c.JSON(fiber.Map{
"path": filepath.ToSlash(relativePath),
"name": filepath.Base(finalPath),
})
}

View file

@ -69,6 +69,7 @@ func main() {
archiveHandler := &handlers.ArchiveHandler{DB: db, Config: cfg} archiveHandler := &handlers.ArchiveHandler{DB: db, Config: cfg}
shareGuard := middleware.NewBruteForceGuard(3, 60) shareGuard := middleware.NewBruteForceGuard(3, 60)
shareHandler := &handlers.ShareHandler{DB: db, Config: cfg, Guard: shareGuard} shareHandler := &handlers.ShareHandler{DB: db, Config: cfg, Guard: shareGuard}
onlyOfficeHandler := &handlers.OnlyOfficeHandler{Config: cfg}
// Initialize background workers // Initialize background workers
workers.InitTaskManager(cfg) workers.InitTaskManager(cfg)
@ -135,6 +136,9 @@ func main() {
public.Post("/share/:id", shareGuard.Check(), shareHandler.GetPublicShare) public.Post("/share/:id", shareGuard.Check(), shareHandler.GetPublicShare)
public.Get("/download/:id", shareHandler.DownloadPublicShare) public.Get("/download/:id", shareHandler.DownloadPublicShare)
// OnlyOffice callback (public but validates query token)
public.Post("/onlyoffice/callback", onlyOfficeHandler.Callback)
// Auth routes (with brute force protection) // Auth routes (with brute force protection)
auth := app.Group("/api/auth") auth := app.Group("/api/auth")
auth.Post("/login", guard.Check(), authHandler.Login) auth.Post("/login", guard.Check(), authHandler.Login)
@ -184,6 +188,9 @@ func main() {
protected.Post("/files/upload", fsHandler.Upload) protected.Post("/files/upload", fsHandler.Upload)
protected.Post("/files/check-conflicts", fsHandler.CheckConflicts) protected.Post("/files/check-conflicts", fsHandler.CheckConflicts)
// OnlyOffice blanks
protected.Post("/onlyoffice/create", onlyOfficeHandler.CreateBlank)
protected.Get("/files/folders-tree", fsHandler.GetFolderTree) protected.Get("/files/folders-tree", fsHandler.GetFolderTree)
// File listing and download (with path jail) // File listing and download (with path jail)

View file

@ -8,6 +8,7 @@
"name": "drive-frontend", "name": "drive-frontend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@onlyoffice/document-editor-react": "^2.2.0",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
@ -988,6 +989,27 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@onlyoffice/doceditor-types": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/@onlyoffice/doceditor-types/-/doceditor-types-9.3.1.tgz",
"integrity": "sha512-2RZEyes+TA/1PbxSGKU8kMQn0upImDgmA3osqvFux+KrUfAW41yB26ZZmA9zKGR57jYiyOuUBtSvFQsXn8C2dw==",
"license": "Apache-2.0",
"peer": true
},
"node_modules/@onlyoffice/document-editor-react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@onlyoffice/document-editor-react/-/document-editor-react-2.2.0.tgz",
"integrity": "sha512-SPgeJ1lK+I5mpcCUi6Qz98QVtlZR6axxCArgHoKNNBfFqXNmb6iDOf5Z1QUiVQ0xr57oo84D0ptyXZaF8K51nA==",
"license": "Apache-2.0",
"dependencies": {
"lodash": "4.18.1"
},
"peerDependencies": {
"@onlyoffice/doceditor-types": ">=9.3.1",
"react": "^16.9.0 || ^17 || ^18 || ^19",
"react-dom": "^16.9.0 || ^17 || ^18 || ^19"
}
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -1819,6 +1841,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash._baseiteratee": { "node_modules/lodash._baseiteratee": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz", "resolved": "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz",

View file

@ -9,6 +9,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@onlyoffice/document-editor-react": "^2.2.0",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",

View file

@ -8,7 +8,7 @@ import {
MoreVertical, Star, Download, Pencil, Copy, Move, MoreVertical, Star, Download, Pencil, Copy, Move,
FolderPlus, ArrowUp, X, Check, RefreshCw, Info, Link as LinkIcon, Image as ImageIcon, Film as FilmIcon, Home, FolderPlus, ArrowUp, X, Check, RefreshCw, Info, Link as LinkIcon, Image as ImageIcon, Film as FilmIcon, Home,
Sun, Moon, Music as MusicIcon, Code as CodeIcon, Archive as ArchiveIcon, CornerDownRight, Sun, Moon, Music as MusicIcon, Code as CodeIcon, Archive as ArchiveIcon, CornerDownRight,
CheckCircle, AlertCircle, Menu CheckCircle, AlertCircle, Menu, FileText, Presentation
} from 'lucide-react'; } from 'lucide-react';
import api from '@/lib/api'; import api from '@/lib/api';
import { useIsMobile } from '@/hooks/useIsMobile'; import { useIsMobile } from '@/hooks/useIsMobile';
@ -26,6 +26,8 @@ import TaskManager from './TaskManager';
import ShareModal from './ShareModal'; import ShareModal from './ShareModal';
import FileInfoModal from './FileInfoModal'; import FileInfoModal from './FileInfoModal';
import SharedFilesPage from './SharedFilesPage'; import SharedFilesPage from './SharedFilesPage';
import OfficeHome from './OfficeHome';
import OnlyOfficeEditor from './OnlyOfficeEditor';
interface FileItem { interface FileItem {
name: string; name: string;
@ -237,6 +239,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const [files, setFiles] = useState<FileItem[]>([]); const [files, setFiles] = useState<FileItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState<ViewMode>('grid'); const [viewMode, setViewMode] = useState<ViewMode>('grid');
const [appMode, setAppMode] = useState<'drive' | 'docs' | 'slides'>('drive');
const [editingFile, setEditingFile] = useState<FileItem | null>(null);
const [activeSection, setActiveSection] = useState<SidebarSection>('files'); const [activeSection, setActiveSection] = useState<SidebarSection>('files');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<FileItem[] | null>(null); const [searchResults, setSearchResults] = useState<FileItem[] | null>(null);
@ -548,8 +552,17 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
return next; return next;
}); });
} else { } else {
if (file.is_dir) navigateToFolder(file); if (file.is_dir) {
else setPreviewFile(file); navigateToFolder(file);
} else if (file.name.endsWith('.docx')) {
setAppMode('docs');
setEditingFile(file);
} else if (file.name.endsWith('.pptx')) {
setAppMode('slides');
setEditingFile(file);
} else {
setPreviewFile(file);
}
} }
} }
@ -1300,21 +1313,53 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
borderColor: 'var(--color-border-subtle)', borderColor: 'var(--color-border-subtle)',
}} }}
> >
<div className="flex items-center gap-3 px-5 py-5"> <div className="flex flex-col gap-1 px-3 py-4 border-b" style={{ borderColor: 'var(--color-border-subtle)' }}>
<div <button
className="w-9 h-9 rounded-xl flex items-center justify-center" onClick={() => { setAppMode('drive'); setEditingFile(null); }}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-bold transition-all duration-150"
style={{ style={{
background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)', backgroundColor: appMode === 'drive' ? 'var(--color-accent-subtle)' : 'transparent',
color: appMode === 'drive' ? 'var(--color-accent)' : 'var(--color-text-primary)',
}} }}
> >
<HardDrive className="w-4.5 h-4.5 text-white" /> <div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: appMode === 'drive' ? 'linear-gradient(135deg, var(--color-accent), #8b5cf6)' : 'var(--color-bg-elevated)' }}>
<HardDrive className={`w-4 h-4 ${appMode === 'drive' ? 'text-white' : ''}`} style={{ color: appMode !== 'drive' ? 'var(--color-text-secondary)' : undefined }} />
</div> </div>
<span className="text-base font-bold" style={{ color: 'var(--color-text-primary)' }}>
Drive Drive
</span> </button>
<button
onClick={() => { setAppMode('docs'); setEditingFile(null); }}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-bold transition-all duration-150"
style={{
backgroundColor: appMode === 'docs' ? 'var(--color-accent-subtle)' : 'transparent',
color: appMode === 'docs' ? 'var(--color-accent)' : 'var(--color-text-primary)',
}}
>
<div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: appMode === 'docs' ? 'linear-gradient(135deg, #3b82f6, #2563eb)' : 'var(--color-bg-elevated)' }}>
<FileText className={`w-4 h-4 ${appMode === 'docs' ? 'text-white' : ''}`} style={{ color: appMode !== 'docs' ? 'var(--color-text-secondary)' : undefined }} />
</div>
Docs
</button>
<button
onClick={() => { setAppMode('slides'); setEditingFile(null); }}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-bold transition-all duration-150"
style={{
backgroundColor: appMode === 'slides' ? 'var(--color-accent-subtle)' : 'transparent',
color: appMode === 'slides' ? 'var(--color-accent)' : 'var(--color-text-primary)',
}}
>
<div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: appMode === 'slides' ? 'linear-gradient(135deg, #f59e0b, #d97706)' : 'var(--color-bg-elevated)' }}>
<Presentation className={`w-4 h-4 ${appMode === 'slides' ? 'text-white' : ''}`} style={{ color: appMode !== 'slides' ? 'var(--color-text-secondary)' : undefined }} />
</div>
Slides
</button>
</div> </div>
<nav className="flex-1 px-3 py-2 space-y-1 overflow-y-auto"> <nav className="flex-1 px-3 py-2 space-y-1 overflow-y-auto">
{appMode === 'drive' && (
<>
<div className="flex flex-col"> <div className="flex flex-col">
<button <button
id="nav-files" id="nav-files"
@ -1454,6 +1499,42 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
{label} {label}
</button> </button>
))} ))}
</>
)}
{appMode === 'docs' && (
<>
<div className="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500">Documents</div>
<button
onClick={() => { setActiveSection('files'); setEditingFile(null); }}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150"
style={{
backgroundColor: !editingFile ? 'var(--color-accent-subtle)' : 'transparent',
color: !editingFile ? 'var(--color-accent)' : 'var(--color-text-secondary)',
}}
>
<FileText className="w-4.5 h-4.5" />
Recent Documents
</button>
</>
)}
{appMode === 'slides' && (
<>
<div className="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500">Presentations</div>
<button
onClick={() => { setActiveSection('files'); setEditingFile(null); }}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150"
style={{
backgroundColor: !editingFile ? 'var(--color-accent-subtle)' : 'transparent',
color: !editingFile ? 'var(--color-accent)' : 'var(--color-text-secondary)',
}}
>
<Presentation className="w-4.5 h-4.5" />
Recent Presentations
</button>
</>
)}
</nav> </nav>
<div className="px-3 py-4 border-t" style={{ borderColor: 'var(--color-border-subtle)' }}> <div className="px-3 py-4 border-t" style={{ borderColor: 'var(--color-border-subtle)' }}>
@ -1481,6 +1562,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<header <header
className="flex items-center gap-4 px-6 py-3.5 border-b" className="flex items-center gap-4 px-6 py-3.5 border-b"
style={{ style={{
display: appMode === 'drive' ? 'flex' : 'none',
backgroundColor: 'var(--color-bg-secondary)', backgroundColor: 'var(--color-bg-secondary)',
borderColor: 'var(--color-border-subtle)', borderColor: 'var(--color-border-subtle)',
}} }}
@ -1776,6 +1858,18 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<StorageDashboard /> <StorageDashboard />
) : activeSection === 'shared' ? ( ) : activeSection === 'shared' ? (
<SharedFilesPage /> <SharedFilesPage />
) : editingFile ? (
<OnlyOfficeEditor
file={editingFile}
type={editingFile.name.endsWith('.docx') ? 'word' : editingFile.name.endsWith('.pptx') ? 'slide' : 'word'}
onClose={() => setEditingFile(null)}
/>
) : (appMode === 'docs' || appMode === 'slides') && activeSection === 'files' ? (
<OfficeHome
type={appMode}
onFileSelect={(file) => setEditingFile(file)}
onCreateBlank={(file) => setEditingFile(file)}
/>
) : ( ) : (
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
<div <div

View file

@ -0,0 +1,125 @@
import React, { useState, useEffect } from 'react';
import { Plus, FileText, Presentation } from 'lucide-react';
import api from '@/lib/api';
interface FileItem {
name: string;
path: string;
is_dir: boolean;
size: number;
mime_type: string;
is_pinned: boolean;
is_trashed: boolean;
mod_time: string;
}
interface OfficeHomeProps {
type: 'docs' | 'slides';
onFileSelect: (file: FileItem) => void;
onCreateBlank: (file: FileItem) => void;
}
export default function OfficeHome({ type, onFileSelect, onCreateBlank }: OfficeHomeProps) {
const [recentFiles, setRecentFiles] = useState<FileItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch all files from root directory (as a simple way to find recent files of this type)
// Or we could use a search endpoint. For now we will fetch the root directory and filter.
api.listFiles('.')
.then(files => {
const ext = type === 'docs' ? '.docx' : '.pptx';
const filtered = files
.filter(f => !f.is_dir && f.name.endsWith(ext) && !f.is_trashed)
.sort((a, b) => new Date(b.mod_time).getTime() - new Date(a.mod_time).getTime())
.slice(0, 10);
setRecentFiles(filtered);
setLoading(false);
})
.catch(err => {
console.error(err);
setLoading(false);
});
}, [type]);
const handleCreateBlank = async () => {
try {
const res = await api.client.post('/onlyoffice/create', { type: type === 'docs' ? 'word' : 'slide' });
const { path, name } = res.data;
const newFile: FileItem = {
name,
path,
is_dir: false,
size: 0,
mime_type: type === 'docs' ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
is_pinned: false,
is_trashed: false,
mod_time: new Date().toISOString()
};
onCreateBlank(newFile);
} catch (err) {
console.error("Failed to create document", err);
}
};
const title = type === 'docs' ? 'Documents' : 'Presentations';
const Icon = type === 'docs' ? FileText : Presentation;
const colorClass = type === 'docs' ? 'text-blue-500' : 'text-amber-500';
return (
<div className="flex-1 overflow-y-auto" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
{/* Top Banner (Template Gallery placeholder) */}
<div className="py-8 px-10" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
<div className="max-w-6xl mx-auto">
<h2 className="text-lg font-medium mb-4" style={{ color: 'var(--color-text-primary)' }}>Start a new document</h2>
<div className="flex gap-4">
{/* Blank Document */}
<div className="flex flex-col gap-3">
<button
onClick={handleCreateBlank}
className="w-40 h-52 bg-white rounded-lg border border-gray-200 hover:border-blue-500 transition-colors flex items-center justify-center shadow-sm group"
>
<Plus className="w-12 h-12 text-gray-300 group-hover:text-blue-500 transition-colors" />
</button>
<span className="text-sm font-medium" style={{ color: 'var(--color-text-primary)' }}>Blank</span>
</div>
</div>
</div>
</div>
{/* Recent Documents */}
<div className="py-8 px-10">
<div className="max-w-6xl mx-auto">
<h2 className="text-lg font-medium mb-6" style={{ color: 'var(--color-text-primary)' }}>Recent {title.toLowerCase()}</h2>
{loading ? (
<div className="text-sm text-gray-500">Loading...</div>
) : recentFiles.length === 0 ? (
<div className="text-sm text-gray-500">No recent {title.toLowerCase()} found.</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6">
{recentFiles.map(file => (
<div key={file.path} className="flex flex-col gap-2 group">
<button
onClick={() => onFileSelect(file)}
className="w-full aspect-[1/1.4] bg-white rounded-lg border border-gray-200 group-hover:border-blue-500 transition-colors flex items-center justify-center shadow-sm overflow-hidden"
>
<Icon className={`w-16 h-16 ${colorClass} opacity-20 group-hover:opacity-100 transition-opacity`} />
</button>
<div className="flex items-center gap-2">
<Icon className={`w-4 h-4 flex-shrink-0 ${colorClass}`} />
<span className="text-sm font-medium truncate" style={{ color: 'var(--color-text-primary)' }} title={file.name}>
{file.name}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,108 @@
import React, { useEffect, useState } from 'react';
import { DocumentEditor } from '@onlyoffice/document-editor-react';
import api from '@/lib/api';
interface FileItem {
name: string;
path: string;
is_dir: boolean;
size: number;
mime_type: string;
is_pinned: boolean;
is_trashed: boolean;
mod_time: string;
}
interface OnlyOfficeEditorProps {
file: FileItem;
type: 'word' | 'cell' | 'slide';
onClose: () => void;
}
export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEditorProps) {
const [downloadToken, setDownloadToken] = useState<string | null>(null);
useEffect(() => {
// We need a short-lived download token to pass to the Document Server
api.client.get('/auth/download-token')
.then(res => setDownloadToken(res.data.token))
.catch(console.error);
}, [file.path]);
if (!downloadToken) {
return (
<div className="flex-1 flex items-center justify-center" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
<div className="text-sm" style={{ color: 'var(--color-text-tertiary)' }}>Loading Editor...</div>
</div>
);
}
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
// URL that OnlyOffice will use to download the file
const fileUrl = `${API_BASE}/api/files/download/${encodeURIComponent(file.path)}?token=${downloadToken}`;
// Callback URL that OnlyOffice will post to when saving
const callbackUrl = `${API_BASE}/api/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`;
const config = {
document: {
fileType: file.name.split('.').pop() || 'docx',
key: `${file.path}_${new Date(file.mod_time).getTime()}`,
title: file.name,
url: fileUrl,
},
documentType: type,
editorConfig: {
callbackUrl: callbackUrl,
mode: 'edit',
customization: {
forcesave: true,
goback: {
url: '',
}
}
},
};
const onDocumentReady = function (event: any) {
console.log("Document is loaded");
};
const onLoadComponentError = function (errorCode: number, errorDescription: string) {
switch (errorCode) {
case -1: // Unknown error loading component
console.log(errorDescription);
break;
case -2: // Error load DocsAPI from http://documentserver/
console.log(errorDescription);
break;
case -3: // DocsAPI is not defined
console.log(errorDescription);
break;
}
};
return (
<div className="flex-1 w-full h-full flex flex-col" style={{ backgroundColor: '#fff' }}>
<div className="flex items-center justify-between px-4 py-2 bg-gray-100 border-b border-gray-200">
<span className="font-medium text-sm text-gray-700">{file.name}</span>
<button
onClick={onClose}
className="px-3 py-1 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded text-sm transition-colors"
>
Close Document
</button>
</div>
<div className="flex-1 relative">
<DocumentEditor
id="docxEditor"
documentServerUrl="http://192.168.50.81:8084/"
config={config as any}
events_onDocumentReady={onDocumentReady}
onLoadComponentError={onLoadComponentError}
/>
</div>
</div>
);
}