Add ACP user deletion and split frontend bundles
All checks were successful
CI/CD Pipeline / deploy (push) Successful in 30s
CI/CD Pipeline / promote_stable (push) Successful in 2s

This commit is contained in:
2026-03-17 16:49:11 +01:00
parent ef84b73cb5
commit a2fe31925f
12 changed files with 442 additions and 51 deletions

View File

@@ -16,6 +16,7 @@ import {
listRanks,
listRoles,
listUsers,
deleteUser,
listAuditLogs,
reorderForums,
saveSetting,
@@ -126,6 +127,9 @@ function Acp({ isAdmin }) {
const [rankEditImage, setRankEditImage] = useState(null)
const [showUserModal, setShowUserModal] = useState(false)
const [userForm, setUserForm] = useState({ id: null, name: '', email: '', rankId: '', roles: [] })
const [showUserDelete, setShowUserDelete] = useState(false)
const [userDeleteTarget, setUserDeleteTarget] = useState(null)
const [userDeleting, setUserDeleting] = useState(false)
const [roleQuery, setRoleQuery] = useState('')
const [roleMenuOpen, setRoleMenuOpen] = useState(false)
const roleMenuRef = useRef(null)
@@ -204,6 +208,12 @@ function Acp({ isAdmin }) {
favicon_128: '',
favicon_256: '',
})
const [unitsSettings, setUnitsSettings] = useState({
date: 'dd.mm.yyyy',
datetime: 'dd.mm.yyyy hh:mm',
byte_calculation_base: 'decimal',
byte_prefixes: 'decimal',
})
const [systemCliSettings, setSystemCliSettings] = useState({
php_mode: 'php',
php_custom: '',
@@ -998,7 +1008,7 @@ function Acp({ isAdmin }) {
<Button
variant="dark"
title={t('user.delete')}
onClick={() => console.log('delete user', row)}
onClick={() => handleUserDelete(row)}
>
<i className="bi bi-trash" aria-hidden="true" />
</Button>
@@ -1116,17 +1126,39 @@ function Acp({ isAdmin }) {
const formatBytes = (bytes) => {
if (bytes === null || bytes === undefined) return '—'
if (bytes === 0) return '0 B'
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
const idx = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)))
const value = bytes / 1024 ** idx
const base = unitsSettings.byte_calculation_base === 'decimal' ? 1000 : 1024
const units = unitsSettings.byte_prefixes === 'binary'
? ['B', 'KiB', 'MiB', 'GiB', 'TiB']
: ['B', 'KB', 'MB', 'GB', 'TB']
const idx = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(base)))
const value = bytes / base ** idx
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[idx]}`
}
const pad2 = (value) => String(value).padStart(2, '0')
const formatDateTime = (value) => {
if (!value) return '—'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '—'
return date.toLocaleString()
const day = pad2(date.getDate())
const month = pad2(date.getMonth() + 1)
const year = date.getFullYear()
const hours24 = pad2(date.getHours())
const minutes = pad2(date.getMinutes())
switch (unitsSettings.datetime) {
case 'yyyy-mm-dd hh:mm':
return `${year}-${month}-${day} ${hours24}:${minutes}`
case 'mm/dd/yyyy h:mm a': {
const hour = date.getHours() % 12 || 12
const meridiem = date.getHours() >= 12 ? 'PM' : 'AM'
return `${month}/${day}/${year} ${hour}:${minutes} ${meridiem}`
}
case 'dd.mm.yyyy hh:mm':
default:
return `${day}.${month}.${year} ${hours24}:${minutes}`
}
}
const formatBool = (value) => {
@@ -1867,6 +1899,35 @@ function Acp({ isAdmin }) {
}
}
const handleUserDelete = (user) => {
const deleteLocked = (user.roles || []).includes('ROLE_FOUNDER') && !canManageFounder
if (deleteLocked) {
setUsersError(t('user.founder_locked'))
return
}
setUsersError('')
setUserDeleteTarget(user)
setShowUserDelete(true)
}
const confirmUserDelete = async () => {
if (!userDeleteTarget) return
setUserDeleting(true)
setUsersError('')
try {
await deleteUser(userDeleteTarget.id)
setUsers((prev) => prev.filter((user) => user.id !== userDeleteTarget.id))
setShowUserDelete(false)
setUserDeleteTarget(null)
} catch (err) {
setUsersError(err.message)
} finally {
setUserDeleting(false)
}
}
const openAttachmentExtensionEdit = (extension) => {
setAttachmentExtensionEdit(extension)
setNewAttachmentExtension({
@@ -3379,6 +3440,94 @@ function Acp({ isAdmin }) {
</div>
</div>
</Tab>
<Tab eventKey="units" title="Units">
<div className="border border-1 border-dark border-top-0 rounded-bottom p-3">
<div className="bb-acp-panel">
<div className="bb-acp-panel-header">
<h5 className="mb-0">Units</h5>
</div>
<div className="bb-acp-panel-body">
<Row className="g-3">
<Col lg={4}>
<Form.Group>
<Form.Label>Date</Form.Label>
<Form.Select
value={unitsSettings.date}
onChange={(event) =>
setUnitsSettings((prev) => ({ ...prev, date: event.target.value }))
}
>
<option value="dd.mm.yyyy">DD.MM.YYYY</option>
<option value="yyyy-mm-dd">YYYY-MM-DD</option>
<option value="mm/dd/yyyy">MM/DD/YYYY</option>
</Form.Select>
</Form.Group>
</Col>
<Col lg={4}>
<Form.Group>
<Form.Label>Date &amp; time</Form.Label>
<Form.Select
value={unitsSettings.datetime}
onChange={(event) =>
setUnitsSettings((prev) => ({ ...prev, datetime: event.target.value }))
}
>
<option value="dd.mm.yyyy hh:mm">DD.MM.YYYY HH:mm</option>
<option value="yyyy-mm-dd hh:mm">YYYY-MM-DD HH:mm</option>
<option value="mm/dd/yyyy h:mm a">MM/DD/YYYY h:mm A</option>
</Form.Select>
</Form.Group>
</Col>
<Col xs={12}>
<div className="bb-acp-panel mt-2">
<div className="bb-acp-panel-header">
<h6 className="mb-0">Byte values</h6>
</div>
<div className="bb-acp-panel-body">
<Row className="g-3">
<Col lg={6}>
<Form.Group>
<Form.Label>Calculation base</Form.Label>
<Form.Select
value={unitsSettings.byte_calculation_base}
onChange={(event) =>
setUnitsSettings((prev) => ({
...prev,
byte_calculation_base: event.target.value,
}))
}
>
<option value="binary">Binary | One KB/KiB corresponds to 1024 bytes.</option>
<option value="decimal">Decimal | One KB/KiB corresponds to 1000 bytes.</option>
</Form.Select>
</Form.Group>
</Col>
<Col lg={6}>
<Form.Group>
<Form.Label>Prefixes</Form.Label>
<Form.Select
value={unitsSettings.byte_prefixes}
onChange={(event) =>
setUnitsSettings((prev) => ({
...prev,
byte_prefixes: event.target.value,
}))
}
>
<option value="binary">Binary | KiB, MiB, GiB, TiB</option>
<option value="decimal">Decimal | KB, MB, GB, TB</option>
</Form.Select>
</Form.Group>
</Col>
</Row>
</div>
</div>
</Col>
</Row>
</div>
</div>
</div>
</Tab>
<Tab eventKey="client-communication" title={t('acp.client_communication')}>
<div className="border border-1 border-dark border-top-0 rounded-bottom p-3">
<div className="bb-acp-panel">
@@ -3431,7 +3580,6 @@ function Acp({ isAdmin }) {
</div>
</Tab>
<Tab eventKey="forums" title={t('acp.forums')}>
<p className="bb-muted">{t('acp.forums_hint')}</p>
{error && <p className="text-danger">{error}</p>}
<Row className="g-4">
<Col lg={12}>
@@ -3510,12 +3658,25 @@ function Acp({ isAdmin }) {
paginationComponent={UsersPagination}
subHeader
subHeaderComponent={
<Form.Control
className="bb-user-search"
value={userSearch}
onChange={(event) => setUserSearch(event.target.value)}
placeholder={t('user.search')}
/>
<div className="bb-search-field">
<Form.Control
className="bb-user-search bb-search-field-input"
value={userSearch}
onChange={(event) => setUserSearch(event.target.value)}
placeholder={t('user.search')}
/>
{userSearch && (
<button
type="button"
className="bb-search-clear"
onClick={() => setUserSearch('')}
aria-label={t('acp.clear')}
title={t('acp.clear')}
>
<i className="bi bi-x-lg" aria-hidden="true" />
</button>
)}
</div>
}
/>
)}
@@ -4482,6 +4643,50 @@ function Acp({ isAdmin }) {
</Form>
</Modal.Body>
</Modal>
<Modal
show={showUserDelete}
onHide={() => {
if (userDeleting) return
setShowUserDelete(false)
setUserDeleteTarget(null)
}}
dialogClassName="bb-confirm-modal"
centered
>
<Modal.Header closeButton={!userDeleting}>
<Modal.Title>{t('user.delete_title')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>{t('user.delete_confirm')}</p>
<p className="bb-muted">
{userDeleteTarget?.email || userDeleteTarget?.name}
</p>
<div className="d-flex justify-content-between gap-2">
<Button
type="button"
variant="outline-secondary"
onClick={() => {
setShowUserDelete(false)
setUserDeleteTarget(null)
}}
disabled={userDeleting}
>
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')}
</Button>
<Button
type="button"
className="bb-accent-button"
variant="dark"
onClick={confirmUserDelete}
disabled={userDeleting}
>
<i className="bi bi-trash me-2" aria-hidden="true" />
{userDeleting ? t('form.saving') : t('acp.delete')}
</Button>
</div>
</Modal.Body>
</Modal>
<Modal
show={showRoleModal}
onHide={() => setShowRoleModal(false)}