mirror of
https://github.com/koel/koel
synced 2024-09-20 06:11:53 +00:00
Better error handling for settings saving
This commit is contained in:
parent
73b5c89a85
commit
3aa7cb5ec4
11 changed files with 131 additions and 25 deletions
|
@ -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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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]), []);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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',
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Reference in a new issue