Better error handling for settings saving

This commit is contained in:
An Phan 2015-12-16 00:28:54 +08:00
parent 73b5c89a85
commit 3aa7cb5ec4
11 changed files with 131 additions and 25 deletions

View file

@ -22,7 +22,7 @@ class SettingRequest extends Request
public function rules()
{
return [
'media_path' => 'string|required|valid_path',
'media_path' => 'string|required|path.valid',
];
}
}

View file

@ -15,7 +15,7 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Add some custom validation rules
Validator::extend('valid_path', function ($attribute, $value, $parameters, $validator) {
Validator::extend('path.valid', function ($attribute, $value, $parameters, $validator) {
return is_dir($value) && is_readable($value);
});
}

View file

@ -8,7 +8,7 @@
<site-header></site-header>
<main-wrapper></main-wrapper>
<site-footer></site-footer>
<overlay v-show="loading"></overlay>
<overlay :state.sync="overlayState"></overlay>
</div>
</template>
@ -36,13 +36,19 @@
data() {
return {
loading: false,
prefs: preferenceStore.state,
overlayState: {
showing: true,
dismissable: false,
type: 'loading',
message: '',
},
};
},
ready() {
this.toggleOverlay();
this.showOverlay();
// Make the most important HTTP request to get all necessary data from the server.
// Afterwards, init all mandatory stores and services.
@ -50,8 +56,7 @@
this.initStores();
playback.init(this);
// Hide the overlaying loading screen.
this.toggleOverlay();
this.hideOverlay();
// Ask for user's notificatio permission.
this.requestNotifPermission();
@ -174,11 +179,32 @@
},
/**
* Show or hide the loading overlay.
* Shows the overlay.
*
* @param {String} message The message to display.
* @param {String} type (loading|success|info|warning|error)
* @param {Boolean} dismissable Whether to show the Close button
*/
toggleOverlay() {
this.loading = !this.loading;
}
showOverlay(message = 'Just a little patience…', type = 'loading', dismissable = false) {
this.overlayState.message = message;
this.overlayState.type = type;
this.overlayState.dismissable = dismissable;
this.overlayState.showing = true;
},
/**
* Hides the overlay.
*/
hideOverlay() {
this.overlayState.showing = false;
},
/**
* Shows the close button, allowing the user to close the overlay.
*/
setOverlayDimissable() {
this.overlayState.dismissable = true;
},
},
};

View file

@ -26,6 +26,7 @@
<script>
import settingStore from '../../../stores/setting';
import utils from '../../../services/utils';
export default {
data() {
@ -39,7 +40,7 @@
* Save the settings.
*/
save() {
this.$root.toggleOverlay();
this.$root.showOverlay();
settingStore.update(() => {
// Data changed.
@ -48,6 +49,12 @@
// We need refresh the page.
// Goodbye.
document.location.reload();
}, (error, status) => {
if (status === 422) {
error = utils.parseValidationError(error)[0];
}
this.$root.showOverlay(`Error: ${error}`, 'error', true);
});
},
},

View file

@ -1,7 +1,16 @@
<template>
<div id="overlay">
<sound-bar></sound-bar>
<span class="gnr">Just a little patience</span>
<div id="overlay" v-show="state.showing" class="{{ state.type }}">
<div class="display">
<sound-bar v-show="state.type === 'loading'"></sound-bar>
<i class="fa fa-exclamation-circle" v-show="state.type === 'error'"></i>
<i class="fa fa-exclamation-triangle" v-show="state.type === 'warning'"></i>
<i class="fa fa-info-circle" v-show="state.type === 'info'"></i>
<i class="fa fa-check-circle" v-show="state.type === 'success'"></i>
<span>{{{ state.message }}}</span>
</div>
<button v-show="state.dismissable" @click.prevent="state.showing = false">Close</button>
</div>
</template>
@ -9,6 +18,7 @@
import soundBar from './sound-bar.vue';
export default {
props: ['state'],
components: { soundBar },
};
</script>
@ -27,9 +37,39 @@
background-color: rgba(0, 0, 0, 1);
@include vertical-center();
flex-direction: column;
.gnr {
opacity: .7;
.display {
@include vertical-center();
i {
margin-right: 6px;
}
}
button {
font-size: 12px;
margin-top: 16px;
}
&.error {
color: $colorRed;
}
&.success {
color: $colorGreen;
}
&.info {
color: $colorBlue;
}
&.loading {
color: $color2ndText;
}
&.warning {
color: $colorOrange;
}
}
</style>

View file

@ -10,13 +10,13 @@ import { extend } from 'lodash';
*/
export default {
request(method, url, data, cb = null, options = {}) {
options = extend(options, {
options = extend({
error: (data, status, request) => {
if (status === 401) {
document.location.href = "/login";
}
},
});
}, options);
switch (method) {
case 'get':

View file

@ -26,4 +26,15 @@ export default {
return (h === '00' ? '' : h + ':') + i + ':' + s;
},
/**
* Parse the validation error from the server into a flattened array of messages.
*
* @param {Object} error The error object in JSON format.
*
* @return {Array}
*/
parseValidationError(error) {
return Object.keys(error).reduce((messages, field) => messages.concat(error[field]), []);
}
};

View file

@ -17,11 +17,11 @@ export default {
return this.state.settings;
},
update(cb = null) {
update(cb = null, error = null) {
http.post('settings', this.all(), msg => {
if (cb) {
cb();
}
});
}, { error });
},
};

View file

@ -12,4 +12,23 @@ describe('services/utils', () => {
utils.secondsToHis(314).should.equal('05:14');
});
});
describe('#parseValidationError', () => {
it('correctly parses single-level validation error', () => {
let error = {
err_1: ['Foo'],
};
utils.parseValidationError(error).should.eql(['Foo']);
});
it('correctly parses multi-level validation error', () => {
let error = {
err_1: ['Foo', 'Bar'],
err_2: ['Baz', 'Qux'],
};
utils.parseValidationError(error).should.eql(['Foo', 'Bar', 'Baz', 'Qux']);
});
});
});

View file

@ -10,6 +10,7 @@ $colorHeart: #bf2043;
$colorGreen: #56a052;
$colorBlue: #4c769a;
$colorRed: #c34848;
$colorOrange: #ff7d2e;
$colorSidebarBgr: #212121;
$colorExtraBgr: #212121;
@ -19,7 +20,7 @@ $colorPlayerControlsBgr: transparent;
$colorLink: #aaa;
$colorLinkHovered: #fff;
$colorHighlight: #ff7d2e;
$colorHighlight: $colorOrange;
$fontFamily: 'Roboto', sans-serif;
$fontSize: 13px;

View file

@ -77,6 +77,10 @@ return [
'unique' => 'The :attribute has already been taken.',
'url' => 'The :attribute format is invalid.',
'path' => [
'valid' => 'The :attribute is not a valid or readable path.',
],
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
@ -89,9 +93,7 @@ return [
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*