mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
843582863c
The Documents addon put a "Document sync" button on every trip's Files tab, and the seed ships every provider switched off, so after the upgrade each of those buttons led into "No document providers are available". The button now shows on a trip that is already bound, to every member, and otherwise only to somebody who may bind the trip on an instance that offers a provider. Without it the Files toolbar is the one from before the feature, trash flush right, on both shells. /api/addons carries no document providers for a normal user, so the answer comes from the docsync routes through a new online-only docSyncRepo and is kept per session in docSyncOfferStore. A revisit renders the last answer at once and checks it again; offline nothing is asked and the last answer stands. Creating a binding, removing one and removing a connection now ping the trip with docsync:changed and zero counts, so a member already on the Files tab sees the button come and go without leaving it. Both shells keep listing a binding whose provider the admin has since switched off instead of claiming there are no providers, and name its store through one storeName() helper. A load that answers 403 or 404 clears the bindings as well, so they do not linger once the Documents addon is gone. The phone admin screen gets the Documents shelf the desktop one has: the document providers as sub rows under the addon, toggled through the same admin route and re-read after Documents itself is toggled, because the server switches the providers off with it. isTripOwner and the isOwner prop become canManageSync and canManage, which is what canManageDocSync answers. Both folder pickers take loadScopes out of the sync object before their effect, so exhaustive-deps is satisfied without the effect running on every render.
101 lines
5.8 KiB
TypeScript
101 lines
5.8 KiB
TypeScript
import { FolderSync, Star, Trash2 } from 'lucide-react'
|
|
import type { FileManagerState } from './useFileManager'
|
|
|
|
export function FileManagerToolbar(S: FileManagerState) {
|
|
const { showTrash, t, files, filterType, setFilterType, toggleTrash, setShowDocSync, docSyncOffered } = S
|
|
return (
|
|
<div style={{ padding: '24px 28px 0', flexShrink: 0 }} className="max-md:!px-4 max-md:!pt-4">
|
|
<div style={{
|
|
background: 'var(--bg-tertiary)', borderRadius: 18,
|
|
padding: '14px 16px 14px 22px',
|
|
display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
|
|
}}>
|
|
<h2 style={{ margin: 0, fontSize: 'calc(18px * var(--fs-scale-subtitle, 1))', fontWeight: 600, color: 'var(--text-primary)', letterSpacing: '-0.01em', flexShrink: 0 }}>
|
|
{showTrash ? (t('files.trash') || 'Trash') : t('files.title')}
|
|
</h2>
|
|
|
|
{!showTrash && (
|
|
<>
|
|
<div className="hidden md:block" style={{ width: 1, height: 22, background: 'var(--border-faint)', flexShrink: 0 }} />
|
|
<div className="hidden md:inline-flex" style={{ gap: 4, flexWrap: 'wrap', flex: 1, minWidth: 0 }}>
|
|
{[
|
|
{ id: 'all', label: t('files.filterAll') },
|
|
...(files.some(f => f.starred) ? [{ id: 'starred', icon: Star } as const] : []),
|
|
{ id: 'pdf', label: t('files.filterPdf') },
|
|
{ id: 'image', label: t('files.filterImages') },
|
|
{ id: 'doc', label: t('files.filterDocs') },
|
|
...(files.some(f => f.note_id) ? [{ id: 'collab', label: t('files.filterCollab') || 'Collab' }] : []),
|
|
].map(tab => {
|
|
const active = filterType === tab.id
|
|
const TabIcon = 'icon' in tab ? tab.icon : null
|
|
const count = tab.id === 'all' ? files.length
|
|
: tab.id === 'starred' ? files.filter(f => f.starred).length
|
|
: tab.id === 'pdf' ? files.filter(f => (f.mime_type || '').includes('pdf') || /\.pdf$/i.test(f.original_name)).length
|
|
: tab.id === 'image' ? files.filter(f => (f.mime_type || '').startsWith('image/')).length
|
|
: tab.id === 'doc' ? files.filter(f => /\.(docx?|xlsx?|txt|csv)$/i.test(f.original_name)).length
|
|
: tab.id === 'collab' ? files.filter(f => f.note_id).length
|
|
: 0
|
|
return (
|
|
<button type="button" key={tab.id} onClick={() => setFilterType(tab.id)}
|
|
style={{
|
|
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
|
|
display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
padding: '6px 12px', borderRadius: 99, fontSize: 'calc(13px * var(--fs-scale-body, 1))', whiteSpace: 'nowrap',
|
|
background: active ? 'var(--bg-card)' : 'transparent',
|
|
color: active ? 'var(--text-primary)' : 'var(--text-muted)',
|
|
fontWeight: active ? 500 : 400,
|
|
boxShadow: active ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
|
|
transition: 'all 0.15s ease',
|
|
}}
|
|
>
|
|
{TabIcon ? <TabIcon size={13} fill={active ? '#facc15' : 'none'} color={active ? '#facc15' : 'currentColor'} /> : null}
|
|
{'label' in tab && tab.label}
|
|
<span style={{
|
|
fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600,
|
|
background: active ? 'var(--bg-tertiary)' : 'rgba(0,0,0,0.06)',
|
|
color: 'var(--text-faint)',
|
|
padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center',
|
|
}}>{count}</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Opens the sync panel. Placed next to the trash rather than in a menu
|
|
because the question it answers, "where do these documents live",
|
|
belongs on the same screen as the documents. Only where there is
|
|
something behind it: see useDocSyncOffered. */}
|
|
{docSyncOffered && (
|
|
<button type="button" onClick={() => setShowDocSync(true)} title={t('docsync.title')} style={{
|
|
appearance: 'none', border: '1px solid var(--edge)', cursor: 'pointer', fontFamily: 'inherit',
|
|
display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
padding: '9px 12px', borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500,
|
|
background: 'transparent', color: 'var(--content-muted)', flexShrink: 0, marginLeft: 'auto',
|
|
}}>
|
|
<FolderSync size={14} strokeWidth={2.5} /> <span className="hidden sm:inline">{t('docsync.title')}</span>
|
|
</button>
|
|
)}
|
|
|
|
{/* The first of the two takes the free space, so without the sync
|
|
button the trash sits flush right the way it always has. */}
|
|
<button type="button" onClick={toggleTrash} style={{
|
|
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
|
|
display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
padding: '9px 14px', borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500,
|
|
background: 'var(--accent)', color: 'var(--accent-text)',
|
|
flexShrink: 0, marginLeft: docSyncOffered ? undefined : 'auto',
|
|
opacity: showTrash ? 1 : 0.88,
|
|
transition: 'opacity 0.15s ease',
|
|
}}
|
|
onMouseEnter={e => e.currentTarget.style.opacity = '1'}
|
|
onMouseLeave={e => e.currentTarget.style.opacity = showTrash ? '1' : '0.88'}
|
|
>
|
|
<Trash2 size={14} strokeWidth={2.5} /> <span className="hidden sm:inline">{t('files.trash') || 'Trash'}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|