From 709c13995c29d9de219c2731a794297da501a061 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 25 May 2026 09:57:09 -0700 Subject: [PATCH] Initial xlxs support --- backend/handlers/onlyoffice.go | 54 ++++++++++++++++++++++-- backend/test_xlsx.py | 40 ++++++++++++++++++ frontend/src/components/FileExplorer.tsx | 51 ++++++++++++++++++++-- frontend/src/components/OfficeHome.tsx | 37 ++++++++++------ frontend/src/lib/api.ts | 4 +- 5 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 backend/test_xlsx.py diff --git a/backend/handlers/onlyoffice.go b/backend/handlers/onlyoffice.go index 52a98fd..a49ad99 100644 --- a/backend/handlers/onlyoffice.go +++ b/backend/handlers/onlyoffice.go @@ -127,12 +127,16 @@ func (h *OnlyOfficeHandler) CreateBlank(c *fiber.Ctx) error { req.Name = "Untitled Document" if req.Type == "slide" { req.Name = "Untitled Presentation" + } else if req.Type == "cell" { + req.Name = "Untitled Spreadsheet" } } ext := ".docx" if req.Type == "slide" { ext = ".pptx" + } else if req.Type == "cell" { + ext = ".xlsx" } // Add extension if missing @@ -227,13 +231,15 @@ func (h *OnlyOfficeHandler) CreateFromTemplate(c *fiber.Ctx) error { }) } -// ListOfficeFiles returns all .docx or .pptx files across the entire storage directory. -// GET /api/onlyoffice/files?type=docx|pptx +// ListOfficeFiles returns all .docx, .pptx, or .xlsx files across the entire storage directory. +// GET /api/onlyoffice/files?type=docx|pptx|xlsx func (h *OnlyOfficeHandler) ListOfficeFiles(c *fiber.Ctx) error { - fileType := c.Query("type", "docx") // "docx" or "pptx" + fileType := c.Query("type", "docx") // "docx", "pptx", or "xlsx" ext := ".docx" if fileType == "pptx" { ext = ".pptx" + } else if fileType == "xlsx" { + ext = ".xlsx" } type OfficeFile struct { @@ -295,6 +301,8 @@ func createBlankOfficeFile(path string, ext string) error { if ext == ".pptx" { return writePptxContents(w) + } else if ext == ".xlsx" { + return writeXlsxContents(w) } return writeDocxContents(w) } @@ -458,6 +466,46 @@ func writePptxContents(w *zip.Writer) error { return nil } +func writeXlsxContents(w *zip.Writer) error { + files := map[string]string{ + "[Content_Types].xml": ` + + + + + +`, + "_rels/.rels": ` + + +`, + "xl/workbook.xml": ` + + + + +`, + "xl/_rels/workbook.xml.rels": ` + + +`, + "xl/worksheets/sheet1.xml": ` + + +`, + } + for name, content := range files { + fw, err := w.Create(name) + if err != nil { + return err + } + if _, err := fw.Write([]byte(content)); err != nil { + return err + } + } + return nil +} + // SignConfig takes the OnlyOffice configuration payload and signs it with the JWT secret func (h *OnlyOfficeHandler) SignConfig(c *fiber.Ctx) error { if h.Config.OnlyOfficeJWT == "" { diff --git a/backend/test_xlsx.py b/backend/test_xlsx.py new file mode 100644 index 0000000..720c917 --- /dev/null +++ b/backend/test_xlsx.py @@ -0,0 +1,40 @@ +import zipfile + +def create_empty_xlsx(filename): + with zipfile.ZipFile(filename, 'w') as zf: + # [Content_Types].xml + zf.writestr('[Content_Types].xml', """ + + + + + +""") + + # _rels/.rels + zf.writestr('_rels/.rels', """ + + +""") + + # xl/workbook.xml + zf.writestr('xl/workbook.xml', """ + + + + +""") + + # xl/_rels/workbook.xml.rels + zf.writestr('xl/_rels/workbook.xml.rels', """ + + +""") + + # xl/worksheets/sheet1.xml + zf.writestr('xl/worksheets/sheet1.xml', """ + + +""") + +create_empty_xlsx('test.xlsx') diff --git a/frontend/src/components/FileExplorer.tsx b/frontend/src/components/FileExplorer.tsx index e8805c3..000a4f1 100644 --- a/frontend/src/components/FileExplorer.tsx +++ b/frontend/src/components/FileExplorer.tsx @@ -239,7 +239,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState('grid'); - const [appMode, setAppMode] = useState<'drive' | 'docs' | 'slides'>('drive'); + const [appMode, setAppMode] = useState<'drive' | 'docs' | 'slides' | 'sheets'>('drive'); const [editingFile, setEditingFile] = useState(null); const [activeSection, setActiveSection] = useState('files'); const [searchQuery, setSearchQuery] = useState(''); @@ -558,6 +558,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { } else if (file.name.endsWith('.pptx')) { setAppMode('slides'); setEditingFile(file); + } else if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { + setAppMode('sheets'); + setEditingFile(file); } else { setPreviewFile(file); } @@ -1181,6 +1184,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { ); } + + if (ext === 'xlsx' || ext === 'xls') { + return ( +
+ X +
+ ); + } const fallbackIcon = (() => { if (isImage) { @@ -1447,6 +1458,20 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { Slides + +
@@ -1620,7 +1663,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { )) ) : ( - {appMode === 'docs' ? 'Documents' : 'Presentations'} + {appMode === 'docs' ? 'Documents' : appMode === 'slides' ? 'Presentations' : 'Spreadsheets'} )}
@@ -1896,7 +1939,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { ) : editingFile ? ( setEditingFile(null)} onRename={(oldPath, newName, newPath) => { setEditingFile(prev => prev ? { ...prev, name: newName, path: newPath } : null); @@ -1904,7 +1947,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { theme={theme as 'light' | 'dark'} toggleTheme={toggleTheme} /> - ) : (appMode === 'docs' || appMode === 'slides') && activeSection === 'files' ? ( + ) : (appMode === 'docs' || appMode === 'slides' || appMode === 'sheets') && activeSection === 'files' ? ( setEditingFile(file)} diff --git a/frontend/src/components/OfficeHome.tsx b/frontend/src/components/OfficeHome.tsx index 383f2a6..d7bf363 100644 --- a/frontend/src/components/OfficeHome.tsx +++ b/frontend/src/components/OfficeHome.tsx @@ -14,7 +14,7 @@ interface FileItem { } interface OfficeHomeProps { - type: 'docs' | 'slides'; + type: 'docs' | 'slides' | 'sheets'; onFileSelect: (file: FileItem) => void; onCreateBlank: (file: FileItem) => void; onPinChange?: () => void; @@ -38,6 +38,15 @@ function PptIcon({ className }: { className?: string }) { ); } +// Custom Excel icon (green) +function SheetIcon({ className }: { className?: string }) { + return ( +
+ X +
+ ); +} + interface ContextMenuState { x: number; y: number; @@ -55,8 +64,8 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank, onPinCha const [renameValue, setRenameValue] = useState(''); const renameInputRef = useRef(null); - const ext = type === 'docs' ? '.docx' : '.pptx'; - const fileTypeParam = type === 'docs' ? 'docx' : 'pptx'; + const ext = type === 'docs' ? '.docx' : type === 'slides' ? '.pptx' : '.xlsx'; + const fileTypeParam = type === 'docs' ? 'docx' : type === 'slides' ? 'pptx' : 'xlsx'; const fetchFiles = () => { // Fetch recent files from root directory @@ -132,7 +141,7 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank, onPinCha const handleCreateBlank = async () => { try { - const res = await api.createOnlyOfficeFile(type === 'docs' ? 'word' : 'slide'); + const res = await api.createOnlyOfficeFile(type === 'docs' ? 'word' : type === 'slides' ? 'slide' : 'cell'); const { path, name } = res; const newFile: FileItem = { @@ -140,7 +149,9 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank, onPinCha path, is_dir: false, size: 0, - mime_type: type === 'docs' ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + mime_type: type === 'docs' ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : + type === 'slides' ? 'application/vnd.openxmlformats-officedocument.presentationml.presentation' : + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', is_pinned: false, is_trashed: false, mod_time: new Date().toISOString() @@ -204,9 +215,9 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank, onPinCha } }; - const title = type === 'docs' ? 'Documents' : 'Presentations'; - const Icon = type === 'docs' ? WordIcon : PptIcon; - const accentColor = type === 'docs' ? '#2B579A' : '#D24726'; + const title = type === 'docs' ? 'Documents' : type === 'slides' ? 'Presentations' : 'Spreadsheets'; + const Icon = type === 'docs' ? WordIcon : type === 'slides' ? PptIcon : SheetIcon; + const accentColor = type === 'docs' ? '#2B579A' : type === 'slides' ? '#D24726' : '#107C41'; const renderFileCard = (file: FileItem) => { const isRenaming = renaming === file.path; @@ -218,7 +229,7 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank, onPinCha