FileBrowser 1177 -> 712 Zeilen, PdfViewer 1193 -> 848. Herausgeloest wurde, was fuer sich zusammenhaengt: hooks/useUploads Warteschlange, Ordnerbaeume, Ablage components/files/ Werkzeugleiste, Dialoge, Menues, Reihenfolge components/pdf/ Werkzeugleiste, Zoomstufen, Seitenmasse, Suche src/format.ts formatBytes -- lag in einer *Seite* Dabei gefundene und behobene Fehler: * Ordner-Upload haengte Fortschritt und Fehler an die falsche Zeile, sobald in einem Ordner Dateien *nach* einem Unterordner lagen: Die Zeilen standen in Fundreihenfolge, hochgeladen wurde nach Ordnern gruppiert, und ein mitlaufender Zaehler verband beides falsch. * formatBytes gab es zweimal, zeichengleich (Dateibrowser + Admin). * Beide Sortierungen liefen bei *jedem* Bild -- der Rahmen beim Ziehen ist eines je Mausbewegung. Jetzt gemerkt. * Die Suche im PDF las bei jedem Tastendruck den Text aller Seiten neu. Jetzt 160 ms Wartezeit; "Searching..." bleibt auch bei einem Fehler nicht mehr stehen. * Seitenmasse werden in Buendeln statt streng nacheinander geholt. Pruefinfrastruktur: vitest stand auf 'jsdom', ohne dass jsdom je installiert war -- "npm test" brach ab, bevor es etwas ausfuehrte. Jetzt 'node' und 32 Tests fuer die herausgeloesten reinen Funktionen. Das Abbild baut nur noch mit gruenen Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
5.4 KiB
TypeScript
129 lines
5.4 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import { aclApi, filesApi, type SharedResource } from '../api/client'
|
||
import { formatBytes } from '../format'
|
||
import PageHeader from '../components/PageHeader'
|
||
import { FolderGlyph } from '../components/icons'
|
||
|
||
interface Props {
|
||
/** Öffnet einen freigegebenen Ordner in der Dateiansicht. */
|
||
onOpenFolder: (folderId: string, folderName: string) => void
|
||
}
|
||
|
||
/** „Geteilt mit mir": alles, worauf man Zugriff bekommen hat, ohne es zu
|
||
* besitzen. Die Daten liegen weiterhin beim Eigentümer – hier wird nur
|
||
* darauf verwiesen. */
|
||
export default function SharedWithMePage({ onOpenFolder }: Props) {
|
||
const [items, setItems] = useState<SharedResource[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
useEffect(() => {
|
||
aclApi.sharedWithMe()
|
||
.then(d => setItems(d ?? []))
|
||
.catch(() => {})
|
||
.finally(() => setLoading(false))
|
||
}, [])
|
||
|
||
return (
|
||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||
<PageHeader
|
||
icon={
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
||
<circle cx="12" cy="13" r="2" />
|
||
<path d="M8.5 18a3.5 3.5 0 017 0" />
|
||
</svg>
|
||
}
|
||
title="Shared with me"
|
||
meta="Files and folders other people gave you access to"
|
||
/>
|
||
|
||
<div className="aether-content" style={{ flex: 1, overflowY: 'auto' }}>
|
||
|
||
{loading ? (
|
||
<p style={{ fontSize: 13, color: 'var(--color-text-muted)' }}>Loading…</p>
|
||
) : items.length === 0 ? (
|
||
<div style={{
|
||
padding: 40, textAlign: 'center', borderRadius: 12,
|
||
border: '1px dashed var(--color-border)',
|
||
}}>
|
||
<p style={{ margin: 0, fontSize: 14, color: 'var(--color-text)' }}>Nothing shared with you yet</p>
|
||
<p style={{ margin: '6px 0 0', fontSize: 12, color: 'var(--color-text-muted)' }}>
|
||
When someone shares a file or folder, it shows up here.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, maxWidth: 760 }}>
|
||
{items.map(item => (
|
||
<div
|
||
key={`${item.type}-${item.id}`}
|
||
onClick={() => item.type === 'folder' && onOpenFolder(item.id, item.name)}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 14,
|
||
padding: '13px 16px', borderRadius: 11,
|
||
border: '1px solid var(--color-border)',
|
||
background: 'var(--color-surface)',
|
||
cursor: item.type === 'folder' ? 'pointer' : 'default',
|
||
}}
|
||
onMouseEnter={e => { if (item.type === 'folder') e.currentTarget.style.borderColor = 'var(--color-primary)' }}
|
||
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--color-border)' }}
|
||
>
|
||
<span style={{ flexShrink: 0, display: 'flex' }}>
|
||
{item.type === 'folder' ? (
|
||
<FolderGlyph size={24} />
|
||
) : (
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-muted)" strokeWidth="1.8">
|
||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
||
<polyline points="14 2 14 8 20 8" />
|
||
</svg>
|
||
)}
|
||
</span>
|
||
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{
|
||
fontSize: 14, fontWeight: 600, color: 'var(--color-text)',
|
||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||
}}>
|
||
{item.name}
|
||
</div>
|
||
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2 }}>
|
||
Shared by {item.owner_name}
|
||
{/* Ohne diesen Hinweis wundert man sich, woher eine Freigabe
|
||
kommt, die man nie persönlich bekommen hat. */}
|
||
{item.via_group && ` via ${item.via_group}`}
|
||
{item.type === 'file' && ` · ${formatBytes(item.size)}`}
|
||
</div>
|
||
</div>
|
||
|
||
<span style={{
|
||
flexShrink: 0, padding: '3px 10px', borderRadius: 999,
|
||
fontSize: 11, fontWeight: 600,
|
||
background: item.permission === 'write'
|
||
? 'rgba(var(--color-primary-rgb), 0.12)'
|
||
: 'var(--color-surface-2)',
|
||
color: item.permission === 'write' ? 'var(--color-primary)' : 'var(--color-text-muted)',
|
||
}}>
|
||
{item.permission === 'write' ? 'Can edit' : 'View only'}
|
||
</span>
|
||
|
||
{item.type === 'file' && (
|
||
<a
|
||
href={filesApi.download(item.id)}
|
||
download={item.name}
|
||
onClick={e => e.stopPropagation()}
|
||
style={{
|
||
flexShrink: 0, padding: '5px 12px', borderRadius: 7,
|
||
border: '1px solid var(--color-border)',
|
||
color: 'var(--color-text)', fontSize: 12, textDecoration: 'none',
|
||
}}
|
||
>
|
||
Download
|
||
</a>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
} |