Getting the basic wiring of the importer over into Livewire

WIP: Wiring up more and more of the actions on the importer

Files now upload okay, a little glitchy on the display-side though

add to readmes so i dont forget
This commit is contained in:
Brady Wetherington 2022-01-13 01:19:13 -08:00
parent 23ca124e94
commit 0a085af0a0
13 changed files with 641 additions and 638 deletions

11
FIXME.txt Normal file
View file

@ -0,0 +1,11 @@
remove Ziggy
and ziggy-js too
And what is /public/js/snipeit.js ? That looks like a generated file
The 'flash' (forced refresh/fake refresh) on uploads is dumb
I'm not sure if the order on the uploaded files is right?
The Livewire.first() thing is still dumb (but Id o'nt know that we can fix it).
Deletes need to work (I got this working before using $.ajax; it's not even hard)
Then mapping and so on.

View file

@ -15,8 +15,8 @@ class ImportsController extends Controller
public function index()
{
$this->authorize('import');
$imports = (new ImportsTransformer)->transformImports(Import::latest()->get());
// $imports = (new ImportsTransformer)->transformImports(Import::latest()->get());
return view('importer/import')->with('imports', $imports);
return view('importer/import'); //->with('imports', $imports);
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Import;
use Log;
class Importer extends Component
{
public $files;
public $processDetails;
public $forcerefresh;
protected $rules = [
'files.*.file_path' => 'required|string',
'files.*.created_at' => 'required|string',
'files.*.filesize' => 'required|integer'
];
public function mount()
{
//$this->files = Import::all(); // this *SHOULD* be how it works, but...it doesn't?
$this->forcerefresh = 0;
}
public function test()
{
Log::error("Test Button Clicked!!!!");
}
public function toggleEvent($id)
{
Log::error("toggled on: ".$id);
$this->processDetails = Import::find($id);
}
public function render()
{
$this->files = Import::all(); //HACK - slows down renders.
return view('livewire.importer');
}
}

View file

@ -0,0 +1,151 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\CustomField;
use Log;
global $general, $accessories, $assets, $consumables, $licenses, $users;
$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',
];
$accessories = [
'model_number' => 'Model Number',
];
$assets = [
'asset_tag' => 'Asset Tag',
'asset_model' => 'Model Name',
'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',
];
$consumables = [
'item_no' => "Item Number",
'model_number' => "Model Number",
'min_amt' => "Minimum Quantity",
];
$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',
];
$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',
];
class ImporterFile extends Component
{
public $activeFile; //should this get auto-filled?
public $customFields;
public $importTypes;
public $columnOptions;
public $importType; // too similar to 'TypeS'?
private function getColumns($type)
{
global $general, $accessories, $assets, $consumables, $licenses, $users;
$customFields = [];
foreach($this->customFields AS $field) {
$customFields[$field->id] = $field->name;
}
switch($type) {
case 'asset':
$results = $general + $assets + $customFields;
break;
case 'accessory':
$results = $general + $accessories;
break;
case 'consumable':
$results = $general + $consumables;
break;
case 'license':
$results = $general + $licenses;
break;
case 'user':
$results = $general + $users;
break;
default:
$results = $general;
}
asort($results); // FIXME - this isn't sorting right yet.
return $results;
}
public function mount()
{
$this->customFields = CustomField::all();
$this->importTypes = [
'asset' => 'Assets', // TODO - translate!
'accessory' => 'Accessories',
'consumable' => 'Consumables',
'component' => 'Components',
'license' => 'Licenses',
'user' => 'Users'
];
Log::error("import types: ".print_r($this->importTypes,true));
$columnOptions = [];
$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);
}
}
public function changeTypes()
{
Log::error("type changed!");
}
public function render()
{
return view('livewire.importer-file');
}
}

View file

@ -217,7 +217,7 @@ class Setting extends Model
*
* @author Mogilev Arseny
*/
public static function fileSizeConvert($bytes): string
public static function fileSizeConvert(int $bytes): string
{
$result = 0;
$bytes = floatval($bytes);
@ -244,6 +244,7 @@ class Setting extends Model
],
];
$result = $bytes; // handles the zero case
foreach ($arBytes as $arItem) {
if ($bytes >= $arItem['VALUE']) {
$result = $bytes / $arItem['VALUE'];

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,325 +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' },
],
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'},
],
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

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

@ -9,112 +9,20 @@
{{-- 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>
{{-- <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>
THIS IS VUE STUFF ISNT IT?
</style> --}}
@livewire('importer') {{-- Yes, this is stupid - we should be able to route straight over and not have this, but Livewire doesn't work in this app that way :/ --}}
@stop
@section('moar_scripts')
<script nonce="{{ csrf_token() }}">
{{-- <script nonce="{{ csrf_token() }}">
new Vue({
el: '#app'
});
</script>
</script> --}}
@endsection

View file

@ -0,0 +1,207 @@
{{-- <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">
{{ Form::select('importType', $importTypes, 0 /* FIXME whats' the old value? */, ['placeholder' => '', 'wire:model' => 'importType', 'wire:change' => 'changeTypes']) }}
{{-- <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="iCheck 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="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="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"> --}}
@if($activeFile->header_row)
@foreach($activeFile->header_row AS $index => $header)
<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> --}}
{{ Form::select('something', $columnOptions[$importType], null /* FIXME whats' the old value? */,['placeholder' => 'Do Not Import']) }}
</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
{{-- </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>
<script>
unused_var_thing = {
data() {
return {
activeFile: this.file,
processDetail: false,
statusText: null,
statusType: null,
options: {
importType: this.file.import_type,
update: false,
statusText: null,
},
activeColumn: null,
}
},
created() {
this.populateSelect2ActiveItems();
},
computed: {
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(route('api.imports.importFile', 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)
}
}
}
},
updateModel(header, value) {
this.columnMappings[header] = value;
}
},
}
</script>
</tr>
{{-- </template> --}}

View file

@ -0,0 +1,212 @@
<div id="not-app">
{{-- <importer inline-template v-cloak> --}} {{-- like, this, here, that's a literal Vue directive --}}
<div class="row">
{{-- <alert v-show="alert.visible" :alert-type="alert.type" v-on:hide="alert.visible = false">@{{ alert.message }}</alert> --}}
<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>
{{-- alert --}}
<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>
fixme = {
/*
* The component's data.
*/
props: ['alertType', 'title'],
computed: {
alertClassName() {
return 'alert-' + this.alertType;
}
},
methods: {
hideEvent() {
this.$emit('hide');
}
}
}
</script>
{{-- errors thing that's built-in maybe? --}}
{{-- <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" style="padding-bottom:20px; display:none" 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" class="progress-bar-warning" role="progressbar" style="width: 0%">
<span id='progress-text'></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('admin/importer/general.select_import_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;">
<button wire:click="test">Test!</button><br />
<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('admin/importer/table.file') }}</th>
<th class="col-md-3">{{ trans('admin/importer/table.created') }}</th>
<th class="col-md-1">{{ trans('admin/importer/table.size') }}</th>
<th class="col-md-1 text-right"><span class="sr-only">{{ trans('admin/importer/table.process') }}</span></th>
<th class="col-md-1 text-right"><span class="sr-only">{{ trans('admin/importer/table.delete') }}</span></th>
</tr>
{{-- <template v-for="currentFile in files"> --}}
@foreach($files as $currentFile)
<?php
\Log::error(print_r($currentFile,true))
?>
<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 }})">
{{ trans('admin/importer/button.process') }}
</button>
</td>
<td class="col-md-1 text-right">
<button class="btn btn-sm btn-danger" @click="deleteFile(currentFile)">
<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])
{{-- <import-file
:key="currentFile.id"
:file="currentFile"
:custom-fields="customFields"
@alert="updateAlert(alert)">
</import-file> --}}
@endif
@endforeach
{{-- </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>
@push('js')
<script>
document.addEventListener('livewire:load', function () {
console.log("OKAY - we are gonna dump us out some files here!")
console.dir(Livewire.first().files)
})
$('#fileupload').fileupload({
dataType: 'json',
done: function(e, data) {
$('#progress-bar').attr("class", "progress-bar-success");
$('#progress-text').text("Success!"); // same here? TODO - internationalize!
$('#progress-bar').attr('style', 'width: 100%'); // weird, wasn't needed before....
console.log("Dumping livewire files!!!!!!!!!")
console.dir(Livewire.first().files)
console.log("And now dumping data.result.files!!!!!")
console.dir(data.result.files)
//Livewire.first().files = data.result.files.concat(Livewire.first().files); // FIXME - how to get in and out of the Livewire.first().something.... (this doesn't work either)
// Livewire.first().files = Livewire.first().files.concat(data.result.files) //I don't quite see why this should be like this, but, well, whatever.
//fuckit, let's just force a refresh?
Livewire.first().forcerefresh = Livewire.first().forcerefresh+1 // this is a horrible hack; please forgive me :(
console.log(data.result.header_row);
console.dir()
},
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();});
$('#progress-container').show();
},
progress: function(e, data) {
var progress = parseInt((data.loaded / data.total * 100, 10));
$('#progress-bar').attr('style', 'width: '+progress+'%');
$('#progress-text').text(progress+'% Complete');
},
fail: function(e, data) {
$('#progress-bar').attr("class", "progress-bar-danger");
// Display any errors returned from the $.ajax()
console.dir(data.jqXHR.responseJSON.messages) // FIXME don't dupm to console
$('#progress-bar').attr('style', 'width: 100%');
$('#progress-text').text(data.jqXHR.responseJSON.messages);
}
})
</script>
@endpush