Add Nextcloud directory browser to backup_path field in Settings
The Backup Directory field in the Nextcloud integration card gains a Browse button that opens an inline panel with: breadcrumb navigation, scrollable directory list, and a Select button to pick the current path. Falls back to manual text entry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3cfa092f73
commit
8a8c4073e5
1 changed files with 156 additions and 1 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { CheckCircle, XCircle, RefreshCw, ChevronDown, ChevronUp, Clock } from 'lucide-react'
|
||||
import { CheckCircle, XCircle, RefreshCw, ChevronDown, ChevronUp, Clock, Folder, FolderOpen, ChevronRight } from 'lucide-react'
|
||||
import { Sidebar } from '../components/Sidebar'
|
||||
import type { User } from '../types'
|
||||
|
||||
|
|
@ -113,6 +113,11 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
|
|||
const [wfLocations, setWfLocations] = useState<{ id: string; name: string; short_name: string | null }[]>([])
|
||||
const [fetchingLocs, setFetchingLocs] = useState(false)
|
||||
const [locError, setLocError] = useState<string | null>(null)
|
||||
const [ncBrowseOpen, setNcBrowseOpen] = useState(false)
|
||||
const [ncBrowsePath, setNcBrowsePath] = useState('/')
|
||||
const [ncBrowseDirs, setNcBrowseDirs] = useState<{ name: string; path: string }[]>([])
|
||||
const [ncBrowseLoading, setNcBrowseLoading] = useState(false)
|
||||
const [ncBrowseError, setNcBrowseError] = useState<string | null>(null)
|
||||
|
||||
function openForm() {
|
||||
const initial: Record<string, string> = {}
|
||||
|
|
@ -120,9 +125,28 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
|
|||
setForm(initial)
|
||||
setChanging(new Set())
|
||||
setTestResult(null)
|
||||
setNcBrowseOpen(false)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
async function ncBrowse(path: string) {
|
||||
setNcBrowseLoading(true)
|
||||
setNcBrowseError(null)
|
||||
setNcBrowsePath(path)
|
||||
try {
|
||||
const res = await fetch(`/settings/api/integrations/nextcloud/browse?path=${encodeURIComponent(path)}`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) { setNcBrowseError(data.error || `Error ${res.status}`); return }
|
||||
setNcBrowseDirs(data.dirs)
|
||||
} catch {
|
||||
setNcBrowseError('Network error')
|
||||
} finally {
|
||||
setNcBrowseLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function setField(key: string, value: string) {
|
||||
setForm(prev => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
|
@ -193,6 +217,137 @@ function IntegrationCard({ integration, onSaved }: { integration: Integration; o
|
|||
|
||||
{/* Config fields (plaintext) */}
|
||||
{Object.entries(integration.config).map(([k]) => {
|
||||
if (integration.slug === 'nextcloud' && k === 'backup_path') {
|
||||
const pathSegments = ncBrowsePath === '/' ? [] : ncBrowsePath.split('/').filter(Boolean)
|
||||
return (
|
||||
<div key={k} style={{ display: 'flex', flexDirection: 'column', gap: '0.3rem' }}>
|
||||
<span style={{ fontSize: '0.78rem', fontWeight: 600, color: 'var(--text-mid)' }}>
|
||||
Backup Directory
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<input value={form[k] ?? ''} onChange={e => setField(k, e.target.value)}
|
||||
placeholder="e.g. HNF-Backups" style={{ ...inputStyle, flex: 1 }} />
|
||||
<button
|
||||
onClick={() => {
|
||||
if (ncBrowseOpen) { setNcBrowseOpen(false); return }
|
||||
setNcBrowseOpen(true)
|
||||
setNcBrowsePath('/')
|
||||
ncBrowse('/')
|
||||
}}
|
||||
style={{
|
||||
background: 'var(--body-bg)', border: '1px solid var(--card-border)',
|
||||
borderRadius: '6px', padding: '0.45rem 0.75rem', fontSize: '0.78rem',
|
||||
cursor: 'pointer', color: 'var(--text-dark)',
|
||||
display: 'flex', alignItems: 'center', gap: '0.35rem', whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Folder size={13} strokeWidth={1.75} />
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{ncBrowseOpen && (
|
||||
<div style={{
|
||||
border: '1px solid var(--card-border)', borderRadius: '6px',
|
||||
background: 'var(--body-bg)', overflow: 'hidden',
|
||||
}}>
|
||||
{/* Breadcrumb */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: '0.2rem', flexWrap: 'wrap',
|
||||
padding: '0.5rem 0.75rem', borderBottom: '1px solid var(--card-border)',
|
||||
fontSize: '0.78rem',
|
||||
}}>
|
||||
<button onClick={() => ncBrowse('/')}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer',
|
||||
color: ncBrowsePath === '/' ? 'var(--text-dark)' : 'var(--gold)',
|
||||
fontSize: '0.78rem', fontWeight: 600, padding: 0 }}>
|
||||
/
|
||||
</button>
|
||||
{pathSegments.map((seg, i) => {
|
||||
const segPath = '/' + pathSegments.slice(0, i + 1).join('/')
|
||||
const isLast = i === pathSegments.length - 1
|
||||
return (
|
||||
<span key={segPath} style={{ display: 'flex', alignItems: 'center', gap: '0.2rem' }}>
|
||||
<ChevronRight size={11} strokeWidth={2} style={{ color: 'var(--text-mid)' }} />
|
||||
<button onClick={() => !isLast && ncBrowse(segPath)}
|
||||
style={{ background: 'none', border: 'none', cursor: isLast ? 'default' : 'pointer',
|
||||
color: isLast ? 'var(--text-dark)' : 'var(--gold)',
|
||||
fontSize: '0.78rem', fontWeight: isLast ? 600 : 400, padding: 0 }}>
|
||||
{seg}
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Directory list */}
|
||||
<div style={{ maxHeight: '180px', overflowY: 'auto' }}>
|
||||
{ncBrowseLoading && (
|
||||
<div style={{ padding: '0.75rem', fontSize: '0.78rem', color: 'var(--text-mid)' }}>
|
||||
Loading…
|
||||
</div>
|
||||
)}
|
||||
{ncBrowseError && (
|
||||
<div style={{ padding: '0.75rem', fontSize: '0.78rem', color: 'var(--danger)' }}>
|
||||
{ncBrowseError}
|
||||
</div>
|
||||
)}
|
||||
{!ncBrowseLoading && !ncBrowseError && ncBrowseDirs.length === 0 && (
|
||||
<div style={{ padding: '0.75rem', fontSize: '0.78rem', color: 'var(--text-mid)' }}>
|
||||
No subdirectories here
|
||||
</div>
|
||||
)}
|
||||
{!ncBrowseLoading && ncBrowseDirs.map(dir => (
|
||||
<button
|
||||
key={dir.path}
|
||||
onClick={() => ncBrowse(dir.path)}
|
||||
style={{
|
||||
width: '100%', background: 'none', border: 'none',
|
||||
borderBottom: '1px solid var(--card-border)',
|
||||
padding: '0.45rem 0.75rem', fontSize: '0.82rem',
|
||||
color: 'var(--text-dark)', cursor: 'pointer', textAlign: 'left',
|
||||
display: 'flex', alignItems: 'center', gap: '0.5rem',
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={13} strokeWidth={1.75} style={{ color: 'var(--gold)', flexShrink: 0 }} />
|
||||
{dir.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer: select current or cancel */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '0.5rem 0.75rem', borderTop: '1px solid var(--card-border)',
|
||||
background: 'var(--card-bg)',
|
||||
}}>
|
||||
<span style={{ fontSize: '0.72rem', color: 'var(--text-mid)', fontFamily: 'monospace' }}>
|
||||
{ncBrowsePath}
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<button onClick={() => setNcBrowseOpen(false)}
|
||||
style={{ background: 'none', border: '1px solid var(--card-border)',
|
||||
borderRadius: '5px', padding: '0.25rem 0.65rem', fontSize: '0.75rem',
|
||||
cursor: 'pointer', color: 'var(--text-mid)' }}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setField(k, ncBrowsePath.replace(/^\//, ''))
|
||||
setNcBrowseOpen(false)
|
||||
}}
|
||||
style={{ background: 'var(--navy)', color: '#fff', border: 'none',
|
||||
borderRadius: '5px', padding: '0.25rem 0.65rem', fontSize: '0.75rem',
|
||||
fontWeight: 600, cursor: 'pointer' }}>
|
||||
Select
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (integration.slug === 'workforce' && k === 'location_id') {
|
||||
return (
|
||||
<div key={k} style={{ display: 'flex', flexDirection: 'column', gap: '0.3rem' }}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue