3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00

Added blade components for checkboxes, radios, and inline checkboxes

This commit is contained in:
snipe
2026-07-02 12:38:19 +01:00
parent 80cba24b73
commit 5cc6bad2ad
17 changed files with 964 additions and 227 deletions

View File

@ -84,23 +84,17 @@
</div>
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input type="checkbox" id="enable_sounds" name="enable_sounds" value="1" {{ old('enable_sounds', $user->enable_sounds) ? 'checked' : '' }}>
{{ trans('account/general.enable_sounds') }}
</label>
</div>
</div>
<x-form.checkbox-row
name="enable_sounds"
:label="trans('account/general.enable_sounds')"
:item="$user"
/>
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input type="checkbox" name="enable_confetti" id="enable_confetti" value="1" {{ old('enable_confetti', $user->enable_confetti) ? 'checked' : '' }}>
{{ trans('account/general.enable_confetti') }}
</label>
</div>
</div>
<x-form.checkbox-row
name="enable_confetti"
:label="trans('account/general.enable_confetti')"
:item="$user"
/>

View File

@ -0,0 +1,41 @@
@props([
'name' => null,
'item' => null,
'label' => null,
'value' => '1',
'required' => null,
'disabled' => false,
])
@php
// Old-input aware check-state. On a fresh render, session()->hasOldInput()
// is false, so we fall back to the model. On a validation-failure redisplay,
// hasOldInput() is true and we trust the (possibly missing) old value — an
// unchecked box comes back correctly unchecked instead of falling through
// to the stale $item->{$name}.
$is_redisplay = session()->hasOldInput();
$checked = $is_redisplay
? (bool) old($name)
: (bool) ($item?->{$name} ?? false);
// Helper::checkIfRequired dereferences $item statically via $item::rules(),
// so it needs a real class/object. Fall back to false when no model was
// supplied (transient forms have no persistent model).
$really_required = $required ?? ($item ? Helper::checkIfRequired($item, $name) : false);
@endphp
{{-- Inline variant: no form-group wrapper. The caller drops this into an
existing form row (e.g. next to a text input in a bulk-edit view) and
controls the containing column themselves. --}}
<label class="form-control">
<x-input.checkbox
:name="$name"
:id="$name"
:value="$value"
:checked="$checked"
:required="$really_required"
:disabled="$disabled"
:aria-label="$name"
/>
{{ $label }}
</label>

View File

@ -0,0 +1,144 @@
@props([
'name' => null,
'item' => null,
'label' => null,
'options' => null,
'selected' => null,
'value' => '1',
'required' => null,
'disabled' => false,
'help_text' => null,
'info_tooltip_text' => null,
// Default input column: only skip the offset when a left-hand label
// column is being rendered (i.e. multi mode with a label). Single mode
// never has a left label; multi mode without a label lays out the same
// way. Keeping the grid classes centralized here means a future Bootstrap /
// AdminLTE upgrade only has to touch this file, not every callsite.
// Note: Blade evaluates @props defaults twice (once via extractPropNames
// before caller attrs are bound, once when applying defaults after). The
// `?? null` guards against "undefined variable" on the first pass; isset()
// is inherently safe against undefined vars, is_array() is not.
'input_div_class' => (is_array($options ?? null) && isset($label)) ? 'col-md-8' : 'col-md-8 col-md-offset-3',
])
@php
// Multi-checkbox mode kicks in when the caller supplies an $options map;
// otherwise this renders as a single boolean checkbox.
$is_multi = is_array($options);
// Old-input aware check-state. On a fresh render, session()->hasOldInput()
// is false, so we fall back to the model (or supplied :selected). On a
// validation-failure redisplay, hasOldInput() is true and we trust the
// (possibly missing) old value — an unchecked box comes back correctly
// unchecked instead of falling through to the stale $item->{$name}.
$is_redisplay = session()->hasOldInput();
if (! $is_multi) {
$single_checked = $is_redisplay
? (bool) old($name)
: (bool) ($item?->{$name} ?? false);
// Helper::checkIfRequired dereferences $item statically via $item::rules(),
// so it needs a real class/object. Fall back to false when no model was
// supplied (transient forms have no persistent model).
$really_required = $required ?? ($item ? Helper::checkIfRequired($item, $name) : false);
} else {
// For multi mode, callers can pass :selected as an array of currently-
// selected values, a comma-joined string (common when the model stores
// it that way, e.g. Setting's modellist_displays), or a callable
// (value): bool for per-value predicates. When :selected is omitted
// the same fallback is applied to $item->{$name}.
if ($selected === null) {
$selected = $item?->{$name};
}
if (is_string($selected)) {
$selected = $selected === '' ? [] : array_map('trim', explode(',', $selected));
}
$old_values = is_array(old($name)) ? old($name) : [];
$is_checked = function ($value) use ($is_redisplay, $old_values, $selected) {
if ($is_redisplay) {
return in_array($value, $old_values);
}
if (is_callable($selected)) {
return (bool) $selected($value);
}
return in_array($value, is_array($selected) ? $selected : []);
};
}
$errors_class = $errors->has($name) ? ' has-error' : '';
@endphp
<div {{ $attributes->merge(['class' => 'form-group'.$errors_class]) }}>
@if (! $is_multi)
{{-- Single checkbox: no left-hand label column; label wraps the input. --}}
<div class="{{ $input_div_class }}">
<label class="form-control">
<x-input.checkbox
:name="$name"
:id="$name"
:value="$value"
:checked="$single_checked"
:required="$really_required"
:disabled="$disabled"
:aria-label="$name"
/>
{{ $label }}
</label>
</div>
@else
{{-- Multi: standard left-hand label + a stack of wrapped checkboxes on the right. --}}
@if (isset($label))
<x-form.label :for="$name" class="col-md-3">{{ $label }}</x-form.label>
@endif
<div class="{{ $input_div_class }}">
@foreach ($options as $option_value => $option_label)
<label class="form-control">
<x-input.checkbox
:name="$name.'[]'"
:value="$option_value"
:checked="$is_checked($option_value)"
:disabled="$disabled"
:aria-label="$name"
/>
{{ $option_label }}
</label>
@endforeach
</div>
@endif
@if ($info_tooltip_text)
<div class="col-md-1 text-left" style="padding-left:0; margin-top: 5px;">
<x-form.tooltip>
{{ $info_tooltip_text }}
</x-form.tooltip>
</div>
@endif
@error($name)
<div class="col-md-8 col-md-offset-3">
<span class="alert-msg" aria-hidden="true">
<x-icon type="x" />
{{ $message }}
</span>
</div>
@enderror
@if ($help_text)
<div class="col-md-8 col-md-offset-3">
<p class="help-block">
{!! $help_text !!}
</p>
</div>
@endif
</div>

