uploads
This commit is contained in:
parent
24a54fd2e6
commit
d80d046f65
@ -6,6 +6,8 @@ import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { queryClient } from "@/lib/queryClient";
|
||||
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
|
||||
import { AuthProvider, useAuth } from "@/hooks/useAuth";
|
||||
import { AuthProvider as OidcProvider } from "react-oidc-context";
|
||||
import { WebStorageStateStore } from "oidc-client-ts";
|
||||
|
||||
import { LogProvider } from "@/contexts/LogContext";
|
||||
|
||||
@ -24,6 +26,7 @@ registerAllWidgets();
|
||||
|
||||
import Index from "./pages/Index";
|
||||
import Auth from "./pages/Auth";
|
||||
import AuthZ from "./pages/AuthZ";
|
||||
|
||||
const UpdatePassword = React.lazy(() => import("./pages/UpdatePassword"));
|
||||
|
||||
@ -55,6 +58,7 @@ let TypesPlayground: any;
|
||||
let VariablePlayground: any;
|
||||
let I18nPlayground: any;
|
||||
let PlaygroundChat: any;
|
||||
let PlaygroundVfs: any;
|
||||
let GridSearch: any;
|
||||
let LocationDetail: any;
|
||||
let Tetris: any;
|
||||
@ -79,6 +83,7 @@ if (enablePlaygrounds) {
|
||||
VariablePlayground = React.lazy(() => import("./components/variables/VariablesEditor").then(module => ({ default: module.VariablesEditor })));
|
||||
I18nPlayground = React.lazy(() => import("./components/playground/I18nPlayground"));
|
||||
PlaygroundChat = React.lazy(() => import("./pages/PlaygroundChat"));
|
||||
PlaygroundVfs = React.lazy(() => import("./pages/PlaygroundVfs"));
|
||||
SupportChat = React.lazy(() => import("./pages/SupportChat"));
|
||||
}
|
||||
|
||||
@ -130,6 +135,7 @@ const AppWrapper = () => {
|
||||
{/* Top-level routes (no organization context) */}
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/auth" element={<Auth />} />
|
||||
<Route path="/authz" element={<AuthZ />} />
|
||||
<Route path="/auth/update-password" element={<React.Suspense fallback={<div>Loading...</div>}><UpdatePassword /></React.Suspense>} />
|
||||
<Route path="/profile/*" element={<Profile />} />
|
||||
<Route path="/post/new" element={<React.Suspense fallback={<div>Loading...</div>}><EditPost /></React.Suspense>} />
|
||||
@ -192,6 +198,7 @@ const AppWrapper = () => {
|
||||
<Route path="/variables-editor" element={<React.Suspense fallback={<div>Loading...</div>}><VariablePlayground /></React.Suspense>} />
|
||||
<Route path="/playground/i18n" element={<React.Suspense fallback={<div>Loading...</div>}><I18nPlayground /></React.Suspense>} />
|
||||
<Route path="/playground/chat" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundChat /></React.Suspense>} />
|
||||
<Route path="/playground/vfs" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundVfs /></React.Suspense>} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -251,10 +258,27 @@ const App = () => {
|
||||
initFormatDetection();
|
||||
}, []);
|
||||
|
||||
const oidcConfig = {
|
||||
authority: "https://auth.polymech.info",
|
||||
client_id: "367440527605432321",
|
||||
redirect_uri: window.location.origin + "/authz", // Where Zitadel sends the code back to
|
||||
post_logout_redirect_uri: window.location.origin,
|
||||
response_type: "code",
|
||||
scope: "openid profile email",
|
||||
loadUserInfo: true, // Specifically instruct the client to fetch /userinfo
|
||||
// Store tokens in localStorage instead of sessionStorage so they survive new tabs
|
||||
userStore: new WebStorageStateStore({ store: window.localStorage }),
|
||||
onSigninCallback: () => {
|
||||
// Clean up the URL after successful login
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<SWRConfig value={{ provider: () => new Map() }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<OidcProvider {...oidcConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<LogProvider>
|
||||
<MediaRefreshProvider>
|
||||
@ -281,9 +305,10 @@ const App = () => {
|
||||
</MediaRefreshProvider>
|
||||
</LogProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
</QueryClientProvider>
|
||||
</OidcProvider>
|
||||
</SWRConfig>
|
||||
</HelmetProvider >
|
||||
</HelmetProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -153,11 +153,12 @@ const FileBrowser: React.FC<{
|
||||
mode?: 'simple' | 'advanced',
|
||||
index?: boolean,
|
||||
disableRoutingSync?: boolean,
|
||||
initialMount?: string,
|
||||
onSelect?: (node: INode | null, mount?: string) => void
|
||||
}> = ({ allowPanels, mode, index, disableRoutingSync, onSelect }) => {
|
||||
}> = ({ allowPanels, mode, index, disableRoutingSync, initialMount: propInitialMount, onSelect }) => {
|
||||
const location = useLocation();
|
||||
|
||||
let initialMount: string | undefined;
|
||||
let initialMount = propInitialMount;
|
||||
let initialPath: string | undefined;
|
||||
|
||||
if (!disableRoutingSync) {
|
||||
@ -166,10 +167,10 @@ const FileBrowser: React.FC<{
|
||||
const segments = urlRest.split('/').filter(Boolean);
|
||||
|
||||
if (segments.length > 0) {
|
||||
initialMount = segments[0];
|
||||
initialMount = initialMount || segments[0];
|
||||
initialPath = segments.slice(1).join('/');
|
||||
} else {
|
||||
initialMount = localStorage.getItem('fb-last-mount') || undefined;
|
||||
initialMount = initialMount || localStorage.getItem('fb-last-mount') || undefined;
|
||||
initialPath = localStorage.getItem('fb-last-path') || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import ImageLightbox from '@/components/ImageLightbox';
|
||||
import LightboxText from '@/modules/storage/views/LightboxText';
|
||||
import LightboxIframe from '@/modules/storage/views/LightboxIframe';
|
||||
import { renderFileViewer } from '@/modules/storage/FileViewerRegistry';
|
||||
import { useDragDrop } from '@/contexts/DragDropContext';
|
||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
|
||||
|
||||
import type { INode, SortKey } from '@/modules/storage/types';
|
||||
import { getMimeCategory, vfsUrl, formatSize } from '@/modules/storage/helpers';
|
||||
import { serverUrl } from '@/lib/db';
|
||||
import FileBrowserToolbar from '@/modules/storage/FileBrowserToolbar';
|
||||
import FileListView from '@/modules/storage/FileListView';
|
||||
import FileGridView from '@/modules/storage/FileGridView';
|
||||
@ -140,6 +142,37 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
}
|
||||
}, [onFilterChange]);
|
||||
|
||||
// ── Drag & Drop Uploads ───────────────────────────────────────
|
||||
|
||||
type UploadingNode = INode & { _uploading: boolean; _progress: number; _error?: string; _file?: File };
|
||||
const { setLocalZoneActive, resetDragState } = useDragDrop();
|
||||
const [uploads, setUploads] = useState<UploadingNode[]>([]);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
if (!e.dataTransfer.types.includes('Files')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setLocalZoneActive(true);
|
||||
setIsDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
if (!e.dataTransfer.types.includes('Files')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isDragOver) setIsDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
if (e.clientX <= rect.left || e.clientX >= rect.right || e.clientY <= rect.top || e.clientY >= rect.bottom) {
|
||||
setIsDragOver(false);
|
||||
setLocalZoneActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Available mounts ─────────────────────────────────────────
|
||||
|
||||
const [availableMounts, setAvailableMounts] = useState<string[]>([]);
|
||||
@ -207,6 +240,90 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
|
||||
// ── View Mode & Zoom ─────────────────────────────────────────
|
||||
|
||||
const displayNodes = useMemo(() => [...sorted, ...(uploads as INode[])], [sorted, uploads]);
|
||||
|
||||
const handleDrop = async (e: React.DragEvent) => {
|
||||
if (!e.dataTransfer.types.includes('Files')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
setLocalZoneActive(false);
|
||||
resetDragState();
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length === 0) return;
|
||||
|
||||
const newUploads = files.map((f, index) => ({
|
||||
name: f.name,
|
||||
path: currentPath ? `${currentPath}/${f.name}` : f.name,
|
||||
size: f.size,
|
||||
mtime: Date.now(),
|
||||
parent: currentPath,
|
||||
type: 'file',
|
||||
_uploading: true,
|
||||
_progress: 0,
|
||||
_file: f
|
||||
}));
|
||||
setUploads(prev => [...prev, ...newUploads]);
|
||||
|
||||
const uploadOne = (uploadItem: typeof newUploads[0]) => new Promise<void>((resolve) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const cleanDir = (currentPath || '').replace(/^\/+|\/+$/g, '');
|
||||
const filePath = cleanDir ? `${cleanDir}/${uploadItem.name}` : uploadItem.name;
|
||||
const uploadUrl = `${serverUrl}/api/vfs/upload/${mount}/${filePath}`;
|
||||
xhr.open('POST', uploadUrl, true);
|
||||
if (accessToken) {
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||
}
|
||||
|
||||
xhr.upload.onprogress = (evt) => {
|
||||
if (evt.lengthComputable) {
|
||||
const percent = Math.round((evt.loaded / evt.total) * 100);
|
||||
setUploads(prev => prev.map(u =>
|
||||
u.name === uploadItem.name ? { ...u, _progress: percent } : u
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
const markError = (msg: string) => {
|
||||
setUploads(prev => prev.map(u =>
|
||||
u.name === uploadItem.name ? { ...u, _uploading: false, _error: msg } : u
|
||||
));
|
||||
setTimeout(() => {
|
||||
setUploads(prev => prev.filter(u => u.name !== uploadItem.name));
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
setUploads(prev => prev.filter(u => u.name !== uploadItem.name));
|
||||
} else {
|
||||
let msg = `Error ${xhr.status}`;
|
||||
try {
|
||||
const body = JSON.parse(xhr.responseText);
|
||||
msg = body.message || body.error || msg;
|
||||
} catch {}
|
||||
markError(msg);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
markError('Network error');
|
||||
resolve();
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
if (uploadItem._file) {
|
||||
formData.append('file', uploadItem._file);
|
||||
}
|
||||
xhr.send(formData);
|
||||
});
|
||||
|
||||
await Promise.all(newUploads.map(uploadOne));
|
||||
fetchDir(currentPath || '/');
|
||||
};
|
||||
|
||||
const [internalViewMode, setInternalViewMode] = useState<'list' | 'thumbs' | 'tree'>(() => {
|
||||
if (autoSaveId) {
|
||||
const saved = localStorage.getItem(`${autoSaveId}-viewMode`);
|
||||
@ -283,7 +400,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
handleItemClick,
|
||||
clearSelection
|
||||
} = useSelection({
|
||||
sorted,
|
||||
sorted: displayNodes,
|
||||
canGoUp,
|
||||
onSelect
|
||||
});
|
||||
@ -348,7 +465,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
const [pendingFileSelect, setPendingFileSelect] = useState<string | null>(null);
|
||||
|
||||
const { goUp } = useDefaultSelectionHandler({
|
||||
sorted,
|
||||
sorted: displayNodes,
|
||||
canGoUp,
|
||||
rawGoUp,
|
||||
currentPath,
|
||||
@ -410,7 +527,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
searchQuery,
|
||||
isSearchMode,
|
||||
onSelect,
|
||||
sorted,
|
||||
sorted: displayNodes,
|
||||
});
|
||||
|
||||
// ── Default Actions ──────────────────────────────────────────
|
||||
@ -436,7 +553,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
pathProp,
|
||||
accessToken,
|
||||
selected,
|
||||
sorted,
|
||||
sorted: displayNodes,
|
||||
canGoUp,
|
||||
setFocusIdx,
|
||||
setSelected,
|
||||
@ -459,9 +576,14 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
ref={containerRef}
|
||||
data-testid="file-browser-panel"
|
||||
tabIndex={viewMode === 'tree' ? undefined : 0}
|
||||
className="fb-panel-container"
|
||||
className={`fb-panel-container ${isDragOver ? 'ring-2 ring-primary bg-primary/5' : ''}`}
|
||||
onKeyDown={handleKeyDown}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0,
|
||||
border: '1px solid var(--border, #334155)', borderRadius: 6, overflow: 'hidden',
|
||||
|
||||
@ -474,6 +596,27 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* ═══ Drop Overlay ═══════════════════════════════ */}
|
||||
{isDragOver && (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, zIndex: 50,
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12,
|
||||
background: 'rgba(59, 130, 246, 0.08)',
|
||||
border: '2px dashed var(--primary, #3b82f6)',
|
||||
borderRadius: 6,
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--primary, #3b82f6)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--primary, #3b82f6)', letterSpacing: '0.02em' }}>
|
||||
Drop files to upload
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ═══ Toolbar ═══════════════════════════════════ */}
|
||||
{showToolbar && (
|
||||
<FileBrowserToolbar
|
||||
@ -553,7 +696,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
{viewMode === 'tree' ? (
|
||||
<div className="flex-1 min-h-0 overflow-hidden pt-1">
|
||||
<FileTree
|
||||
data={sorted}
|
||||
data={displayNodes}
|
||||
canGoUp={canGoUp}
|
||||
onGoUp={goUp}
|
||||
selectedId={selected.length === 1 ? selected[0].path : undefined}
|
||||
@ -587,7 +730,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
<div className="flex-1 min-h-0 overflow-hidden pt-1 flex flex-col w-full h-full">
|
||||
<FileListView
|
||||
listRef={listRef}
|
||||
sorted={sorted}
|
||||
sorted={displayNodes}
|
||||
canGoUp={canGoUp}
|
||||
goUp={goUp}
|
||||
focusIdx={focusIdx}
|
||||
@ -605,7 +748,7 @@ const FileBrowserPanel: React.FC<FileBrowserPanelProps> = ({
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden w-full h-full pt-1">
|
||||
<FileGridView
|
||||
listRef={listRef}
|
||||
sorted={sorted}
|
||||
sorted={displayNodes}
|
||||
canGoUp={canGoUp}
|
||||
goUp={goUp}
|
||||
focusIdx={focusIdx}
|
||||
|
||||
@ -237,7 +237,6 @@ export const CreationWizardPopup: React.FC<CreationWizardPopupProps> = ({
|
||||
for (const img of preloadedImages) {
|
||||
// Handle External Pages (Links)
|
||||
if (img.type === 'page-external') {
|
||||
console.log('Skipping upload for external page:', img.title);
|
||||
await createPicture({
|
||||
user_id: user.id,
|
||||
title: img.title || 'Untitled Link',
|
||||
@ -257,7 +256,6 @@ export const CreationWizardPopup: React.FC<CreationWizardPopupProps> = ({
|
||||
if (file.type.startsWith('video/')) continue;
|
||||
|
||||
const { publicUrl } = await uploadImage(file, user.id);
|
||||
console.log('image uploaded, url:', publicUrl);
|
||||
|
||||
let dbData = {
|
||||
user_id: user.id,
|
||||
@ -514,7 +512,7 @@ export const CreationWizardPopup: React.FC<CreationWizardPopupProps> = ({
|
||||
) : (
|
||||
<>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
<T>Upload Image</T>
|
||||
<T>Upload as Picture</T>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@ -57,16 +57,19 @@ const FileGridView: React.FC<FileGridViewProps> = ({
|
||||
const isDir = getMimeCategory(node) === 'dir';
|
||||
const isFocused = focusIdx === idx;
|
||||
const isSelected = selected.some(sel => sel.path === node.path);
|
||||
const { _uploading, _progress, _error } = node as any;
|
||||
|
||||
return (
|
||||
<div key={node.path || node.name} data-fb-idx={idx}
|
||||
data-testid="file-grid-node"
|
||||
data-node-id={node.path || node.name}
|
||||
onClick={(e) => onItemClick(idx, e)}
|
||||
onDoubleClick={() => onItemDoubleClick(idx)}
|
||||
onDoubleClick={() => !_uploading && onItemDoubleClick(idx)}
|
||||
className="fb-thumb" style={{
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
padding: 6, borderRadius: 6, cursor: 'pointer', gap: 6, overflow: 'hidden',
|
||||
padding: 6, borderRadius: 6, cursor: _uploading ? 'default' : 'pointer', gap: 6, overflow: 'hidden',
|
||||
background: isSelected ? 'rgba(59, 130, 246, 0.05)' : 'transparent',
|
||||
opacity: (_uploading || _error) ? 0.7 : 1,
|
||||
}}>
|
||||
<div style={{
|
||||
width: '100%', aspectRatio: '1/1',
|
||||
@ -77,8 +80,29 @@ const FileGridView: React.FC<FileGridViewProps> = ({
|
||||
borderStyle: isSelected ? 'solid' : 'solid',
|
||||
outline: isFocused ? `2px solid ${FOCUS_BORDER}` : 'none',
|
||||
outlineOffset: '2px',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{isDir ? <NodeIcon node={node} size={Math.max(24, Math.floor(thumbSize * 0.5))} /> : <ThumbPreview node={node} mount={mount} tokenParam={tokenParam} thumbSize={thumbSize} />}
|
||||
|
||||
{_uploading && (
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0, height: 6,
|
||||
background: 'rgba(0,0,0,0.5)', overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%', width: `${_progress || 0}%`,
|
||||
background: 'var(--primary, #3b82f6)', transition: 'width 0.2s linear'
|
||||
}} />
|
||||
</div>
|
||||
)}
|
||||
{_error && (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(239, 68, 68, 0.12)', borderRadius: 6,
|
||||
}}>
|
||||
<span style={{ fontSize: 11, color: '#ef4444', fontWeight: 600, textAlign: 'center', padding: 4 }}>{_error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center', gap: 2 }}>
|
||||
<span style={{
|
||||
|
||||
@ -53,21 +53,25 @@ const FileListView: React.FC<FileListViewProps> = ({
|
||||
const isDir = getMimeCategory(node) === 'dir';
|
||||
const isFocused = focusIdx === idx;
|
||||
const isSelected = selected.some(sel => sel.path === node.path);
|
||||
const { _uploading, _progress, _error } = node as any;
|
||||
|
||||
return (
|
||||
<div key={node.path || node.name} data-fb-idx={idx}
|
||||
data-testid="file-list-node"
|
||||
data-node-id={node.path || node.name}
|
||||
onClick={(e) => onItemClick(idx, e)}
|
||||
onDoubleClick={() => onItemDoubleClick(idx)}
|
||||
onDoubleClick={() => !_uploading && onItemDoubleClick(idx)}
|
||||
className="fb-row" style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, padding: isSearchMode ? '8px 10px' : '5px 10px',
|
||||
cursor: 'pointer', fontSize,
|
||||
cursor: _uploading ? 'default' : 'pointer', fontSize,
|
||||
borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.06)', borderBottomStyle: 'solid',
|
||||
background: isSelected ? SELECTED_BG : isFocused ? FOCUS_BG : 'transparent',
|
||||
borderLeftWidth: 2, borderLeftColor: isSelected ? SELECTED_BORDER : 'transparent',
|
||||
borderLeftStyle: isSelected ? 'outset' : 'solid',
|
||||
outline: isFocused ? `2px solid ${FOCUS_BORDER}` : 'none',
|
||||
outlineOffset: '-2px',
|
||||
opacity: _uploading ? 0.7 : 1,
|
||||
position: 'relative'
|
||||
}}>
|
||||
<NodeIcon node={node} />
|
||||
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', gap: isSearchMode ? 2 : 0 }}>
|
||||
@ -99,13 +103,27 @@ const FileListView: React.FC<FileListViewProps> = ({
|
||||
{node.parent || '/'}
|
||||
</span>
|
||||
)}
|
||||
{_uploading && (
|
||||
<div style={{
|
||||
width: '100%', height: 4, background: 'rgba(0,0,0,0.2)',
|
||||
borderRadius: 2, overflow: 'hidden', marginTop: 2
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%', width: `${_progress || 0}%`,
|
||||
background: 'var(--primary, #3b82f6)', transition: 'width 0.2s linear'
|
||||
}} />
|
||||
</div>
|
||||
)}
|
||||
{_error && (
|
||||
<span style={{ fontSize: 10, color: '#ef4444', marginTop: 2 }}>{_error}</span>
|
||||
)}
|
||||
</div>
|
||||
{node.size !== undefined && (
|
||||
<span style={{ color: 'var(--muted-foreground, #64748b)', fontSize: 10, flexShrink: 0 }}>
|
||||
{formatSize(node.size)}
|
||||
<span style={{ color: _error ? '#ef4444' : 'var(--muted-foreground, #64748b)', fontSize: 10, flexShrink: 0 }}>
|
||||
{_error ? '✕' : _uploading ? `${Math.round(_progress || 0)}%` : formatSize(node.size)}
|
||||
</span>
|
||||
)}
|
||||
{mode === 'advanced' && node.mtime && (
|
||||
{mode === 'advanced' && node.mtime && !_uploading && (
|
||||
<span style={{ color: 'var(--muted-foreground, #64748b)', fontSize: 10, flexShrink: 0, width: 120, textAlign: 'right' }}>
|
||||
{formatDate(node.mtime)}
|
||||
</span>
|
||||
|
||||
@ -52,39 +52,6 @@ const Auth = () => {
|
||||
|
||||
const { data } = await signUp(formData.email, formData.password, formData.username, formData.displayName);
|
||||
|
||||
// If signing up from an organization context, add user to that organization
|
||||
if (data?.user && isOrgContext && orgSlug) {
|
||||
try {
|
||||
// Get organization ID from slug
|
||||
const { data: org, error: orgError } = await supabase
|
||||
.from('organizations')
|
||||
.select('id')
|
||||
.eq('slug', orgSlug)
|
||||
.single();
|
||||
|
||||
if (orgError) throw orgError;
|
||||
|
||||
if (org) {
|
||||
// Add user to organization
|
||||
const { error: memberError } = await supabase
|
||||
.from('user_organizations')
|
||||
.insert({
|
||||
user_id: data.user.id,
|
||||
organization_id: org.id,
|
||||
role: 'member'
|
||||
});
|
||||
|
||||
if (memberError) throw memberError;
|
||||
|
||||
toast({
|
||||
title: translate('Welcome!'),
|
||||
description: translate("You've been added to the organization.")
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding user to organization:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignIn = async (e: React.FormEvent) => {
|
||||
|
||||
78
packages/ui/src/pages/AuthZ.tsx
Normal file
78
packages/ui/src/pages/AuthZ.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Shield, ArrowRight } from 'lucide-react';
|
||||
import { T, translate } from '@/i18n';
|
||||
import { useAuth } from 'react-oidc-context';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const AuthZ = () => {
|
||||
const auth = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Monitor auth state changes and log them for debugging
|
||||
useEffect(() => {
|
||||
console.log("🛡️ [AuthZ] Auth State Update:", {
|
||||
isAuthenticated: auth.isAuthenticated,
|
||||
isLoading: auth.isLoading,
|
||||
hasError: !!auth.error,
|
||||
errorMsg: auth.error?.message
|
||||
});
|
||||
|
||||
if (auth.user) {
|
||||
console.log("🛡️ [AuthZ] Identity Token Profile:", auth.user.profile);
|
||||
console.log("🛡️ [AuthZ] Access Token:", auth.user.access_token);
|
||||
}
|
||||
|
||||
if (auth.isAuthenticated) {
|
||||
console.log(`🛡️ [AuthZ] Successfully logged in as: ${auth.user?.profile.email || 'Unknown'}. Redirecting to /...`);
|
||||
navigate('/');
|
||||
}
|
||||
}, [auth.isAuthenticated, auth.isLoading, auth.error, auth.user, navigate]);
|
||||
|
||||
const handleZitadelLogin = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await auth.signinRedirect();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-secondary/20 to-accent/20 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md glass-morphism border-white/20">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto bg-primary/10 w-12 h-12 rounded-full justify-center items-center flex mb-4">
|
||||
<Shield className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold bg-gradient-primary bg-clip-text text-transparent">
|
||||
<T>Corporate Login</T>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<T>Sign in securely via Zitadel Identity</T>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-center text-muted-foreground">
|
||||
<T>You will be redirected to the secure portal to complete your authentication via Google or company credentials.</T>
|
||||
</p>
|
||||
|
||||
<Button
|
||||
className="w-full mt-4"
|
||||
size="lg"
|
||||
onClick={handleZitadelLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? translate("Redirecting...") : translate("Continue to Security Portal")}
|
||||
</Button>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthZ;
|
||||
17
packages/ui/src/pages/PlaygroundVfs.tsx
Normal file
17
packages/ui/src/pages/PlaygroundVfs.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import FileBrowser from '@/apps/filebrowser/FileBrowser';
|
||||
|
||||
const PlaygroundVfs = () => {
|
||||
return (
|
||||
<div style={{ height: 'calc(100vh - 56px)', overflow: 'hidden' }}>
|
||||
<FileBrowser
|
||||
allowPanels={false}
|
||||
mode="simple"
|
||||
disableRoutingSync={true}
|
||||
initialMount="home"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaygroundVfs;
|
||||
Loading…
Reference in New Issue
Block a user