Merge branch 'develop' into chore/sc-24808

This commit is contained in:
Marcus Moore 2024-02-21 12:33:06 -08:00
commit aa2632fe46
No known key found for this signature in database
112 changed files with 1215 additions and 410 deletions

View file

@ -29,6 +29,7 @@ RUN apk add --no-cache \
php81-sodium \
php81-redis \
php81-pecl-memcached \
php81-exif \
curl \
wget \
vim \

View file

@ -45,8 +45,21 @@ DB_PASSWORD={}
Now you are ready to run the entire test suite from your terminal:
`php artisan test`
```shell
php artisan test
````
To run individual test files, you can pass the path to the test that you want to run:
`php artisan test tests/Unit/AccessoryTest.php`
```shell
php artisan test tests/Unit/AccessoryTest.php
```
Some tests, like ones concerning LDAP, are marked with the `@group` annotation. Those groups can be run, or excluded, using the `--group` or `--exclude-group` flags:
```shell
php artisan test --group=ldap
php artisan test --exclude-group=ldap
```
This can be helpful if a set of tests are failing because you don't have an extension, like LDAP, installed.

View file

@ -5,6 +5,151 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use ZipArchive;
class SQLStreamer {
private $input;
private $output;
// embed the prefix here?
public ?string $prefix;
private bool $reading_beginning_of_line = true;
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
public array $tablenames = [];
private bool $should_guess = false;
private bool $statement_is_permitted = false;
public function __construct($input, $output, string $prefix = null)
{
$this->input = $input;
$this->output = $output;
$this->prefix = $prefix;
}
public function parse_sql(string $line): string {
// take into account the 'start of line or not' setting as an instance variable?
// 'continuation' lines for a permitted statement are PERMITTED.
if($this->statement_is_permitted && $line[0] === ' ') {
return $line;
}
$table_regex = '`?([a-zA-Z0-9_]+)`?';
$allowed_statements = [
"/^(DROP TABLE (?:IF EXISTS )?)`$table_regex(.*)$/" => false,
"/^(CREATE TABLE )$table_regex(.*)$/" => true, //sets up 'continuation'
"/^(LOCK TABLES )$table_regex(.*)$/" => false,
"/^(INSERT INTO )$table_regex(.*)$/" => false,
"/^UNLOCK TABLES/" => false,
// "/^\\) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;/" => false, // FIXME not sure what to do here?
"/^\\)[a-zA-Z0-9_= ]*;$/" => false
// ^^^^^^ that bit should *exit* the 'perimitted' black
];
foreach($allowed_statements as $statement => $statechange) {
// $this->info("Checking regex: $statement...\n");
$matches = [];
if (preg_match($statement,$line,$matches)) {
$this->statement_is_permitted = $statechange;
// matches are: 1 => first part of the statement, 2 => tablename, 3 => rest of statement
// (with of course 0 being "the whole match")
if (@$matches[2]) {
// print "Found a tablename! It's: ".$matches[2]."\n";
if ($this->should_guess) {
@$this->tablenames[$matches[2]] += 1;
continue; //oh? FIXME
} else {
$cleaned_tablename = \DB::getTablePrefix().preg_replace('/^'.$this->prefix.'/','',$matches[2]);
$line = preg_replace($statement,'$1`'.$cleaned_tablename.'`$3' , $line);
}
} else {
// no explicit tablename in this one, leave the line alone
}
//how do we *replace* the tablename?
// print "RETURNING LINE: $line";
return $line;
}
}
// all that is not allowed is denied.
return "";
}
//this is used in exactly *TWO* places, and in both cases should return a prefix I think?
// first - if you do the --sanitize-only one (which is mostly for testing/development)
// next - when you run *without* a guessed prefix, this is run first to figure out the prefix
// I think we have to *duplicate* the call to be able to run it again?
public static function guess_prefix($input):string
{
$parser = new self($input, null);
$parser->should_guess = true;
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
//can't use 'users' because the 'accessories_users' table?
// can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) {
// print "Comparing $tablename to $check_table\n";
if (str_ends_with($tablename,$check_table)) {
// print "Found one!\n";
$check_tables[$check_table] = substr($tablename,0,-strlen($check_table));
}
}
}
$guessed_prefix = null;
foreach ($check_tables as $clean_table => $prefix_guess) {
if(is_null($prefix_guess)) {
print("Couldn't find table $clean_table\n");
die();
}
if(is_null($guessed_prefix)) {
$guessed_prefix = $prefix_guess;
} else {
if ($guessed_prefix != $prefix_guess) {
print("Prefix mismatch! Had guessed $guessed_prefix but got $prefix_guess\n");
die();
}
}
}
return $guessed_prefix;
}
public function line_aware_piping(): int
{
$bytes_read = 0;
if (! $this->input) {
throw new \Exception("No Input available for line_aware_piping");
}
while (($buffer = fgets($this->input, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
if ($this->reading_beginning_of_line) {
// \Log::debug("Buffer is: '$buffer'");
$cleaned_buffer = $this->parse_sql($buffer);
if ($this->output) {
$bytes_written = fwrite($this->output, $cleaned_buffer);
if ($bytes_written === false) {
throw new \Exception("Unable to write to pipe");
}
}
}
// if we got a newline at the end of this, then the _next_ read is the beginning of a line
if($buffer[strlen($buffer)-1] === "\n") {
$this->reading_beginning_of_line = true;
} else {
$this->reading_beginning_of_line = false;
}
}
return $bytes_read;
}
}
class RestoreFromBackup extends Command
{
/**
@ -12,10 +157,13 @@ class RestoreFromBackup extends Command
*
* @var string
*/
// FIXME - , stripping prefixes and nonstandard SQL statements. Without --prefix, guess and return the correct prefix to strip
protected $signature = 'snipeit:restore
{--force : Skip the danger prompt; assuming you enter "y"}
{filename : The zip file to be migrated}
{--no-progress : Don\'t show a progress bar}';
{--no-progress : Don\'t show a progress bar}
{--sanitize-guess-prefix : Guess and output the table-prefix needed to "sanitize" the SQL}
{--sanitize-with-prefix= : "Sanitize" the SQL, using the passed-in table prefix (can be learned from --sanitize-guess-prefix). Pass as just \'--sanitize-with-prefix=\' to use no prefix}';
/**
* The console command description.
@ -34,8 +182,6 @@ class RestoreFromBackup extends Command
parent::__construct();
}
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
/**
* Execute the console command.
*
@ -55,7 +201,7 @@ class RestoreFromBackup extends Command
return $this->error('Missing required filename');
}
if (! $this->option('force') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
if (! $this->option('force') && ! $this->option('sanitize-guess-prefix') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
return $this->error('Data loss not confirmed');
}
@ -158,11 +304,11 @@ class RestoreFromBackup extends Command
}
foreach (array_merge($private_dirs, $public_dirs) as $dir) {
$last_pos = strrpos($raw_path, $dir.'/');
$last_pos = strrpos($raw_path, $dir . '/');
if ($last_pos !== false) {
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
$interesting_files[$raw_path] = ['dest' =>$dir, 'index' => $i];
$interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i];
continue 2;
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) {
// we don't care about that; we just want files with the appropriate prefix
@ -171,7 +317,7 @@ class RestoreFromBackup extends Command
}
}
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico', ];
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico',];
foreach (array_merge($private_files, $public_files) as $file) {
$has_wildcard = (strpos($file, '*') !== false);
if ($has_wildcard) {
@ -180,8 +326,8 @@ class RestoreFromBackup extends Command
$last_pos = strrpos($raw_path, $file); // no trailing slash!
if ($last_pos !== false) {
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
if (! in_array($extension, $good_extensions)) {
$this->warn('Potentially unsafe file '.$raw_path.' is being skipped');
if (!in_array($extension, $good_extensions)) {
$this->warn('Potentially unsafe file ' . $raw_path . ' is being skipped');
$boring_files[] = $raw_path;
continue 2;
}
@ -196,7 +342,6 @@ class RestoreFromBackup extends Command
}
$boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
} // end of pre-processing the ZIP file for-loop
// print_r($interesting_files);exit(-1);
if (count($sqlfiles) != 1) {
@ -208,6 +353,17 @@ class RestoreFromBackup extends Command
//older Snipe-IT installs don't have the db-dumps subdirectory component
}
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']); // maybe copy *THIS* thing?
// OKAY, now that we *found* the sql file if we're doing just the guess-prefix thing, we can do that *HERE* I think?
if ($this->option('sanitize-guess-prefix')) {
$prefix = SQLStreamer::guess_prefix($sql_contents);
$this->line($prefix);
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitze your SQL.");
}
//how to invoke the restore?
$pipes = [];
@ -228,6 +384,7 @@ class RestoreFromBackup extends Command
return $this->error('Unable to invoke mysql via CLI');
}
// I'm not sure about these?
stream_set_blocking($pipes[1], false); // use non-blocking reads for stdout
stream_set_blocking($pipes[2], false); // use non-blocking reads for stderr
@ -238,9 +395,9 @@ class RestoreFromBackup extends Command
//$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']);
// FIXME - this feels like it wants to go somewhere else?
// and it doesn't seem 'right' - if you can't get a stream to the .sql file,
// why do we care what's happening with pipes and stdout and stderr?!
if ($sql_contents === false) {
$stdout = fgets($pipes[1]);
$this->info($stdout);
@ -249,20 +406,27 @@ class RestoreFromBackup extends Command
return false;
}
$bytes_read = 0;
try {
while (($buffer = fgets($sql_contents, self::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
// \Log::debug("Buffer is: '$buffer'");
if ( $this->option('sanitize-with-prefix') === null) {
// "Legacy" direct-piping
$bytes_read = 0;
while (($buffer = fgets($sql_contents, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
// \Log::debug("Buffer is: '$buffer'");
$bytes_written = fwrite($pipes[0], $buffer);
if ($bytes_written === false) {
throw new Exception("Unable to write to pipe");
if ($bytes_written === false) {
throw new Exception("Unable to write to pipe");
}
}
} else {
$sql_importer = new SQLStreamer($sql_contents, $pipes[0], $this->option('sanitize-with-prefix'));
$bytes_read = $sql_importer->line_aware_piping();
}
} catch (\Exception $e) {
\Log::error("Error during restore!!!! ".$e->getMessage());
// FIXME - put these back and/or put them in the right places?!
$err_out = fgets($pipes[1]);
$err_err = fgets($pipes[2]);
\Log::error("Error OUTPUT: ".$err_out);
@ -271,7 +435,6 @@ class RestoreFromBackup extends Command
$this->error($err_err);
throw $e;
}
if (!feof($sql_contents) || $bytes_read == 0) {
return $this->error("Not at end of file for sql file, or zero bytes read. aborting!");
}
@ -303,7 +466,7 @@ class RestoreFromBackup extends Command
$fp = $za->getStream($ugly_file_name);
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
$migrated_file = fopen($file_details['dest'].'/'.basename($pretty_file_name), 'w');
while (($buffer = fgets($fp, self::$buffer_size)) !== false) {
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
fwrite($migrated_file, $buffer);
}
fclose($migrated_file);

View file

@ -36,7 +36,8 @@ class AssetMaintenancesController extends Controller
{
$this->authorize('view', Asset::class);
$maintenances = AssetMaintenance::select('asset_maintenances.*')->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'admin');
$maintenances = AssetMaintenance::select('asset_maintenances.*')
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'admin');
if ($request->filled('search')) {
$maintenances = $maintenances->TextSearch($request->input('search'));
@ -47,7 +48,7 @@ class AssetMaintenancesController extends Controller
}
if ($request->filled('supplier_id')) {
$maintenances->where('supplier_id', '=', $request->input('supplier_id'));
$maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
}
if ($request->filled('asset_maintenance_type')) {
@ -70,10 +71,13 @@ class AssetMaintenancesController extends Controller
'notes',
'asset_tag',
'asset_name',
'serial',
'user_id',
'supplier',
'is_warranty',
'status_label',
];
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
@ -90,6 +94,12 @@ class AssetMaintenancesController extends Controller
case 'asset_name':
$maintenances = $maintenances->OrderByAssetName($order);
break;
case 'serial':
$maintenances = $maintenances->OrderByAssetSerial($order);
break;
case 'status_label':
$maintenances = $maintenances->OrderStatusName($order);
break;
default:
$maintenances = $maintenances->orderBy($sort, $order);
break;

View file

@ -903,6 +903,13 @@ class AssetsController extends Controller
$originalValues['action_date'] = $checkin_at;
}
if(!empty($asset->licenseseats->all())){
foreach ($asset->licenseseats as $seat){
$seat->assigned_to = null;
$seat->save();
}
}
if ($asset->save()) {
event(new CheckoutableCheckedIn($asset, $target, Auth::user(), $request->input('note'), $checkin_at, $originalValues));

View file

@ -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');

View file

@ -33,7 +33,12 @@ class ReportsController extends Controller
if (($request->filled('item_type')) && ($request->filled('item_id'))) {
$actionlogs = $actionlogs->where('item_id', '=', $request->input('item_id'))
->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')));
->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')))
->orWhere(function($query) use ($request)
{
$query->where('target_id', '=', $request->input('item_id'))
->where('target_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')));
});
}
if ($request->filled('action_type')) {

View file

@ -148,7 +148,7 @@ class SettingsController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return Redirect
* @return JsonResponse
*/
public function ajaxTestEmail()
{
@ -170,7 +170,7 @@ class SettingsController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0.0]
* @return Response
* @return JsonResponse
*/
public function purgeBarcodes()
{
@ -211,7 +211,7 @@ class SettingsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0.0]
* @param \Illuminate\Http\Request $request
* @return array
* @return array | JsonResponse
*/
public function showLoginAttempts(Request $request)
{
@ -229,6 +229,12 @@ class SettingsController extends Controller
}
/**
* Lists backup files
*
* @author [A. Gianotto]
* @return array | JsonResponse
*/
public function listBackups() {
$settings = Setting::getSettings();
$path = 'app/backups';
@ -249,12 +255,12 @@ class SettingsController extends Controller
'filesize' => Setting::fileSizeConvert(Storage::size($backup_files[$f])),
'modified_value' => $file_timestamp,
'modified_display' => date($settings->date_display_format.' '.$settings->time_display_format, $file_timestamp),
'backup_url' => config('app.url').'/settings/backups/download/'.basename($backup_files[$f]),
];
$count++;
}
}
}
@ -264,15 +270,56 @@ class SettingsController extends Controller
}
/**
* Downloads a backup file.
* We use response()->download() here instead of Storage::download() because Storage::download()
* exhausts memory on larger files.
*
* @author [A. Gianotto]
* @return JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function downloadBackup($file) {
$path = 'app/backups';
if (Storage::exists($path.'/'.$file)) {
$path = storage_path('app/backups');
if (Storage::exists('app/backups/'.$file)) {
$headers = ['ContentType' => 'application/zip'];
return Storage::download($path.'/'.$file, $file, $headers);
return response()->download($path.'/'.$file, $file, $headers);
} else {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 404);
}
}
/**
* Determines and downloads the latest backup
*
* @author [A. Gianotto]
* @since [v6.3.1]
* @return JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function downloadLatestBackup() {
$fileData = collect();
foreach (Storage::files('app/backups') as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) == 'zip') {
$fileData->push([
'file' => $file,
'date' => Storage::lastModified($file)
]);
}
}
$newest = $fileData->sortByDesc('date')->first();
if (Storage::exists($newest['file'])) {
$headers = ['ContentType' => 'application/zip'];
return response()->download(storage_path($newest['file']), basename($newest['file']), $headers);
} else {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 404);
}
}
}

View file

@ -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();

View file

@ -148,30 +148,20 @@ class AssetMaintenancesController extends Controller
*/
public function edit($assetMaintenanceId = null)
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the improvement management page
return redirect()->route('maintenances.index')
->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif (! $assetMaintenance->asset) {
return redirect()->route('maintenances.index')
->with('error', 'The asset associated with this maintenance does not exist.');
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
if ($assetMaintenance->completion_date == '0000-00-00') {
$assetMaintenance->completion_date = null;
}
if ($assetMaintenance->start_date == '0000-00-00') {
$assetMaintenance->start_date = null;
}
if ($assetMaintenance->cost == '0.00') {
$assetMaintenance->cost = null;
}
// Prepare Improvement Type List
$assetMaintenanceType = [
@ -203,8 +193,10 @@ class AssetMaintenancesController extends Controller
// Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')
->with('error', trans('admin/asset_maintenances/message.not_found'));
return redirect()->route('maintenances.index')->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}

View file

@ -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')

View file

@ -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
@ -168,7 +169,7 @@ class LocationsController extends Controller
{
$this->authorize('delete', Location::class);
if (is_null($location = Location::find($locationId))) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.not_found'));
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.does_not_exist'));
}
if ($location->users()->count() > 0) {
@ -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,97 @@ 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)
->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')->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')]
));
}
}

View file

@ -296,8 +296,8 @@ class ReportsController extends Controller
e($actionlog->itemType()),
($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name,
($actionlog->item) ? $actionlog->item->serial : null,
($actionlog->item->model) ? htmlspecialchars($actionlog->item->model->name, ENT_NOQUOTES) : null,
($actionlog->item->model) ? $actionlog->item->model->model_number : null,
(($actionlog->item) && ($actionlog->item->model)) ? htmlspecialchars($actionlog->item->model->name, ENT_NOQUOTES) : null,
(($actionlog->item) && ($actionlog->item->model)) ? $actionlog->item->model->model_number : null,
$target_name,
($actionlog->note) ? e($actionlog->note) : '',
$actionlog->log_meta,

View file

@ -425,65 +425,64 @@ class SettingsController extends Controller
if (! config('app.lock_passwords')) {
$setting->site_name = $request->input('site_name');
$setting->custom_css = $request->input('custom_css');
}
$setting = $request->handleImages($setting, 600, 'logo', '', 'logo');
$setting = $request->handleImages($setting, 600, 'logo', '', 'logo');
if ('1' == $request->input('clear_logo')) {
if ('1' == $request->input('clear_logo')) {
Storage::disk('public')->delete($setting->logo);
$setting->logo = null;
$setting->logo = null;
$setting->brand = 1;
}
$setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
if ('1' == $request->input('clear_email_logo')) {
Storage::disk('public')->delete($setting->email_logo);
$setting->email_logo = null;
// If they are uploading an image, validate it and upload it
}
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ('1' == $request->input('clear_label_logo')) {
Storage::disk('public')->delete($setting->label_logo);
$setting->label_logo = null;
}
// If the user wants to clear the favicon...
if ($request->hasFile('favicon')) {
$favicon_image = $favicon_upload = $request->file('favicon');
$favicon_ext = $favicon_image->getClientOriginalExtension();
$setting->favicon = $favicon_file_name = 'favicon-uploaded.'.$favicon_ext;
if (($favicon_image->getClientOriginalExtension() != 'ico') && ($favicon_image->getClientOriginalExtension() != 'svg')) {
$favicon_upload = Image::make($favicon_image->getRealPath())->resize(null, 36, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
// This requires a string instead of an object, so we use ($string)
Storage::disk('public')->put($favicon_file_name, (string) $favicon_upload->encode());
} else {
Storage::disk('public')->put($favicon_file_name, file_get_contents($request->file('favicon')));
}
// Remove Current image if exists
if (($setting->favicon) && (file_exists($favicon_file_name))) {
Storage::disk('public')->delete($favicon_file_name);
}
} elseif ('1' == $request->input('clear_favicon')) {
Storage::disk('public')->delete($setting->clear_favicon);
$setting->favicon = null;
$setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
// If they are uploading an image, validate it and upload it
}
if ('1' == $request->input('clear_email_logo')) {
Storage::disk('public')->delete($setting->email_logo);
$setting->email_logo = null;
// If they are uploading an image, validate it and upload it
}
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ('1' == $request->input('clear_label_logo')) {
Storage::disk('public')->delete($setting->label_logo);
$setting->label_logo = null;
}
// If the user wants to clear the favicon...
if ($request->hasFile('favicon')) {
$favicon_image = $favicon_upload = $request->file('favicon');
$favicon_ext = $favicon_image->getClientOriginalExtension();
$setting->favicon = $favicon_file_name = 'favicon-uploaded.'.$favicon_ext;
if (($favicon_image->getClientOriginalExtension() != 'ico') && ($favicon_image->getClientOriginalExtension() != 'svg')) {
$favicon_upload = Image::make($favicon_image->getRealPath())->resize(null, 36, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
// This requires a string instead of an object, so we use ($string)
Storage::disk('public')->put($favicon_file_name, (string) $favicon_upload->encode());
} else {
Storage::disk('public')->put($favicon_file_name, file_get_contents($request->file('favicon')));
}
// Remove Current image if exists
if (($setting->favicon) && (file_exists($favicon_file_name))) {
Storage::disk('public')->delete($favicon_file_name);
}
} elseif ('1' == $request->input('clear_favicon')) {
Storage::disk('public')->delete($setting->clear_favicon);
$setting->favicon = null;
// If they are uploading an image, validate it and upload it
}
}
if ($setting->save()) {
return redirect()->route('settings.index')

View file

@ -97,7 +97,7 @@ class ImageUploadRequest extends Request
if (!config('app.lock_passwords')) {
$ext = $image->getClientOriginalExtension();
$ext = $image->guessExtension();
$file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext;
\Log::info('File name will be: '.$file_name);

View file

@ -28,12 +28,20 @@ class AssetMaintenancesTransformer
'id' => (int) $assetmaintenance->asset->id,
'name'=> ($assetmaintenance->asset->name) ? e($assetmaintenance->asset->name) : null,
'asset_tag'=> e($assetmaintenance->asset->asset_tag),
'serial'=> e($assetmaintenance->asset->serial),
'deleted_at'=> e($assetmaintenance->asset->deleted_at),
'created_at'=> e($assetmaintenance->asset->created_at),
] : null,
'model' => (($assetmaintenance->asset) && ($assetmaintenance->asset->model)) ? [
'id' => (int) $assetmaintenance->asset->model->id,
'name'=> ($assetmaintenance->asset->model->name) ? e($assetmaintenance->asset->model->name).' '.e($assetmaintenance->asset->model->model_number) : null,
] : null,
'status_label' => ($assetmaintenance->asset->assetstatus) ? [
'id' => (int) $assetmaintenance->asset->assetstatus->id,
'name'=> e($assetmaintenance->asset->assetstatus->name),
'status_type'=> e($assetmaintenance->asset->assetstatus->getStatuslabelType()),
'status_meta' => e($assetmaintenance->asset->present()->statusMeta),
] : null,
'company' => (($assetmaintenance->asset) && ($assetmaintenance->asset->company)) ? [
'id' => (int) $assetmaintenance->asset->company->id,
'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null,
@ -64,7 +72,7 @@ class AssetMaintenancesTransformer
];
$permissions_array['available_actions'] = [
'update' => Gate::allows('update', Asset::class),
'update' => (Gate::allows('update', Asset::class) && ($assetmaintenance->asset->deleted_at=='')) ? true : false,
'delete' => Gate::allows('delete', Asset::class),
];

View file

@ -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 == '')),
];

View file

@ -46,10 +46,9 @@ class AccessoryImporter extends ItemImporter
$this->item['min_amt'] = $this->findCsvMatch($row, "min_amt");
$accessory->fill($this->sanitizeItemForStoring($accessory));
//FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything.
// $accessory->unsetEventDispatcher();
// This sets an attribute on the Loggable trait for the action log
$accessory->setImported(true);
if ($accessory->save()) {
$accessory->logCreate('Imported using CSV Importer');
$this->log('Accessory '.$this->item['name'].' was created');
return;

View file

@ -135,10 +135,10 @@ class AssetImporter extends ItemImporter
$asset->{$custom_field} = $val;
}
}
// This sets an attribute on the Loggable trait for the action log
$asset->setImported(true);
if ($asset->save()) {
$asset->logCreate(trans('general.importer.import_note'));
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
// If we have a target to checkout to, lets do so.

View file

@ -48,10 +48,10 @@ class ComponentImporter extends ItemImporter
$this->log('No matching component, creating one');
$component = new Component;
$component->fill($this->sanitizeItemForStoring($component));
//FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything.
$component->unsetEventDispatcher();
// This sets an attribute on the Loggable trait for the action log
$component->setImported(true);
if ($component->save()) {
$component->logCreate('Imported using CSV Importer');
$this->log('Component '.$this->item['name'].' was created');
// If we have an asset tag, checkout to that asset.

View file

@ -45,10 +45,10 @@ class ConsumableImporter extends ItemImporter
$this->item['item_no'] = trim($this->findCsvMatch($row, 'item_number'));
$this->item['min_amt'] = trim($this->findCsvMatch($row, "min_amt"));
$consumable->fill($this->sanitizeItemForStoring($consumable));
//FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything.
$consumable->unsetEventDispatcher();
// This sets an attribute on the Loggable trait for the action log
$consumable->setImported(true);
if ($consumable->save()) {
$consumable->logCreate('Imported using CSV Importer');
$this->log('Consumable '.$this->item['name'].' was created');
return;

View file

@ -85,10 +85,10 @@ class LicenseImporter extends ItemImporter
} else {
$license->fill($this->sanitizeItemForStoring($license));
}
//FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything.
// $license->unsetEventDispatcher();
// This sets an attribute on the Loggable trait for the action log
$license->setImported(true);
if ($license->save()) {
$license->logCreate('Imported using csv importer');
$this->log('License '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
// Lets try to checkout seats if the fields exist and we have seats.

View file

@ -69,9 +69,9 @@ class CheckoutableListener
}
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkout notification: " . $e->getMessage());
Log::warning("Exception caught during checkout notification: " . $e->getMessage());
} catch (Exception $e) {
Log::error("Exception caught during checkout notification: " . $e->getMessage());
Log::warning("Exception caught during checkout notification: " . $e->getMessage());
}
}
@ -115,7 +115,7 @@ class CheckoutableListener
);
}
//slack doesn't include the url in its messaging format so this is needed to hit the endpoint
if(Setting::getSettings()->webhook_selected =='slack') {
if(Setting::getSettings()->webhook_selected =='slack' || Setting::getSettings()->webhook_selected =='general') {
if ($this->shouldSendWebhookNotification()) {
Notification::route('slack', Setting::getSettings()->webhook_endpoint)
@ -124,9 +124,9 @@ class CheckoutableListener
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkout notification: " . $e->getMessage());
Log::warning("Exception caught during checkout notification: " . $e->getMessage());
} catch (Exception $e) {
Log::error("Exception caught during checkin notification: " . $e->getMessage());
Log::warning("Exception caught during checkin notification: " . $e->getMessage());
}
}

View file

@ -19,6 +19,9 @@ class Actionlog extends SnipeModel
{
use HasFactory;
// This is to manually set the source (via setActionSource()) for determineActionSource()
protected ?string $source = null;
protected $presenter = \App\Presenters\ActionlogPresenter::class;
use SoftDeletes;
use Presentable;
@ -341,7 +344,12 @@ class Actionlog extends SnipeModel
* @since v6.3.0
* @return string
*/
public function determineActionSource() {
public function determineActionSource(): string
{
// This is a manually set source
if($this->source) {
return $this->source;
}
// This is an API call
if (((request()->header('content-type') && (request()->header('accept'))=='application/json'))
@ -358,4 +366,10 @@ class Actionlog extends SnipeModel
return 'cli/unknown';
}
// Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void
{
$this->source = $source;
}
}

View file

@ -1560,7 +1560,7 @@ class Asset extends Depreciable
*
* In short, this set of statements tells the query builder to ONLY query against an
* actual field that's being passed if it doesn't meet known relational fields. This
* allows us to query custom fields directly in the assetsv table
* allows us to query custom fields directly in the assets table
* (regardless of their name) and *skip* any fields that we already know can only be
* searched through relational searches that we do earlier in this method.
*

View file

@ -62,7 +62,15 @@ class AssetMaintenance extends Model implements ICompanyableChild
*
* @var array
*/
protected $searchableAttributes = ['title', 'notes', 'asset_maintenance_type', 'cost', 'start_date', 'completion_date'];
protected $searchableAttributes =
[
'title',
'notes',
'asset_maintenance_type',
'cost',
'start_date',
'completion_date'
];
/**
* The relations and their attributes that should be included when searching the model.
@ -70,9 +78,10 @@ class AssetMaintenance extends Model implements ICompanyableChild
* @var array
*/
protected $searchableRelations = [
'asset' => ['name', 'asset_tag'],
'asset' => ['name', 'asset_tag', 'serial'],
'asset.model' => ['name', 'model_number'],
'asset.supplier' => ['name'],
'asset.assetstatus' => ['name'],
'supplier' => ['name'],
];
@ -197,6 +206,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
->orderBy('suppliers_maintenances.name', $order);
}
/**
* Query builder scope to order on admin user
*
@ -239,4 +249,33 @@ class AssetMaintenance extends Model implements ICompanyableChild
return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.name', $order);
}
/**
* Query builder scope to order on serial
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderByAssetSerial($query, $order)
{
return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.serial', $order);
}
/**
* Query builder scope to order on status label name
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderStatusName($query, $order)
{
return $query->join('assets as maintained_asset', 'asset_maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('status_labels as maintained_asset_status', 'maintained_asset_status.id', '=', 'maintained_asset.status_id')
->orderBy('maintained_asset_status.name', $order);
}
}

View file

@ -95,7 +95,10 @@ class Location extends SnipeModel
/**
* Determine whether or not this location can be deleted
* Determine whether or not this location can be deleted.
*
* This method requires the eager loading of the relationships in order to determine whether
* it can be deleted. It's tempting to load those here, but that increases the query load considerably.
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v3.0]
@ -104,9 +107,10 @@ class Location extends SnipeModel
public function isDeletable()
{
return Gate::allows('delete', $this)
&& ($this->assignedAssets()->count() === 0)
&& ($this->assets()->count() === 0)
&& ($this->users()->count() === 0);
&& ($this->assets_count === 0)
&& ($this->assigned_assets_count === 0)
&& ($this->children_count === 0)
&& ($this->users_count === 0);
}
/**

View file

@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Auth;
trait Loggable
{
// an attribute for setting whether or not the item was imported
public ?bool $imported = false;
/**
* @author Daniel Meltzer <dmeltzer.devel@gmail.com>
* @since [v3.4]
@ -18,6 +21,11 @@ trait Loggable
return $this->morphMany(Actionlog::class, 'item');
}
public function setImported(bool $bool): void
{
$this->imported = $bool;
}
/**
* @author Daniel Meltzer <dmeltzer.devel@gmail.com>
* @since [v3.4]

View file

@ -63,34 +63,8 @@ class CheckinAccessoryNotification extends Notification
if ($this->target instanceof User && $this->target->email != '') {
\Log::debug('The target is a user');
/**
* Send an email if the asset requires acceptance,
* so the user can accept or decline the asset
*/
if (($this->item->requireAcceptance()) || ($this->item->getEula()) || ($this->item->checkin_email())) {
$notifyBy[] = 'mail';
}
/**
* Send an email if the asset requires acceptance,
* so the user can accept or decline the asset
*/
if ($this->item->requireAcceptance()) {
\Log::debug('This accessory requires acceptance');
}
/**
* Send an email if the item has a EULA, since the user should always receive it
*/
if ($this->item->getEula()) {
\Log::debug('This accessory has a EULA');
}
/**
* Send an email if an email should be sent at checkin/checkout
*/
if ($this->item->checkin_email()) {
\Log::debug('This accessory has a checkin_email()');
$notifyBy[] = 'mail';
}
}

View file

@ -38,6 +38,9 @@ class AccessoryObserver
$logAction->item_id = $accessory->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
if($accessory->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create');
}

View file

@ -109,6 +109,9 @@ class AssetObserver
$logAction->item_id = $asset->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
if($asset->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create');
}

View file

@ -38,6 +38,9 @@ class ComponentObserver
$logAction->item_id = $component->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
if($component->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create');
}

View file

@ -38,6 +38,9 @@ class ConsumableObserver
$logAction->item_id = $consumable->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
if($consumable->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create');
}

View file

@ -38,6 +38,9 @@ class LicenseObserver
$logAction->item_id = $license->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
if($license->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create');
}

View file

@ -41,6 +41,19 @@ class AssetMaintenancesPresenter extends Presenter
'sortable' => true,
'title' => trans('admin/hardware/table.asset_tag'),
'formatter' => 'assetTagLinkFormatter',
], [
'field' => 'serial',
'searchable' => true,
'sortable' => true,
'title' => trans('admin/hardware/table.serial'),
'formatter' => 'assetSerialLinkFormatter',
], [
'field' => 'status_label',
'searchable' => true,
'sortable' => true,
'title' => trans('admin/hardware/table.status'),
'visible' => true,
'formatter' => 'statuslabelsLinkObjFormatter',
], [
'field' => 'model',
'searchable' => true,

View file

@ -14,7 +14,11 @@ class LocationPresenter extends Presenter
public static function dataTableLayout()
{
$layout = [
[
'field' => 'bulk_selectable',
'checkbox' => true,
'formatter' => 'checkboxEnabledFormatter',
],
[
'field' => 'id',
'searchable' => false,

View file

@ -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;
});

View file

@ -90,13 +90,9 @@ class Label implements View
$assetData->put('id', $asset->id);
$assetData->put('tag', $asset->asset_tag);
if ($template->getSupportTitle()) {
if ($asset->company && !empty($settings->label2_title)) {
$title = str_replace('{COMPANY}', $asset->company->name, $settings->label2_title);
$settings->qr_text;
$assetData->put('title', $title);
}
if ($template->getSupportTitle() && !empty($settings->label2_title)) {
$title = str_replace('{COMPANY}', data_get($asset, 'company.name'), $settings->label2_title);
$assetData->put('title', $title);
}
if ($template->getSupportLogo()) {
@ -216,4 +212,4 @@ class Label implements View
return self::NAME;
}
}
}

View file

@ -77,7 +77,8 @@
"watson/validating": "^6.1"
},
"suggest": {
"ext-ldap": "*"
"ext-ldap": "*",
"ext-zip": "*"
},
"require-dev": {
"brianium/paratest": "^6.6",

44
composer.lock generated
View file

@ -4804,16 +4804,16 @@
},
{
"name": "league/oauth2-server",
"version": "8.3.5",
"version": "8.4.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/oauth2-server.git",
"reference": "7aeb7c42b463b1a6fe4d084d3145e2fa22436876"
"reference": "007dc5f6c0151a73b133fec36c9686cc956209d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/7aeb7c42b463b1a6fe4d084d3145e2fa22436876",
"reference": "7aeb7c42b463b1a6fe4d084d3145e2fa22436876",
"url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/007dc5f6c0151a73b133fec36c9686cc956209d3",
"reference": "007dc5f6c0151a73b133fec36c9686cc956209d3",
"shasum": ""
},
"require": {
@ -4880,7 +4880,7 @@
],
"support": {
"issues": "https://github.com/thephpleague/oauth2-server/issues",
"source": "https://github.com/thephpleague/oauth2-server/tree/8.3.5"
"source": "https://github.com/thephpleague/oauth2-server/tree/8.4.2"
},
"funding": [
{
@ -4888,7 +4888,7 @@
"type": "github"
}
],
"time": "2022-05-03T21:21:28+00:00"
"time": "2023-08-02T22:54:39+00:00"
},
{
"name": "league/uri",
@ -9669,16 +9669,16 @@
},
{
"name": "symfony/http-kernel",
"version": "v5.4.10",
"version": "v5.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "255ae3b0a488d78fbb34da23d3e0c059874b5948"
"reference": "aaeec341582d3c160cc9ecfa8b2419ba6c69954e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/255ae3b0a488d78fbb34da23d3e0c059874b5948",
"reference": "255ae3b0a488d78fbb34da23d3e0c059874b5948",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/aaeec341582d3c160cc9ecfa8b2419ba6c69954e",
"reference": "aaeec341582d3c160cc9ecfa8b2419ba6c69954e",
"shasum": ""
},
"require": {
@ -9761,7 +9761,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v5.4.10"
"source": "https://github.com/symfony/http-kernel/tree/v5.4.20"
},
"funding": [
{
@ -9777,7 +9777,7 @@
"type": "tidelift"
}
],
"time": "2022-06-26T16:57:59+00:00"
"time": "2023-02-01T08:18:48+00:00"
},
{
"name": "symfony/mime",
@ -12647,16 +12647,16 @@
},
{
"name": "composer/composer",
"version": "2.6.6",
"version": "2.7.1",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
"reference": "683557bd2466072777309d039534bb1332d0dda5"
"reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/composer/zipball/683557bd2466072777309d039534bb1332d0dda5",
"reference": "683557bd2466072777309d039534bb1332d0dda5",
"url": "https://api.github.com/repos/composer/composer/zipball/aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc",
"reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc",
"shasum": ""
},
"require": {
@ -12674,7 +12674,7 @@
"seld/jsonlint": "^1.4",
"seld/phar-utils": "^1.2",
"seld/signal-handler": "^2.0",
"symfony/console": "^5.4.11 || ^6.0.11",
"symfony/console": "^5.4.11 || ^6.0.11 || ^7",
"symfony/filesystem": "^5.4 || ^6.0 || ^7",
"symfony/finder": "^5.4 || ^6.0 || ^7",
"symfony/polyfill-php73": "^1.24",
@ -12688,7 +12688,7 @@
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1",
"phpstan/phpstan-symfony": "^1.2.10",
"symfony/phpunit-bridge": "^6.0 || ^7"
"symfony/phpunit-bridge": "^6.4.1 || ^7.0.1"
},
"suggest": {
"ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
@ -12701,7 +12701,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.6-dev"
"dev-main": "2.7-dev"
},
"phpstan": {
"includes": [
@ -12741,7 +12741,7 @@
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/composer/issues",
"security": "https://github.com/composer/composer/security/policy",
"source": "https://github.com/composer/composer/tree/2.6.6"
"source": "https://github.com/composer/composer/tree/2.7.1"
},
"funding": [
{
@ -12757,7 +12757,7 @@
"type": "tidelift"
}
],
"time": "2023-12-08T17:32:26+00:00"
"time": "2024-02-09T14:26:28+00:00"
},
{
"name": "composer/metadata-minifier",
@ -17088,5 +17088,5 @@
"ext-pdo": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View file

@ -1,10 +1,10 @@
<?php
return array (
'app_version' => 'v6.3.0',
'full_app_version' => 'v6.3.0 - build 12490-g9136415bb',
'build_version' => '12490',
'app_version' => 'v6.3.1',
'full_app_version' => 'v6.3.1 - build 12672-g00cea3eb3',
'build_version' => '12672',
'prerelease_version' => '',
'hash_version' => 'g9136415bb',
'full_hash' => 'v6.3.0-729-g9136415bb',
'branch' => 'develop',
'hash_version' => 'g00cea3eb3',
'full_hash' => 'v6.3.1-180-g00cea3eb3',
'branch' => 'master',
);

View file

@ -8,6 +8,7 @@ use App\Models\Location;
use App\Models\Manufacturer;
use App\Models\Supplier;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
class AccessoryFactory extends Factory
@ -140,4 +141,16 @@ class AccessoryFactory extends Factory
$accessory->category->update(['require_acceptance' => 1]);
});
}
public function checkedOutToUser(User $user = null)
{
return $this->afterCreating(function (Accessory $accessory) use ($user) {
$accessory->users()->attach($accessory->id, [
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'user_id' => 1,
'assigned_to' => $user->id ?? User::factory()->create()->id,
]);
});
}
}

93
package-lock.json generated
View file

@ -1565,9 +1565,9 @@
}
},
"@types/eslint": {
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.1.tgz",
"integrity": "sha512-18PLWRzhy9glDQp3+wOgfLYRWlhgX0azxgJ63rdpoUHyrC9z0f5CkFburjQx4uD7ZCruw85ZtMt6K+L+R8fLJQ==",
"version": "8.56.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz",
"integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==",
"requires": {
"@types/estree": "*",
"@types/json-schema": "*"
@ -2350,9 +2350,9 @@
}
},
"alpinejs": {
"version": "3.13.3",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz",
"integrity": "sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg==",
"version": "3.13.5",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.5.tgz",
"integrity": "sha512-1d2XeNGN+Zn7j4mUAKXtAgdc4/rLeadyTMWeJGXF5DzwawPBxwTiBhFFm6w/Ei8eJxUZeyNWWSD9zknfdz1kEw==",
"requires": {
"@vue/reactivity": "~3.1.1"
}
@ -3611,6 +3611,7 @@
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz",
"integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001286",
"electron-to-chromium": "^1.4.17",
@ -3731,7 +3732,8 @@
"caniuse-lite": {
"version": "1.0.30001292",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001292.tgz",
"integrity": "sha512-jnT4Tq0Q4ma+6nncYQVe7d73kmDmE9C3OGTx3MvW7lBM/eY1S1DZTMBON7dqV481RhNiS5OxD7k9JQvmDOTirw=="
"integrity": "sha512-jnT4Tq0Q4ma+6nncYQVe7d73kmDmE9C3OGTx3MvW7lBM/eY1S1DZTMBON7dqV481RhNiS5OxD7k9JQvmDOTirw==",
"dev": true
},
"canvg": {
"version": "3.0.10",
@ -4931,7 +4933,8 @@
"electron-to-chromium": {
"version": "1.4.28",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.28.tgz",
"integrity": "sha512-Gzbf0wUtKfyPaqf0Plz+Ctinf9eQIzxEqBHwSvbGfeOm9GMNdLxyu1dNiCUfM+x6r4BE0xUJNh3Nmg9gfAtTmg=="
"integrity": "sha512-Gzbf0wUtKfyPaqf0Plz+Ctinf9eQIzxEqBHwSvbGfeOm9GMNdLxyu1dNiCUfM+x6r4BE0xUJNh3Nmg9gfAtTmg==",
"dev": true
},
"elliptic": {
"version": "6.5.4",
@ -17418,7 +17421,8 @@
"node-releases": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz",
"integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA=="
"integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==",
"dev": true
},
"normalize-path": {
"version": "3.0.0",
@ -20097,18 +20101,18 @@
"dev": true
},
"webpack": {
"version": "5.89.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
"integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==",
"version": "5.90.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz",
"integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==",
"requires": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.0",
"@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.11.5",
"@webassemblyjs/wasm-edit": "^1.11.5",
"@webassemblyjs/wasm-parser": "^1.11.5",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.9.0",
"browserslist": "^4.14.5",
"browserslist": "^4.21.10",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.15.0",
"es-module-lexer": "^1.2.1",
@ -20122,15 +20126,15 @@
"neo-async": "^2.6.2",
"schema-utils": "^3.2.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.7",
"terser-webpack-plugin": "^5.3.10",
"watchpack": "^2.4.0",
"webpack-sources": "^3.2.3"
},
"dependencies": {
"@jridgewell/resolve-uri": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA=="
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.15",
@ -20138,9 +20142,9 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"@jridgewell/trace-mapping": {
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
"integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
"requires": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@ -20151,16 +20155,42 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
"browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"requires": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
}
},
"caniuse-lite": {
"version": "1.0.30001587",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz",
"integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA=="
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"electron-to-chromium": {
"version": "1.4.670",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.670.tgz",
"integrity": "sha512-hcijYOWjOtjKrKPtNA6tuLlA/bTLO3heFG8pQA6mLpq7dRydSWicXova5lyxDzp1iVJaYhK7J2OQlGE52KYn7A=="
},
"graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
"schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
@ -20172,17 +20202,17 @@
}
},
"serialize-javascript": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"requires": {
"randombytes": "^2.1.0"
}
},
"terser": {
"version": "5.26.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz",
"integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==",
"version": "5.27.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
"integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
"requires": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@ -20202,6 +20232,15 @@
"terser": "^5.26.0"
}
},
"update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"requires": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
}
},
"webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",

