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() public function rules()
{ {
return [ 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() public function boot()
{ {
// Add some custom validation rules // 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); return is_dir($value) && is_readable($value);
}); });
} }

View file

@ -8,7 +8,7 @@
<site-header></site-header> <site-header></site-header>
<main-wrapper></main-wrapper> <main-wrapper></main-wrapper>
<site-footer></site-footer> <site-footer></site-footer>
<overlay v-show="loading"></overlay> <overlay :state.sync="overlayState"></overlay>
</div> </div>
</template> </template>
@ -36,13 +36,19 @@
data() { data() {
return { return {
loading: false,
prefs: preferenceStore.state, prefs: preferenceStore.state,
overlayState: {
showing: true,
dismissable: false,
type: 'loading',
message: '',
},
}; };
}, },
ready() { ready() {
this.toggleOverlay(); this.showOverlay();
// Make the most important HTTP request to get all necessary data from the server. // Make the most important HTTP request to get all necessary data from the server.
// Afterwards, init all mandatory stores and services. // Afterwards, init all mandatory stores and services.
@ -50,8 +56,7 @@
this.initStores(); this.initStores();
playback.init(this); playback.init(this);
// Hide the overlaying loading screen. this.hideOverlay();
this.toggleOverlay();
// Ask for user's notificatio permission. // Ask for user's notificatio permission.
this.requestNotifPermission(); 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() { showOverlay(message = 'Just a little patience…', type = 'loading', dismissable = false) {
this.loading = !this.loading; 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> <script>
import settingStore from '../../../stores/setting'; import settingStore from '../../../stores/setting';
import utils from '../../../services/utils';
export default { export default {
data() { data() {
@ -39,7 +40,7 @@
* Save the settings. * Save the settings.
*/ */
save() { save() {
this.$root.toggleOverlay(); this.$root.showOverlay();
settingStore.update(() => { settingStore.update(() => {
// Data changed. // Data changed.
@ -48,6 +49,12 @@
// We need refresh the page. // We need refresh the page.
// Goodbye. // Goodbye.
document.location.reload(); 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> <template>
<div id="overlay"> <div id="overlay" v-show="state.showing" class="{{ state.type }}">
<sound-bar></sound-bar> <div class="display">
<span class="gnr">Just a little patience</span> <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> </div>
</template> </template>
@ -9,6 +18,7 @@
import soundBar from './sound-bar.vue'; import soundBar from './sound-bar.vue';
export default { export default {
props: ['state'],
components: { soundBar }, components: { soundBar },
}; };
</script> </script>
@ -27,9 +37,39 @@
background-color: rgba(0, 0, 0, 1); background-color: rgba(0, 0, 0, 1);
@include vertical-center(); @include vertical-center();
flex-direction: column;
.gnr { .display {
opacity: .7; @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> </style>

View file

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

View file

@ -26,4 +26,15 @@ export default {
return (h === '00' ? '' : h + ':') + i + ':' + s; 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; return this.state.settings;
}, },
update(cb = null) { update(cb = null, error = null) {
http.post('settings', this.all(), msg => { http.post('settings', this.all(), msg => {
if (cb) { if (cb) {
cb(); cb();
} }
}); }, { error });
}, },
}; };

View file

@ -12,4 +12,23 @@ describe('services/utils', () => {
utils.secondsToHis(314).should.equal('05:14'); 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; $colorGreen: #56a052;
$colorBlue: #4c769a; $colorBlue: #4c769a;
$colorRed: #c34848; $colorRed: #c34848;
$colorOrange: #ff7d2e;
$colorSidebarBgr: #212121; $colorSidebarBgr: #212121;
$colorExtraBgr: #212121; $colorExtraBgr: #212121;
@ -19,7 +20,7 @@ $colorPlayerControlsBgr: transparent;
$colorLink: #aaa; $colorLink: #aaa;
$colorLinkHovered: #fff; $colorLinkHovered: #fff;
$colorHighlight: #ff7d2e; $colorHighlight: $colorOrange;
$fontFamily: 'Roboto', sans-serif; $fontFamily: 'Roboto', sans-serif;
$fontSize: 13px; $fontSize: 13px;

View file

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