1
0
Files
breadcrumb-the-shire/pages/admin/users/edit(default).phtml
fs b749b5d192 feat: centralize breadcrumbs in topbar and remove history navigation
Move breadcrumb rendering from individual page templates into the core
topbar. Each page now sets $breadcrumbs in its action .php file; the
topbar renders it automatically via the shared partial.

- Remove global back/forward buttons and app-nav-history.js component
- Remove Alt+Arrow keyboard shortcuts for history navigation
- Render breadcrumb in topbar-left section (replaces button area)
- Clean up breadcrumb CSS: context-neutral base (flex, no margin)
- Recalculate sticky titlebar offset in details container
- Migrate all 41 pages (core + helpdesk, audit, addressbook, api-docs)
- Add missing breadcrumbs to addressbook detail view
- Update architecture contract tests (nav-history references removed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:17:06 +02:00

361 lines
14 KiB
PHTML

<?php
/**
* @var array<int, string> $errors
* @var array $form
* @var array $user
*/
use MintyPHP\Session;
$values = $form;
$avatarUuid = (string) ($values['uuid'] ?? '');
$userAvatarService = app(\MintyPHP\Service\User\UserAvatarService::class);
$hasAvatar = $avatarUuid !== '' && $userAvatarService->hasAvatar($avatarUuid);
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
$isOwnAccount = $currentUserId > 0 && $currentUserId === (int) ($values['id'] ?? 0);
$canEditUser = !empty($canEditUser);
$titleText = $isOwnAccount ? t('My account') : ($canEditUser ? t('Edit user') : t('View user'));
$canViewUsers = !empty($canViewUsers);
$canViewUserMeta = !empty($canViewUserMeta);
$canViewUserAudit = !empty($canViewUserAudit);
$canAccessPdf = !empty($canAccessPdf);
$canViewSecurityArtifacts = !empty($canViewSecurityArtifacts);
$canViewPermissionsTable = !empty($canViewPermissionsTable);
$canEditUser = !empty($canEditUser);
$canDeleteUser = !empty($canDeleteUser);
$hideNavigation = $isOwnAccount && !$canViewUsers;
$lastLoginProvider = strtolower((string) ($values['last_login_provider'] ?? ($user['last_login_provider'] ?? '')));
$lastLoginAt = (string) ($values['last_login_at'] ?? ($user['last_login_at'] ?? ''));
$lastLoginProviderLabel = '-';
$lastLoginProviderVariant = 'neutral';
if ($lastLoginAt !== '') {
if ($lastLoginProvider === 'microsoft') {
$lastLoginProviderLabel = t('Microsoft');
$lastLoginProviderVariant = 'primary';
} elseif ($lastLoginProvider === 'local') {
$lastLoginProviderLabel = t('Local');
$lastLoginProviderVariant = 'secondary';
} else {
$lastLoginProviderLabel = t('Unknown');
$lastLoginProviderVariant = 'warning';
}
}
?>
<div class="app-details-container">
<section>
<?php
$asideActions = [];
$moduleSlots = is_array($layoutNav['moduleSlots'] ?? null) ? $layoutNav['moduleSlots'] : [];
$moduleAsideActionSlots = is_array($moduleSlots['user.edit.aside_action'] ?? null) ? $moduleSlots['user.edit.aside_action'] : [];
$slotContextMap = [
'{user_id}' => (string) ($values['id'] ?? ''),
'{user_uuid}' => (string) ($values['uuid'] ?? ''),
];
foreach ($moduleAsideActionSlots as $slotAction) {
if (!is_array($slotAction)) {
continue;
}
$slotPermission = trim((string) ($slotAction['permission'] ?? ''));
if ($slotPermission !== '' && empty($layoutAuth[$slotPermission])) {
continue;
}
$slotLabel = trim((string) ($slotAction['label'] ?? ''));
if ($slotLabel === '') {
continue;
}
$slotType = trim((string) ($slotAction['type'] ?? 'link'));
if ($slotType === 'form') {
$slotFormAction = trim((string) ($slotAction['action'] ?? ''));
if ($slotFormAction === '') {
continue;
}
$slotFormAction = strtr($slotFormAction, $slotContextMap);
if (!str_starts_with($slotFormAction, '/') && !str_starts_with($slotFormAction, 'http://') && !str_starts_with($slotFormAction, 'https://')) {
$slotFormAction = lurl($slotFormAction);
}
$asideActions[] = [
'type' => 'form',
'method' => strtoupper(trim((string) ($slotAction['method'] ?? 'POST'))),
'action' => $slotFormAction,
'label' => t($slotLabel),
];
continue;
}
$slotHref = trim((string) ($slotAction['href'] ?? ''));
if ($slotHref === '') {
continue;
}
$slotHref = strtr($slotHref, $slotContextMap);
if (!str_starts_with($slotHref, '/') && !str_starts_with($slotHref, 'http://') && !str_starts_with($slotHref, 'https://')) {
$slotHref = lurl($slotHref);
}
$asideActions[] = [
'type' => 'link',
'href' => $slotHref,
'label' => t($slotLabel),
];
}
if ($canEditUser) {
$asideActions[] = [
'type' => 'form',
'method' => 'post',
'action' => 'admin/users/send-access/' . ($values['uuid'] ?? ''),
'confirm' => t('Send access email to this user?'),
'label' => t('Send access'),
];
}
if ($canAccessPdf) {
$asideActions[] = [
'type' => 'form',
'method' => 'post',
'action' => 'admin/users/access-pdf',
'label' => t('Access PDF'),
'target' => '_blank',
'fields' => [
'uuid' => (string) ($values['uuid'] ?? ''),
],
];
}
$titlebarActions = [];
if ($canEditUser) {
$titlebarActions[] = [
'form' => 'user-form',
'name' => 'action',
'value' => 'save',
'class' => 'secondary outline',
'label' => t('Save'),
];
if (!$hideNavigation) {
$titlebarActions[] = [
'form' => 'user-form',
'name' => 'action',
'value' => 'save_close',
'class' => 'primary',
'label' => t('Save & close'),
];
}
}
$titlebar = [
'title' => $titleText,
'backHref' => $hideNavigation ? '' : 'admin/users',
'backTitle' => t('Cancel'),
'actions' => $titlebarActions,
];
require templatePath('partials/app-details-titlebar.phtml');
?>
<?php
$validationSummaryErrors = $validationSummaryErrors ?? ($errors ?? []);
require templatePath('partials/app-details-validation-summary.phtml');
?>
<?php
$passwordRequired = false;
$passwordLabel = t('New password (optional)');
$passwordLegend = t('Password');
$passwordConfirmLabel = t('Password (again)');
$showTenants = !empty($canEditAssignments);
$tenants = $tenants ?? [];
$selectedTenantIds = $selectedTenantIds ?? [];
$showRoles = !empty($canEditAssignments);
$roles = $roles ?? [];
$selectedRoleIds = $selectedRoleIds ?? [];
$showDepartments = !empty($canEditAssignments);
$departmentOptionsByTenant = $departmentOptionsByTenant ?? [];
$selectedDepartmentIds = $selectedDepartmentIds ?? [];
$isReadOnly = !$canEditUser;
$canEditAssignments = $canEditAssignments ?? false;
$permissionRows = $permissionRows ?? [];
$showPermissions = $canViewPermissionsTable;
$passwordResets = $passwordResets ?? [];
$showPasswordResets = $canViewSecurityArtifacts;
$rememberTokens = $rememberTokens ?? [];
$showRememberTokens = $canViewSecurityArtifacts;
$showDangerZone = $canDeleteUser;
$dangerZoneDeleteFormId = $showDangerZone ? 'user-delete-form' : '';
$dangerZoneWarning = t('This will permanently delete this user.');
$dangerZoneActionLabel = t('Delete user');
$showTokenDangerAction = $canEditUser;
$tokenDangerFormId = $showTokenDangerAction ? 'user-forget-tokens-form' : '';
$tokenDangerWarning = t('This will mark all remember-me tokens as expired. Users will need to sign in again.');
$tokenDangerActionLabel = t('Clear login tokens');
$showCustomFieldsTab = true;
$canEditCustomFieldValues = !empty($canEditCustomFieldValues);
$customFieldDefinitionsByTenant = $customFieldDefinitionsByTenant ?? [];
$customFieldValueMap = $customFieldValueMap ?? [];
$customFieldPostedValues = $customFieldPostedValues ?? [];
$passwordMinLength = $passwordMinLength ?? 12;
$passwordHints = $passwordHints ?? [];
require __DIR__ . '/_form.phtml';
?>
<?php if ($canManageApiTokens ?? false): ?>
<form id="api-token-create-form" method="post"
action="admin/users/api-token-create/<?php e($values['uuid'] ?? ''); ?>" hidden>
<?php Session::getCsrfInput(); ?>
</form>
<?php endif; ?>
<?php if ($showTokenDangerAction): ?>
<form
id="user-forget-tokens-form"
method="post"
action="admin/users/forget-tokens/<?php e($values['uuid'] ?? ''); ?>"
hidden
data-detail-confirm-message="<?php e(t('Clear all login tokens for this user?')); ?>"
data-detail-action-kind="danger"
>
<?php Session::getCsrfInput(); ?>
</form>
<?php endif; ?>
<?php if ($showDangerZone): ?>
<form
id="user-delete-form"
method="post"
action="admin/users/delete/<?php e($values['uuid'] ?? ''); ?>"
hidden
data-detail-confirm-message="<?php e(t('Delete this user?')); ?>"
data-detail-action-kind="delete"
>
<?php Session::getCsrfInput(); ?>
</form>
<?php endif; ?>
</section>
<aside id="app-details-aside-section">
<div class="app-details-aside-section">
<div class="user-avatar-block avatar-round">
<?php if ($hasAvatar): ?>
<a data-fslightbox="user-avatar" href="admin/users/avatar-file?uuid=<?php e($avatarUuid); ?>&size=256">
<img class="user-avatar-image" src="admin/users/avatar-file?uuid=<?php e($avatarUuid); ?>&size=128"
alt="<?php e(t('Profile image')); ?>">
</a>
<?php else: ?>
<?php
$initials = strtoupper(substr((string) ($values['first_name'] ?? ''), 0, 1) . substr((string) ($values['last_name'] ?? ''), 0, 1));
?>
<div class="user-avatar-placeholder"><?php e($initials ?: '?'); ?></div>
<?php endif; ?>
</div>
<hgroup>
<h2><?php e($values['first_name'] ?? ''); ?> <?php e($values['last_name'] ?? ''); ?></h2>
<p><?php e($values['email'] ?? ''); ?></p>
</hgroup>
<hr>
<?php if ($canEditUser): ?>
<details name="user-avatar">
<summary>
<?php e(t('Upload image')); ?>
</summary>
<hr>
<form class="user-avatar-form" method="post" action="admin/users/avatar/<?php e($avatarUuid); ?>"
enctype="multipart/form-data">
<label class="user-avatar-upload">
<input type="file" name="avatar" accept="image/*">
</label>
<div class="grid">
<button type="submit" class="app-action-success">
<?php e(t('Save')); ?>
</button>
<?php if ($hasAvatar): ?>
<button data-tooltip-pos="top" data-tooltip="<?php e(t('Remove image')); ?>" type="submit" class="danger" formaction="admin/users/avatar-delete/<?php e($avatarUuid); ?>"
formmethod="post">
<i class="bi bi-trash3-fill"></i>
</button>
<?php endif; ?>
</div>
<?php Session::getCsrfInput(); ?>
</form>
</details>
<hr>
<?php endif; ?>
<?php
if ($asideActions) {
require templatePath('partials/app-details-aside-actions.phtml');
}
?>
<?php if ($canViewUserMeta): ?>
<details name="user-meta">
<summary><?php e(t('Status & meta')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('ID')); ?></small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['id'] ?? ''); ?>">
<?php e($values['id'] ?? '-'); ?>
</span>
</p>
</div>
<div>
<small><?php e(t('Status')); ?></small>
<p>
<?php if (!empty($values['active'])): ?>
<span class="badge" data-variant="success"><?php e(t('Active')); ?></span>
<?php else: ?>
<span class="badge" data-variant="danger"><?php e(t('Inactive')); ?></span>
<?php endif; ?>
</p>
</div>
</div>
<div>
<small><?php e(t('Email')); ?></small>
<p>
<?php if (!empty($values['email_verified_at'])): ?>
<span class="badge" data-variant="success"><?php e(t('Verified')); ?> <?php e(dt($values['email_verified_at'])); ?></span>
<?php else: ?>
<span class="badge" data-variant="warning"><?php e(t('Not verified')); ?></span>
<?php endif; ?>
</p>
</div>
<div>
<small><?php e(t('Last login via')); ?></small>
<p>
<span class="badge" data-variant="<?php e($lastLoginProviderVariant); ?>">
<?php e($lastLoginProviderLabel); ?>
</span>
</p>
</div>
<div>
<small>
<?php e(t('UUID')); ?>
</small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
<?php
$uuidValue = (string) ($values['uuid'] ?? '');
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
e($uuidDisplay);
?>
</span>
</p>
</div>
</details>
<hr>
<?php endif; ?>
<?php
$asideAuditDetailsName = 'user-audit';
$asideAuditCreated = (string) ($values['created'] ?? '');
$asideAuditCreatedByLabel = (string) ($values['created_by_label'] ?? '');
$asideAuditCreatedById = $values['created_by'] ?? null;
$asideAuditCreatedByUuid = (string) ($values['created_by_uuid'] ?? '');
$asideAuditModified = (string) ($values['modified'] ?? '');
$asideAuditModifiedByLabel = (string) ($values['modified_by_label'] ?? '');
$asideAuditModifiedById = $values['modified_by'] ?? null;
$asideAuditModifiedByUuid = (string) ($values['modified_by_uuid'] ?? '');
$asideAuditStatusChanged = (string) ($values['active_changed_at'] ?? '');
$asideAuditStatusChangedByLabel = (string) ($values['active_changed_by_label'] ?? '');
$asideAuditStatusChangedById = $values['active_changed_by'] ?? null;
$asideAuditStatusChangedByUuid = (string) ($values['active_changed_by_uuid'] ?? '');
?>
<?php if ($canViewUserAudit): ?>
<?php require templatePath('partials/app-details-aside-audit.phtml'); ?>
<?php endif; ?>
</div>
</aside>
</div>