CSV import, also from file, and more UX improvements

This commit is contained in:
2018-08-10 20:19:16 +02:00
parent 1e183f5059
commit a3df7a724c
18 changed files with 371 additions and 155 deletions
+1
View File
@@ -44,6 +44,7 @@ undo # Used in table editor
spinner
paper-plane-o # save proposal
upload # Upload a file
# dark mode
sun-o
@@ -5,7 +5,12 @@ Complex animated column editor for the table edit page
<template>
<div>
<input type="hidden" :name="name" :value="JSON.stringify(columns)" v-if="newTable">
<table :class="[{'table': !newTable}, {'new-table': newTable}, {'mt-3': !newTable}, 'table-narrow', 'table-sm', 'table-fixed', 'td-va-middle']">
<table :class="[
{'table': !newTable},
{'new-table': newTable},
{'mt-3': !newTable},
'table-narrow', 'table-sm', 'table-fixed', 'td-va-middle'
]">
<thead>
<tr>
<th v-if="sortable"></th>
@@ -16,7 +21,12 @@ Complex animated column editor for the table edit page
</tr>
</thead>
<transition-group name="col-list" tag="tbody" ref="col-list">
<tr v-for="(col, i) in columns" :key="col.id" :ref="`col${i}`" :class="{dragging: col._dragging, 'text-success': col._new}">
<tr v-for="(col, i) in columns" :key="col.id" :ref="`col${i}`"
:class="{
dragging: col._dragging,
'text-success': col._new,
remove: col._remove
}">
<td v-if="sortable">
<span class="btn-group">
<button type="button" class="btn btn-outline-secondary drag-btn"
@@ -74,17 +84,17 @@ Complex animated column editor for the table edit page
<td class="text-nowrap"><!--
Save button
--><a href="" :class="['mr-1','btn','btn-outline-secondary',{'disabled': col._remove}]"
--><a href="" :class="['mr-1', 'btn', 'btn-outline-secondary', {'disabled': col._remove}]"
v-if="!newTable"
@click.prevent="toggleColEditing(col)">
<v-icon :class="col._editing ? 'fa-save' : 'fa-pencil'"
:alt="col._editing ? 'Save' : 'Edit'" />
<v-icon v-if="col._editing" class="fa-save" alt="Save" />
<v-icon v-else class="fa-pencil" alt="Edit" />
</a><!--
Delete button
--><button type="button" :class="delBtnClass(col)"
@click.prevent="toggleColDelete(col)">
<v-icon :class="col._remove ? 'fa-undo' : 'fa-trash-o'"
:alt="col._remove ? 'Undo Remove' : 'Remove'" />
<v-icon v-if="col._remove" class="fa-undo" alt="Undo Remove" />
<v-icon v-else class="fa-trash-o" alt="Remove" />
</button><!--
Add button
--><button type="button" :class="['x-add-btn', 'btn', 'btn-outline-secondary']"
@@ -387,7 +397,7 @@ export default {
if (cell == 'title') w = '14'
return {width: `${w}rem`};
}
},
}
}
</script>
+81 -33
View File
@@ -11,7 +11,7 @@ Rows are identified by row._id, columns by col.id
<template>
<div>
<div class="col-md-12 d-flex mt-3" v-if="newRows || hasPages">
<div class="col-md-12 d-flex mt-3">
<div v-if="hasPages" v-html="pager"></div>
<div class="flex-grow-1"></div>
@@ -21,52 +21,68 @@ Rows are identified by row._id, columns by col.id
<input type="hidden" name="action" value="rows.add">
<input type="hidden" name="redirect" :value="pageUrl">
<input name="count" id="newrow-count" type="number"
min=1 step=1 value=1 class="form-control mr-1" style="width: 10em">
min=1 step=1 value=1 class="form-control mr-1" style="width: 8em">
<button class="btn btn-outline-success">Add Rows</button>
</form>
<a href="" class="btn btn-outline-success mr-2" v-if="newRows">
<a :href="loadCsvUrl" class="btn btn-outline-success mr-2" v-if="newRows && loadCsvUrl">
<v-icon class="fa-file-excel-o fa-pr" alt="CSV"></v-icon><!--
-->CSV
</a>
<form :action="route" method="POST" v-if="newRows" class="form-inline mr-2">
<input type="hidden" name="_token" :value="csrf">
<input type="hidden" name="action" value="rows.remove-empty-new">
<input type="hidden" name="action" value="row.remove-empty-new">
<input type="hidden" name="redirect" :value="pageUrl">
<button class="btn btn-outline-danger">Remove Empty</button>
</form>
<button @click="saveAllChanges" type="button" v-if="newRows"
<button @click="saveAllChanges" type="button"
:class="['btn', this.dirtyRows ? 'btn-info' : 'btn-outline-secondary']">
Save Rows
</button>
</div>
<div class="col-md-12 mt-3">
<table class="table table-hover table-sm table-fixed td-va-middle">
<table :class="[
'table', 'table-sm', 'table-fixed', 'td-va-middle',
{'new-rows': newRows}
]">
<thead>
<tr>
<th style="width:6rem"></th>
<th v-for="col in columns" :class="colClasses(col)" :title="col.name">{{col.title}}</th>
<th style="width:2rem"><!-- revert icon --></th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :style="rowStyle(row)">
<tr v-for="row in rows" :class="row._remove ? 'remove' : ''">
<td class="text-nowrap">
<button :class="['mr-1', 'btn', 'btn-outline-danger', {'active': row._remove}]"
<button :class="[
'mr-1', 'btn', 'btn-outline-danger',
{'active': row._remove}
]"
@click.prevent="toggleRowDelete(row._id)">
<v-icon :class="row._remove ? 'fa-undo' : 'fa-trash-o'"
:alt="row._remove ? 'Undo Remove' : 'Remove'" />
</button><!--
--><button v-if="newRows" :class="['mr-1', 'btn', isRowChanged(row) ? 'btn-info' : 'btn-outline-secondary']"
--><button v-if="newRows"
:class="[
'mr-1', 'btn',
isRowChanged(row) ? 'btn-info' : 'btn-outline-secondary'
]"
@click.prevent="submitRowChange(row)">
<v-icon class="fa-save" alt="Save" />
</button><!--
--><button v-else :class="['btn', 'btn-outline-secondary', {'disabled': row._remove}]"
--><button v-else
:class="[
'btn',
isRowChanged(row) ? 'btn-info' : 'btn-outline-secondary',
{'disabled': row._remove}
]"
@click.prevent="toggleRowEditing(row._id)">
<v-icon :class="row._editing ? 'fa-save' : 'fa-pencil'"
:alt="row._editing ? 'Save' : 'Edit'" />
<v-icon v-if="row._editing" class="fa-save" alt="Save" />
<v-icon v-else class="fa-pencil" alt="Edit" />
</button>
</td>
@@ -90,6 +106,13 @@ Rows are identified by row._id, columns by col.id
alt="Revert Change" />
</td>
</template>
<td style="text-align: center;">
<v-icon @click="discardEdit(row)"
v-if="isRowChanged(row) && (row._editing || newRows)"
class="fa-undo text-danger pointer"
alt="Revert Change" />
</td>
</tr>
</tbody>
</table>
@@ -107,6 +130,10 @@ Rows are identified by row._id, columns by col.id
th {
border-top: 0 none;
}
table.new-rows td {
border: 0 none;
}
</style>
<script>
@@ -128,6 +155,8 @@ Rows are identified by row._id, columns by col.id
csrf: String,
/** Full URL of the current page */
pageUrl: String,
/** URL to import CSV */
loadCsvUrl: String,
/** Indicate this is the Add Rows page */
newRows: {
type: Boolean,
@@ -144,12 +173,14 @@ Rows are identified by row._id, columns by col.id
},
watch: {
dirtyRows: function (value) {
console.log(`Dirty Rows ${value}`)
if (value > 0) {
$(window).on('beforeunload', function () {
return 'Some row changes are not saved.';
});
return 'Some row changes are not saved.'
})
} else {
$(window).off('beforeunload');
$(window).off('beforeunload')
}
}
},
@@ -222,24 +253,24 @@ Rows are identified by row._id, columns by col.id
}
},
/** Compute style for a row */
rowStyle (row) {
return {
opacity: row._remove ? .8 : 1,
backgroundColor:
row._remove ? '#FFC4CC' :
'transparent'
}
},
/** Test if a value cell is changed */
isChanged (row, colId) {
return row._changed && row._changed.indexOf(colId) > -1
},
/** Test if any field in the row is changed */
isRowChanged(row) {
return row._changed && row._changed.length
isRowChanged (row, cached = false) {
if (this.newRows) {
return row._changed && row._changed.length
} else {
if (cached) return row._dirty
// check against loadvals array
for (const [key, value] of Object.entries(row._loadvals)) {
// changed if differs from orig value and also from previous value from revision
if (row[key] != value && row[key] != row._orig[key]) return true
}
return false
}
},
/** Revert a value cell */
@@ -247,10 +278,9 @@ Rows are identified by row._id, columns by col.id
this.submitRowChange(_.merge({}, row, {[colId]: row._orig[colId]}))
},
markRowDirty(row) {
if (!this.newRows) return
let wasDirty = this.isRowChanged(row)
/** Mark row as dirty and needing a save */
markRowDirty (row) {
let wasDirty = this.isRowChanged(row, true)
let changes = []
_.each(row._orig, (v, k) => {
@@ -259,14 +289,17 @@ Rows are identified by row._id, columns by col.id
}
})
this.rows[row._id]._changed = changes;
this.rows[row._id]._changed = changes
let isDirty = this.isRowChanged(row)
if (wasDirty && !isDirty) this.dirtyRows--
else if (!wasDirty && isDirty) this.dirtyRows++
row._dirty = isDirty
},
saveAllChanges() {
/** Save all changed rows */
saveAllChanges () {
let toChange = _.filter(this.rows, (r) => {
return this.isRowChanged(r)
})
@@ -286,6 +319,21 @@ Rows are identified by row._id, columns by col.id
// this.$set(this.rows[row._id], '_errors', er.errors)
// }
})
},
discardEdit (row) {
let wasDirty = this.isRowChanged(row)
_.each(this.newRows ? row._orig : row._loadvals, (val, id) => {
row[id] = val
})
row._editing = false
row._dirty = false
if (this.newRows) row._changed = []
this.$set(this.rows, row._id, row)
if (wasDirty) this.dirtyRows--
}
}
}
+5
View File
@@ -1 +1,6 @@
@import "../../sass/bootstrap-base";
tr.remove, tr.remove:hover {
//opacity: .8;
background-color: #FFC4CC;
}
@@ -8,19 +8,42 @@
@extends('table.propose.layout')
@section('tab-content')
<div class="form-inline py-2 px-1">
<form action="{{$table->draftUpdateRoute}}" method="POST" class="form-inline">
@csrf
<input type="hidden" name="action" value="rows.add-csv">
<input type="hidden" name="redirect" value="{{request()->fullUrl()}}">
<label class="pr-2" for="csv-data">Add CSV:</label>
<textarea name="data" id="csv-data"
title="{{$errors->has('data') ? $errors->first('data') : ''}}"
class="form-control mr-1 {{ $errors->has('data')?'is-invalid':'' }}"
style="width:30em; height:10em">{{old('data')}}</textarea>
<button class="btn btn-outline-success">Append</button>
</form>
</div>
<form action="{{$table->draftUpdateRoute}}" method="POST" class="form col-md-12 mt-3" enctype="multipart/form-data">
@csrf
<input type="hidden" name="action" value="row.add-csv">
<input type="hidden" name="redirect" value="{{$table->getDraftRoute('add-rows')}}">
<?php Widget::setLayout(3,7) ?>
{!! Widget::header(3, "Import CSV") !!}
@php
$cols = [];
foreach ($columns as $column) {
$cols[] = '<b>'.e($column->title) . '</b>';
}
@endphp
{!! Widget::par('Append rows from pasted CSV lines or uploaded CSV file') !!}
{{-- TODO interactive widget to select which cols are included, and in which order --}}
{!! Widget::labeledPar('Columns', implode(', ', $cols), '', false) !!}
{!! Widget::textarea('data', 'CSV as text')->help('
Comma-separated values, one row per line.
Do not put spaces after commas, they would be
included in the values.
')->minHeight('10em') !!}
{!! Widget::file('csv-file', 'CSV file')->accept("text/csv") !!}
<div class="row form-group">
<div class="col-md-7 offset-md-3">
<button class="btn btn-outline-success">
@icon(fa-upload fa-pr)Upload
</button>
</div>
</div>
</form>
@stop
@push('scripts')
@@ -32,6 +32,7 @@
xRows: {!! toJSON($xrows, true) !!},
newRows: true, // indicate all are new
pageUrl: @json(request()->fullUrl()),
loadCsvUrl: @json($table->getDraftRoute('add-rows-csv')),
csrf: @json(csrf_token()),
})