Add OfficeHome with All Documents, context menu, custom icons, editor rename, text wrapping
Some checks failed
Automated Container Build / build-and-push (push) Failing after 1m13s
Some checks failed
Automated Container Build / build-and-push (push) Failing after 1m13s
This commit is contained in:
parent
e738a2cb72
commit
51850e75a2
5 changed files with 295 additions and 30 deletions
|
|
@ -194,6 +194,7 @@ func main() {
|
||||||
// OnlyOffice
|
// OnlyOffice
|
||||||
protected.Post("/onlyoffice/create", onlyOfficeHandler.CreateBlank)
|
protected.Post("/onlyoffice/create", onlyOfficeHandler.CreateBlank)
|
||||||
protected.Post("/onlyoffice/sign", onlyOfficeHandler.SignConfig)
|
protected.Post("/onlyoffice/sign", onlyOfficeHandler.SignConfig)
|
||||||
|
protected.Get("/onlyoffice/files", onlyOfficeHandler.ListOfficeFiles)
|
||||||
|
|
||||||
protected.Get("/files/folders-tree", fsHandler.GetFolderTree)
|
protected.Get("/files/folders-tree", fsHandler.GetFolderTree)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1168,6 +1168,22 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ext === 'docx' || ext === 'doc') {
|
||||||
|
return (
|
||||||
|
<div className={`${large ? 'w-20 h-20 rounded-2xl text-2xl' : 'w-5 h-5 rounded text-[6px] flex-shrink-0 aspect-square'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#2B579A]`}>
|
||||||
|
W
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ext === 'pptx' || ext === 'ppt') {
|
||||||
|
return (
|
||||||
|
<div className={`${large ? 'w-20 h-20 rounded-2xl text-2xl' : 'w-5 h-5 rounded text-[6px] flex-shrink-0 aspect-square'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#D24726]`}>
|
||||||
|
P
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const fallbackIcon = (() => {
|
const fallbackIcon = (() => {
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1863,6 +1879,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
file={editingFile}
|
file={editingFile}
|
||||||
type={editingFile.name.endsWith('.docx') ? 'word' : editingFile.name.endsWith('.pptx') ? 'slide' : 'word'}
|
type={editingFile.name.endsWith('.docx') ? 'word' : editingFile.name.endsWith('.pptx') ? 'slide' : 'word'}
|
||||||
onClose={() => setEditingFile(null)}
|
onClose={() => setEditingFile(null)}
|
||||||
|
onRename={(oldPath, newName, newPath) => {
|
||||||
|
setEditingFile(prev => prev ? { ...prev, name: newName, path: newPath } : null);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (appMode === 'docs' || appMode === 'slides') && activeSection === 'files' ? (
|
) : (appMode === 'docs' || appMode === 'slides') && activeSection === 'files' ? (
|
||||||
<OfficeHome
|
<OfficeHome
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Plus, FileText, Presentation } from 'lucide-react';
|
import { Plus, Pencil, Trash2 } from 'lucide-react';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
|
|
||||||
interface FileItem {
|
interface FileItem {
|
||||||
|
|
@ -19,17 +19,48 @@ interface OfficeHomeProps {
|
||||||
onCreateBlank: (file: FileItem) => void;
|
onCreateBlank: (file: FileItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom Word icon (blue)
|
||||||
|
function WordIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-center font-black tracking-tighter text-white bg-[#2B579A] rounded-lg ${className || ''}`}>
|
||||||
|
W
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom PowerPoint icon (orange)
|
||||||
|
function PptIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-center font-black tracking-tighter text-white bg-[#D24726] rounded-lg ${className || ''}`}>
|
||||||
|
P
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContextMenuState {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
file: FileItem;
|
||||||
|
}
|
||||||
|
|
||||||
export default function OfficeHome({ type, onFileSelect, onCreateBlank }: OfficeHomeProps) {
|
export default function OfficeHome({ type, onFileSelect, onCreateBlank }: OfficeHomeProps) {
|
||||||
const [recentFiles, setRecentFiles] = useState<FileItem[]>([]);
|
const [recentFiles, setRecentFiles] = useState<FileItem[]>([]);
|
||||||
|
const [allFiles, setAllFiles] = useState<FileItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadingAll, setLoadingAll] = useState(true);
|
||||||
|
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||||
|
const [renaming, setRenaming] = useState<string | null>(null); // path of file being renamed
|
||||||
|
const [renameValue, setRenameValue] = useState('');
|
||||||
|
const renameInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const ext = type === 'docs' ? '.docx' : '.pptx';
|
||||||
// Fetch all files from root directory (as a simple way to find recent files of this type)
|
const fileTypeParam = type === 'docs' ? 'docx' : 'pptx';
|
||||||
// Or we could use a search endpoint. For now we will fetch the root directory and filter.
|
|
||||||
|
const fetchFiles = () => {
|
||||||
|
// Fetch recent files from root directory
|
||||||
api.listDirectory('.')
|
api.listDirectory('.')
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const files: FileItem[] = data.files || [];
|
const files: FileItem[] = data.files || [];
|
||||||
const ext = type === 'docs' ? '.docx' : '.pptx';
|
|
||||||
const filtered = files
|
const filtered = files
|
||||||
.filter(f => !f.is_dir && f.name.endsWith(ext) && !f.is_trashed)
|
.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())
|
.sort((a, b) => new Date(b.mod_time).getTime() - new Date(a.mod_time).getTime())
|
||||||
|
|
@ -41,8 +72,51 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch ALL office files recursively
|
||||||
|
api.listOfficeFiles(fileTypeParam)
|
||||||
|
.then(data => {
|
||||||
|
const files = (data.files || []).map((f: any) => ({
|
||||||
|
...f,
|
||||||
|
is_dir: false,
|
||||||
|
mime_type: '',
|
||||||
|
is_pinned: false,
|
||||||
|
is_trashed: false,
|
||||||
|
}));
|
||||||
|
// Sort by mod_time descending
|
||||||
|
files.sort((a: FileItem, b: FileItem) => new Date(b.mod_time).getTime() - new Date(a.mod_time).getTime());
|
||||||
|
setAllFiles(files);
|
||||||
|
setLoadingAll(false);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
setLoadingAll(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchFiles();
|
||||||
}, [type]);
|
}, [type]);
|
||||||
|
|
||||||
|
// Close context menu on click elsewhere
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = () => setContextMenu(null);
|
||||||
|
if (contextMenu) {
|
||||||
|
window.addEventListener('click', handleClick);
|
||||||
|
return () => window.removeEventListener('click', handleClick);
|
||||||
|
}
|
||||||
|
}, [contextMenu]);
|
||||||
|
|
||||||
|
// Focus rename input
|
||||||
|
useEffect(() => {
|
||||||
|
if (renaming && renameInputRef.current) {
|
||||||
|
renameInputRef.current.focus();
|
||||||
|
// Select the name without extension
|
||||||
|
const dotIdx = renameValue.lastIndexOf('.');
|
||||||
|
renameInputRef.current.setSelectionRange(0, dotIdx > 0 ? dotIdx : renameValue.length);
|
||||||
|
}
|
||||||
|
}, [renaming]);
|
||||||
|
|
||||||
const handleCreateBlank = async () => {
|
const handleCreateBlank = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.createOnlyOfficeFile(type === 'docs' ? 'word' : 'slide');
|
const res = await api.createOnlyOfficeFile(type === 'docs' ? 'word' : 'slide');
|
||||||
|
|
@ -65,18 +139,89 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContextMenu = (e: React.MouseEvent, file: FileItem) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setContextMenu({ x: e.clientX, y: e.clientY, file });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = async () => {
|
||||||
|
if (!renaming || !renameValue.trim()) {
|
||||||
|
setRenaming(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.rename(renaming, renameValue);
|
||||||
|
setRenaming(null);
|
||||||
|
fetchFiles(); // Refresh
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to rename", err);
|
||||||
|
setRenaming(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (file: FileItem) => {
|
||||||
|
try {
|
||||||
|
await api.deleteFile(file.path);
|
||||||
|
fetchFiles(); // Refresh
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to delete", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const title = type === 'docs' ? 'Documents' : 'Presentations';
|
const title = type === 'docs' ? 'Documents' : 'Presentations';
|
||||||
const Icon = type === 'docs' ? FileText : Presentation;
|
const Icon = type === 'docs' ? WordIcon : PptIcon;
|
||||||
const colorClass = type === 'docs' ? 'text-blue-500' : 'text-amber-500';
|
const accentColor = type === 'docs' ? '#2B579A' : '#D24726';
|
||||||
|
|
||||||
|
const renderFileCard = (file: FileItem) => {
|
||||||
|
const isRenaming = renaming === file.path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={file.path} className="flex flex-col gap-2 group">
|
||||||
|
<button
|
||||||
|
onClick={() => !isRenaming && onFileSelect(file)}
|
||||||
|
onContextMenu={(e) => handleContextMenu(e, file)}
|
||||||
|
className="w-full aspect-[1/1.4] bg-white rounded-lg border border-gray-200 group-hover:border-blue-400 transition-colors flex items-center justify-center shadow-sm overflow-hidden relative"
|
||||||
|
>
|
||||||
|
<Icon className="w-16 h-16 text-3xl opacity-30 group-hover:opacity-60 transition-opacity" />
|
||||||
|
</button>
|
||||||
|
<div className="flex items-start gap-2 min-w-0">
|
||||||
|
<Icon className="w-4 h-4 flex-shrink-0 mt-0.5 text-[10px]" />
|
||||||
|
{isRenaming ? (
|
||||||
|
<input
|
||||||
|
ref={renameInputRef}
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
onBlur={handleRename}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleRename();
|
||||||
|
if (e.key === 'Escape') setRenaming(null);
|
||||||
|
}}
|
||||||
|
className="text-sm font-medium w-full bg-transparent border-b border-blue-500 outline-none"
|
||||||
|
style={{ color: 'var(--color-text-primary)' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="text-sm font-medium leading-tight break-words"
|
||||||
|
style={{ color: 'var(--color-text-primary)', wordBreak: 'break-word' }}
|
||||||
|
title={file.name}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
<div className="flex-1 overflow-y-auto" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||||
{/* Top Banner (Template Gallery placeholder) */}
|
{/* Top Banner */}
|
||||||
<div className="py-8 px-10" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
|
<div className="py-8 px-10" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
|
||||||
<div className="max-w-6xl mx-auto">
|
<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>
|
<h2 className="text-lg font-medium mb-4" style={{ color: 'var(--color-text-primary)' }}>Start a new {type === 'docs' ? 'document' : 'presentation'}</h2>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{/* Blank Document */}
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateBlank}
|
onClick={handleCreateBlank}
|
||||||
|
|
@ -101,26 +246,65 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
|
||||||
<div className="text-sm text-gray-500">No recent {title.toLowerCase()} found.</div>
|
<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">
|
<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 => (
|
{recentFiles.map(file => renderFileCard(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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* All Documents */}
|
||||||
|
<div className="py-8 px-10 border-t" style={{ borderColor: 'var(--color-border)' }}>
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<h2 className="text-lg font-medium mb-6" style={{ color: 'var(--color-text-primary)' }}>All {title.toLowerCase()}</h2>
|
||||||
|
|
||||||
|
{loadingAll ? (
|
||||||
|
<div className="text-sm text-gray-500">Loading...</div>
|
||||||
|
) : allFiles.length === 0 ? (
|
||||||
|
<div className="text-sm text-gray-500">No {title.toLowerCase()} found in your drive.</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">
|
||||||
|
{allFiles.map(file => renderFileCard(file))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Context Menu */}
|
||||||
|
{contextMenu && (
|
||||||
|
<div
|
||||||
|
className="fixed z-50 min-w-[160px] py-1 rounded-lg shadow-lg border"
|
||||||
|
style={{
|
||||||
|
top: contextMenu.y,
|
||||||
|
left: contextMenu.x,
|
||||||
|
backgroundColor: 'var(--color-bg-elevated, #fff)',
|
||||||
|
borderColor: 'var(--color-border, #e5e7eb)',
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-black/5 transition-colors"
|
||||||
|
style={{ color: 'var(--color-text-primary)' }}
|
||||||
|
onClick={() => {
|
||||||
|
setRenaming(contextMenu.file.path);
|
||||||
|
setRenameValue(contextMenu.file.name);
|
||||||
|
setContextMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-black/5 transition-colors text-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
handleDelete(contextMenu.file);
|
||||||
|
setContextMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { DocumentEditor } from '@onlyoffice/document-editor-react';
|
import { DocumentEditor } from '@onlyoffice/document-editor-react';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
|
|
||||||
|
|
@ -17,12 +17,37 @@ interface OnlyOfficeEditorProps {
|
||||||
file: FileItem;
|
file: FileItem;
|
||||||
type: 'word' | 'cell' | 'slide';
|
type: 'word' | 'cell' | 'slide';
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onRename?: (oldPath: string, newName: string, newPath: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEditorProps) {
|
export default function OnlyOfficeEditor({ file, type, onClose, onRename }: OnlyOfficeEditorProps) {
|
||||||
const [downloadToken, setDownloadToken] = useState<string | null>(null);
|
const [downloadToken, setDownloadToken] = useState<string | null>(null);
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
const [config, setConfig] = useState<any>(null);
|
const [config, setConfig] = useState<any>(null);
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const [renameValue, setRenameValue] = useState(file.name);
|
||||||
|
const renameInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleRename = async () => {
|
||||||
|
const trimmed = renameValue.trim();
|
||||||
|
if (!trimmed || trimmed === file.name) {
|
||||||
|
setRenameValue(file.name);
|
||||||
|
setIsRenaming(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await api.rename(file.path, trimmed);
|
||||||
|
// Build new path from the response or derive it
|
||||||
|
const dir = file.path.includes('/') ? file.path.substring(0, file.path.lastIndexOf('/') + 1) : '';
|
||||||
|
const newPath = dir + trimmed;
|
||||||
|
onRename?.(file.path, trimmed, newPath);
|
||||||
|
setIsRenaming(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to rename', err);
|
||||||
|
setRenameValue(file.name);
|
||||||
|
setIsRenaming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// We need a short-lived download token to pass to the Document Server
|
// We need a short-lived download token to pass to the Document Server
|
||||||
|
|
@ -106,7 +131,38 @@ export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEdit
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 w-full h-full flex flex-col" style={{ backgroundColor: '#fff' }}>
|
<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">
|
<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>
|
{isRenaming ? (
|
||||||
|
<input
|
||||||
|
ref={renameInputRef}
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
onBlur={handleRename}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleRename();
|
||||||
|
if (e.key === 'Escape') { setRenameValue(file.name); setIsRenaming(false); }
|
||||||
|
}}
|
||||||
|
className="font-medium text-sm text-gray-700 bg-white border border-blue-400 rounded px-2 py-0.5 outline-none min-w-[200px]"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsRenaming(true);
|
||||||
|
setRenameValue(file.name);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (renameInputRef.current) {
|
||||||
|
renameInputRef.current.focus();
|
||||||
|
const dotIdx = file.name.lastIndexOf('.');
|
||||||
|
renameInputRef.current.setSelectionRange(0, dotIdx > 0 ? dotIdx : file.name.length);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}}
|
||||||
|
className="font-medium text-sm text-gray-700 hover:text-blue-600 hover:underline cursor-pointer transition-colors"
|
||||||
|
title="Click to rename"
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-3 py-1 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded text-sm transition-colors"
|
className="px-3 py-1 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded text-sm transition-colors"
|
||||||
|
|
|
||||||
|
|
@ -485,6 +485,11 @@ class ApiClient {
|
||||||
});
|
});
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listOfficeFiles(type: 'docx' | 'pptx') {
|
||||||
|
const res = await this.request(`/api/onlyoffice/files?type=${type}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = new ApiClient();
|
export const api = new ApiClient();
|
||||||
|
|
|
||||||
Reference in a new issue