CSV import, also from file, and more UX improvements
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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()),
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user