View file

@ -33,7 +33,7 @@
"acorn-import-assertions": "^1.9.0",
"admin-lte": "^2.4.18",
"ajv": "^6.12.6",
"alpinejs": "^3.13.3",
"alpinejs": "^3.13.5",
"blueimp-file-upload": "^9.34.0",
"bootstrap": "^3.4.1",
"bootstrap-colorpicker": "^2.5.3",
@ -59,6 +59,6 @@
"tableexport.jquery.plugin": "1.28.0",
"tether": "^1.4.0",
"vue-resource": "^1.5.2",
"webpack": "^5.89.0"
"webpack": "^5.90.0"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/dist/all.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -1,24 +1,24 @@
{
"/js/build/app.js": "/js/build/app.js?id=ea5f3edebafdb29b616d23fa89106080",
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=f677207c6cf9678eb539abecb408c374",
"/css/build/overrides.css": "/css/build/overrides.css?id=9a69d65b9f2b35e6d17c6a72e76424d5",
"/css/build/app.css": "/css/build/app.css?id=5a516232bc9f1488514577cf3c8108eb",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=dc383f8560a8d4adb51d44fb4043e03b",
"/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=6f0563e726c2fe4fab4026daaa5bfdf2",
"/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=e6e53eef152bba01a4c666a4d8b01117",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=07273f6ca3c698a39e8fc2075af4fa07",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=c1f33574ecb9d3e69d9b8fe5bd68e101",
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=7b315b9612b8fde8f9c5b0ddb6bba690",
"/css/dist/skins/skin-purple-dark.css": "/css/dist/skins/skin-purple-dark.css?id=7d92dea45d94be7e1d4e427c728d335d",
"/css/dist/skins/skin-purple.css": "/css/dist/skins/skin-purple.css?id=6fe68325d5356197672c27bc77cedcb4",
"/css/dist/skins/skin-red-dark.css": "/css/dist/skins/skin-red-dark.css?id=8ca888bbc050d9680cbb65021382acba",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=b061bb141af3bdb6280c6ee772cf8f4f",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=76482123f6c70e866d6b971ba91de7bb",
"/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=d419cb63a12dc175d71645c876bfc2ab",
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=0a82a6ae6bb4e58fe62d162c4fb50397",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=da6c7997d9de2f8329142399f0ce50da",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=44bf834f2110504a793dadec132a5898",
"/css/dist/all.css": "/css/dist/all.css?id=ebd6663d2f61487038c9947111be2c73",
"/js/build/app.js": "/js/build/app.js?id=cad71122a8a3f0cd6c5960e95f4f0f8a",
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=392cc93cfc0be0349bab9697669dd091",
"/css/build/overrides.css": "/css/build/overrides.css?id=6a7f37afafaaf9ccea99a7391cdf02b2",
"/css/build/app.css": "/css/build/app.css?id=ea3875faceb1d09c162d00fbf5b4df57",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=f25c77ed07053646a42e9c19923d24fa",
"/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=268041e902b019730c23ee3875838005",
"/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=d409d9b1a3b69247df8b98941ba06e33",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=4a9e8c5e7b09506fa3e3a3f42849e07f",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=21fef066e0bb1b02fd83fcb6694fad5f",
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=fc7adb943668ac69fe4b646625a7571f",
"/css/dist/skins/skin-purple-dark.css": "/css/dist/skins/skin-purple-dark.css?id=9f944e8021781af1ce45d27765d1c0c2",
"/css/dist/skins/skin-purple.css": "/css/dist/skins/skin-purple.css?id=cf6c8c340420724b02d6e787ef9bded5",
"/css/dist/skins/skin-red-dark.css": "/css/dist/skins/skin-red-dark.css?id=7f0eb9e355b36b41c61c3af3b4d41143",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=6cf460bed48ab738041f60231a3f005a",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
"/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=0ed42b67f9b02a74815e885bfd9e3f66",
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=f0fbbb0ac729ea092578fb05ca615460",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=b9a74ec0cd68f83e7480d5ae39919beb",
"/css/dist/all.css": "/css/dist/all.css?id=2ef1965d45a0a72336dd8e9b93f82d80",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=69e5d8e4e818f05fd882cceb758d1eba",
@ -32,21 +32,21 @@
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=afa255bf30b2a7c11a97e3165128d183",
"/js/build/vendor.js": "/js/build/vendor.js?id=a2b971da417306a63385c8098acfe4af",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=29340c70d13855fa0165cd4d799c6f5b",
"/js/dist/all.js": "/js/dist/all.js?id=13bdb521e0c745d7f81dae3fb110b650",
"/js/dist/all-defer.js": "/js/dist/all-defer.js?id=7f9a130eda6916eaa32a0a57e81918f3",
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=0a82a6ae6bb4e58fe62d162c4fb50397",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=d419cb63a12dc175d71645c876bfc2ab",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=76482123f6c70e866d6b971ba91de7bb",
"/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=b061bb141af3bdb6280c6ee772cf8f4f",
"/css/dist/skins/skin-blue.min.css": "/css/dist/skins/skin-blue.min.css?id=f677207c6cf9678eb539abecb408c374",
"/css/dist/skins/skin-blue-dark.min.css": "/css/dist/skins/skin-blue-dark.min.css?id=07273f6ca3c698a39e8fc2075af4fa07",
"/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=7b315b9612b8fde8f9c5b0ddb6bba690",
"/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=c1f33574ecb9d3e69d9b8fe5bd68e101",
"/css/dist/skins/skin-red.min.css": "/css/dist/skins/skin-red.min.css?id=44bf834f2110504a793dadec132a5898",
"/css/dist/skins/skin-red-dark.min.css": "/css/dist/skins/skin-red-dark.min.css?id=8ca888bbc050d9680cbb65021382acba",
"/css/dist/skins/skin-purple.min.css": "/css/dist/skins/skin-purple.min.css?id=6fe68325d5356197672c27bc77cedcb4",
"/css/dist/skins/skin-purple-dark.min.css": "/css/dist/skins/skin-purple-dark.min.css?id=7d92dea45d94be7e1d4e427c728d335d",
"/css/dist/skins/skin-orange.min.css": "/css/dist/skins/skin-orange.min.css?id=6f0563e726c2fe4fab4026daaa5bfdf2",
"/css/dist/skins/skin-orange-dark.min.css": "/css/dist/skins/skin-orange-dark.min.css?id=e6e53eef152bba01a4c666a4d8b01117",
"/css/dist/skins/skin-contrast.min.css": "/css/dist/skins/skin-contrast.min.css?id=da6c7997d9de2f8329142399f0ce50da"
"/js/dist/all.js": "/js/dist/all.js?id=a9bde3f908f73634c0d1b4cd3f835092",
"/js/dist/all-defer.js": "/js/dist/all-defer.js?id=19ccc62a8f1ea103dede4808837384d4",
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=0ed42b67f9b02a74815e885bfd9e3f66",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
"/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=6cf460bed48ab738041f60231a3f005a",
"/css/dist/skins/skin-blue.min.css": "/css/dist/skins/skin-blue.min.css?id=392cc93cfc0be0349bab9697669dd091",
"/css/dist/skins/skin-blue-dark.min.css": "/css/dist/skins/skin-blue-dark.min.css?id=4a9e8c5e7b09506fa3e3a3f42849e07f",
"/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=fc7adb943668ac69fe4b646625a7571f",
"/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=21fef066e0bb1b02fd83fcb6694fad5f",
"/css/dist/skins/skin-red.min.css": "/css/dist/skins/skin-red.min.css?id=b9a74ec0cd68f83e7480d5ae39919beb",
"/css/dist/skins/skin-red-dark.min.css": "/css/dist/skins/skin-red-dark.min.css?id=7f0eb9e355b36b41c61c3af3b4d41143",
"/css/dist/skins/skin-purple.min.css": "/css/dist/skins/skin-purple.min.css?id=cf6c8c340420724b02d6e787ef9bded5",
"/css/dist/skins/skin-purple-dark.min.css": "/css/dist/skins/skin-purple-dark.min.css?id=9f944e8021781af1ce45d27765d1c0c2",
"/css/dist/skins/skin-orange.min.css": "/css/dist/skins/skin-orange.min.css?id=268041e902b019730c23ee3875838005",
"/css/dist/skins/skin-orange-dark.min.css": "/css/dist/skins/skin-orange-dark.min.css?id=d409d9b1a3b69247df8b98941ba06e33",
"/css/dist/skins/skin-contrast.min.css": "/css/dist/skins/skin-contrast.min.css?id=f0fbbb0ac729ea092578fb05ca615460"
}

View file

@ -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.',
@ -459,6 +459,7 @@ return [
'no_autoassign_licenses_help' => 'Do not include user for bulk-assigning through the license UI or cli tools.',
'modal_confirm_generic' => 'Are you sure?',
'cannot_be_deleted' => 'This item cannot be deleted',
'cannot_be_edited' => 'This item cannot be edited.',
'undeployable_tooltip' => 'This item cannot be checked out. Check the quantity remaining.',
'serial_number' => 'Serial Number',
'item_notes' => ':item Notes',
@ -501,5 +502,17 @@ return [
'action_source' => 'Action Source',
'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',
],
],
];

View file

@ -279,7 +279,7 @@
</strong>
</div>
<div class="col-md-12">
{!! nl2br(e($component->notes)) !!}
{!! nl2br(Helper::parseEscapedMarkedownInline($component->notes)) !!}
</div>
</div>
@endif

View file

@ -77,7 +77,7 @@
@can('update', $custom_fieldset)
<form method="post" action="{{ route('fields.disassociate', [$field, $custom_fieldset->id]) }}">
@csrf
<button type="submit" class="btn btn-sm btn-danger">{{ trans('button.remove') }}</button>
<button type="submit" class="btn btn-sm btn-danger"><i class="fa fa-trash icon-white" aria-hidden="true"></i></button>
</form>
@endcan
</td>
@ -90,35 +90,34 @@
<td colspan="8">
{{ Form::open(['route' =>
["fieldsets.associate",$custom_fieldset->id],
'class'=>'form-horizontal',
'class'=>'form-inline',
'id' => 'ordering']) }}
<div class="form-group col-md-4">
<div class="form-group">
<label for="field_id" class="sr-only">
{{ trans('admin/custom-field/general.add_field_to_fieldset')}}
</label>
{{ Form::select("field_id",$custom_fields_list,"",['aria-label'=>'field_id', 'class'=>'select2']) }}
{{ Form::select("field_id",$custom_fields_list,"",['aria-label'=>'field_id', 'class'=>'select2', 'style' => 'min-width:400px;']) }}
</div>
<div class="form-group col-md-2" style="vertical-align: middle;">
<label class="form-control">
{{ Form::checkbox('required', 'on', old('required'), array('aria-label'=>'required')) }}
{{ trans('admin/custom_fields/general.required') }}
</label>
</div>
<div class="form-group col-md-2" style="display: none;">
{{ Form::text('order', $maxid, array('class' => 'form-control col-sm-1 col-md-1', 'style'=> 'width: 80px; padding-;right: 10px;', 'aria-label'=>'order', 'maxlength'=>'3', 'size'=>'3')) }}
<div class="form-group" style="display: none;">
{{ Form::text('order', $maxid, array('aria-label'=>'order', 'maxlength'=>'3', 'size'=>'3')) }}
<label for="order">{{ trans('admin/custom_fields/general.order') }}</label>
</div>
<div class="form-group col-md-3">
<button type="submit" class="btn btn-primary"> {{ trans('general.save') }}</button>
<div class="checkbox-inline">
<label>
{{ Form::checkbox('required', 'on', old('required')) }}
<span style="padding-left: 10px;">{{ trans('admin/custom_fields/general.required') }}</span>
</label>
</div>
<span style="padding-left: 10px;">
<button type="submit" class="btn btn-primary"> {{ trans('general.save') }}</button>
</span>
{{ Form::close() }}
</td>

