{{-- Shared form frame for a sync-adapter settings tab. The credentials partial drops its schema-driven credential inputs into the default slot. The URL row, active/heartbeat toggles, and mapping fieldset stay here because they're identical across every adapter regardless of credential shape. Called from resources/views/blade/sync-adapter/credentials.blade.php which passes the hydrated adapter through the `adapter` prop and puts the credential inputs in the default slot. --}} @props(['adapter']) @php $slug = $adapter->name(); $urlField = $slug.'_url'; $activeField = $slug.'_active'; $locked = config('app.lock_passwords') === true; // Memoized per-request via Laravel's Str/once so N adapter tab-panes // don't each fire their own companies-count query. The pickers below // (both the per-instance company scope and the per-vendor-group // mapping picker further down) are pure noise on a single-tenant // install with zero company rows. $hasCompanies = once(fn () => \App\Models\Company::query()->exists()); @endphp {{-- Rename field. Slug is immutable because SyncAdapterConfig keys off it, but label is free to change so admins can rename after creation (e.g. "Fleet" -> "Production Fleet" once they add a second staging instance). --}} {{-- Company scope. Set at Add-adapter time in the modal. This row surfaces it in the per-adapter settings so admins can see and change what company the instance is scoped to without having to delete and recreate. Blank means shared across every company (available to all). The id is slug-scoped because every adapter tab-pane renders this same form, and duplicate DOM ids silently break select2's init on all but the first one. Only rendered when at least one company exists, since on a single-tenant install the picker would only ever show "no selection" and confuses more than it helps. --}} @if ($hasCompanies) @endif @if ($adapter->usesConfigurableUrl()) @php // Optional: adapters can pull the Base URL row into a // settingsSections() group so it renders inside the // relevant fieldset (typically "auth"). The credentials // partial's schema loop tracks currentSection starting // at this value and won't re-open the fieldset for its // first same-sectioned schema entries. $urlSection = $adapter->baseUrlSection(); $urlSectionData = $urlSection ? ($adapter->settingsSections()[$urlSection] ?? null) : null; @endphp @if ($urlSectionData)
{{ $urlSectionData['title'] }} @endif {{-- We deliberately do NOT close the fieldset here even when we opened one above. The credentials partial's schema loop starts with $currentSection = $urlSection, so its first section transition (to any section other than $urlSection) closes this fieldset. --}} @endif {{ $slot }} {{-- Everything from Model Category down through "Check assets in when the vendor reports no assigned user" is grouped in a single fieldset so it visually separates from whatever the previous section left open (on the Custom HTTP adapter the "Custom Extras" section sits directly above this block, and without the wrapper these controls read as if they were part of Custom Extras). Applies to every adapter for consistency. --}}
{{ trans('admin/settings/sync_adapters.defaults_section_title') }} {{-- Default category for AssetModels auto-created from this adapter. Vendors don't send Snipe-IT's category concept but every AssetModel needs one. --}} {{-- Default status label for auto-created assets. Required so admins have to consciously pick a workflow bucket for discovered devices instead of Snipe-IT defaulting to something surprising. --}} @can('create', \App\Models\Statuslabel::class) {{ trans('button.new') }} @endcan {{-- Asset tag pattern. Optional. Grouped with the mapping-related options below rather than the required identity fields above so the required section stays contiguous. --}} {{-- User assignment via sync. Strategy 'none' disables the lookup entirely. When set, the vendor's user email or username on each record is matched against Snipe-IT users and the asset checks out to the matched user. --}} {{-- Select2 for visual consistency with the other pickers on this page, but the option pool is a fixed 3-value enum so the built-in search box is hidden via data-minimum-results-for-search. --}} @php // On a validation-error redisplay, old() carries the submitted // value so the admin does not have to re-pick. Falls back to // the stored value on the first render. $userMatchSelected = old($slug.'_user_match_strategy', $adapter->userMatchStrategy()); @endphp {{-- Suppress the checkout notification email on sync-driven assignments. Default is suppress so admins don't inbox-spam users every time a scheduled sync re-confirms the same assignment. --}} {{-- Opt-in: check the asset in when the vendor reports no assigned user. Default off because a single missed sync cycle (device offline, empty field on a fresh enrollment) would flap the assignment. --}} {{-- Adapter-specific "fetch per-device details" opt-in. Lets admins pay the extra per-device cost for fields the vendor's LIST endpoint doesn't populate (Intune's ethernet MAC + physical memory). --}} @php $perDeviceDetailsToggle = $adapter->perDeviceDetailsToggle(); @endphp @if ($perDeviceDetailsToggle !== null) @endif
{{-- Push dry-run only shows for adapters that actually support push. Turning it on makes push() log the payload instead of sending, so admins can verify their config end-to-end against a real Snipe-IT dataset without any real vendor side effect. The composed-notes template + target field live further down in their own fieldset under the vendor-specific mapping. --}} @if ($adapter instanceof \App\SyncAdapters\PushableAdapter && $adapter->canPush()) @endif @if ($adapter->supportsGroupScoping() && $adapter->isEnabled() && $hasCompanies) @php $cachedGroups = $adapter->cachedGroups(); $groupMappings = $adapter->groupMappings(); @endphp
{{ trans('admin/settings/sync_adapters.group_mapping_title', ['label' => $adapter->vendorGroupLabel()]) }} @if (empty($cachedGroups))

{{ trans('admin/settings/sync_adapters.group_mapping_empty', ['label' => strtolower($adapter->vendorGroupLabel())]) }}

@else @foreach ($cachedGroups as $group) @endforeach @endif
@endif {{-- Per-field target mapping. --}} @php // Show push controls only when the adapter implements // PushableAdapter AND canPush() (instance-level runtime check // (Fleet Free returns false because manual labels are Premium). $supportsPush = $adapter instanceof \App\SyncAdapters\PushableAdapter && $adapter->canPush(); $directionOptions = [ 'pull' => trans('admin/settings/sync_adapters.direction_pull'), 'push' => trans('admin/settings/sync_adapters.direction_push'), 'both' => trans('admin/settings/sync_adapters.direction_both'), 'skip' => trans('admin/settings/sync_adapters.direction_skip'), ]; @endphp
{{ trans('admin/settings/sync_adapters.mapping_section_title', ['type' => $adapter::typeLabel()]) }} @foreach (\App\SyncAdapters\MappingTargets::FIELDS as $mappingField) @if ($supportsPush) {{-- Push-capable adapters get a two-column input: target picker on the left, direction picker on the right. Pull-only adapters render just the target picker at full width. --}}
@else @endif
@endforeach
@php $extraFields = $adapter->extraFields(); // Precompute entries for the mapping-picker widget (extras // section on adapters that declare adapter-owned extras). // Entry shapes supported: // 'literal label string' // ['label' => 'literal string', 'type' => 'text|boolean', 'admin_defined' => bool] // ['label_key' => 'admin/settings/sync_adapters.extra_foo', 'type' => ..., 'admin_defined' => ...] // label_key entries auto-resolve through trans() with // ':vendor' bound to the adapter's typeLabel so per-vendor // labels share one translation string. $extraEntries = []; foreach ($extraFields as $extraKey => $extraEntry) { if (is_array($extraEntry) && isset($extraEntry['label_key'])) { $extraLabel = trans($extraEntry['label_key'], ['vendor' => $adapter::typeLabel()]); } elseif (is_array($extraEntry)) { $extraLabel = $extraEntry['label'] ?? $extraKey; } else { $extraLabel = $extraEntry; } $extraType = (is_array($extraEntry) && isset($extraEntry['type'])) ? $extraEntry['type'] : 'text'; $extraAdminDefined = is_array($extraEntry) && ! empty($extraEntry['admin_defined']); $extraEntries[$extraKey] = [ 'label' => $extraLabel, 'target_options' => \App\SyncAdapters\MappingTargets::optionsForExtra($extraType, $extraAdminDefined), 'stored_target' => $adapter->mappingFor($extraKey), 'stored_direction' => $adapter->directionFor($extraKey), ]; } @endphp @foreach ($adapter->settingsSchema() as $fmField) @if (($fmField['type'] ?? '') === 'field_map') @php $fmName = $slug.'_'.$fmField['key']; $fmStored = $adapter->credentialForDisplay($fmField['key']); $fmMap = is_string($fmStored) && $fmStored !== '' ? (json_decode($fmStored, true) ?: []) : []; $fmMap = old($fmName, is_array($fmMap) ? $fmMap : []); $fmMap = is_array($fmMap) ? $fmMap : []; // Load stored per-row direction values so the widget's // dropdowns pre-select the correct value. Framework's // direction. storage is the single source of // truth, shared with the standard mapping loop above. $fmDirections = []; foreach (array_keys($fmMap) as $fmKey) { $fmDirections[$fmKey] = $adapter->directionFor($fmKey); } @endphp
{{ $fmField['label'] }}
@endif @endforeach @if (! empty($extraFields))
{{ trans('admin/settings/sync_adapters.extra_fields_section_title', ['type' => $adapter::typeLabel()]) }}
@endif {{-- Admin-defined custom extras. Each row pairs a Snipe-IT destination (custom:X / native:X) with a vendor-record dot-path the adapter extracts at pull time. Opt-in per adapter via supportsAdminDefinedExtras(). Rows show up as targets in the mapping picker above once saved. --}} @if ($adapter->supportsAdminDefinedExtras()) @php $cxName = $slug.'_field_paths'; $cxStored = $adapter->adminDefinedExtras(); $cxStored = old($cxName, is_array($cxStored) ? $cxStored : []); $cxStored = is_array($cxStored) ? $cxStored : []; @endphp
{{ trans('admin/settings/sync_adapters.custom_extras_section_title') }}
@endif {{-- Composed notes fieldset. Renders a multi-field notes blob from Snipe-IT (asset_tag + status + assigned user + custom fields, etc.) into a single vendor field. Blank template means "don't push notes." Target defaults to the adapter's suggested field (Kandji notes, Jamf general.notes, Intune notes, etc.) via notesFieldTarget(). Admins override to a Custom Attribute / Custom Field name when the vendor exposes multiple candidates. Positioned after the standard + extras mapping so admins finish configuring the per-field targets before deciding what goes into the composed notes blob. --}} @if ($adapter instanceof \App\SyncAdapters\PushableAdapter && $adapter->canPush())
{{ trans('admin/settings/sync_adapters.push_notes_section_title') }} @php $storedNotesConfig = $adapter->storedPushNotesConfig(); @endphp
@endif