feat: install order, dependency blocking, batch install
- Sort extensions by install order (HebrewLanguage first, then deps) - Grey out extensions with missing dependencies (show lock icon + "נדרש: X") - Checkbox selection for batch install with progress indicator - "בחר הכל" button to select all installable extensions - Server returns dependencies + install_order in customer API Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+111
-13
@@ -201,11 +201,41 @@ function CustomerDetail({ customerId }) {
|
||||
const [deploying, setDeploying] = useState(null)
|
||||
const [deployResult, setDeployResult] = useState(null)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [selected, setSelected] = useState(new Set())
|
||||
const [batchDeploying, setBatchDeploying] = useState(false)
|
||||
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, module: '' })
|
||||
|
||||
if (!customer) return <Loader />
|
||||
|
||||
const assignedIds = new Set((customer.extensions || []).map(e => e.id))
|
||||
const available = (allExtensions || []).filter(e => !assignedIds.has(e.id))
|
||||
const order = customer.install_order || []
|
||||
const deps = customer.dependencies || {}
|
||||
const exts = customer.extensions || []
|
||||
const installedNames = new Set(exts.filter(e => e.installed_version).map(e => e.name))
|
||||
|
||||
const sorted = [...exts].sort((a, b) => {
|
||||
const ia = order.indexOf(a.name)
|
||||
const ib = order.indexOf(b.name)
|
||||
return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib)
|
||||
})
|
||||
|
||||
const toggleSelect = (id) => {
|
||||
setSelected(prev => {
|
||||
const next = new Set(prev)
|
||||
next.has(id) ? next.delete(id) : next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const selectAll = () => {
|
||||
const installable = sorted.filter(e => {
|
||||
if (e.installed_version) return false
|
||||
const missing = (deps[e.name] || []).filter(d => !installedNames.has(d))
|
||||
return missing.length === 0
|
||||
})
|
||||
setSelected(new Set(installable.map(e => e.id)))
|
||||
}
|
||||
|
||||
const deploy = async (module, version) => {
|
||||
setDeploying(module)
|
||||
@@ -224,6 +254,34 @@ function CustomerDetail({ customerId }) {
|
||||
setDeploying(null)
|
||||
}
|
||||
|
||||
const batchDeploy = async () => {
|
||||
const toDeploy = sorted.filter(e => selected.has(e.id) && !e.installed_version)
|
||||
setBatchDeploying(true)
|
||||
setBatchProgress({ current: 0, total: toDeploy.length, module: '' })
|
||||
const results = []
|
||||
for (let i = 0; i < toDeploy.length; i++) {
|
||||
const e = toDeploy[i]
|
||||
setBatchProgress({ current: i + 1, total: toDeploy.length, module: e.name })
|
||||
try {
|
||||
const r = await fetch(`${API}/deploy`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ customer_id: customerId, module: e.name, version: e.latest_version })
|
||||
})
|
||||
const result = await r.json()
|
||||
results.push({ module: e.name, ...result })
|
||||
} catch (err) {
|
||||
results.push({ module: e.name, status: 'error', message: err.message })
|
||||
}
|
||||
}
|
||||
const ok = results.filter(r => r.status === 'ok').length
|
||||
const fail = results.filter(r => r.status !== 'ok').length
|
||||
setDeployResult({ module: `${ok} הרחבות`, status: fail === 0 ? 'ok' : 'error',
|
||||
message: fail > 0 ? `${ok} הצליחו, ${fail} נכשלו` : undefined })
|
||||
setBatchDeploying(false)
|
||||
setSelected(new Set())
|
||||
reload()
|
||||
}
|
||||
|
||||
const remove = async (extId) => {
|
||||
await fetch(`${API}/customers/${customerId}/extensions/${extId}`, { method: 'DELETE' })
|
||||
reload()
|
||||
@@ -265,38 +323,76 @@ function CustomerDetail({ customerId }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{batchDeploying && (
|
||||
<div className="p-4 rounded-xl mb-6 bg-blue-50 text-blue-800 text-sm font-medium flex items-center gap-3">
|
||||
<Icon name="sync" className="text-base animate-spin" />
|
||||
מתקין {batchProgress.current}/{batchProgress.total}: {batchProgress.module}...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="bg-white rounded-xl overflow-hidden">
|
||||
<div className="p-6 flex justify-between items-center">
|
||||
<h4 className="font-bold text-slate-900">הרחבות מורשות</h4>
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-widest">{(customer.extensions || []).length} הרחבות</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="font-bold text-slate-900">הרחבות מורשות</h4>
|
||||
{selected.size > 0 && (
|
||||
<button onClick={batchDeploy} disabled={batchDeploying}
|
||||
className="deep-action-gradient text-white px-5 py-1.5 rounded-lg text-xs font-bold shadow-sm active:scale-95 transition-all disabled:opacity-50">
|
||||
{batchDeploying ? `מתקין ${batchProgress.current}/${batchProgress.total}...` : `התקן ${selected.size} הרחבות`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{sorted.some(e => !e.installed_version && (deps[e.name] || []).every(d => installedNames.has(d))) && (
|
||||
<button onClick={selectAll}
|
||||
className="text-xs text-blue-600 hover:text-blue-800 font-medium">
|
||||
בחר הכל
|
||||
</button>
|
||||
)}
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-widest">{exts.length} הרחבות</span>
|
||||
</div>
|
||||
</div>
|
||||
<table className="w-full text-right border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50/50">
|
||||
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">שם ההרחבה</th>
|
||||
<th className="px-3 py-4 w-10"></th>
|
||||
<th className="px-4 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">שם ההרחבה</th>
|
||||
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">מותקנת</th>
|
||||
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">עדכון זמין</th>
|
||||
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest text-center">פעולות</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{(customer.extensions || []).map(e => {
|
||||
{sorted.map(e => {
|
||||
const installed = e.installed_version
|
||||
const hasUpdate = installed && e.latest_version && installed !== e.latest_version
|
||||
const missingDeps = (deps[e.name] || []).filter(d => !installedNames.has(d))
|
||||
const blocked = !installed && missingDeps.length > 0
|
||||
return (
|
||||
<tr key={e.id} className="hover:bg-slate-50/80 transition-all">
|
||||
<td className="px-8 py-5">
|
||||
<tr key={e.id} className={`transition-all ${blocked ? 'opacity-40' : 'hover:bg-slate-50/80'}`}>
|
||||
<td className="px-3 py-5 text-center">
|
||||
{!installed && !blocked && (
|
||||
<input type="checkbox" checked={selected.has(e.id)}
|
||||
onChange={() => toggleSelect(e.id)} disabled={batchDeploying}
|
||||
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500 cursor-pointer" />
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${installed ? 'bg-blue-50 text-blue-700' : 'bg-slate-100 text-slate-400'}`}>
|
||||
<Icon name="extension" className="text-base" />
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${installed ? 'bg-blue-50 text-blue-700' : blocked ? 'bg-slate-50 text-slate-300' : 'bg-slate-100 text-slate-400'}`}>
|
||||
<Icon name={blocked ? 'lock' : 'extension'} className="text-base" />
|
||||
</div>
|
||||
<div>
|
||||
<span className={`font-bold ${blocked ? 'text-slate-400' : 'text-slate-900'}`}>{e.name}</span>
|
||||
{blocked && (
|
||||
<p className="text-[10px] text-slate-400 mt-0.5">נדרש: {missingDeps.join(', ')}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="font-bold text-slate-900">{e.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-5">
|
||||
{installed
|
||||
? <VersionBadge>v{installed}</VersionBadge>
|
||||
: <span className="text-sm text-red-500 font-medium">לא מותקן</span>}
|
||||
: <span className={`text-sm font-medium ${blocked ? 'text-slate-300' : 'text-red-500'}`}>לא מותקן</span>}
|
||||
</td>
|
||||
<td className="px-8 py-5">
|
||||
{hasUpdate
|
||||
@@ -305,9 +401,11 @@ function CustomerDetail({ customerId }) {
|
||||
</td>
|
||||
<td className="px-8 py-5">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{!installed ? (
|
||||
{blocked ? (
|
||||
<span className="text-xs text-slate-300">—</span>
|
||||
) : !installed ? (
|
||||
<button onClick={() => deploy(e.name, e.latest_version)}
|
||||
disabled={deploying === e.name}
|
||||
disabled={deploying === e.name || batchDeploying}
|
||||
className="deep-action-gradient text-white px-4 py-1.5 rounded-lg text-xs font-bold shadow-sm active:scale-95 transition-all disabled:opacity-50">
|
||||
{deploying === e.name ? '...' : 'התקנה'}
|
||||
</button>
|
||||
@@ -315,7 +413,7 @@ function CustomerDetail({ customerId }) {
|
||||
<>
|
||||
{hasUpdate && (
|
||||
<button onClick={() => deploy(e.name, e.latest_version)}
|
||||
disabled={deploying === e.name}
|
||||
disabled={deploying === e.name || batchDeploying}
|
||||
className="p-2 text-amber-500 hover:text-amber-700 hover:bg-amber-50 rounded-lg transition-all"
|
||||
title="עדכון">
|
||||
<Icon name="upgrade" className="text-lg" />
|
||||
|
||||
Reference in New Issue
Block a user