View file

@ -73,7 +73,14 @@
<nobr>
@can('update', $fieldset)
<a href="{{ route('fieldsets.edit', $fieldset->id) }}" class="btn btn-warning btn-sm">
<a href="{{ route('fieldsets.show', ['fieldset' => $fieldset->id]) }}" data-tooltip="true" title="{{ trans('general.edit_fieldset') }}">
<button type="submit" class="btn btn-info btn-sm">
<i class="fa-regular fa-rectangle-list"></i>
</button>
</a>
<a href="{{ route('fieldsets.edit', $fieldset->id) }}" class="btn btn-warning btn-sm" data-tooltip="true" title="{{ trans('general.update') }}">
<i class="fas fa-pencil-alt" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.edit') }}</span>
</a>
@ -82,9 +89,9 @@
@can('delete', $fieldset)
{{ Form::open(['route' => array('fieldsets.destroy', $fieldset->id), 'method' => 'delete','style' => 'display:inline-block']) }}
@if($fieldset->models->count() > 0)
<button type="submit" class="btn btn-danger btn-sm disabled" disabled><i class="fas fa-trash"></i></button>
<button type="submit" class="btn btn-danger btn-sm disabled" data-tooltip="true" title="{{ trans('general.cannot_be_deleted') }}" disabled><i class="fas fa-trash"></i></button>
@else
<button type="submit" class="btn btn-danger btn-sm"><i class="fas fa-trash"></i></button>
<button type="submit" class="btn btn-danger btn-sm" data-tooltip="true" title="{{ trans('general.delete') }}"><i class="fas fa-trash"></i></button>
@endif
{{ Form::close() }}
@endcan
@ -188,7 +195,7 @@
<nobr>
{{ Form::open(array('route' => array('fields.destroy', $field->id), 'method' => 'delete', 'style' => 'display:inline-block')) }}
@can('update', $field)
<a href="{{ route('fields.edit', $field->id) }}" class="btn btn-warning btn-sm">
<a href="{{ route('fields.edit', $field->id) }}" class="btn btn-warning btn-sm" data-tooltip="true" title="{{ trans('general.update') }}">
<i class="fas fa-pencil-alt" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.edit') }}</span>
</a>
@ -197,11 +204,11 @@
@can('delete', $field)
@if($field->fieldset->count()>0)
<button type="submit" class="btn btn-danger btn-sm disabled" disabled>
<button type="submit" class="btn btn-danger btn-sm disabled" data-tooltip="true" title="{{ trans('general.cannot_be_deleted') }}" disabled>
<i class="fas fa-trash" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.delete') }}</span></button>
@else
<button type="submit" class="btn btn-danger btn-sm">
<button type="submit" class="btn btn-danger btn-sm" data-tooltip="true" title="{{ trans('general.delete') }}">
<i class="fas fa-trash" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.delete') }}</span>
</button>

