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

@ -8,6 +8,7 @@
"name": "drive-frontend",
"version": "1.0.0",
"dependencies": {
"@onlyoffice/document-editor-react": "^2.2.0",
"@types/react-syntax-highlighter": "^15.5.13",
"framer-motion": "^11.15.0",
"lucide-react": "^0.468.0",
@ -988,6 +989,27 @@
"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": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -1819,6 +1841,12 @@
"dev": true,
"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": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz",

View file

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

View file

@ -8,7 +8,7 @@ import {
MoreVertical, Star, Download, Pencil, Copy, Move,
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,
CheckCircle, AlertCircle, Menu
CheckCircle, AlertCircle, Menu, FileText, Presentation
} from 'lucide-react';
import api from '@/lib/api';
import { useIsMobile } from '@/hooks/useIsMobile';
@ -26,6 +26,8 @@ import TaskManager from './TaskManager';
import ShareModal from './ShareModal';
import FileInfoModal from './FileInfoModal';
import SharedFilesPage from './SharedFilesPage';
import OfficeHome from './OfficeHome';
import OnlyOfficeEditor from './OnlyOfficeEditor';
interface FileItem {
name: string;
@ -237,6 +239,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const [files, setFiles] = useState<FileItem[]>([]);
const [loading, setLoading] = useState(true);
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 [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<FileItem[] | null>(null);
@ -548,8 +552,17 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
return next;
});
} else {
if (file.is_dir) navigateToFolder(file);
else setPreviewFile(file);
if (file.is_dir) {
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,22 +1313,54 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
borderColor: 'var(--color-border-subtle)',
}}
>
<div className="flex items-center gap-3 px-5 py-5">
<div
className="w-9 h-9 rounded-xl flex items-center justify-center"
<div className="flex flex-col gap-1 px-3 py-4 border-b" style={{ borderColor: 'var(--color-border-subtle)' }}>
<button
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={{
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>
<span className="text-base font-bold" style={{ color: 'var(--color-text-primary)' }}>
<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>
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>
<nav className="flex-1 px-3 py-2 space-y-1 overflow-y-auto">
<div className="flex flex-col">
{appMode === 'drive' && (
<>
<div className="flex flex-col">
<button
id="nav-files"
onClick={() => { handleSectionChange('files'); navigateTo('.'); }}
@ -1454,6 +1499,42 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
{label}
</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>
<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
className="flex items-center gap-4 px-6 py-3.5 border-b"
style={{
display: appMode === 'drive' ? 'flex' : 'none',
backgroundColor: 'var(--color-bg-secondary)',
borderColor: 'var(--color-border-subtle)',
}}
@ -1776,6 +1858,18 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<StorageDashboard />
) : activeSection === 'shared' ? (
<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

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>
);
}