mirror of
https://github.com/snipe/snipe-it.git
synced 2024-11-09 23:24:06 -08:00
Merge pull request #14304 from snipe/feature/sc-24018
Added ability to bulk delete locations
This commit is contained in:
commit
71610fb20f
|
@ -25,9 +25,27 @@ class LocationsController extends Controller
|
|||
{
|
||||
$this->authorize('view', Location::class);
|
||||
$allowed_columns = [
|
||||
'id', 'name', 'address', 'address2', 'city', 'state', 'country', 'zip', 'created_at',
|
||||
'updated_at', 'manager_id', 'image',
|
||||
'assigned_assets_count', 'users_count', 'assets_count','assigned_assets_count', 'assets_count', 'rtd_assets_count', 'currency', 'ldap_ou', ];
|
||||
'id',
|
||||
'name',
|
||||
'address',
|
||||
'address2',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
'zip',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'manager_id',
|
||||
'image',
|
||||
'assigned_assets_count',
|
||||
'users_count',
|
||||
'assets_count',
|
||||
'assigned_assets_count',
|
||||
'assets_count',
|
||||
'rtd_assets_count',
|
||||
'currency',
|
||||
'ldap_ou',
|
||||
];
|
||||
|
||||
$locations = Location::with('parent', 'manager', 'children')->select([
|
||||
'locations.id',
|
||||
|
@ -50,6 +68,7 @@ class LocationsController extends Controller
|
|||
])->withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('children as children_count')
|
||||
->withCount('users as users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
|
@ -80,6 +99,10 @@ class LocationsController extends Controller
|
|||
$locations->where('locations.country', '=', $request->input('country'));
|
||||
}
|
||||
|
||||
if ($request->filled('manager_id')) {
|
||||
$locations->where('locations.manager_id', '=', $request->input('manager_id'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
|
|
|
@ -274,11 +274,6 @@ class UsersController extends Controller
|
|||
$offset = ($request->input('offset') > $users->count()) ? $users->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
|
||||
\Log::debug('Requested offset: '. $request->input('offset'));
|
||||
\Log::debug('App offset: '. app('api_offset_value'));
|
||||
\Log::debug('Actual offset: '. $offset);
|
||||
\Log::debug('Limit: '. $limit);
|
||||
|
||||
$total = $users->count();
|
||||
$users = $users->skip($offset)->take($limit)->get();
|
||||
|
||||
|
|
|
@ -442,7 +442,6 @@ class AssetModelsController extends Controller
|
|||
$del_count = 0;
|
||||
|
||||
foreach ($models as $model) {
|
||||
\Log::debug($model->id);
|
||||
|
||||
if ($model->assets_count > 0) {
|
||||
$del_error_count++;
|
||||
|
@ -452,8 +451,6 @@ class AssetModelsController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
\Log::debug($del_count);
|
||||
\Log::debug($del_error_count);
|
||||
|
||||
if ($del_error_count == 0) {
|
||||
return redirect()->route('models.index')
|
||||
|
|
|
@ -8,6 +8,7 @@ use App\Models\Location;
|
|||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* This controller handles all actions related to Locations for
|
||||
|
@ -238,7 +239,7 @@ class LocationsController extends Controller
|
|||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $locationId
|
||||
* @since [v6.0.14]
|
||||
* @return View
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
public function getClone($locationId = null)
|
||||
{
|
||||
|
@ -272,8 +273,92 @@ class LocationsController extends Controller
|
|||
|
||||
}
|
||||
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a view that allows the user to bulk delete locations
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v6.3.1]
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
public function postBulkDelete(Request $request)
|
||||
{
|
||||
$locations_raw_array = $request->input('ids');
|
||||
|
||||
// Make sure some IDs have been selected
|
||||
if ((is_array($locations_raw_array)) && (count($locations_raw_array) > 0)) {
|
||||
$locations = Location::whereIn('id', $locations_raw_array)->get();
|
||||
|
||||
$valid_count = 0;
|
||||
foreach ($locations as $location) {
|
||||
if ($location->isDeletable()) {
|
||||
$valid_count++;
|
||||
}
|
||||
}
|
||||
return view('locations/bulk-delete', compact('locations'))->with('valid_count', $valid_count);
|
||||
}
|
||||
|
||||
return redirect()->route('models.index')
|
||||
->with('error', 'You must select at least one model to edit.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that locations can be deleted and deletes them if they can
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v6.3.1]
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function postBulkDeleteStore(Request $request) {
|
||||
$locations_raw_array = $request->input('ids');
|
||||
|
||||
if ((is_array($locations_raw_array)) && (count($locations_raw_array) > 0)) {
|
||||
$locations = Location::whereIn('id', $locations_raw_array)->get();
|
||||
|
||||
$success_count = 0;
|
||||
$error_count = 0;
|
||||
|
||||
foreach ($locations as $location) {
|
||||
|
||||
// Can we delete this location?
|
||||
if ($location->isDeletable()) {
|
||||
$location->delete();
|
||||
$success_count++;
|
||||
} else {
|
||||
$error_count++;
|
||||
}
|
||||
}
|
||||
|
||||
\Log::debug('Success count: '.$success_count);
|
||||
\Log::debug('Error count: '.$error_count);
|
||||
// Complete success
|
||||
if ($success_count == count($locations_raw_array)) {
|
||||
return redirect()
|
||||
->route('locations.index')
|
||||
->with('success', trans_choice('general.bulk.delete.success', $success_count,
|
||||
['object_type' => trans_choice('general.location_plural', $success_count), 'count' => $success_count]
|
||||
));
|
||||
}
|
||||
|
||||
// Partial success
|
||||
if ($error_count > 0) {
|
||||
return redirect()
|
||||
->route('locations.index')
|
||||
->with('warning', trans('general.bulk.partial_success',
|
||||
['success' => $success_count, 'error' => $error_count, 'object_type' => trans('general.locations')]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Nothing was selected - return to the index
|
||||
return redirect()
|
||||
->route('locations.index')
|
||||
->with('error', trans('general.bulk.nothing_selected',
|
||||
['object_type' => trans('general.locations')]
|
||||
));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,9 @@ class LocationsTransformer
|
|||
$permissions_array['available_actions'] = [
|
||||
'update' => Gate::allows('update', Location::class) ? true : false,
|
||||
'delete' => $location->isDeletable(),
|
||||
'bulk_selectable' => [
|
||||
'delete' => $location->isDeletable()
|
||||
],
|
||||
'clone' => (Gate::allows('create', Location::class) && ($location->deleted_at == '')),
|
||||
];
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ class Location extends SnipeModel
|
|||
return Gate::allows('delete', $this)
|
||||
&& ($this->assignedAssets()->count() === 0)
|
||||
&& ($this->assets()->count() === 0)
|
||||
&& ($this->children()->count() === 0)
|
||||
&& ($this->users()->count() === 0);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,11 @@ class LocationPresenter extends Presenter
|
|||
public static function dataTableLayout()
|
||||
{
|
||||
$layout = [
|
||||
|
||||
[
|
||||
'field' => 'bulk_selectable',
|
||||
'checkbox' => true,
|
||||
'formatter' => 'checkboxEnabledFormatter',
|
||||
],
|
||||
[
|
||||
'field' => 'id',
|
||||
'searchable' => false,
|
||||
|
|
|
@ -39,24 +39,12 @@ class SettingsServiceProvider extends ServiceProvider
|
|||
$limit = abs($int_limit);
|
||||
}
|
||||
|
||||
// \Log::debug('Max in env: '.config('app.max_results'));
|
||||
// \Log::debug('Original requested limit: '.request('limit'));
|
||||
// \Log::debug('Int limit: '.$int_limit);
|
||||
// \Log::debug('Modified limit: '.$limit);
|
||||
// \Log::debug('------------------------------');
|
||||
|
||||
|
||||
return $limit;
|
||||
});
|
||||
|
||||
// Make sure the offset is actually set and is an integer
|
||||
\App::singleton('api_offset_value', function () {
|
||||
$offset = intval(request('offset'));
|
||||
// \Log::debug('Original requested offset: '.request('offset'));
|
||||
// \Log::debug('Modified offset: '.$offset);
|
||||
// \Log::debug('------------------------------');
|
||||
|
||||
|
||||
return $offset;
|
||||
});
|
||||
|
||||
|
|
|
@ -182,6 +182,7 @@ return [
|
|||
'lock_passwords' => 'This field value will not be saved in a demo installation.',
|
||||
'feature_disabled' => 'This feature has been disabled for the demo installation.',
|
||||
'location' => 'Location',
|
||||
'location_plural' => 'Location|Locations',
|
||||
'locations' => 'Locations',
|
||||
'logo_size' => 'Square logos look best with Logo + Text. Logo maximum display size is 50px high x 500px wide. ',
|
||||
'logout' => 'Logout',
|
||||
|
@ -443,7 +444,6 @@ return [
|
|||
'sample_value' => 'Sample Value',
|
||||
'no_headers' => 'No Columns Found',
|
||||
'error_in_import_file' => 'There was an error reading the CSV file: :error',
|
||||
'percent_complete' => ':percent % Complete',
|
||||
'errors_importing' => 'Some Errors occurred while importing: ',
|
||||
'warning' => 'WARNING: :warning',
|
||||
'success_redirecting' => '"Success... Redirecting.',
|
||||
|
@ -503,5 +503,16 @@ return [
|
|||
'or' => 'or',
|
||||
'url' => 'URL',
|
||||
'edit_fieldset' => 'Edit fieldset fields and options',
|
||||
'bulk' => [
|
||||
'delete' =>
|
||||
[
|
||||
'header' => 'Bulk Delete :object_type',
|
||||
'warn' => 'You are about to delete one :object_type|You are about to delete :count :object_type',
|
||||
'success' => ':object_type successfully deleted|Successfully deleted :count :object_type',
|
||||
'error' => 'Could not delete :object_type',
|
||||
'nothing_selected' => 'No :object_type selected - nothing to do',
|
||||
'partial' => 'Deleted :success_count :object_type, but :error_count :object_type could not be deleted',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
70
resources/views/locations/bulk-delete.blade.php
Normal file
70
resources/views/locations/bulk-delete.blade.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
@extends('layouts/default')
|
||||
|
||||
{{-- Page title --}}
|
||||
@section('title')
|
||||
{{ trans('general.bulk.delete.header', ['object_type' => trans_choice('general.location_plural', $valid_count)]) }}
|
||||
@parent
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<a href="{{ URL::previous() }}" class="btn btn-primary pull-right">
|
||||
{{ trans('general.back') }}</a>
|
||||
@stop
|
||||
|
||||
{{-- Page content --}}
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<!-- left column -->
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<form class="form-horizontal" method="post" action="{{ route('locations.bulkdelete.store') }}" autocomplete="off" role="form">
|
||||
{{csrf_field()}}
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h2 class="box-title" style="color: red">{{ trans_choice('general.bulk.delete.warn', $valid_count, ['count' => $valid_count,'object_type' => trans_choice('general.location_plural', $valid_count)]) }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="box-body">
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="col-md-1">
|
||||
<label>
|
||||
<input type="checkbox" id="checkAll" checked="checked">
|
||||
</label>
|
||||
</td>
|
||||
<td class="col-md-10">{{ trans('general.name') }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($locations as $location)
|
||||
<tr{!! (($location->assets_count > 0 ) ? ' class="danger"' : '') !!}>
|
||||
<td>
|
||||
<input type="checkbox" name="ids[]" class="{ ($location->isDeletable() ? '' : ' disabled') }}" value="{{ $location->id }}" {!! (($location->isDeletable()) ? ' checked="checked"' : ' disabled') !!}>
|
||||
</td>
|
||||
<td>{{ $location->name }}</td>
|
||||
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div><!-- /.box-body -->
|
||||
|
||||
<div class="box-footer text-right">
|
||||
<a class="btn btn-link pull-left" href="{{ URL::previous() }}">{{ trans('button.cancel') }}</a>
|
||||
<button type="submit" class="btn btn-success" id="submit-button"><i class="fas fa-check icon-white" aria-hidden="true"></i> {{ trans('general.delete') }}</button>
|
||||
</div><!-- /.box-footer -->
|
||||
</div><!-- /.box -->
|
||||
</form>
|
||||
</div> <!-- .col-md-12-->
|
||||
</div><!--.row-->
|
||||
@stop
|
||||
@section('moar_scripts')
|
||||
<script>
|
||||
|
||||
|
||||
$("#checkAll").change(function () {
|
||||
$("input:checkbox").prop('checked', $(this).prop("checked"));
|
||||
});
|
||||
|
||||
</script>
|
||||
@stop
|
|
@ -20,11 +20,17 @@
|
|||
<div class="box-body">
|
||||
<div class="table-responsive">
|
||||
|
||||
@include('partials.locations-bulk-actions')
|
||||
|
||||
<table
|
||||
data-columns="{{ \App\Presenters\LocationPresenter::dataTableLayout() }}"
|
||||
data-cookie-id-table="locationTable"
|
||||
data-click-to-select="true"
|
||||
data-pagination="true"
|
||||
data-id-table="locationTable"
|
||||
data-toolbar="#locationsBulkEditToolbar"
|
||||
data-bulk-button-id="#bulkLocationsEditButton"
|
||||
data-bulk-form-id="#locationsBulkForm"
|
||||
data-search="true"
|
||||
data-show-footer="true"
|
||||
data-side-pagination="server"
|
||||
|
|
|
@ -139,12 +139,12 @@
|
|||
});
|
||||
|
||||
|
||||
// Handle whether or not the edit button should be disabled
|
||||
// Handle whether the edit button should be disabled
|
||||
$('.snipe-table').on('uncheck.bs.table', function () {
|
||||
|
||||
var buttonName = $(this).data('bulk-button-id');
|
||||
|
||||
if ($(this).bootstrapTable('getSelections').length == 0) {
|
||||
|
||||
$(buttonName).attr('disabled', 'disabled');
|
||||
}
|
||||
});
|
||||
|
@ -398,17 +398,35 @@
|
|||
// Convert line breaks to <br>
|
||||
function notesFormatter(value) {
|
||||
if (value) {
|
||||
return value.replace(/(?:\r\n|\r|\n)/g, '<br />');;
|
||||
return value.replace(/(?:\r\n|\r|\n)/g, '<br />');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if checkbox should be selectable
|
||||
// Selectability is determined by the API field "selectable" which is set at the Presenter/API Transformer
|
||||
// However since different bulk actions have different requirements, we have to walk through the available_actions object
|
||||
// to determine whether to disable it
|
||||
function checkboxEnabledFormatter (value, row) {
|
||||
|
||||
// add some stuff to get the value of the select2 option here?
|
||||
|
||||
if ((row.available_actions) && (row.available_actions.bulk_selectable) && (row.available_actions.bulk_selectable.delete !== true)) {
|
||||
console.log('value for ID ' + row.id + ' is NOT true:' + row.available_actions.bulk_selectable.delete);
|
||||
return {
|
||||
disabled:true,
|
||||
//checked: false, <-- not sure this will work the way we want?
|
||||
}
|
||||
}
|
||||
console.log('value for ID ' + row.id + ' IS true:' + row.available_actions.bulk_selectable.delete);
|
||||
}
|
||||
|
||||
|
||||
// We need a special formatter for license seats, since they don't work exactly the same
|
||||
// Checkouts need the license ID, checkins need the specific seat ID
|
||||
|
||||
function licenseSeatInOutFormatter(value, row) {
|
||||
// The user is allowed to check the license seat out and it's available
|
||||
if ((row.available_actions.checkout == true) && (row.user_can_checkout == true) && ((!row.asset_id) && (!row.assigned_to))) {
|
||||
if ((row.available_actions.checkout === true) && (row.user_can_checkout === true) && ((!row.asset_id) && (!row.assigned_to))) {
|
||||
return '<a href="{{ config('app.url') }}/licenses/' + row.license_id + '/checkout/'+row.id+'" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
|
||||
} else {
|
||||
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="Check in this license seat.">{{ trans('general.checkin') }}</a>';
|
||||
|
|
20
resources/views/partials/locations-bulk-actions.blade.php
Normal file
20
resources/views/partials/locations-bulk-actions.blade.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
@can('delete', \App\Models\Location::class)
|
||||
<div id="locationsBulkEditToolbar">
|
||||
{{ Form::open([
|
||||
'method' => 'POST',
|
||||
'route' => ['locations.bulkdelete.show'],
|
||||
'class' => 'form-inline',
|
||||
'id' => 'locationsBulkForm']) }}
|
||||
|
||||
<div id="locations-toolbar">
|
||||
<label for="bulk_actions" class="sr-only">{{ trans('general.bulk_actions') }}</label>
|
||||
<select name="bulk_actions" class="form-control select2" style="width: 200px;" aria-label="bulk_actions">
|
||||
<option value="delete">{{ trans('general.bulk_delete') }}</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" id="bulkLocationsEditButton" disabled>{{ trans('button.go') }}</button>
|
||||
</div>
|
||||
|
||||
{{ Form::close() }}
|
||||
</div>
|
||||
@endcan
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
'id' => 'modelsBulkForm']) }}
|
||||
|
||||
@if (request('status')!='deleted')
|
||||
@can('delete', \App\Models\User::class)
|
||||
@can('delete', \App\Models\AssetModel::class)
|
||||
<div id="models-toolbar">
|
||||
<label for="bulk_actions" class="sr-only">{{ trans('general.bulk_actions') }}</label>
|
||||
<select name="bulk_actions" class="form-control select2" style="width: 200px;" aria-label="bulk_actions">
|
||||
|
|
|
@ -1015,23 +1015,36 @@
|
|||
</div><!-- /.tab-pane -->
|
||||
|
||||
<div class="tab-pane" id="managed">
|
||||
<div class="table-responsive">
|
||||
<table class="table display table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-8">{{ trans('general.name') }}</th>
|
||||
<th class="col-md-4">{{ trans('general.date') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($user->managedLocations as $location)
|
||||
<tr>
|
||||
<td>{!! $location->present()->nameUrl() !!}</td>
|
||||
<td>{{ $location->created_at }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@include('partials.locations-bulk-actions')
|
||||
|
||||
|
||||
<table
|
||||
data-columns="{{ \App\Presenters\LocationPresenter::dataTableLayout() }}"
|
||||
data-cookie-id-table="locationTable"
|
||||
data-click-to-select="true"
|
||||
data-pagination="true"
|
||||
data-id-table="locationTable"
|
||||
data-toolbar="#locationsBulkEditToolbar"
|
||||
data-bulk-button-id="#bulkLocationsEditButton"
|
||||
data-bulk-form-id="#locationsBulkForm"
|
||||
data-search="true"
|
||||
data-show-footer="true"
|
||||
data-side-pagination="server"
|
||||
data-show-columns="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-export="true"
|
||||
data-show-refresh="true"
|
||||
data-sort-order="asc"
|
||||
id="locationTable"
|
||||
class="table table-striped snipe-table"
|
||||
data-url="{{ route('api.locations.index', ['manager_id' => $user->id]) }}"
|
||||
data-export-options='{
|
||||
"fileName": "export-locations-{{ date('Y-m-d') }}",
|
||||
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
|
||||
}'>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div><!-- /consumables-tab -->
|
||||
</div><!-- /.tab-content -->
|
||||
|
|
|
@ -54,7 +54,18 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
*/
|
||||
|
||||
Route::group(['prefix' => 'locations', 'middleware' => ['auth']], function () {
|
||||
|
||||
|
||||
Route::post(
|
||||
'bulkdelete',
|
||||
[LocationsController::class, 'postBulkDelete']
|
||||
)->name('locations.bulkdelete.show');
|
||||
|
||||
Route::post(
|
||||
'bulkedit',
|
||||
[LocationsController::class, 'postBulkDeleteStore']
|
||||
)->name('locations.bulkdelete.store');
|
||||
|
||||
|
||||
Route::get('{locationId}/clone',
|
||||
[LocationsController::class, 'getClone']
|
||||
)->name('clone/location');
|
||||
|
@ -68,6 +79,7 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
'{locationId}/printallassigned',
|
||||
[LocationsController::class, 'print_all_assigned']
|
||||
)->name('locations.print_all_assigned');
|
||||
|
||||
});
|
||||
|
||||
Route::resource('locations', LocationsController::class, [
|
||||
|
|
Loading…
Reference in a new issue