View file

@ -872,11 +872,13 @@
@can('update', $asset)
@if ($asset->deleted_at=='')
<div class="col-md-12" style="padding-top: 5px;">
<a href="{{ route('hardware.edit', $asset->id) }}" style="width: 100%;" class="btn btn-sm btn-primary hidden-print">
{{ trans('admin/hardware/general.edit') }}
</a>
</div>
@endif
@endcan
@can('create', $asset)

View file

@ -140,17 +140,15 @@
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
@can('index', \App\Models\Asset::class)
<li aria-hidden="true"
{!! (Request::is('hardware*') ? ' class="active"' : '') !!} tabindex="-1">
<li aria-hidden="true"{!! (Request::is('hardware*') ? ' class="active"' : '') !!}>
<a href="{{ url('hardware') }}" accesskey="1" tabindex="-1">
<i class="fas fa-barcode fa-fw" aria-hidden="true"></i>
<i class="fas fa-barcode fa-fw"></i>
<span class="sr-only">{{ trans('general.assets') }}</span>
</a>
</li>
@endcan
@can('view', \App\Models\License::class)
<li aria-hidden="true"
{!! (Request::is('licenses*') ? ' class="active"' : '') !!} tabindex="-1">
<li aria-hidden="true"{!! (Request::is('licenses*') ? ' class="active"' : '') !!}>
<a href="{{ route('licenses.index') }}" accesskey="2" tabindex="-1">
<i class="far fa-save fa-fw"></i>
<span class="sr-only">{{ trans('general.licenses') }}</span>
@ -158,8 +156,7 @@
</li>
@endcan
@can('index', \App\Models\Accessory::class)
<li aria-hidden="true"
{!! (Request::is('accessories*') ? ' class="active"' : '') !!} tabindex="-1">
<li aria-hidden="true"{!! (Request::is('accessories*') ? ' class="active"' : '') !!}>
<a href="{{ route('accessories.index') }}" accesskey="3" tabindex="-1">
<i class="far fa-keyboard fa-fw"></i>
<span class="sr-only">{{ trans('general.accessories') }}</span>
@ -233,7 +230,8 @@
<li {!! (Request::is('accessories/create') ? 'class="active"' : '') !!}>
<a href="{{ route('accessories.create') }}" tabindex="-1">
<i class="far fa-keyboard fa-fw" aria-hidden="true"></i>
{{ trans('general.accessory') }}</a>
{{ trans('general.accessory') }}
</a>
</li>
@endcan
@can('create', \App\Models\Consumable::class)
@ -982,7 +980,7 @@
container: 'body',
animation: true,
});
$('[data-toggle="popover"]').popover();
$('.select2 span').addClass('needsclick');
$('.select2 span').removeAttr('title');