View File

@ -0,0 +1,87 @@
@props([
'name' => null,
'item' => null,
'label' => null,
'options' => [],
'selected' => null,
'required' => null,
'disabled' => false,
'help_text' => null,
'info_tooltip_text' => null,
// Default input column depends on whether the row has a left-hand label.
// With a label, the row already spends col-md-3 on the left; without one
// the options need to be offset. Concentrating the grid class here means
// a future Bootstrap / AdminLTE upgrade only has to touch this file, not
// every place it's invoked.
'input_div_class' => isset($label) ? 'col-md-8' : 'col-md-8 col-md-offset-3',
])
@php
// Redisplay-safe current value. On validation-failure redisplay
// session()->hasOldInput() is true and we trust old($name); on fresh
// render we take the caller's :selected value, or fall back to the
// model attribute. This is the same guard as checkbox-row: an old-input
// value of null on redisplay means "nothing selected", not "fall back
// to the stale model default".
$is_redisplay = session()->hasOldInput();
if ($is_redisplay) {
$current_value = old($name);
} else {
$current_value = $selected ?? $item?->{$name};
}
// Helper::checkIfRequired dereferences $item statically via $item::rules(),
// so it needs a real class/object. Fall back to false when no model was
// supplied (transient forms like bulk checkin have no persistent model).
$really_required = $required ?? ($item ? Helper::checkIfRequired($item, $name) : false);
$errors_class = $errors->has($name) ? ' has-error' : '';
@endphp
<div {{ $attributes->merge(['class' => 'form-group'.$errors_class]) }}>
@if (isset($label))
<x-form.label :for="$name" class="col-md-3">{{ $label }}</x-form.label>
@endif
<div class="{{ $input_div_class }}">
@foreach ($options as $option_value => $option_label)
<label class="form-control">
<x-input.radio
:name="$name"
:value="$option_value"
:checked="$current_value !== null && (string) $current_value === (string) $option_value"
:required="$really_required && $loop->first"
:disabled="$disabled"
:aria-label="$name"
/>
{{ $option_label }}
</label>
@endforeach
</div>
@if ($info_tooltip_text)
<div class="col-md-1 text-left" style="padding-left:0; margin-top: 5px;">
<x-form.tooltip>
{{ $info_tooltip_text }}
</x-form.tooltip>
</div>
@endif
@error($name)
<div class="col-md-8 col-md-offset-3">
<span class="alert-msg" aria-hidden="true">
<x-icon type="x" />
{{ $message }}
</span>
</div>
@enderror
@if ($help_text)
<div class="col-md-8 col-md-offset-3">
<p class="help-block">
{!! $help_text !!}
</p>
</div>
@endif
</div>

View File

@ -0,0 +1,19 @@
@props([
'name' => null,
'value' => '1',
'checked' => false,
'required' => false,
'disabled' => false,
'id' => null,
])
<!-- input-checkbox blade component -->
<input
type="checkbox"
name="{{ $name }}"
value="{{ $value }}"
@if ($id) id="{{ $id }}" @endif
{{ $attributes }}
@checked($checked)
@required($required)
@disabled($disabled)
/>

View File

@ -0,0 +1,19 @@
@props([
'name' => null,
'value' => null,
'checked' => false,
'required' => false,
'disabled' => false,
'id' => null,
])
<!-- input-radio blade component -->
<input
type="radio"
name="{{ $name }}"
value="{{ $value }}"
@if ($id) id="{{ $id }}" @endif
{{ $attributes }}
@checked($checked)
@required($required)
@disabled($disabled)
/>

