{{-- Multi-step LDAP settings wizard. Four steps: 1. Connection (server URL, TLS, cert) 2. Authenticate & Scope (bind creds + base DN + filters) 3. Attribute Mapping (LDAP-attr → Snipe-IT-field) 4. Sync & Defaults (enable toggle, default group, forgot-pass URL) Layout order inside the box: box-header (title) box-body: step title + help text wizard progress indicator alerts (session flash + inline test result) step-specific field content box-footer: step-specific action buttons (Save & Continue, Back) A11y treatments: - Complete steps carry a checkmark icon in addition to color state so color-blind users have a non-color affordance (WCAG SC 1.4.1). - Every step button carries an sr-only "Step N of 5, {title}, {state}" so screen readers announce full progress context (WCAG SC 1.3.1 / 2.4.6). - Locked buttons reference #wizard-locked-note via aria-describedby so SR users hear WHY the button is disabled. - wire:confirm arms on step buttons only when $dirty is true so users don't lose in-progress edits by clicking around the header. - On step change we dispatch a browser event; the Alpine listener on the wizard panel focuses the new panel's fieldset so keyboard + SR users move with the visual context. --}}
{{-- Scoped dark-mode fix: for reasons I couldn't fully diagnose, .form-control inputs render white on dark inside this component while other forms elsewhere in the app read the theme correctly. Rather than reach for a global override that might collide with whatever mechanism the rest of the app uses, target only fields inside this wizard. Uses the theme's --box-bg + --color-fg so it matches whatever dark-mode palette the site is running. --}}

{{ $currentStep === 5 ? trans('admin/settings/general.ldap_wizard.done.title') : ($this->stepTitles[$currentStep] ?? '') }}

{{-- One-click disable when LDAP is currently on. Renders on every step (via the shared header) so admins never have to walk to step 4 to turn it off. Fires an AdminLTE .modal-danger red-header confirm rather than a browser native prompt so it matches the rest of the app's destructive-action UX. --}} @if ($snipeSettings->ldap_enabled == '1')
@endif
{{-- Step title + help text, always the current step's copy. --}} @php $stepHelpKey = match ($currentStep) { 1 => 'admin/settings/general.ldap_wizard.step_connection_help', 2 => 'admin/settings/general.ldap_wizard.step_authscope_help', 3 => 'admin/settings/general.ldap_wizard.step_mapping_help', 4 => 'admin/settings/general.ldap_wizard.step_sync_help', default => null, }; @endphp @if ($stepHelpKey) @endif @if ($isReadOnly) This is a demo. Every LDAP config field is read-only, but you can still enter a sample username on step 3 and use the Test Find User button to see the wizard search against the pre-seeded readonly directory. (You can search on tesla, einstein, or curie.) The wizard will not actually save any LDAP settings in this demo. @endif {{-- Wizard progress indicator. Same .bs-wizard class the quickstart setup layout + importer modal use. Flex + flex:1 on children rather than bootstrap col-md-*, so the layout stays uniform regardless of step count. --}}
@foreach ($this->stepTitles as $stepNum => $stepTitle) @php $state = $stepNum < $currentStep ? 'complete' : ($stepNum === $currentStep ? 'active' : 'disabled'); $reachable = $stepNum <= $highestStepReached; $srState = match ($state) { 'complete' => trans('admin/settings/general.ldap_wizard.state_complete'), 'active' => trans('admin/settings/general.ldap_wizard.state_current'), default => trans('admin/settings/general.ldap_wizard.state_locked'), }; @endphp
{{-- The circular indicator on the progress line is now itself a click target (matching the text-based click target above) --}}
@endforeach
{{-- Step-specific field content. Focusable wrapper so the Alpine listener can move focus here after a step change. Inline test-status alert renders INSIDE this panel (right above the fields it relates to) so a failed network test sits next to what the user needs to fix. Session flashes are intentionally NOT rendered here. Steps 1-3 advance in place (the wizard checkmark IS the success signal), and the step 4 / disableLdap paths redirect to a page that renders session flashes via the app layout. --}}
{{-- Panel-top alert is for INFRASTRUCTURE feedback (connect failures, TLS handshake, bind rejection). Step 3's test-lookup alerts (success AND error) render inside the well next to the search box instead, so the response appears where the user clicked. The check below suppresses this location for that case. --}} @if ($testStatus && $currentStep !== 3) {!! $testMessage !!} @endif @if ($currentStep === 1) @if ($is_ad) @endif @elseif ($currentStep === 2) {{-- Merged Authenticate + Scope step. Bind credentials appear next to the base DN so users composing a full admin DN can see the base DN portion right alongside. That was the friction that pushed us to combine what used to be two separate steps. --}} {{-- Only renders when a client cert AND key are populated on step 1. When runtime lacks SASL support (via ldap_sasl_bind) the hint would actively mislead admins into leaving bind fields blank for an auth path that can't execute. --}} @if ($ldap_client_tls_cert !== '' && $ldap_client_tls_key !== '') @if (\App\Models\Ldap::saslExternalAvailable()) {{ trans('admin/settings/general.ldap_wizard.sasl_external_step2_hint') }} @else {!! trans('admin/settings/general.ldap_wizard.sasl_external_unavailable_hint') !!} @endif @endif {{-- Placeholder swaps based on the step-1 AD flag: UPN form for AD, full DN form otherwise. Not required when SASL EXTERNAL is on because the bind identity comes from the client cert. --}} @elseif ($currentStep === 3) {{-- Single-column x-form.row layout. Only ldap_username_field + ldap_fname_field are required. Everything else is optional. The directory just skips syncing whatever attribute isn't mapped. Trans labels shortened to drop the "LDAP " prefix (the step title already provides that context). --}} {{-- Field descriptors come from $this->mappingFields (computed on the component). Placeholders swap between AD (samaccountname, streetaddress, etc.) and non-AD (uid, street, etc.) based on the is_ad flag from step 1. Fifth tuple element is an optional help-text trans key, only set on fields with per-field operational notes carried over from the legacy form. --}} @foreach ($this->mappingFields as [$propName, $labelKey, $placeholderExample, $required, $helpKey]) @endforeach {{-- Invert active flag lives in its own row below the grid. It's a checkbox, not a text input, and belongs conceptually with ldap_active_flag but doesn't visually fit the two-column layout. --}} {{-- Sample-lookup section, boxed in an x-well so it reads as a "try it" tool distinct from the field list above. Fires wire:click directly (not the Save & Continue flow) so users can iterate on the preview without triggering step advance. --}}

{{ trans('admin/settings/general.ldap_wizard.mapping.sample_username_label') }}

{!! trans('admin/settings/general.ldap_wizard.mapping.sample_username_help') !!}

{{-- Inline lookup errors (not_found, multiple_found, empty sample) render here, right below the search box where results would appear. --}} {{-- Test-lookup alerts live inside the well so they sit with the search box and the preview table. Success renders green with the checkmark icon, everything else renders red with the warning icon. Bind and connect failures include their own "Go back to step" language in the trans string, so the user still gets that cue without needing a top-of-panel banner. --}} @if ($testStatus) {!! $testMessage !!} @endif {{-- Preview table populated by a successful lookup. Muted rows for unmapped fields or mapped attributes the entry doesn't carry. Makes it obvious where the mapping produces no data. --}} @if (! empty($step3TestAttributes))

{{ $step3TestDn }}

@foreach ($step3TestAttributes as $snipeField => $preview) @endforeach
{{ trans('admin/settings/general.ldap_wizard.mapping.preview_snipe_field') }} {{ trans('admin/settings/general.ldap_wizard.mapping.preview_ldap_attribute') }} {{ trans('admin/settings/general.ldap_wizard.mapping.preview_ldap_value') }}
{{ $preview['label'] ?? $snipeField }} @if ($preview['attr']) {{ $preview['attr'] }} @else {{ trans('admin/settings/general.ldap_wizard.mapping.field_not_mapped') }} @endif @if ($preview['value'] !== null) {{ $preview['value'] }} @elseif ($preview['attr']) {{ trans('admin/settings/general.ldap_wizard.mapping.value_missing') }} @endif
@endif
@elseif ($currentStep === 4) {{-- ldap_enabled is intentionally not rendered here. Reaching step 4 means the earlier steps verified the connection, bind, and mapping. The save action flips ldap_enabled=1 on the user's behalf. Users who want to disable LDAP later toggle it from the LDAP settings page directly. --}} @elseif ($currentStep === 5) {{-- Completion summary. Reached only via a successful save on step 4. Points admins at sync-scheduling options. Login is live but recurring user sync has to be scheduled separately since the app ships no default schedule for snipeit:ldap-sync. --}}

{{ trans('admin/settings/general.ldap_wizard.done.subtitle') }}

{{ trans('admin/settings/general.ldap_wizard.done.intro') }}

{{ trans('admin/settings/general.ldap_wizard.done.sync_intro') }}

{{-- Persisted-config summary, grouped by wizard step with an "Edit" button that jumps back to that step. Sensitive fields (bind password, TLS client key) are shown as a set/not-set indicator rather than the actual value. Empty fields are hidden. --}} @php // Group field keys by the wizard step they // live on. Kept as an inline map so the // summary stays self-contained; if steps // move fields around, only this array needs // to update (plus the corresponding form // sections above, of course). $summaryGroups = [ 1 => [ 'title' => trans('admin/settings/general.ldap_wizard.step_connection'), 'fields' => [ 'ldap_server' => trans('admin/settings/general.ldap_server'), 'ldap_tls' => trans('admin/settings/general.ldap_tls'), 'ldap_server_cert_ignore' => trans('admin/settings/general.ldap_server_cert_ignore'), 'is_ad' => trans('admin/settings/general.is_ad'), 'ad_domain' => trans('admin/settings/general.ad_domain'), ], ], 2 => [ 'title' => trans('admin/settings/general.ldap_wizard.step_authscope'), 'fields' => [ 'ldap_uname' => trans('admin/settings/general.ldap_uname'), 'ldap_pword' => trans('admin/settings/general.ldap_pword'), 'ldap_basedn' => trans('admin/settings/general.ldap_basedn'), 'ldap_filter' => trans('admin/settings/general.ldap_filter'), 'ldap_auth_filter_query' => trans('admin/settings/general.ldap_auth_filter_query'), ], ], 3 => [ 'title' => trans('admin/settings/general.ldap_wizard.step_mapping'), 'fields' => [ 'ldap_username_field' => trans('admin/settings/general.ldap_username_field'), 'ldap_fname_field' => trans('admin/settings/general.ldap_fname_field'), 'ldap_lname_field' => trans('admin/settings/general.ldap_lname_field'), 'ldap_display_name' => trans('admin/settings/general.ldap_display_name'), 'ldap_email' => trans('admin/settings/general.ldap_email'), 'ldap_emp_num' => trans('admin/settings/general.ldap_emp_num'), 'ldap_phone_field' => trans('admin/settings/general.ldap_phone'), 'ldap_mobile' => trans('admin/settings/general.ldap_mobile'), 'ldap_jobtitle' => trans('admin/settings/general.ldap_jobtitle'), 'ldap_manager' => trans('admin/settings/general.ldap_manager'), 'ldap_dept' => trans('admin/settings/general.ldap_dept'), 'ldap_location' => trans('admin/settings/general.ldap_location'), 'ldap_active_flag' => trans('admin/settings/general.ldap_active_flag'), 'ldap_invert_active_flag' => trans('admin/settings/general.ldap_invert_active_flag'), ], ], 4 => [ 'title' => trans('admin/settings/general.ldap_wizard.step_sync'), 'fields' => [ 'ldap_pw_sync' => trans('admin/settings/general.ldap_pw_sync'), 'ldap_default_group' => trans('admin/settings/general.ldap_default_group'), 'custom_forgot_pass_url' => trans('admin/settings/general.custom_forgot_pass_url'), ], ], ]; // Bind password and TLS client key/cert are // never rendered as their raw persisted // value in the summary - show only whether // they are set. Keeps sensitive material off // any incidental screenshot/screen-share. $summarySecretFields = ['ldap_pword', 'ldap_client_tls_key', 'ldap_client_tls_cert']; @endphp

{{ trans('admin/settings/general.ldap_wizard.done.summary_heading') }}

@foreach ($summaryGroups as $stepNum => $group)

{{ $stepNum }}. {{ $group['title'] }}

@foreach ($group['fields'] as $field => $label) @php $value = $this->{$field} ?? null; // Booleans always render (yes/no // is meaningful); everything else // (secrets included) hides when // unset so the summary stays // focused on what the admin // actually configured. $isBool = is_bool($value); $isSecret = in_array($field, $summarySecretFields, true); // ldap_pword deliberately stays '' on // the component (never round-tripped // to the browser). Check the persisted // row via the computed helper so a // stored password renders as masked // asterisks instead of being hidden. $secretIsSet = $isSecret ? ($field === 'ldap_pword' ? $this->hasPersistedLdapPword : ($value !== null && $value !== '')) : false; if ($isSecret && ! $secretIsSet) { continue; } if (! $isBool && ! $isSecret && ($value === null || $value === '')) { continue; } if ($field === 'ldap_default_group' && $value !== null && $value !== '') { // Resolve the id to the group name for readability. $group_name = \App\Models\Group::find($value)?->name; $displayValue = $group_name ?? trans('general.unknown'); } elseif ($isBool) { $displayValue = $value ? trans('general.yes') : trans('general.no'); } elseif ($isSecret) { // Show masked asterisks when a // value is stored so operators // can see the credential IS set, // without ever surfacing the // actual value. $displayValue = '************'; } else { $displayValue = $value; } @endphp {{-- Boolean rows render icon + label for scan-ability; secret rows skip copy_what so the masked asterisks aren't offered as clipboard content. Everything else uses the standard copy-to-clipboard treatment so admins can grab the value the same way the hardware view lets them copy asset details. --}} @if ($isBool) {{ $displayValue }} @elseif ($isSecret) {{ $displayValue }} @else {{ $displayValue }} @endif @endforeach
@endforeach
@endif
{{-- Verification-in-progress hint. Only visible while saveAndAdvance is running so users don't wonder why the button "hung". The actual delay is the LDAP handshake + bind, which is out of our control. .delay.longer waits 500ms before showing so fast requests (no-op advances, cached lookups) don't briefly flash the hint at the user, which read as "you missed something" on quick steps. Only slow LDAP round-trips actually paint it. --}}

{{ trans('admin/settings/general.ldap_wizard.verifying_help') }}

Having trouble with the new wizard? You can find the legacy form here, but please do let us know what trouble you're having so we can fix it. {{-- Disable-LDAP confirm modal. Only useful when LDAP is currently on; skipped otherwise so we don't emit dead markup. wire:ignore so Livewire's morphdom leaves Bootstrap's `.in`/`body.modal-open` state alone between wizard re-renders. The confirm button still fires wire:click because Livewire delegates click events at the document level. --}} @if ($snipeSettings->ldap_enabled == '1') @endif