{{-- 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 {{-- Adapter type is immutable post-create (it keys the class map) so render as a static row rather than an editable control. Lives at the top of the form so admins recognize the vendor they're configuring at a glance without having to read the tab label or the browser URL. --}} {{ $adapter::typeLabel() }} {{-- 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 fieldset for AssetModels auto-created --}} {{-- Default status label for auto-created assets --}} @can('create', \App\Models\Statuslabel::class) @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
{{-- Dry-run checkboxes render inside the panel partial instead of here, so they sit just above the Pull Now / Push Now buttons. They still belong to this form's save POST via the HTML `form` attribute on their inputs (see panel.blade.php). --}} @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'), ]; // Per-field pushability lookup. For adapters like Custom HTTP // whose push path only resolves values for a subset of // standard fields (hostname/serial/asset_tag/model), hide the // push + both options for the rest. Otherwise the admin picks // push on a field we silently drop at build time. $pushableFieldsList = $supportsPush ? $adapter->pushableFields() : []; $directionOptionsFor = function (string $field) use ($directionOptions, $pushableFieldsList, $supportsPush) { if (! $supportsPush) { return $directionOptions; } if (in_array($field, $pushableFieldsList, true)) { return $directionOptions; } // Non-pushable field. Strip push + both so the dropdown // only exposes the directions that actually work for it. return array_diff_key($directionOptions, array_flip(['push', 'both'])); }; @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. The js-mapping-row / js-mapping-target / js-mapping-direction hooks let the JS below hide the direction column when the admin picks target=Skip (direction is moot in that state). --}}
@php // Clamp stored direction to the options // we're actually offering: a legacy // direction=push on a now-non-pushable // field falls back to pull in the UI // so the dropdown doesn't render with // a value that isn't in its options. $fieldDirOptions = $directionOptionsFor($mappingField); $storedDir = $adapter->directionFor($mappingField); $renderDir = array_key_exists($storedDir, $fieldDirOptions) ? $storedDir : 'pull'; @endphp
@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) && $adapter->rendersExtrasPicker())
{{ 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() && $adapter->supportsComposedNotesPush())
{{ trans('admin/settings/sync_adapters.push_notes_section_title') }} @php $storedNotesConfig = $adapter->storedPushNotesConfig(); @endphp
@endif {{-- Diagnostics: Preview push payload + Test 1 Push + Test 1 Pull. AJAX buttons that pick the first linked (push) or first vendor-returned (pull) record, run the capture, and dump the output into the
 below. Scoped to this adapter via the
                     data-adapter-slug attribute on the wrapper. --}}
                
{{ trans('admin/settings/sync_adapters.diagnostics_section_title') }}
@if ($adapter instanceof \App\SyncAdapters\PushableAdapter && $adapter->canPush()) @endif
{{-- Copy-to-clipboard --}}
{{ trans('admin/settings/sync_adapters.diagnostics_output_placeholder') }}