View File

@ -81,18 +81,14 @@
/>
<!-- Update actual location -->
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input name="update_default_location" type="radio" value="1" @checked(old('update_default_location', '1') == '1') aria-label="update_default_location" />
{{ trans('admin/hardware/form.asset_location') }}
</label>
<label class="form-control">
<input name="update_default_location" type="radio" value="0" @checked(old('update_default_location') === '0') aria-label="update_default_location" />
{{ trans('admin/hardware/form.asset_location_update_default_current') }}
</label>
</div>
</div>
<x-form.radio-row
name="update_default_location"
selected="1"
:options="[
'1' => trans('admin/hardware/form.asset_location'),
'0' => trans('admin/hardware/form.asset_location_update_default_current'),
]"
/>
<!-- Checkin Date -->
<div class="form-group {{ $errors->has('checkin_at') ? 'error' : '' }}">

View File

@ -46,10 +46,10 @@
<i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
<div class="col-md-5">
<label class="form-control">
<input type="checkbox" name="null_name" value="1">
{{ trans_choice('general.set_to_null', count($assets), ['selection_count' => count($assets)]) }}
</label>
<x-form.checkbox-inline
name="null_name"
:label="trans_choice('general.set_to_null', count($assets), ['selection_count' => count($assets)])"
/>
</div>
</div>
@ -67,10 +67,10 @@
{!! $errors->first('purchase_date', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
<div class="col-md-5">
<label class="form-control">
<input type="checkbox" name="null_purchase_date" value="1">
{{ trans_choice('general.set_to_null', count($assets),['selection_count' => count($assets)]) }}
</label>
<x-form.checkbox-inline
name="null_purchase_date"
:label="trans_choice('general.set_to_null', count($assets), ['selection_count' => count($assets)])"
/>
</div>
</div>
@ -87,10 +87,10 @@
{!! $errors->first('expected_checkin', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
<div class="col-md-5">
<label class="form-control">
<input type="checkbox" name="null_expected_checkin_date" value="1">
{{ trans_choice('general.set_to_null', count($assets),['selection_count' => count($assets)]) }}
</label>
<x-form.checkbox-inline
name="null_expected_checkin_date"
:label="trans_choice('general.set_to_null', count($assets), ['selection_count' => count($assets)])"
/>
</div>
</div>
@ -107,10 +107,10 @@
{!! $errors->first('asset_eol_date', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
<div class="col-md-5">
<label class="form-control">
<input type="checkbox" name="null_asset_eol_date" value="1">
{{ trans_choice('general.set_to_null', count($assets),['selection_count' => count($assets)]) }}
</label>
<x-form.checkbox-inline
name="null_asset_eol_date"
:label="trans_choice('general.set_to_null', count($assets), ['selection_count' => count($assets)])"
/>
</div>
</div>
@ -225,10 +225,10 @@
{!! $errors->first('next_audit_date', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
<div class="col-md-5">
<label class="form-control">
<input type="checkbox" name="null_next_audit_date" value="1">
{{ trans_choice('general.set_to_null', count($assets),['selection_count' => count($assets)]) }}
</label>
<x-form.checkbox-inline
name="null_next_audit_date"
:label="trans_choice('general.set_to_null', count($assets), ['selection_count' => count($assets)])"
/>
</div>
<div class="col-md-8 col-md-offset-3">
<p class="help-block">{!! trans('general.next_audit_date_help') !!}</p>

View File

@ -145,18 +145,14 @@
/>
<!-- Update actual location -->
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input name="update_default_location" type="radio" value="1" checked="checked" aria-label="update_default_location" />
{{ trans('admin/hardware/form.asset_location') }}
</label>
<label class="form-control">
<input name="update_default_location" type="radio" value="0" aria-label="update_default_location" />
{{ trans('admin/hardware/form.asset_location_update_default_current') }}
</label>
</div>
</div> <!--/form-group-->
<x-form.radio-row
name="update_default_location"
selected="1"
:options="[
'1' => trans('admin/hardware/form.asset_location'),
'0' => trans('admin/hardware/form.asset_location_update_default_current'),
]"
/> <!--/form-group-->
<!-- Checkout/Checkin Date -->
<div class="form-group{{ $errors->has('checkin_at') ? ' has-error' : '' }}">

View File

@ -102,25 +102,15 @@
</div>
<!-- requestable -->
<div class="form-group{{ $errors->has('requestable') ? ' has-error' : '' }}">
<div class="col-md-7 col-md-offset-3">
<label for="requestable_nochange" class="form-control">
<input type="radio" name="requestable" id="requestable_nochange" value="" aria-label="requestable_nochange" checked>
{{ trans('admin/hardware/general.requestable_status_warning')}}
</label>
<label for="requestable" class="form-control">
<input type="radio" name="requestable" id="requestable" value="1" aria-label="requestable">
{{ trans('admin/hardware/general.requestable')}}
</label>
<label for="not_requestable" class="form-control">
<input type="radio" name="requestable" id="not_requestable" value="0" aria-label="not_requestable">
{{ trans('admin/hardware/general.not_requestable')}}
</label>
</div>
</div>
<x-form.radio-row
name="requestable"
selected=""
:options="[
'' => trans('admin/hardware/general.requestable_status_warning'),
'1' => trans('admin/hardware/general.requestable'),
'0' => trans('admin/hardware/general.not_requestable'),
]"
/>
@foreach ($models as $model)
<input type="hidden" name="ids[{{ $model->id }}]" value="{{ $model->id }}">

View File

@ -420,38 +420,15 @@
</label>
</div>
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input
name="deleted_components"
id="deleted_components_exclude_deleted"
type="radio"
value="exclude_deleted"
@checked($template->radioValue('deleted_components', 'exclude_deleted', true))
aria-label="deleted_components"
>{{ trans('admin/components/general.exclude_deleted') }}
</label>
<label class="form-control">
<input
name="deleted_components"
id="deleted_components_include_deleted"
type="radio"
value="include_deleted"
@checked($template->radioValue('deleted_components', 'include_deleted'))
aria-label="deleted_components"
>{{ trans('admin/components/general.include_deleted') }}
</label>
<label class="form-control">
<input
name="deleted_components"
type="radio"
id="deleted_components_only_deleted"
value="only_deleted"
@checked($template->radioValue('deleted_components', 'only_deleted'))
aria-label="deleted_components"
>{{ trans('admin/components/general.only_deleted') }}
</label>
</div>
<x-form.radio-row
name="deleted_components"
:selected="$template->options['deleted_components'] ?? 'exclude_deleted'"
:options="[
'exclude_deleted' => trans('admin/components/general.exclude_deleted'),
'include_deleted' => trans('admin/components/general.include_deleted'),
'only_deleted' => trans('admin/components/general.only_deleted'),
]"
/>
</div>
</div> <!-- /.box-body-->

View File

@ -47,24 +47,18 @@
</x-form.legend>
<!-- Menu Alerts Enabled -->
<div class="form-group{{ $errors->has('show_alerts_in_menu') ? ' error' : '' }}">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input type="checkbox" name="show_alerts_in_menu" value="1" @checked(old('show_alerts_in_menu', $setting->show_alerts_in_menu))>
{{ trans('admin/settings/general.show_alerts_in_menu') }}
</label>
</div>
</div>
<x-form.checkbox-row
name="show_alerts_in_menu"
:label="trans('admin/settings/general.show_alerts_in_menu')"
:item="$setting"
/>
<!-- Alerts Enabled -->
<div class="form-group {{ $errors->has('alerts_enabled') ? 'error' : '' }}">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input type="checkbox" name="alerts_enabled" value="1" @checked(old('alerts_enabled', $setting->alerts_enabled))>
{{ trans('admin/settings/general.alerts_enabled') }}
</label>
</div>
</div>
<x-form.checkbox-row
name="alerts_enabled"
:label="trans('admin/settings/general.alerts_enabled')"
:item="$setting"
/>
</fieldset>
@ -106,28 +100,14 @@
{!! $errors->first('admin_cc_email', '<span class="alert-msg" aria-hidden="true">:message</span><br>') !!}
</div>
</div>
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<label class="form-control">
<input
type="radio"
name="admin_cc_always"
value="1"
@checked($setting->admin_cc_always == 1)
>
{{ trans('admin/settings/general.admin_cc_always') }}
</label>
<label class="form-control">
<input
type="radio"
name="admin_cc_always"
value="0"
@checked($setting->admin_cc_always == 0)
>
{{ trans('admin/settings/general.admin_cc_when_acceptance_required') }}
</label>
</div>
</div>
<x-form.radio-row
name="admin_cc_always"
:item="$setting"
:options="[
'1' => trans('admin/settings/general.admin_cc_always'),
'0' => trans('admin/settings/general.admin_cc_when_acceptance_required'),
]"
/>
</fieldset>
<fieldset name="remote-login">

View File

@ -39,18 +39,12 @@
{{ trans('admin/settings/general.legends.scoping') }}
</x-form.legend>
<!-- Full Multiple Companies Support -->
<div class="form-group {{ $errors->has('full_multiple_companies_support') ? 'error' : '' }}">
<div class="col-md-8 col-md-offset-3">
<label class="form-control">
<input type="checkbox" name="full_multiple_companies_support" value="1" @checked(old('full_multiple_companies_support', $setting->full_multiple_companies_support)) aria-label="full_multiple_companies_support" />
{{ trans('admin/settings/general.full_multiple_companies_support_text') }}
</label>
{!! $errors->first('full_multiple_companies_support', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<p class="help-block">
{{ trans('admin/settings/general.full_multiple_companies_support_help_text') }}
</p>
</div>
</div>
<x-form.checkbox-row
name="full_multiple_companies_support"
:label="trans('admin/settings/general.full_multiple_companies_support_text')"
:item="$setting"
:help_text="trans('admin/settings/general.full_multiple_companies_support_help_text')"
/>
<!-- /.form-group -->
<!-- Scope Locations with Full Multiple Companies Support -->
@ -62,18 +56,13 @@
<!-- /.form-group -->
<!-- Null Company Is Floater -->
<div class="form-group {{ $errors->has('null_company_is_floater') ? 'error' : '' }}">
<div class="col-md-8 col-md-offset-3">
<label class="form-control">
<input type="checkbox" name="null_company_is_floater" value="1" @checked(old('null_company_is_floater', $setting->null_company_is_floater)) aria-label="null_company_is_floater" @disabled(! $setting->full_multiple_companies_support) />
{{ trans('admin/settings/general.null_company_is_floater_text') }}
</label>
{!! $errors->first('null_company_is_floater', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<p class="help-block">
{{ trans('admin/settings/general.null_company_is_floater_help_text') }}
</p>
</div>
</div>
<x-form.checkbox-row
name="null_company_is_floater"
:label="trans('admin/settings/general.null_company_is_floater_text')"
:item="$setting"
:disabled="! $setting->full_multiple_companies_support"
:help_text="trans('admin/settings/general.null_company_is_floater_help_text')"
/>
<!-- /.form-group -->
</fieldset>
@ -156,16 +145,12 @@
</x-form.legend>
<!-- Require signature for acceptance -->
<div class="form-group {{ $errors->has('require_accept_signature') ? 'error' : '' }}">
<div class="col-md-8 col-md-offset-3">
<label class="form-control">
<input type="checkbox" name="require_accept_signature" value="1" @checked(old('require_accept_signature', $setting->require_accept_signature)) />
{{ trans('admin/settings/general.require_accept_signature') }}
</label>
{!! $errors->first('require_accept_signature', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<p class="help-block">{{ trans('admin/settings/general.require_accept_signature_help_text') }}</p>
</div>
</div>
<x-form.checkbox-row
name="require_accept_signature"
:label="trans('admin/settings/general.require_accept_signature')"
:item="$setting"
:help_text="trans('admin/settings/general.require_accept_signature_help_text')"
/>
<!-- /.form-group -->
<!-- Default EULA -->
@ -204,53 +189,34 @@
</div>
<!-- Model List prefs -->
<div class="form-group {{ $errors->has('show_in_model_list') ? 'error' : '' }}">
<div class="col-md-3">
<strong>{{ trans('admin/settings/general.show_in_model_list') }}</strong>
</div>
<div class="col-md-8">
<label class="form-control">
<input type="checkbox" name="show_in_model_list[]" value="image" @checked(old('show_in_model_list', $snipeSettings->modellistCheckedValue('image'))) aria-label="show_in_model_list"/>
{{ trans('general.image') }}
</label>
<label class="form-control">
<input type="checkbox" name="show_in_model_list[]" value="category" @checked(old('show_in_model_list', $snipeSettings->modellistCheckedValue('category'))) aria-label="show_in_model_list"/>
{{ trans('general.category') }}
</label>
<label class="form-control">
<input type="checkbox" name="show_in_model_list[]" value="manufacturer" @checked(old('show_in_model_list', $snipeSettings->modellistCheckedValue('manufacturer'))) aria-label="show_in_model_list"/>
{{ trans('general.manufacturer') }} </label>
<label class="form-control">
<input type="checkbox" name="show_in_model_list[]" value="model_number" @checked(old('show_in_model_list', $snipeSettings->modellistCheckedValue('model_number'))) aria-label="show_in_model_list"/>
{{ trans('general.model_no') }}
</label>
</div>
</div>
<x-form.checkbox-row
name="show_in_model_list"
:label="trans('admin/settings/general.show_in_model_list')"
:options="[
'image' => trans('general.image'),
'category' => trans('general.category'),
'manufacturer' => trans('general.manufacturer'),
'model_number' => trans('general.model_no'),
]"
:selected="$snipeSettings->modellist_displays"
/>
<!-- Shortcuts enable -->
<div class="form-group {{ $errors->has('shortcuts_enabled') ? 'error' : '' }}">
<div class="col-md-8 col-md-offset-3">
<label class="form-control">
<input type="checkbox" name="shortcuts_enabled" value="1" {{ old('shortcuts_enabled', $setting->shortcuts_enabled) ? 'checked' : '' }}>
{{ trans('admin/settings/general.shortcuts_enabled') }}
</label>
{!! $errors->first('shortcuts_enabled', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<p class="help-block">{!!trans('admin/settings/general.shortcuts_help_text') !!}</p>
</div>
</div>
<x-form.checkbox-row
name="shortcuts_enabled"
:label="trans('admin/settings/general.shortcuts_enabled')"
:item="$setting"
:help_text="trans('admin/settings/general.shortcuts_help_text')"
/>
<!-- Archived in List -->
<div class="form-group {{ $errors->has('show_archived_in_list') ? 'error' : '' }}">
<div class="col-md-8 col-md-offset-3">
<label class="form-control">
<input type="checkbox" name="show_archived_in_list" value="1" @checked(old('show_archived_in_list', $setting->show_archived_in_list)) aria-label="show_archived_in_list" />
{{ trans('admin/settings/general.show_archived_in_list_text') }}
</label>
{!! $errors->first('show_archived_in_list', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<x-form.checkbox-row
name="show_archived_in_list"
:label="trans('admin/settings/general.show_archived_in_list_text')"
:item="$setting"
/>
<!-- Show assets assigned to user's assets -->
<div class="form-group {{ $errors->has('show_assigned_assets') ? 'error' : '' }}">

View File

@ -0,0 +1,130 @@
<?php
namespace Tests\Feature\Blade;
use App\Http\Middleware\CheckForDebug;
use App\Http\Middleware\CheckForSetup;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
class CheckboxInlineTest extends TestCase
{
private function render(array $data, ?array $oldInput = null): string
{
Route::get('/__test/checkbox-inline', function () use ($data) {
return view('blade.form.checkbox-inline', $data);
});
$call = $this->withoutMiddleware([
CheckForSetup::class,
CheckForDebug::class,
]);
if ($oldInput !== null) {
$call = $call->withSession(['_old_input' => $oldInput]);
}
return $call->get('/__test/checkbox-inline')->assertOk()->getContent();
}
private function item(): object
{
return new class
{
public bool $enabled = true;
public bool $disabled_flag = false;
public static function rules()
{
return ['enabled' => 'required|boolean'];
}
};
}
public function test_does_not_emit_form_group_wrapper()
{
// Inline variant must not carry a form-group class; the caller
// controls its own row layout in bulk-edit views.
$html = $this->render([
'name' => 'null_name',
'label' => 'Set to null',
]);
$this->assertStringNotContainsString('form-group', $html);
$this->assertStringContainsString('<label class="form-control">', $html);
}
public function test_no_item_does_not_crash_when_deriving_required()
{
// The classic bulk-edit case: transient sentinel with no model.
$html = $this->render([
'name' => 'null_name',
'label' => 'Set to null',
]);
$this->assertMatchesRegularExpression('/type="checkbox"[^>]*name="null_name"/', $html);
$this->assertDoesNotMatchRegularExpression('/name="null_name"[^>]*required/', $html);
}
public function test_fresh_render_reads_model_value()
{
$html = $this->render([
'name' => 'enabled',
'label' => 'Enabled',
'item' => $this->item(),
]);
$this->assertMatchesRegularExpression('/name="enabled"[^>]*checked/', $html);
}
public function test_redisplay_unchecked_does_not_fall_back_to_model()
{
// Model says true; user unchecked before submit; validation failed
// elsewhere. On redisplay the box must stay unchecked.
$html = $this->render(
data: [
'name' => 'enabled',
'label' => 'Enabled',
'item' => $this->item(),
],
oldInput: ['some_other_field' => 'x'],
);
$this->assertDoesNotMatchRegularExpression('/name="enabled"[^>]*checked/', $html);
}
public function test_redisplay_checked_shows_checked()
{
$html = $this->render(
data: [
'name' => 'null_name',
'label' => 'Set to null',
],
oldInput: ['null_name' => '1'],
);
$this->assertMatchesRegularExpression('/name="null_name"[^>]*checked/', $html);
}
public function test_derives_required_from_model_rules()
{
$html = $this->render([
'name' => 'enabled',
'label' => 'Enabled',
'item' => $this->item(),
]);
$this->assertMatchesRegularExpression('/name="enabled"[^>]*required/', $html);
}
public function test_disabled_propagates_to_input()
{
$html = $this->render([
'name' => 'null_name',
'label' => 'Set to null',
'disabled' => true,
]);
$this->assertMatchesRegularExpression('/name="null_name"[^>]*disabled/', $html);
}
}

View File

@ -0,0 +1,197 @@
<?php
namespace Tests\Feature\Blade;
use App\Http\Middleware\CheckForDebug;
use App\Http\Middleware\CheckForSetup;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
class CheckboxRowTest extends TestCase
{
/**
* Bind a temporary route that renders the checkbox-row component with
* the caller-supplied data. Actual HTTP requests are what wire the
* session onto the Request, so this is the shortest way to exercise
* old() / session()->hasOldInput() correctly.
*/
private function render(array $data, ?array $oldInput = null): string
{
Route::get('/__test/checkbox-row', function () use ($data) {
return view('blade.form.checkbox-row', $data);
});
$call = $this->withoutMiddleware([
CheckForSetup::class,
CheckForDebug::class,
]);
if ($oldInput !== null) {
$call = $call->withSession(['_old_input' => $oldInput]);
}
return $call->get('/__test/checkbox-row')->assertOk()->getContent();
}
private function item(): object
{
return new class
{
public bool $enabled = true;
public bool $disabled_flag = false;
public array $prefs = ['email', 'sms'];
public static function rules()
{
return ['enabled' => 'required|boolean'];
}
};
}
public function test_single_fresh_render_reads_model_value_true()
{
$html = $this->render([
'name' => 'enabled',
'label' => 'Enabled',
'item' => $this->item(),
]);
$this->assertMatchesRegularExpression('/name="enabled"[^>]*checked/', $html);
}
public function test_single_fresh_render_reads_model_value_false()
{
$html = $this->render([
'name' => 'disabled_flag',
'label' => 'Disabled',
'item' => $this->item(),
]);
$this->assertDoesNotMatchRegularExpression('/name="disabled_flag"[^>]*checked/', $html);
}
public function test_single_redisplay_unchecked_does_not_fall_back_to_model()
{
$html = $this->render(
data: [
'name' => 'enabled',
'label' => 'Enabled',
'item' => $this->item(),
],
oldInput: ['some_other_field' => 'x'],
);
$this->assertDoesNotMatchRegularExpression('/name="enabled"[^>]*checked/', $html);
}
public function test_single_redisplay_checked_shows_checked()
{
$html = $this->render(
data: [
'name' => 'disabled_flag',
'label' => 'Disabled',
'item' => $this->item(),
],
oldInput: ['disabled_flag' => '1'],
);
$this->assertMatchesRegularExpression('/name="disabled_flag"[^>]*checked/', $html);
}
public function test_multi_fresh_render_from_model_array_checks_matching_options()
{
$html = $this->render([
'name' => 'prefs',
'label' => 'Notifications',
'options' => ['email' => 'Email', 'sms' => 'SMS', 'push' => 'Push'],
'item' => $this->item(),
]);
$this->assertMatchesRegularExpression('/value="email"[^>]*checked/', $html);
$this->assertMatchesRegularExpression('/value="sms"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="push"[^>]*checked/', $html);
}
public function test_multi_fresh_render_with_callable_selected()
{
$html = $this->render([
'name' => 'modellist_displays',
'label' => 'Model Columns',
'options' => ['image' => 'Image', 'category' => 'Category'],
'selected' => fn ($v) => $v === 'image',
]);
$this->assertMatchesRegularExpression('/value="image"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="category"[^>]*checked/', $html);
}
public function test_multi_redisplay_reflects_old_selection_not_model()
{
$html = $this->render(
data: [
'name' => 'prefs',
'label' => 'Notifications',
'options' => ['email' => 'Email', 'sms' => 'SMS', 'push' => 'Push'],
'item' => $this->item(),
],
oldInput: ['prefs' => ['sms']],
);
$this->assertDoesNotMatchRegularExpression('/value="email"[^>]*checked/', $html);
$this->assertMatchesRegularExpression('/value="sms"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="push"[^>]*checked/', $html);
}
public function test_multi_redisplay_all_unchecked_does_not_fall_back_to_model()
{
$html = $this->render(
data: [
'name' => 'prefs',
'label' => 'Notifications',
'options' => ['email' => 'Email', 'sms' => 'SMS', 'push' => 'Push'],
'item' => $this->item(),
],
oldInput: ['some_other_field' => 'x'],
);
$this->assertDoesNotMatchRegularExpression('/value="email"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="sms"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="push"[^>]*checked/', $html);
}
public function test_no_item_does_not_crash_when_deriving_required()
{
// Transient forms with no persistent model must not try to dereference
// a null $item via checkIfRequired (which does $item::rules()).
$html = $this->render([
'name' => 'consent',
'label' => 'I consent',
]);
$this->assertDoesNotMatchRegularExpression('/name="consent"[^>]*required/', $html);
}
public function test_single_derives_required_from_model_rules()
{
$html = $this->render([
'name' => 'enabled',
'label' => 'Enabled',
'item' => $this->item(),
]);
$this->assertMatchesRegularExpression('/name="enabled"[^>]*required/', $html);
}
public function test_single_uses_explicit_required_prop_over_helper()
{
$html = $this->render([
'name' => 'disabled_flag',
'label' => 'Disabled',
'item' => $this->item(),
'required' => true,
]);
$this->assertMatchesRegularExpression('/name="disabled_flag"[^>]*required/', $html);
}
}

View File

@ -0,0 +1,202 @@
<?php
namespace Tests\Feature\Blade;
use App\Http\Middleware\CheckForDebug;
use App\Http\Middleware\CheckForSetup;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
class RadioRowTest extends TestCase
{
private function render(array $data, ?array $oldInput = null): string
{
Route::get('/__test/radio-row', function () use ($data) {
return view('blade.form.radio-row', $data);
});
$call = $this->withoutMiddleware([
CheckForSetup::class,
CheckForDebug::class,
]);
if ($oldInput !== null) {
$call = $call->withSession(['_old_input' => $oldInput]);
}
return $call->get('/__test/radio-row')->assertOk()->getContent();
}
private function item(): object
{
return new class
{
public string $admin_cc_always = '1';
public ?string $preference = null;
public static function rules()
{
return ['admin_cc_always' => 'required'];
}
};
}
public function test_fresh_render_checks_the_matching_model_value()
{
$html = $this->render([
'name' => 'admin_cc_always',
'label' => 'CC Admin',
'options' => ['1' => 'Always', '0' => 'Never'],
'item' => $this->item(),
]);
$this->assertMatchesRegularExpression('/value="1"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="0"[^>]*checked/', $html);
}
public function test_fresh_render_with_null_model_value_checks_nothing()
{
$html = $this->render([
'name' => 'preference',
'label' => 'Preference',
'options' => ['a' => 'A', 'b' => 'B', 'c' => 'C'],
'item' => $this->item(),
]);
$this->assertDoesNotMatchRegularExpression('/value="a"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="b"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="c"[^>]*checked/', $html);
}
public function test_explicit_selected_overrides_model_value()
{
$html = $this->render([
'name' => 'admin_cc_always',
'label' => 'CC Admin',
'options' => ['1' => 'Always', '0' => 'Never'],
'item' => $this->item(),
'selected' => '0',
]);
$this->assertMatchesRegularExpression('/value="0"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="1"[^>]*checked/', $html);
}
public function test_redisplay_reflects_old_value_not_model()
{
// Model says 1; user submitted 0 and validation failed elsewhere.
// Old input has admin_cc_always=0. Redisplay must reflect that.
$html = $this->render(
data: [
'name' => 'admin_cc_always',
'label' => 'CC Admin',
'options' => ['1' => 'Always', '0' => 'Never'],
'item' => $this->item(),
],
oldInput: ['admin_cc_always' => '0'],
);
$this->assertMatchesRegularExpression('/value="0"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="1"[^>]*checked/', $html);
}
public function test_redisplay_with_no_old_value_does_not_fall_back_to_model()
{
// Session has old input but this specific field is absent. Radios
// should render nothing checked; they must NOT fall through to the
// stale $item->admin_cc_always default (which would mislead the
// user about what they submitted).
$html = $this->render(
data: [
'name' => 'admin_cc_always',
'label' => 'CC Admin',
'options' => ['1' => 'Always', '0' => 'Never'],
'item' => $this->item(),
],
oldInput: ['some_other_field' => 'x'],
);
$this->assertDoesNotMatchRegularExpression('/value="1"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="0"[^>]*checked/', $html);
}
public function test_no_label_uses_offset_column_default()
{
$html = $this->render([
'name' => 'admin_cc_always',
'options' => ['1' => 'Always', '0' => 'Never'],
'item' => $this->item(),
]);
$this->assertStringContainsString('col-md-8 col-md-offset-3', $html);
}
public function test_with_label_uses_non_offset_column_default()
{
$html = $this->render([
'name' => 'admin_cc_always',
'label' => 'CC Admin',
'options' => ['1' => 'Always', '0' => 'Never'],
'item' => $this->item(),
]);
// With a left-hand label the option column shouldn't have the
// col-md-offset-3 push (that offset conflicts with the col-md-3
// label the row already emits).
preg_match_all('/class="col-md-8[^"]*"/', $html, $matches);
$classes = $matches[0] ?? [];
$this->assertNotEmpty($classes);
foreach ($classes as $class) {
// The @error and help_text blocks legitimately use offset even
// when there's a label, so we only care about the primary
// options container: at least one col-md-8 that isn't offset.
if (! str_contains($class, 'col-md-offset-3')) {
return;
}
}
$this->fail('Expected at least one non-offset col-md-8 option container.');
}
public function test_required_is_applied_to_first_radio_only()
{
$html = $this->render([
'name' => 'admin_cc_always',
'label' => 'CC Admin',
'options' => ['1' => 'Always', '0' => 'Never'],
'item' => $this->item(),
]);
// Browser treats any-required-in-a-radio-group as "the group is
// required," so we emit required on the first option only.
$this->assertMatchesRegularExpression('/value="1"[^>]*required/', $html);
$this->assertDoesNotMatchRegularExpression('/value="0"[^>]*required/', $html);
}
public function test_no_item_does_not_crash_when_deriving_required()
{
// Transient forms like bulk-checkin have no persistent model backing.
// The row must not try to dereference a null $item via checkIfRequired.
$html = $this->render([
'name' => 'update_default_location',
'selected' => '1',
'options' => ['1' => 'Update default', '0' => 'Leave alone'],
]);
$this->assertMatchesRegularExpression('/value="1"[^>]*checked/', $html);
$this->assertDoesNotMatchRegularExpression('/value="1"[^>]*required/', $html);
}
public function test_disabled_is_applied_to_every_radio()
{
$html = $this->render([
'name' => 'admin_cc_always',
'label' => 'CC Admin',
'options' => ['1' => 'Always', '0' => 'Never'],
'item' => $this->item(),
'disabled' => true,
]);
$this->assertMatchesRegularExpression('/value="1"[^>]*disabled/', $html);
$this->assertMatchesRegularExpression('/value="0"[^>]*disabled/', $html);
}
}

View File

@ -4,6 +4,7 @@ namespace Tests\Unit\Rules;
use App\Rules\ExternalUrl;
use Illuminate\Support\Facades\Validator;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class ExternalUrlTest extends TestCase
@ -60,9 +61,7 @@ class ExternalUrlTest extends TestCase
];
}
/**
* @dataProvider rejectedProvider
*/
#[DataProvider('rejectedProvider')]
public function test_rejects_dangerous_or_malformed_urls(string $url)
{
$this->assertFalse($this->passes($url), 'Should have been rejected: '.$url);