View 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

View file

@ -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"

View file

@ -309,7 +309,7 @@
@if ($model->notes)
<li>
{{ trans('general.notes') }}:
{{ $model->notes }}
{!! nl2br(Helper::parseEscapedMarkedownInline($model->notes)) !!}
</li>
@endif

View file

@ -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');
}
});
@ -296,6 +296,10 @@
if ((row.available_actions) && (row.available_actions.update === true)) {
actions += '<a href="{{ config('app.url') }}/' + dest + '/' + row.id + '/edit" class="actions btn btn-sm btn-warning" data-tooltip="true" title="{{ trans('general.update') }}"><i class="fas fa-pencil-alt" aria-hidden="true"></i><span class="sr-only">{{ trans('general.update') }}</span></a>&nbsp;';
} else {
if ((row.available_actions) && (row.available_actions.update != true)) {
actions += '<span data-tooltip="true" title="{{ trans('general.cannot_be_edited') }}"><a class="btn btn-warning btn-sm disabled" onClick="return false;"><i class="fas fa-pencil-alt"></i></a></span>&nbsp;';
}
}
if ((row.available_actions) && (row.available_actions.delete === true)) {
@ -394,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>';
@ -623,6 +645,9 @@
function assetTagLinkFormatter(value, row) {
if ((row.asset) && (row.asset.id)) {
if (row.asset.deleted_at!='') {
return '<span style="white-space: nowrap;"><i class="fas fa-times text-danger"></i><span class="sr-only">deleted</span> <del><a href="{{ config('app.url') }}/hardware/' + row.asset.id + '" data-tooltip="true" title="{{ trans('admin/hardware/general.deleted') }}">' + row.asset.asset_tag + '</a></del></span>';
}
return '<a href="{{ config('app.url') }}/hardware/' + row.asset.id + '">' + row.asset.asset_tag + '</a>';
}
return '';
@ -640,7 +665,17 @@
if ((row.asset) && (row.asset.name)) {
return '<a href="{{ config('app.url') }}/hardware/' + row.asset.id + '">' + row.asset.name + '</a>';
}
}
function assetSerialLinkFormatter(value, row) {
if ((row.asset) && (row.asset.serial)) {
if (row.asset.deleted_at!='') {
return '<span style="white-space: nowrap;"><i class="fas fa-times text-danger"></i><span class="sr-only">deleted</span> <del><a href="{{ config('app.url') }}/hardware/' + row.asset.id + '" data-tooltip="true" title="{{ trans('admin/hardware/general.deleted') }}">' + row.asset.serial + '</a></del></span>';
}
return '<a href="{{ config('app.url') }}/hardware/' + row.asset.id + '">' + row.asset.serial + '</a>';
}
return '';
}
function trueFalseFormatter(value) {

View file

@ -7,10 +7,10 @@
</label>
</div>
<div class="col-md-9">
<label class="btn btn-default">
<label class="btn btn-default{{ (config('app.lock_passwords')) ? ' disabled' : '' }}">
{{ trans('button.select_file') }}
<input type="file" name="{{ $logoVariable }}" class="js-uploadFile" id="{{ $logoId }}" accept="image/gif,image/jpeg,image/webp,image/png,image/svg,image/svg+xml" data-maxsize="{{ $maxSize ?? Helper::file_upload_max_size() }}"
style="display:none; max-width: 90%">
<input type="file" name="{{ $logoVariable }}" class="js-uploadFile" id="{{ $logoId }}" accept="{{ (isset($allowedTypes) ? $allowedTypes : "image/gif,image/jpeg,image/webp,image/png,image/svg,image/svg+xml") }}" data-maxsize="{{ $maxSize ?? Helper::file_upload_max_size() }}"
style="display:none; max-width: 90%"{{ (config('app.lock_passwords')) ? ' disabled' : '' }}>
</label>
<span class='label label-default' id="{{ $logoId }}-info"></span>

View 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

View file

@ -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">

View file

@ -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 -->

Some files were not shown because too many files have changed in this diff Show more