Merge branch 'livewire_importer_2_squashed_and_rebased' of https://github.com/uberbrady/snipe-it into uberbrady-livewire_importer_2_squashed_and_rebased

This commit is contained in:
snipe 2023-03-07 21:38:04 -08:00
commit 094859cfe8
17 changed files with 739 additions and 725 deletions

View file

@ -1,22 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Transformers\ImportsTransformer;
use App\Models\Asset;
use App\Models\Import;
class ImportsController extends Controller
{
/**
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index()
{
$this->authorize('import');
$imports = (new ImportsTransformer)->transformImports(Import::latest()->get());
return view('importer/import')->with('imports', $imports);
}
}

View file

@ -0,0 +1,104 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Import;
use Storage;
use Log;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Importer extends Component
{
use AuthorizesRequests;
public $files;
public $processDetails;
public $progress; //upload progress - '-1' means don't show
public $progress_message; //progress message
public $progress_bar_class;
public $message; //status/error message?
public $message_type; //success/error?
public $import_errors; //
protected $rules = [
'files.*.file_path' => 'required|string',
'files.*.created_at' => 'required|string',
'files.*.filesize' => 'required|integer'
];
protected $listeners = [
'hideDetails' => 'hideDetails',
'importError' => 'importError',
'alert' => 'alert'
]; // TODO - try using the 'short' form of this?
public function mount()
{
$this->authorize('import');
$this->progress = -1; // '-1' means 'don't show the progressbar'
$this->progress_bar_class = 'progress-bar-warning';
}
public function hideMessages()
{
$this->message='';
}
public function importError($errors)
{
\Log::debug("Errors fired!!!!");
\Log::debug(" Here they are...".print_r($errors,true));
$this->import_errors = $errors;
}
public function alert($obj)
{
\Log::debug("Alert object received: ".print_r($obj,true));
$this->message = $obj;
$this->message_type = "danger";
}
public function toggleEvent($id)
{
$this->processDetails = Import::find($id);
}
public function hideDetails()
{
$this->processDetails = null;
}
public function destroy($id)
{
foreach($this->files as $file) {
\Log::debug("File id is: ".$file->id);
if($id == $file->id) {
if(Storage::delete('private_uploads/imports/'.$file->file_path)) {
$file->delete();
$this->message = trans('admin/hardware/message.import.file_delete_success');
$this->message_type = 'success';
return;
} else {
$this->message = trans('admin/hardware/message.import.file_delete_error');
$this->message_type = 'danger';
}
}
}
}
public function render()
{
$this->files = Import::orderBy('id','desc')->get(); //HACK - slows down renders.
return view('livewire.importer')
->extends('layouts.default')
->section('content');
}
}

View file

@ -0,0 +1,221 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\CustomField;
use Log;
class ImporterFile extends Component
{
public $activeFile; //this gets automatically populated on instantiation
public $importTypes;
public $columnOptions;
public $statusType;
public $statusText;
public $update;
public $send_welcome;
public $run_backup;
public $field_map; // we need a separate variable for the field-mapping, because the keys in the normal array are too complicated for Livewire to understand
protected $rules = [
'activeFile.import_type' => 'string',
'activeFile.field_map' => 'array',
'activeFile.header_row' => 'array',
'field_map' => 'array'
];
public function generate_field_map()
{
$tmp = array_combine($this->activeFile->header_row, $this->field_map);
return json_encode(array_filter($tmp));
}
static $general = [
'category' => 'Category',
'company' => 'Company',
'email' => 'Email',
'item_name' => 'Item Name',
'location' => 'Location',
'maintained' => 'Maintained',
'manufacturer' => 'Manufacturer',
'notes' => 'Notes',
'order_number' => 'Order Number',
'purchase_cost' => 'Purchase Cost',
'purchase_date' => 'Purchase Date',
'quantity' => 'Quantity',
'requestable' => 'Requestable',
'serial' => 'Serial Number',
'supplier' => 'Supplier',
'username' => 'Username',
'department' => 'Department',
];
static $accessories = [
'model_number' => 'Model Number',
];
static $assets = [
'asset_tag' => 'Asset Tag',
'asset_model' => 'Model Name',
'byod' => 'BYOD',
'checkout_class' => 'Checkout Type',
'checkout_location' => 'Checkout Location',
'image' => 'Image Filename',
'model_number' => 'Model Number',
'full_name' => 'Full Name',
'status' => 'Status',
'warranty_months' => 'Warranty Months',
];
static $consumables = [
'item_no' => "Item Number",
'model_number' => "Model Number",
'min_amt' => "Minimum Quantity",
];
static $licenses = [
'asset_tag' => 'Assigned To Asset',
'expiration_date' => 'Expiration Date',
'full_name' => 'Full Name',
'license_email' => 'Licensed To Email',
'license_name' => 'Licensed To Name',
'purchase_order' => 'Purchase Order',
'reassignable' => 'Reassignable',
'seats' => 'Seats',
];
static $users = [
'employee_num' => 'Employee Number',
'first_name' => 'First Name',
'jobtitle' => 'Job Title',
'last_name' => 'Last Name',
'phone_number' => 'Phone Number',
'manager_first_name' => 'Manager First Name',
'manager_last_name' => 'Manager Last Name',
'activated' => 'Activated',
'address' => 'Address',
'city' => 'City',
'state' => 'State',
'country' => 'Country',
'vip' => 'VIP'
];
//array of "real fieldnames" to a list of aliases for that field
static $aliases = [
'model_number' => ['model', 'model no','model no.','model number', 'model num', 'model num.'],
'warranty_months' => ['Warranty', 'Warranty Months']
];
private function getColumns($type)
{
switch($type) {
case 'asset':
$results = self::$general + self::$assets;
break;
case 'accessory':
$results = self::$general + self::$accessories;
break;
case 'consumable':
$results = self::$general + self::$consumables;
break;
case 'license':
$results = self::$general + self::$licenses;
break;
case 'user':
$results = self::$general + self::$users;
break;
default:
$results = self::$general;
}
asort($results, SORT_FLAG_CASE|SORT_STRING);
if($type == "asset") {
// add Custom Fields after a horizontal line
$results['-'] = "———".trans('admin/custom_fields/general.custom_fields')."———’";
foreach(CustomField::orderBy('name')->get() AS $field) {
$results[$field->db_column_name()] = $field->name;
}
}
return $results;
}
public function updating($name, $new_import_type)
{
if ($name == "activeFile.import_type") {
\Log::info("WE ARE CHANGING THE import_type!!!!! TO: ".$new_import_type);
// go through each header, find a matching field to try and map it to.
foreach($this->activeFile->header_row as $i => $header) {
// do we have something mapped already?
if (array_key_exists($i, $this->field_map)) {
// yes, we do. Is it valid for this type of import?
// (e.g. the import type might have been changed...?)
if (array_key_exists($this->field_map[$i], $this->columnOptions[$new_import_type])) {
//yes, this key *is* valid. Continue on to the next field.
continue;
} else {
//no, this key is *INVALID* for this import type. Better set it to null
// and we'll hope that the aliases or something else picks it up.
$this->field_map[$i] = null; // fingers crossed! But it's not likely, tbh.
} // TODO - strictly speaking, this isn't necessary here I don't think.
}
// first, check for exact matches
foreach ($this->columnOptions[$new_import_type] AS $value => $text) {
if (strcasecmp($text, $header) === 0) { // case-INSENSITIVe on purpose!
$this->field_map[$i] = $value;
continue 2; //don't bother with the alias check, go to the next header
}
}
// if you got here, we didn't find a match. Try the aliases
foreach(self::$aliases as $key => $alias_values) {
foreach($alias_values as $alias_value) {
if (strcasecmp($alias_value,$header) === 0) { // aLsO CaSe-INSENSitiVE!
$this->field_map[$i] = $key;
continue 3; // bust out of both of these loops; as well as the surrounding one - e.g. move on to the next header
}
}
}
// and if you got here, we got nothing. Let's recommend 'null'
$this->field_map[$i] = null; // Booooo :(
}
}
}
public function mount()
{
$this->importTypes = [
'asset' => 'Assets', // TODO - translate!
'accessory' => 'Accessories',
'consumable' => 'Consumables',
'component' => 'Components',
'license' => 'Licenses',
'user' => 'Users'
];
$this->columnOptions[''] = $this->getColumns(''); //blank mode? I don't know what this is supposed to mean
foreach($this->importTypes AS $type => $name) {
$this->columnOptions[$type] = $this->getColumns($type);
}
$this->field_map = $this->activeFile->field_map ? array_values($this->activeFile->field_map): [];
}
public function postSave()
{
if (!$this->activeFile->import_type) {
Log::error("didn't find an import type :(");
$this->statusType ='error';
$this->statusText = "An import type is required... "; // TODO - translate me!
return false;
}
$this->statusType = 'pending';
$this->statusText = "Processing...";
}
public function render()
{
return view('livewire.importer-file');
}
}

View file

@ -238,7 +238,7 @@ class CustomField extends Model
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
* @return string
*/
public function db_column_name()
{

View file

@ -39,7 +39,7 @@ class RouteServiceProvider extends ServiceProvider
{
Route::group([
'middleware' => 'web',
'namespace' => $this->namespace,
// 'namespace' => $this->namespace, //okay, I don't know what this means, but somehow this might be a problem for us?
], function ($router) {
require base_path('routes/web/hardware.php');
require base_path('routes/web/models.php');
@ -65,7 +65,7 @@ class RouteServiceProvider extends ServiceProvider
{
Route::group([
'middleware' => 'auth:api',
'namespace' => $this->namespace,
// 'namespace' => $this->namespace, // this might also be a problem? I don't really know :/
'prefix' => 'api',
], function ($router) {
require base_path('routes/api.php');

View file

@ -1,36 +0,0 @@
<style scoped>
</style>
<template>
<div class="col-md-12" :class="alertType">
<div class="alert" :class="alertClassName">
<button type="button" class="close" @click="hideEvent">&times;</button>
<i class="fas fa-check faa-pulse animated" aria-hidden="true" v-show="alertType == 'success'"></i>
<strong>{{ title }} </strong>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
/*
* The component's data.
*/
props: ['alertType', 'title'],
computed: {
alertClassName() {
return 'alert-' + this.alertType;
}
},
methods: {
hideEvent() {
this.$emit('hide');
}
}
}
</script>

View file

@ -1,42 +0,0 @@
<style scoped>
</style>
<template>
<div class="box" v-if="errors">
<div class="box-body">
<div class="alert alert-warning">
<strong>Warning</strong> Some Errors occured while importing
</div>
<div class="errors-table">
<table class="table table-striped table-bordered" id="errors-table">
<thead>
<th>Item</th>
<th>Errors</th>
</thead>
<tbody>
<tr v-for="(error, item) in errors">
<td>{{ item }}</td>
<td v-for="(value, field) in error">
<b>{{ field }}:</b>
<span v-for="errorString in value">{{errorString[0]}}</span>
<br />
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
/*
* The component's data.
*/
props: ['errors'],
}
</script>

View file

@ -1,327 +0,0 @@
<template>
<tr v-show="processDetail">
<td colspan="5">
<div class="col-md-12">
<div class="row">
<div class="dynamic-form-row">
<div class="col-md-5 col-xs-12">
<label for="import-type">Import Type:</label>
</div>
<div class="col-md-7 col-xs-12">
<select2 :options="options.importTypes" v-model="options.importType" required>
<option disabled value="0"></option>
</select2>
</div>
</div><!-- /dynamic-form-row -->
<div class="dynamic-form-row">
<div class="col-md-5 col-xs-12">
<label for="import-update">Update Existing Values?:</label>
</div>
<div class="col-md-7 col-xs-12">
<input type="checkbox" class="icheckbox_minimal" name="import-update" v-model="options.update">
</div>
</div><!-- /dynamic-form-row -->
<div class="dynamic-form-row">
<div class="col-md-5 col-xs-12">
<label for="send-welcome">Send Welcome Email for new Users?</label>
</div>
<div class="col-md-7 col-xs-12">
<input type="checkbox" class="icheckbox_minimal" name="send-welcome" v-model="options.send_welcome">
</div>
</div><!-- /dynamic-form-row -->
<div class="dynamic-form-row">
<div class="col-md-5 col-xs-12">
<label for="run-backup">Backup before importing?</label>
</div>
<div class="col-md-7 col-xs-12">
<input type="checkbox" class="icheckbox_minimal" name="run-backup" v-model="options.run_backup">
</div>
</div><!-- /dynamic-form-row -->
<div class="alert col-md-8 col-md-offset-2" style="text-align:left"
:class="alertClass"
v-if="statusText">
{{ this.statusText }}
</div><!-- /alert -->
</div> <!-- /div row -->
<div class="row">
<div class="col-md-12" style="padding-top: 30px;">
<div class="col-md-4 text-right"><h4>Header Field</h4></div>
<div class="col-md-4"><h4>Import Field</h4></div>
<div class="col-md-4"><h4>Sample Value</h4></div>
</div>
</div><!-- /div row -->
<template v-for="(header, index) in file.header_row">
<div class="row">
<div class="col-md-12">
<div class="col-md-4 text-right">
<label :for="header" class="control-label">{{ header }}</label>
</div>
<div class="col-md-4 form-group">
<div required>
<select2 :options="columns" v-model="columnMappings[header]">
<option value="0">Do Not Import</option>
</select2>
</div>
</div>
<div class="col-md-4">
<p class="form-control-static">{{ activeFile.first_row[index] }}</p>
</div>
</div><!-- /div col-md-8 -->
</div><!-- /div row -->
</template>
<div class="row">
<div class="col-md-6 col-md-offset-2 text-right" style="padding-top: 20px;">
<button type="button" class="btn btn-sm btn-default" @click="processDetail = false">Cancel</button>
<button type="submit" class="btn btn-sm btn-primary" @click="postSave">Import</button>
<br><br>
</div>
</div><!-- /div row -->
<div class="row">
<div class="alert col-md-8 col-md-offset-2" style="padding-top: 20px;"
:class="alertClass"
v-if="statusText">
{{ this.statusText }}
</div>
</div><!-- /div row -->
</div><!-- /div v-show -->
</td>
</tr>
</template>
<script>
var baseUrl = $('meta[name="baseUrl"]').attr('content');
export default {
props: ['file', 'customFields'],
data() {
return {
activeFile: this.file,
processDetail: false,
statusText: null,
statusType: null,
options: {
importType: this.file.import_type,
update: false,
send_welcome: false,
run_backup: false,
importTypes: [
{ id: 'asset', text: 'Assets' },
{ id: 'accessory', text: 'Accessories' },
{ id: 'consumable', text: 'Consumables' },
{ id: 'component', text: 'Components' },
{ id: 'license', text: 'Licenses' },
{ id: 'user', text: 'Users' }
],
statusText: null,
},
columnOptions: {
general: [
{id: 'category', text: 'Category' },
{id: 'company', text: 'Company' },
{id: 'email', text: 'Email' },
{id: 'item_name', text: 'Item Name' },
{id: 'location', text: 'Location' },
{id: 'maintained', text: 'Maintained' },
{id: 'manufacturer', text: 'Manufacturer' },
{id: 'order_number', text: 'Order Number' },
{id: 'purchase_cost', text: 'Purchase Cost' },
{id: 'purchase_date', text: 'Purchase Date' },
{id: 'quantity', text: 'Quantity' },
{id: 'requestable', text: 'Requestable' },
{id: 'serial', text: 'Serial Number' },
{id: 'supplier', text: 'Supplier' },
{id: 'username', text: 'Username' },
{id: 'department', text: 'Department' },
],
accessories:[
{id: 'model_number', text: 'Model Number'},
{id: 'notes', text: 'Notes' },
],
assets: [
{id: 'asset_tag', text: 'Asset Tag' },
{id: 'asset_model', text: 'Model Name' },
{id: 'checkout_class', text: 'Checkout Type' },
{id: 'checkout_location', text: 'Checkout Location' },
{id: 'image', text: 'Image Filename' },
{id: 'model_number', text: 'Model Number' },
{id: 'asset_notes', text: 'Asset Notes' },
{id: 'model_notes', text: 'Model Notes' },
{id: 'full_name', text: 'Full Name' },
{id: 'status', text: 'Status' },
{id: 'warranty_months', text: 'Warranty Months' },
{id: 'last_audit_date', text: 'Last Audit Date' },
{id: 'next_audit_date', text: 'Audit Date' },
{id: 'byod', text: 'BYOD' },
],
consumables: [
{id: 'item_no', text: "Item Number"},
{id: 'model_number', text: "Model Number"},
{id: 'min_amt', text: "Minimum Quantity"},
{id: 'notes', text: 'Notes' },
],
licenses: [
{id: 'asset_tag', text: 'Assigned To Asset'},
{id: 'expiration_date', text: 'Expiration Date' },
{id: 'full_name', text: 'Full Name' },
{id: 'license_email', text: 'Licensed To Email' },
{id: 'license_name', text: 'Licensed To Name' },
{id: 'notes', text: 'Notes' },
{id: 'purchase_order', text: 'Purchase Order' },
{id: 'reassignable', text: 'Reassignable' },
{id: 'seats', text: 'Seats' },
],
users: [
{id: 'employee_num', text: 'Employee Number' },
{id: 'first_name', text: 'First Name' },
{id: 'jobtitle', text: 'Job Title' },
{id: 'last_name', text: 'Last Name' },
{id: 'phone_number', text: 'Phone Number' },
{id: 'manager_first_name', text: 'Manager First Name' },
{id: 'notes', text: 'Notes' },
{id: 'manager_last_name', text: 'Manager Last Name' },
{id: 'activated', text: 'Activated' },
{id: 'address', text: 'Address' },
{id: 'city', text: 'City' },
{id: 'state', text: 'State' },
{id: 'country', text: 'Country' },
{id: 'zip', text: 'ZIP' },
{id: 'remote', text: 'Remote'},
{id: 'vip', text: 'VIP'},
],
customFields: this.customFields,
},
columnMappings: this.file.field_map || {},
activeColumn: null,
}
},
created() {
window.eventHub.$on('showDetails', this.toggleExtendedDisplay)
this.populateSelect2ActiveItems();
},
computed: {
columns() {
// function to sort objects by their display text.
function sorter(a,b) {
if (a.text < b.text)
return -1;
if (a.text > b.text)
return 1;
return 0;
}
switch(this.options.importType) {
case 'asset':
return this.columnOptions.general
.concat(this.columnOptions.assets)
.concat(this.columnOptions.customFields)
.sort(sorter);
case 'accessory':
return this.columnOptions.general
.concat(this.columnOptions.accessories)
.sort(sorter);
case 'consumable':
console.log('Returned consumable');
return this.columnOptions.general
.concat(this.columnOptions.consumables)
.sort(sorter);
case 'license':
return this.columnOptions.general.concat(this.columnOptions.licenses).sort(sorter);
case 'user':
return this.columnOptions.general.concat(this.columnOptions.users).sort(sorter);
}
return this.columnOptions.general;
},
alertClass() {
if(this.statusType=='success') {
return 'alert-success';
}
if(this.statusType=='error') {
return 'alert-danger';
}
return 'alert-info';
},
},
watch: {
columns() {
this.populateSelect2ActiveItems();
}
},
methods: {
postSave() {
console.log('saving');
console.log(this.options.importType);
if(!this.options.importType) {
this.statusType='error';
this.statusText= "An import type is required... ";
return;
}
this.statusType='pending';
this.statusText = "Processing...";
this.$http.post(baseUrl + 'api/v1/imports/process/' + this.file.id, {
'import-update': this.options.update,
'send-welcome': this.options.send_welcome,
'import-type': this.options.importType,
'run-backup': this.options.run_backup,
'column-mappings': this.columnMappings
}).then( ({body}) => {
// Success
this.statusType="success";
this.statusText = "Success... Redirecting.";
window.location.href = body.messages.redirect_url;
}, ({body}) => {
// Failure
if(body.status == 'import-errors') {
window.eventHub.$emit('importErrors', body.messages);
this.statusType='error';
this.statusText = "Error";
} else {
this.$emit('alert', {
message: body.messages,
type: "danger",
visible: true,
})
}
this.displayImportModal=false;
});
},
populateSelect2ActiveItems() {
if(this.file.field_map == null) {
// Begin by populating the active selection in dropdowns with blank values.
for (var i=0; i < this.file.header_row.length; i++) {
this.$set(this.columnMappings, this.file.header_row[i], null);
}
// Then, for any values that have a likely match, we make that active.
for(var j=0; j < this.columns.length; j++) {
let column = this.columns[j];
let lower = this.file.header_row.map((value) => value.toLowerCase());
let index = lower.indexOf(column.text.toLowerCase())
if(index != -1) {
this.$set(this.columnMappings, this.file.header_row[index], column.id)
}
}
}
},
toggleExtendedDisplay(fileId) {
if(fileId == this.file.id) {
this.processDetail = !this.processDetail
}
},
updateModel(header, value) {
this.columnMappings[header] = value;
}
},
components: {
select2: require('../select2.vue').default
}
}
</script>

View file

@ -1,130 +0,0 @@
<script>
require('blueimp-file-upload');
var baseUrl = $('meta[name="baseUrl"]').attr('content');
export default {
/*
* The component's data.
*/
data() {
return {
files: [],
displayImportModal: false,
activeFile: null,
alert: {
type: null,
message: null,
visible: false,
},
importErrors: null,
progress: {
currentClass: "progress-bar-warning",
currentPercent: "0",
statusText: '',
visible: false
},
customFields: [],
};
},
mounted() {
window.eventHub.$on('importErrors', this.updateImportErrors);
this.fetchFiles();
this.fetchCustomFields();
let vm = this;
$('#fileupload').fileupload({
dataType: 'json',
done(e, data) {
vm.progress.currentClass="progress-bar-success";
vm.progress.statusText = "Success!";
vm.files = data.result.files.concat(vm.files);
console.log(data.result.header_row);
},
add(e, data) {
data.headers = {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": Laravel.csrfToken
};
data.process().done( () => {data.submit();});
vm.progress.visible=true;
},
progress(e, data) {
var progress = parseInt((data.loaded / data.total * 100, 10));
vm.progress.currentPercent = progress;
vm.progress.statusText = progress+'% Complete';
},
fail(e, data) {
vm.progress.currentClass = "progress-bar-danger";
// Display any errors returned from the $.ajax()
vm.progress.statusText = data.jqXHR.responseJSON.messages;
}
})
},
methods: {
fetchFiles() {
this.$http.get(baseUrl + 'api/v1/imports')
.then( ({data}) => this.files = data, // Success
//Fail
(response) => {
this.alert.type="danger";
this.alert.visible=true;
this.alert.message="Something went wrong fetching files...";
});
},
fetchCustomFields() {
this.$http.get(baseUrl + 'api/v1/fields')
.then( ({data}) => {
data = data.rows;
data.forEach((item) => {
this.customFields.push({
'id': item.db_column_name,
'text': item.name,
})
})
});
},
deleteFile(file, key) {
this.$http.delete(baseUrl + 'api/v1/imports/' + file.id)
.then(
// Success, remove file from array.
(response) => {
this.files.splice(key, 1);
this.alert.type = response.body.status; // A failed delete can still cause a 200 status code.
this.alert.visible = true;
this.alert.message = response.body.messages;
},
(response) => {// Fail
// this.files.splice(key, 1);
this.alert.type="error";
this.alert.visible=true;
this.alert.message=response.body.messages;
}
);
},
toggleEvent(fileId) {
window.eventHub.$emit('showDetails', fileId)
},
updateAlert(alert) {
this.alert = alert;
},
updateImportErrors(errors) {
this.importErrors = errors;
},
},
computed: {
progressWidth() {
return "width: "+this.progress.currentPercent*10+'%';
}
},
components: {
alert: require('../alert.vue').default,
errors: require('./importer-errors.vue').default,
importFile: require('./importer-file.vue').default,
}
}
</script>

View file

@ -605,3 +605,54 @@ function htmlEntities(str) {
};
})(jQuery);
/**
* Universal Livewire Select2 and iCheck integration
*
* How to use:
*
* 1. Set the class of your select2 elements to 'livewire-select2' and your icheck elements to 'livewire-icheck' (as appropriate).
* (For iCheck, you may still need to apply the other iCheck classes like 'minimal' or 'iCheck')
* 2. Name your element to match a property in your Livewire component
* 3. Add an attribute called 'data-livewire-component' that points to $_instance->id (via `{{ }}` if you're in a blade,
* or just $_instance->id if not).
* 4. For iCheck, you need to wrap the 'checkbox' element with wire:ignore - perhaps in the <label> if it wraps the
* <input> element, or just put a <span wire:ignore></span> around just the input element.
* 5. If you have dynamically shown/hidden checkboxes, you might need to initialize iCheck on them on component page-load.
* Just use $('.livewire-icheck').iCheck(), or for the minimal-style, use:
*
* $('input[type="checkbox"].minimal.livewire-icheck, input[type="radio"].minimal.livewire-icheck').iCheck({
* checkboxClass: 'icheckbox_minimal-blue',
* radioClass: 'iradio_minimal-blue'
* });
*
* (which is stolen from above here in this JS file)
*/
$(function () {
$('.livewire-select2').select2()
$(document).on('select2:select', '.livewire-select2', function (event) {
var target = $(event.target)
if(!event.target.name || !target.data('livewire-component')) {
console.error("You need to set both name (which should match a Livewire property) and data-livewire-component on your Livewire-ed select2 elements!")
console.error("For data-livewire-component, you probably want to use $_instance->id or {{ $_instance->id }}, as appropriate")
return false
}
window.livewire.find(target.data('livewire-component')).set(event.target.name, this.options[this.selectedIndex].value)
})
window.livewire.hook('message.processed', function (el,component) {
$('.livewire-select2').select2();
//$('.livewire-icheck').iCheck(); //this seems to blow up pretty badly.
});
$(document).on('ifToggled', '.livewire-icheck', function (event) {
if(!event.target.name || !$(event.target).data('livewire-component')) {
console.error("You need to set both name (which should match a Livewire property) and data-livewire-component on your iCheck elements!")
console.error("For data-livewire-component, you probably want to use $_instance->id or {{ $_instance->id }}, as appropriate")
return false
}
window.livewire.find($(event.target).data('livewire-component')).set(event.target.name, event.target.checked)
})
})

View file

@ -26,10 +26,11 @@ Vue.component(
require('./components/passport/PersonalAccessTokens.vue').default
);
Vue.component(
'importer',
require('./components/importer/importer.vue').default
);
// This component has been removed and replaced with a Livewire implementation
// Vue.component(
// 'importer',
// require('./components/importer/importer.vue').default
// );
// This component has been removed and replaced with a Livewire implementation
// Vue.component(

View file

@ -1,120 +0,0 @@
@extends('layouts/default')
{{-- Page title --}}
@section('title')
{{ trans('general.import') }}
@parent
@stop
{{-- Page content --}}
@section('content')
{{-- Hide importer until vue has rendered it, if we continue using vue for other things we should move this higher in the style --}}
<style>
[v-cloak] {
display:none;
}
</style>
<div id="app">
<importer inline-template v-cloak>
<div class="row">
<alert v-show="alert.visible" :alert-type="alert.type" v-on:hide="alert.visible = false">@{{ alert.message }}</alert>
<errors :errors="importErrors"></errors>
<div class="col-md-9">
<div class="box">
<div class="box-body">
<div class="row">
<div class="col-md-12">
<div class="col-md-9" v-show="progress.visible" style="padding-bottom:20px">
<div class="progress progress-striped-active" style="margin-top: 8px">
<div class="progress-bar" :class="progress.currentClass" role="progressbar" :style="progressWidth">
<span>@{{ progress.statusText }}</span>
</div>
</div>
</div>
<div class="col-md-3 text-right pull-right">
<!-- The fileinput-button span is used to style the file input field as button -->
@if (!config('app.lock_passwords'))
<span class="btn btn-primary fileinput-button">
<span>{{ trans('button.select_file') }}</span>
<!-- The file input field used as target for the file upload widget -->
<label for="files[]"><span class="sr-only">{{ trans('button.select_file') }}</span></label>
<input id="fileupload" type="file" name="files[]" data-url="{{ route('api.imports.index') }}" accept="text/csv" aria-label="files[]">
</span>
@endif
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 table-responsive" style="padding-top: 30px;">
<table data-pagination="true"
data-id-table="upload-table"
data-search="true"
data-side-pagination="client"
id="upload-table"
class="col-md-12 table table-striped snipe-table">
<tr>
<th class="col-md-6">{{ trans('general.file_name') }}</th>
<th class="col-md-3">{{ trans('general.created_at') }}</th>
<th class="col-md-1">{{ trans('general.filesize') }}</th>
<th class="col-md-1 text-right"><span class="sr-only">{{ trans('general.actions') }}</span></th>
</tr>
<template v-for="(currentFile, index) in files">
<tr>
<td class="col-md-6">@{{ currentFile.file_path }}</td>
<td class="col-md-3">@{{ currentFile.created_at }} </td>
<td class="col-md-1">@{{ currentFile.filesize }}</td>
<td class="col-md-1 text-right">
<button class="btn btn-sm btn-info" @click="toggleEvent(currentFile.id)">
<i class="fas fa-retweet fa-fw" aria-hidden="true"></i>
<span class="sr-only">{{ trans('general.import') }}</span>
</button>
<button class="btn btn-sm btn-danger" @click="deleteFile(currentFile, index)">
<i class="fas fa-trash icon-white" aria-hidden="true"></i><span class="sr-only"></span></button>
</td>
</tr>
<import-file
:key="currentFile.id"
:file="currentFile"
:custom-fields="customFields"
@alert="updateAlert(alert)">
</import-file>
</template>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<h2>{{ trans('general.importing') }}</h2>
<p>{!! trans('general.importing_help') !!}</p>
</div>
</div>
</importer>
</div>
@stop
@section('moar_scripts')
<script nonce="{{ csrf_token() }}">
new Vue({
el: '#app'
});
</script>
@endsection

View file

@ -1,15 +1,13 @@
<span> {{-- This <span> doesn't seem to fix it, neither does a div? --}}
<div class="form-group{{ $errors->has('custom_fieldset') ? ' has-error' : '' }}">
<label for="custom_fieldset" class="col-md-3 control-label">{{ trans('admin/models/general.fieldset') }}</label>
<span wire:ignore> {{-- wire:ignore is because Select 2 mangles the dom in many awful ways, and so does iCheckbox --}}
<div class="col-md-9">
{{ Form::select('custom_fieldset', Helper::customFieldsetList(), old('custom_fieldset', $fieldset_id), array('class'=>'select2 js-fieldset-field', 'style'=>'width:350px', 'aria-label'=>'custom_fieldset', 'wire:model' => 'fieldset_id', 'id' => 'glooobits')) }} {{-- when we have this wrapped in 'ignore', the wire:model won't work --}}
{{ Form::select('fieldset_id', Helper::customFieldsetList(), old('fieldset_id', $fieldset_id), array('class'=>'select2 js-fieldset-field livewire-select2', 'style'=>'width:350px', 'aria-label'=>'custom_fieldset', 'data-livewire-component' => $_instance->id)) }}
{!! $errors->first('custom_fieldset', '<span class="alert-msg" aria-hidden="true"><br><i class="fas fa-times"></i> :message</span>') !!}
<label class="m-l-xs">
{{ Form::checkbox('add_default_values', 1, Request::old('add_default_values', $add_default_values), ['class' => 'minimal', 'wire:model' => "add_default_values", 'id' => 'add_default_values']) }}
<label class="m-l-xs" wire:ignore>
{{ Form::checkbox('add_default_values', 1, Request::old('add_default_values', $add_default_values), ['class' => 'minimal livewire-icheck', 'data-livewire-component' => $_instance->id, 'id' => 'add_default_values']) }}
{{ trans('admin/models/general.add_default_values') }}
</label>
</span>
</div>
</div>
@if ($this->add_default_values ) {{-- 'if the checkbox is enabled *AND* there are more than 0 fields in the fieldsset' --}}
@ -63,34 +61,4 @@
</div>
</div>
@endif
<script>
// *still* haven't figured out why this doesn't seem to work at all...
// And even if it did, I hate having $(function () {}) as my DOM-ready checker in some places, and
// DOMContentLoaded in another...
/* document.addEventListener("DOMContentLoaded", function () {
Livewire.hook('component.initialized', function (component) {
$('#glooobits').on('select2:select',function (event) { //'change' seems to be the jquery-compatible version but I think the select2 versions might be nicer.
console.log("Select2 has changed!!!!!")
console.dir(event)
@this.set('fieldset_id',event.params.data.id)
// Livewire.first().set('fieldset_id',event.params.data.id) // I still don't know why @this does'nt work here?
})
})
}) */
</script>
@push('js')
<script>
$(function () {
$('#glooobits').on('select2:select',function (event) { //'change' seems to be the jquery-compatible version but I think the select2 versions might be nicer.
{{-- @this.set('fieldset_id',event.params.data.id) --}}
Livewire.first().set('fieldset_id',event.params.data.id) // I still don't know why @this does'nt work here?
})
$('#add_default_values').on('ifToggled',function (event) {
Livewire.first().set('add_default_values',event.target.checked)
})
})
</script>
@endpush
</span>

View file

@ -0,0 +1,176 @@
<tr id="importer-file">
<td colspan="5">
<div class="col-md-12">
<div class="row">
<div class="dynamic-form-row">
<div class="col-md-5 col-xs-12">
<label for="import-type">Import Type:</label>
</div>
<div class="col-md-7 col-xs-12">
{{ Form::select('activeFile.import_type', $importTypes, $activeFile->import_type, [
'id' => 'import_type',
'class' => 'livewire-select2',
'style' => 'min-width: 350px',
'data-placeholder' => 'Select an import type...', /* TODO: translate me */
'data-livewire-component' => $_instance->id
]) }}
</div>
</div><!-- /dynamic-form-row -->
<div class="dynamic-form-row">
<div class="col-md-5 col-xs-12">
<label for="import-update">Update Existing Values?:</label>
</div>
<div class="col-md-7 col-xs-12" wire:ignore>
<input type="checkbox" class="minimal livewire-icheck" name="update" data-livewire-component="{{ $_instance->id }}">
</div>
</div><!-- /dynamic-form-row -->
<div class="dynamic-form-row">
<div class="col-md-5 col-xs-12">
<label for="send_welcome">Send Welcome Email for new Users?</label>
</div>
<div class="col-md-7 col-xs-12" wire:ignore>
<input type="checkbox" class="minimal livewire-icheck" name="send_welcome" data-livewire-component="{{ $_instance->id }}">
</div>
</div><!-- /dynamic-form-row -->
<div class="dynamic-form-row">
<div class="col-md-5 col-xs-12">
<label for="run_backup">Backup before importing?</label>
</div>
<div class="col-md-7 col-xs-12" wire:ignore>
<input type="checkbox" class="minimal livewire-icheck" name="run_backup" data-livewire-component="{{ $_instance->id }}">
</div>
</div><!-- /dynamic-form-row -->
@if($statusText)
<div class="alert col-md-8 col-md-offset-2 {{ $statusType == 'success' ? 'alert-success' : ($statusType == 'error' ? 'alert-danger' : 'alert-info') }}" style="text-align:left">
{{ $statusText }}
</div><!-- /alert -->
@endif
</div> <!-- /div row -->
@if($activeFile->import_type)
<div class="row">
<div class="col-md-12" style="padding-top: 30px;">
<div class="col-md-4 text-right"><h4>Header Field</h4></div>
<div class="col-md-4"><h4>Import Field</h4></div>
<div class="col-md-4"><h4>Sample Value</h4></div>
</div>
</div><!-- /div row -->
@if($activeFile->header_row)
@foreach($activeFile->header_row AS $index => $header)
<div class="row" wire:key="header-row-{{ $index }}">
<div class="col-md-12">
<div class="col-md-4 text-right">
<label for="field_map.{{ $index }}" class="control-label">{{ $header }}</label>
</div>
<div class="col-md-4 form-group">
<div required>
{{ Form::select('field_map.'.$index, $columnOptions[$activeFile->import_type], @$field_map[$index],
[
'class' => 'mappings livewire-select2',
'placeholder' => 'Do Not Import',
'data-livewire-component' => $_instance->id
],[
'-' => ['disabled' => true] // this makes the "-----" line unclickable
])
}}
</div>
</div>
<div class="col-md-4">
<p class="form-control-static">{{ $activeFile->first_row[$index] }}</p>
</div>
</div><!-- /div col-md-8 -->
</div><!-- /div row -->
@endforeach
@else
No Columns Found!
@endif
<div class="row">
<div class="col-md-6 col-md-offset-2 text-right" style="padding-top: 20px;">
<button type="button" class="btn btn-sm btn-default" wire:click="$emit('hideDetails')">Cancel</button>
<button type="submit" class="btn btn-sm btn-primary" id="import">Import</button>
<br><br>
</div>
</div><!-- /div row -->
<div class="row">
@if($statusText)
<div class="alert col-md-8 col-md-offset-2 {{ $statusType == 'success' ? 'alert-success' : ($statusType == 'error' ? 'alert-danger' : 'alert-info') }}"
style="padding-top: 20px;"
>
{{ $statusText }}
</div>
@endif
</div><!-- /div row -->
@endif {{-- end of if ... activeFile->import_type --}}
</div><!-- /div v-show -->
</td>
</tr>
<script>
$(function () {
// initialize iCheck for use with livewire
$('.minimal.livewire-icheck').iCheck({
checkboxClass: 'icheckbox_minimal-blue',
})
// we have to hook up to the `<tr id='importer-file'>` at the root of this display,
// because the #import button isn't visible until you click an import_type
$('#importer-file').on('click', '#import', function () {
console.warn("You clicked it!!!!")
if(!@this.activeFile.import_type) {
@this.statusType='error';
@this.statusText= "An import type is required... "; //TODO: translate?
return;
}
@this.statusType='pending';
@this.statusText = "Processing...";
@this.generate_field_map().then(function (mappings_raw) {
var mappings = JSON.parse(mappings_raw)
// console.warn("Here is the mappings:")
// console.dir(mappings)
$.post({
url: "{{ route('api.imports.importFile', $activeFile->id) }}",
contentType: 'application/json',
data: JSON.stringify({
'import-update': !!@this.update,
'send-welcome': !!@this.send_welcome,
'import-type': @this.activeFile.import_type,
'run-backup': !!@this.run_backup,
'column-mappings': mappings
}),
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
}
}).done( function (body) {
// Success
@this.statusType="success";
@this.statusText = "Success... Redirecting.";
console.dir(body)
window.location.href = body.messages.redirect_url;
}).fail( function (jqXHR, textStatus, error) {
// Failure
var body = jqXHR.responseJSON
if(body.status == 'import-errors') {
@this.emit('importError', body.messages);
@this.statusType='error';
@this.statusText = "Error";
} else {
console.warn("Not import-errors, just regular errors")
console.dir(body)
@this.emit('alert', body.error)
}
@this.emit('hideDetails')
});
})
return false;
});})
</script>

View file

@ -0,0 +1,171 @@
@section('title')
{{ trans('general.import') }}
@parent
@stop
<div>
{{-- Livewire requires a 'master' <div>, above --}}
<div class="row">
{{-- alert --}}
@if($message != '')
<div class="col-md-12" class="{{ $message_type }}">
<div class="alert alert-{{ $this->message_type }} ">
<button type="button" class="close" wire:click="hideMessages">&times;</button>
@if($message_type == 'success')
<i class="fas fa-check faa-pulse animated" aria-hidden="true"></i>
@endif
<strong>{{-- title --}} </strong>
{{ $message }}
</div>
</div>
@endif
@if($import_errors)
<div class="box">
<div class="box-body">
<div class="alert alert-warning">
<strong>Warning</strong> Some Errors occurred while importing {{-- TODO: hardcoded string --}}
</div>
<div class="errors-table">
<table class="table table-striped table-bordered" id="errors-table">
<thead>
<th>Item</th>
<th>Errors</th>
</thead>
<tbody>
@foreach($import_errors as $field => $error_list)
<tr>
<td>{{ $processDetails->file_path ?? "Unknown File" }}</td>
<td>
<b>{{ $field }}:</b>
<span>{{ implode(", ",$error_list) }}</span>
<br />
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endif
<div class="col-md-9">
<div class="box">
<div class="box-body">
<div class="row">
<div class="col-md-12">
@if($progress != -1)
<div class="col-md-9" style="padding-bottom:20px" id='progress-container'>
<div class="progress progress-striped-active" style="margin-top: 8px"> {{-- so someof these values are in importer.vue! --}}
<div id='progress-bar' class="progress-bar {{ $progress_bar_class }}" role="progressbar" style="width: {{ $progress }}%">
<span id='progress-text'>{{ $progress_message }}</span>
</div>
</div>
</div>
@endif
<div class="col-md-3 text-right pull-right">
<!-- The fileinput-button span is used to style the file input field as button -->
@if (!config('app.lock_passwords'))
<span class="btn btn-primary fileinput-button">
<span>{{ trans('button.select_file') }}</span>
<!-- The file input field used as target for the file upload widget -->
<label for="files[]"><span class="sr-only">{{ trans('admin/importer/general.select_file') }}</span></label>
<input id="fileupload" type="file" name="files[]" data-url="{{ route('api.imports.index') }}" accept="text/csv" aria-label="files[]">
</span>
@endif
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 table-responsive" style="padding-top: 30px;">
<table data-pagination="true"
data-id-table="upload-table"
data-search="true"
data-side-pagination="client"
id="upload-table"
class="col-md-12 table table-striped snipe-table">
<tr>
<th class="col-md-6">{{ trans('general.file_name') }}</th>
<th class="col-md-3">{{ trans('general.created_at') }}</th>
<th class="col-md-1">{{ trans('general.filesize') }}</th>
<th class="col-md-1 text-right"><span class="sr-only">{{ trans('general.actions') }}</span></th>
</tr>
@foreach($files as $currentFile)
<tr>
<td class="col-md-6">{{ $currentFile->file_path }}</td>
<td class="col-md-3">{{ $currentFile->created_at }} </td>
<td class="col-md-1">{{ $currentFile->filesize }}</td>
<td class="col-md-1 text-right">
<button class="btn btn-sm btn-info" wire:click="toggleEvent({{ $currentFile->id }})">
<i class="fas fa-retweet fa-fw" aria-hidden="true"></i>
<span class="sr-only">{{ trans('general.import') }}</span>
</button>
<button class="btn btn-sm btn-danger" wire:click="destroy({{ $currentFile->id }})">
<i class="fas fa-trash icon-white" aria-hidden="true"></i><span class="sr-only"></span></button>
</td>
</tr>
@if( $currentFile && $processDetails && ($currentFile->id == $processDetails->id))
@livewire('importer-file', ['activeFile' => $currentFile])
@endif
@endforeach
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<h2>{{ trans('general.importing') }}</h2>
<p>{!! trans('general.importing_help') !!}</p>
</div>
</div>
</div>
@push('js')
<script>
{{-- TODO: Maybe change this to the file upload thing that's baked-in to Livewire? --}}
$('#fileupload').fileupload({
dataType: 'json',
done: function(e, data) {
@this.progress_bar_class = 'progress-bar-success';
@this.progress_message = 'Success!'; // TODO - we're already round-tripping to the server here - I'd love it if we could get internationalized text here
@this.progress = 100;
},
add: function(e, data) {
data.headers = {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
};
data.process().done( function () {data.submit();});
@this.progress = 0;
},
progress: function(e, data) {
@this.progress = parseInt((data.loaded / data.total * 100, 10));
@this.progress_message = @this.progress+'% Complete'; // TODO - this should come from server (so it can be internationalized)
},
fail: function(e, data) {
@this.progress_bar_class = "progress-bar-danger";
@this.progress = 100;
var error_message = ''
for(var i in data.jqXHR.responseJSON.messages) {
error_message += i+": "+data.jqXHR.responseJSON.messages[i].join(", ")
}
@this.progress_message = error_message;
}
})
</script>
@endpush

View file

@ -699,7 +699,7 @@ $(document).ready(function() {
data: {},
headers: {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content') // TODO` - we should do this in ajaxSetup
},
dataType: 'json',

View file

@ -236,11 +236,10 @@ Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'authorize:superuser
|
|
*/
Route::group(['prefix' => 'import', 'middleware' => ['auth']], function () {
Route::get('/',
[ImportsController::class, 'index']
)->name('imports.index');
});
Route::get('/import',
\App\Http\Livewire\Importer::class
)->middleware('auth')->name('imports.index');
/*
|--------------------------------------------------------------------------