mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
Custom fields feature.
This commit is contained in:
58
client/src/components/SvgIcon/index.vue
Normal file
58
client/src/components/SvgIcon/index.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
|
||||
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
|
||||
<use :xlink:href="iconName" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SvgIcon',
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isExternal() {
|
||||
return Boolean(this.iconClass)
|
||||
},
|
||||
iconName() {
|
||||
return `#icon-${this.iconClass}`;
|
||||
},
|
||||
svgClass() {
|
||||
if (this.className) {
|
||||
return `svg-icon ${this.className}`;
|
||||
}
|
||||
return 'svg-icon';
|
||||
},
|
||||
styleExternalIcon() {
|
||||
return {
|
||||
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
|
||||
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-external-icon {
|
||||
background-color: currentColor;
|
||||
mask-size: cover!important;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -2,4 +2,21 @@ export default {
|
||||
login: 'Login',
|
||||
password: 'Password',
|
||||
username_password: 'Username or Email',
|
||||
with_your_account: 'With your Ratteb account.',
|
||||
if_you_forget_your_password: 'If you forgot your password, well, enter your Moosher ID than secret key to reset your password.',
|
||||
sign_in: 'Sign in',
|
||||
dashboard: 'Dashboard',
|
||||
reset_the_password: 'Reset the Password',
|
||||
enter_your_new_password: 'Enter You New Password',
|
||||
reset_password_link_is_invalid: 'The link is invalid or expired. Please reset your password again.',
|
||||
your_password_has_been_changed: 'Success Your password has been changed.',
|
||||
username_password_do_not_match: 'The username and password you entered did not match our records.',
|
||||
the_account_suspended_please_contact: 'This account is suspended, Please contact the administrator.',
|
||||
email_has_been_sent: 'An e-mail has been sent to {email} with further instructions.',
|
||||
there_is_no_email_with_that_address: 'There is no email with that address, Please try again.',
|
||||
reset_your_password: 'Reset Your Password',
|
||||
return_to_login: 'Return to Login',
|
||||
forget_password: 'Forget your password?',
|
||||
reset_password: 'Reset Password',
|
||||
email_password_do_not_match: 'Email and password do not match.',
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Vue from 'vue';
|
||||
import {Form, FormItem, Input} from 'element-ui';
|
||||
import {Form, FormItem, Input, Tabs, TabPane, Button, Alert} from 'element-ui';
|
||||
import App from '@/App';
|
||||
import router from '@/router';
|
||||
import store from '@/store';
|
||||
import '@/plugins/icons';
|
||||
|
||||
// Plugins
|
||||
import '@/plugins/i18n';
|
||||
@@ -12,6 +13,10 @@ Vue.config.productionTip = false;
|
||||
Vue.use(Form);
|
||||
Vue.use(FormItem);
|
||||
Vue.use(Input);
|
||||
Vue.use(Tabs);
|
||||
Vue.use(TabPane);
|
||||
Vue.use(Button);
|
||||
Vue.use(Alert);
|
||||
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
|
||||
20
client/src/mixins/Reloadable.js
Normal file
20
client/src/mixins/Reloadable.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'reloadable',
|
||||
created() {
|
||||
this.$eventBus.$on('reload-data', this.onReloadData);
|
||||
},
|
||||
methods: {
|
||||
async onReloadData() {
|
||||
this.$nprogress.start();
|
||||
await this.reloadData();
|
||||
this.$nprogress.done();
|
||||
},
|
||||
|
||||
reloadData() {},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('reload-data', this.onReloadData);
|
||||
},
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div class="auth-page">
|
||||
<div class="auth-page__logo">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="auth-page__card">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'auth-warpper',
|
||||
beforeRouteEnter(to, from, next) {
|
||||
document.body.classList.add('page-auth');
|
||||
next();
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
document.body.classList.remove('page-auth');
|
||||
next();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body.page-auth{
|
||||
background: red;
|
||||
}
|
||||
|
||||
.auth-page{
|
||||
width: 600px;
|
||||
|
||||
&__logo{
|
||||
|
||||
}
|
||||
|
||||
&__card{
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
207
client/src/pages/Auth/AuthContainer.vue
Normal file
207
client/src/pages/Auth/AuthContainer.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="page-auth page-auth--login">
|
||||
<div class="page-auth__content-side">
|
||||
<div class="page-auth__content-header">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="page-auth__content-form">
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<AuthFooter></AuthFooter>
|
||||
</div>
|
||||
|
||||
<AuthMedia></AuthMedia>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AuthMedia from '@/pages/Auth/AuthMedia.vue';
|
||||
import AuthFooter from '@/pages/Auth/AuthFooter.vue';
|
||||
|
||||
export default {
|
||||
name: 'auth-container',
|
||||
components: {
|
||||
AuthMedia,
|
||||
AuthFooter,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-auth{
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
|
||||
&__content-side{
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
// @include media-breakpoint-up('lg'){
|
||||
width: 62%;
|
||||
// }
|
||||
|
||||
.remember-forget-wrapper{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 20px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.form-note{
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.btn-primary{
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content{
|
||||
|
||||
&-header{
|
||||
max-width: 440px;
|
||||
width: 100%;
|
||||
padding: 0 30px;
|
||||
margin: 0 auto;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
// @include media-breakpoint-down('xs'){
|
||||
// padding-top: 25px;
|
||||
// padding-left: 25px;
|
||||
// padding-right: 25px;
|
||||
// }
|
||||
|
||||
.icon-logo{
|
||||
// @include media-breakpoint-down('xs'){
|
||||
// width: 140px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
&-title{
|
||||
font-size: 42px;
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.025em;
|
||||
color: #253992;
|
||||
line-height: 1.2;
|
||||
padding-bottom: 23px;
|
||||
font-size: 2.4em;
|
||||
|
||||
// @include media-breakpoint-down('xs'){
|
||||
// font-size: 2.2rem;
|
||||
// }
|
||||
|
||||
small{
|
||||
font-size: 0.46em;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
color: #495463;
|
||||
letter-spacing: normal;
|
||||
margin-top: 10px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.text-success{
|
||||
color: #00d285;
|
||||
font-size: 0.41em;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&-form{
|
||||
width: 440px;
|
||||
max-width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 40px 30px 50px;
|
||||
|
||||
// @include media-breakpoint-down('xs'){
|
||||
// padding-left: 25px;
|
||||
// padding-right: 25px;
|
||||
// }
|
||||
}
|
||||
|
||||
&-footer{
|
||||
margin: 0 auto;
|
||||
width: 440px;
|
||||
max-width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0 30px;
|
||||
padding-bottom: 25px;
|
||||
|
||||
.footer-links{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li{
|
||||
padding: 2px 15px;
|
||||
font-size: 13px;
|
||||
color: #758698;
|
||||
display: inline-block;
|
||||
|
||||
&:first-child{
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child{
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:hover{
|
||||
color: #2c80ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Media Side **/
|
||||
&__media-side{
|
||||
display: flex;
|
||||
width: 38%;
|
||||
background-image: url('/images/cover.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
background-position: 50% 0;
|
||||
|
||||
// @include media-breakpoint-down('md'){
|
||||
// width: 38%;
|
||||
// }
|
||||
|
||||
// @include media-breakpoint-down('xs'){
|
||||
// display: none;
|
||||
// }
|
||||
}
|
||||
|
||||
&__media-overlay{
|
||||
position: absolute;
|
||||
background: #202141;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
client/src/pages/Auth/AuthFooter.vue
Normal file
15
client/src/pages/Auth/AuthFooter.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="page-auth__content-footer">
|
||||
<ul class="footer-links">
|
||||
<li><a href="#">{{ $t('privacy_policy') }}</a></li>
|
||||
<li><a href="#">{{ $t('terms_and_conditions') }}</a></li>
|
||||
<li>© 2019 Moosher.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'auth-footer',
|
||||
}
|
||||
</script>
|
||||
11
client/src/pages/Auth/AuthMedia.vue
Normal file
11
client/src/pages/Auth/AuthMedia.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="page-auth__media-side">
|
||||
<div class="page-auth__media-overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'auth-media',
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div id="reset-password">
|
||||
<h2 class="page-auth__content-title">
|
||||
{{ $t('reset_your_password') }}
|
||||
<small>{{ $t('if_you_forget_your_password') }}</small>
|
||||
</h2>
|
||||
|
||||
<el-form ref="form" class="form-container">
|
||||
<el-form-item :label="$t('username_password')" prop="title">
|
||||
<el-input v-model="form.crediential" :maxlength="100" name="name" required />
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<h2 class="page-auth__content-title">
|
||||
{{ $t('sign_in') }}
|
||||
<small>{{ $t('with_your_account') }}</small>
|
||||
</h2>
|
||||
|
||||
<el-alert :active="isCredentialError" :type="'error'">
|
||||
{{ $t('email_password_do_not_match') }}
|
||||
</el-alert>
|
||||
|
||||
<el-alert :isActive="isInactiveError" :type="'error'">
|
||||
{{ $t('the_account_suspended_please_contact') }}
|
||||
</el-alert>
|
||||
|
||||
<el-alert :isActive="isResetPasswordSuccess" type="success">
|
||||
{{ $t('your_password_has_been_changed') }}
|
||||
</el-alert>
|
||||
|
||||
<el-form ref="form" class="form-container">
|
||||
<el-form-item :label="$t('username_password')" prop="title">
|
||||
<el-input v-model="form.crediential" :maxlength="100" name="name" required />
|
||||
@@ -37,6 +54,17 @@ export default {
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isCredentialError() {
|
||||
return false;
|
||||
},
|
||||
isInactiveError() {
|
||||
return false;
|
||||
},
|
||||
isResetPasswordSuccess() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login']),
|
||||
|
||||
@@ -47,7 +75,7 @@ export default {
|
||||
const form = {};
|
||||
|
||||
this.login({ form }).then(() => {
|
||||
|
||||
this.$route.push({ name: 'dashboard.home' });
|
||||
}).catch((error) => {
|
||||
const { response } = error;
|
||||
const { data } = response;
|
||||
|
||||
14
client/src/pages/Dashboard/Accounts/AccountForm.vue
Normal file
14
client/src/pages/Dashboard/Accounts/AccountForm.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'account-form',
|
||||
data() {
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'products-list',
|
||||
}
|
||||
name: 'accounts-list',
|
||||
};
|
||||
</script>
|
||||
34
client/src/pages/Dashboard/Items/ItemForm.vue
Normal file
34
client/src/pages/Dashboard/Items/ItemForm.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="item-form" id="item-form">
|
||||
<el-form ref="form" class="form-container">
|
||||
<el-form-item :label="$t('item_name')" prop="title">
|
||||
<el-input v-model="form.name" :maxlength="100" name="name" required />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('SKU')" prop="title">
|
||||
<el-input v-model="form.SKU" :maxlength="100" name="SKU" required />
|
||||
</el-form-item>
|
||||
|
||||
<el-button type="primary">{{ $t('login') }}</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'item-form',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
name: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
onSubmit() {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
55
client/src/pages/Dashboard/Items/ItemsDatatable.vue
Normal file
55
client/src/pages/Dashboard/Items/ItemsDatatable.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
<template>
|
||||
<el-table v-loading="isLoading" :data="items.list" border fit highlight-current-row>
|
||||
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Reloadable from '@/mixins/Reloadable';
|
||||
|
||||
const STATE = {
|
||||
LOADING: 1,
|
||||
};
|
||||
export default {
|
||||
name: 'items-datatable',
|
||||
mixins: [Reloadable],
|
||||
data() {
|
||||
return {
|
||||
current_state: 0,
|
||||
page: 1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
items: 'getItems',
|
||||
}),
|
||||
isLoading() {
|
||||
return this.current_state === STATE.LOADING;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.current_state = STATE.LOADING;
|
||||
this.fetchData();
|
||||
this.current_state = 0;
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchItems']),
|
||||
|
||||
/**
|
||||
* Handle the reload data.
|
||||
*/
|
||||
reloadData() {
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the fetch data of datatable.
|
||||
*/
|
||||
async fetchData() {
|
||||
await this.fetchItems({ page: this.page });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
27
client/src/pages/Dashboard/Items/ItemsList.vue
Normal file
27
client/src/pages/Dashboard/Items/ItemsList.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick">
|
||||
<el-tab-pane label="User" name="first" :lazy="true">User</el-tab-pane>
|
||||
<el-tab-pane label="Config" name="second" :lazy="true">Config</el-tab-pane>
|
||||
<el-tab-pane label="Role" name="third" :lazy="true">Role</el-tab-pane>
|
||||
<el-tab-pane label="Task" name="fourth" :lazy="true">Task</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'products-list',
|
||||
data() {
|
||||
return {
|
||||
activeName: 'first',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleClick(tab, event) {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'item-category-form',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table v-loading="isLoading" :data="items.list" border fit highlight-current-row>
|
||||
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'items-categories',
|
||||
data() {
|
||||
return {
|
||||
current_state: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
categories: 'getItemsCategories',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchItemsCategories']),
|
||||
|
||||
/**
|
||||
* Handle the reload data of the current view.
|
||||
*/
|
||||
async reloadData() {
|
||||
await this.fetchData();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the fetch data of the datatable.
|
||||
*/
|
||||
async fetchData() {
|
||||
await this.fetchItemsCategories();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
0
client/src/pages/Dashboard/Roles/RoleForm.vue
Normal file
0
client/src/pages/Dashboard/Roles/RoleForm.vue
Normal file
0
client/src/pages/Dashboard/Roles/RolesList.vue
Normal file
0
client/src/pages/Dashboard/Roles/RolesList.vue
Normal file
0
client/src/pages/Dashboard/Users/UserForm.vue
Normal file
0
client/src/pages/Dashboard/Users/UserForm.vue
Normal file
14
client/src/pages/Dashboard/Users/UsersList.vue
Normal file
14
client/src/pages/Dashboard/Users/UsersList.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>Users List</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'users-list',
|
||||
afterRouteEnter(to, from, next) {
|
||||
debugger;
|
||||
this.$store.commit('setPageTitle', this.$t('users_list'));
|
||||
next();
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -3,9 +3,7 @@ import Vue from 'vue';
|
||||
export default {
|
||||
|
||||
get(resource, params) {
|
||||
return Vue.axios.get(`api/${resource}`, params).catch((error) => {
|
||||
throw new Error(`[Moosher] ApiService ${error}`);
|
||||
});
|
||||
return Vue.axios.get(`api/${resource}`, params);
|
||||
},
|
||||
|
||||
post(resource, params) {
|
||||
@@ -21,8 +19,6 @@ export default {
|
||||
},
|
||||
|
||||
delete(resource) {
|
||||
return Vue.axios.delete(`api/${resource}`).catch((error) => {
|
||||
throw new Error(`[Moosher] ApiService ${error}`);
|
||||
});
|
||||
return Vue.axios.delete(`api/${resource}`);
|
||||
}
|
||||
};
|
||||
|
||||
9
client/src/plugins/icons.js
Normal file
9
client/src/plugins/icons.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Vue from 'vue';
|
||||
import SvgIcon from '@/components/SvgIcon';
|
||||
|
||||
// register globally
|
||||
Vue.component('svg-icon', SvgIcon)
|
||||
|
||||
const req = require.context('../../static/icons', false, /\.svg$/);
|
||||
const requireAll = requireContext => requireContext.keys().map(requireContext);
|
||||
requireAll(req);
|
||||
@@ -3,8 +3,8 @@ const routes = [
|
||||
{
|
||||
name: 'auth',
|
||||
path: '/',
|
||||
component: () => import(/* webpackChunkName: "auth" */
|
||||
'@/pages/Auth/Auth.vue'),
|
||||
component: () => import(/* webpackChunkName: "auth_container" */
|
||||
'@/pages/Auth/AuthContainer.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'login',
|
||||
@@ -32,6 +32,56 @@ const routes = [
|
||||
component: () => import(/* webpackChunkName: "dashboard_home" */
|
||||
'@/pages/Dashboard/Home.vue'),
|
||||
},
|
||||
|
||||
/**
|
||||
* Items. (Products/Services).
|
||||
* --------------------------------
|
||||
*/
|
||||
{
|
||||
name: 'dashboard.items.list',
|
||||
path: '/items',
|
||||
component: () => import(/* webpackChunkName: "items_list" */
|
||||
'@/pages/Dashboard/Items/ItemsList.vue'),
|
||||
accessControl: { resource: 'items', permissions: ['view'] },
|
||||
},
|
||||
{
|
||||
name: 'dashboard.items.new',
|
||||
path: '/items/new',
|
||||
component: () => import(/* webpackChunkName: "items_form" */
|
||||
'@/pages/Dashboard/Items/ItemForm.vue'),
|
||||
accessControl: { resource: 'items', permissions: ['create'] },
|
||||
},
|
||||
|
||||
/**
|
||||
* Accounts
|
||||
* ---------------------------
|
||||
*/
|
||||
{
|
||||
name: 'dashboard.accounts.list',
|
||||
path: '/accounts/list',
|
||||
component: () => import(/* webpackChunkName: "accounts_list" */
|
||||
'@/pages/Dashboard/Accounts/AccountsList.vue'),
|
||||
accessControl: { resource: 'accounts', permissions: ['view'] },
|
||||
},
|
||||
|
||||
/**
|
||||
* Users.
|
||||
* --------------------------------
|
||||
*/
|
||||
{
|
||||
name: 'dashboard.users.list',
|
||||
path: '/users',
|
||||
component: () => import(/* webpackChunkName: "users_list" */
|
||||
'@/pages/Dashboard/Users/UsersList.vue'),
|
||||
accessControl: { resource: 'users', permissions: ['view'] },
|
||||
},
|
||||
{
|
||||
name: 'dasboard.user.new',
|
||||
path: '/users',
|
||||
component: () => import(/* webpackChunkName: "user_form" */
|
||||
'@/pages/Dashboard/Users/UserForm.vue'),
|
||||
accessControl: { resource: 'users', permissions: ['create'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,6 +7,9 @@ const state = {
|
||||
|
||||
pageTitle: 'Welcome',
|
||||
actions: [],
|
||||
|
||||
contentState: 0,
|
||||
sidebarOpened: true,
|
||||
};
|
||||
|
||||
const getters = {
|
||||
@@ -23,4 +26,16 @@ const actions = {
|
||||
},
|
||||
};
|
||||
|
||||
export default { state, actions, getters };
|
||||
const mutations = {
|
||||
toggleSidebar() {
|
||||
state.sidebarOpened = !state.sidebarOpened;
|
||||
|
||||
if (state.sidebarOpened) {
|
||||
localStorage.set('sidebarStatus', 1)
|
||||
} else {
|
||||
localStorage.set('sidebarStatus', 0)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default { state, actions, mutations, getters };
|
||||
|
||||
@@ -33,7 +33,7 @@ const actions = {
|
||||
return ApiService.post('auth/send_reset_password', { email });
|
||||
},
|
||||
|
||||
newPassword(, { form }) {
|
||||
newPassword(null, { form }) {
|
||||
return ApiService.post('auth/new_password', form);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,9 +22,7 @@ const actions = {
|
||||
commit('setItems', data);
|
||||
|
||||
if (count) {
|
||||
commit('setSidebarItemCount', {
|
||||
name: 'customers', count,
|
||||
});
|
||||
commit('setSidebarItemCount', { name: 'customers', count });
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
28
client/src/store/modules/errorLog.js
Normal file
28
client/src/store/modules/errorLog.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const state = {
|
||||
logs: [],
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
ADD_ERROR_LOG: (s, log) => {
|
||||
s.logs.push(log);
|
||||
},
|
||||
CLEAR_ERROR_LOG: (s) => {
|
||||
s.logs.splice(0);
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
addErrorLog({ commit }, log) {
|
||||
commit('ADD_ERROR_LOG', log);
|
||||
},
|
||||
clearErrorLog({ commit }) {
|
||||
commit('CLEAR_ERROR_LOG');
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
||||
@@ -3,11 +3,15 @@ import ApiService from '@/plugins/api-service';
|
||||
const state = {
|
||||
list: {},
|
||||
details: [],
|
||||
categories: {},
|
||||
categoriesDetails: [],
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getItems: s => s.list,
|
||||
getItem: s => id => s.details.find(i => i.id === id),
|
||||
getItemsCategories: s => s.categories,
|
||||
getItemCategory: s => id => s.categoriesDetails.find(i => i.id === id),
|
||||
};
|
||||
|
||||
const actions = {
|
||||
@@ -54,6 +58,41 @@ const actions = {
|
||||
async updateItem({}, { form, id }) {
|
||||
return ApiService.post(`items/${id}`, form);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches items categories paged list.
|
||||
*/
|
||||
async fetchItemsCategories({}, { query }) {
|
||||
return ApiService.get('/items_categories', { params: query });
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch details of the given item category.
|
||||
*/
|
||||
async fetchItemCategory({}, { id }) {
|
||||
return ApiService.get(`/item/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the given item category.
|
||||
*/
|
||||
async deleteItemCategory({}, { id }) {
|
||||
return ApiService.delete(`/items_categories/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Post a new item category.
|
||||
*/
|
||||
async newItemCategory({}, { form }) {
|
||||
return ApiService.post('/items_categories', form);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update details of the given item category.
|
||||
*/
|
||||
async updateItemCategory({}, { id, form }) {
|
||||
return ApiService.post(`/items_categories/${id}`, form);
|
||||
},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
@@ -66,6 +105,15 @@ const mutations = {
|
||||
s.details = s.details.filter(i => i.id !== item.id);
|
||||
s.details.push(item);
|
||||
},
|
||||
|
||||
setItemCategories(s, categories) {
|
||||
s.categories = categories;
|
||||
},
|
||||
|
||||
setItemCategory(s, category) {
|
||||
s.categoriesDetails = s.categoriesDetails.filter(i => i.id !== category.id);
|
||||
s.categoriesDetails.push(category);
|
||||
},
|
||||
};
|
||||
|
||||
export default { state, actions, mutations, getters };
|
||||
|
||||
0
client/src/store/modules/roles.js
Normal file
0
client/src/store/modules/roles.js
Normal file
@@ -9,7 +9,7 @@ const state = {
|
||||
},
|
||||
{
|
||||
name: 'Products',
|
||||
to: 'dashboard.home',
|
||||
to: 'dashboard.items.list',
|
||||
},
|
||||
{
|
||||
name: 'Customers',
|
||||
@@ -23,12 +23,54 @@ const state = {
|
||||
name: 'Reports',
|
||||
to: 'dashboard.home',
|
||||
},
|
||||
{
|
||||
name: 'Users',
|
||||
to: 'dashboard.users.list',
|
||||
children: {
|
||||
name: 'New User',
|
||||
to: 'dashboard.user.new',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Accounting',
|
||||
to: 'dashboard.accounts.list',
|
||||
},
|
||||
],
|
||||
|
||||
quickActions: [
|
||||
{
|
||||
route: 'dashboard.items.list',
|
||||
actions: [
|
||||
{
|
||||
dialog: 'global-search',
|
||||
label: 'Search',
|
||||
icon: 'search',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
route: 'dashboard.items.list',
|
||||
actions: [
|
||||
{
|
||||
dialog: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getSidebarItems: s => s.items,
|
||||
getSidebarItem: s => name => s.items.find(item => item.name === name),
|
||||
|
||||
getAllQuickActions: s => s.quickActions,
|
||||
getQuickActions: s => (route) => {
|
||||
const foundDefault = s.quickActions.find(q => q.default === true);
|
||||
const found = s.quickActions.find(q => q.route === route);
|
||||
const defaultActions = foundDefault ? foundDefault.actions : [];
|
||||
|
||||
return found ? [...defaultActions, found.actions] : defaultActions;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
|
||||
38
client/src/utils.js
Normal file
38
client/src/utils.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const title = 'Ratteb';
|
||||
|
||||
export const getPageTitle = (pageTitle) => {
|
||||
if (pageTitle) {
|
||||
return `${pageTitle} - ${title}`;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
// const clipboardSuccess = () => {
|
||||
// Vue.prototype.$message({
|
||||
// message: 'Copy successfully',
|
||||
// type: 'success',
|
||||
// duration: 1500,
|
||||
// });
|
||||
// };
|
||||
|
||||
// const clipboardError = () => {
|
||||
// Vue.prototype.$message({
|
||||
// message: 'Copy failed',
|
||||
// type: 'error',
|
||||
// });
|
||||
// };
|
||||
|
||||
export const handleClipboard = (text, event) => {
|
||||
// const clipboard = new Clipboard(event.target, {
|
||||
// text: () => text
|
||||
// })
|
||||
// clipboard.on('success', () => {
|
||||
// clipboardSuccess()
|
||||
// clipboard.destroy()
|
||||
// })
|
||||
// clipboard.on('error', () => {
|
||||
// clipboardError()
|
||||
// clipboard.destroy()
|
||||
// })
|
||||
// clipboard.onClick(event)
|
||||
}
|
||||
14
client/src/views/Dialogs/SearchDialog.vue
Normal file
14
client/src/views/Dialogs/SearchDialog.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'search-dialog',
|
||||
data() {
|
||||
return {
|
||||
current_state: 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="sidebar__menu-item" :class="computedClasses">
|
||||
<router-link class="sidebar__menu-item-anchor" :to="to">
|
||||
<router-link class="sidebar__menu-item-anchor" :to="{name: to}">
|
||||
<span class="title">{{ name }}</span>
|
||||
<span v-if="count" class="count">{{ count }}</span>
|
||||
</router-link>
|
||||
@@ -18,6 +18,7 @@ export default {
|
||||
to: String,
|
||||
icon: String,
|
||||
children: Array,
|
||||
count: [Number, Boolean],
|
||||
},
|
||||
computed: {
|
||||
computedClasses() {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
v-for="(item, index) in sidebarItems" :key="index"
|
||||
:to="item.to"
|
||||
:name="item.name"
|
||||
:count="item.count"
|
||||
:count="item.count || false"
|
||||
:children="item.children"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
</div>
|
||||
|
||||
<div class="topbar__actions">
|
||||
|
||||
<div class="topbar__actions-list">
|
||||
<div v-for="(action, index) in actions" :key="index">
|
||||
<button @click.prevent="onClickItem(action)">{{ action.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,8 +26,18 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
pageTitle: 'getPageTitle',
|
||||
quickActions: 'getQuickActions',
|
||||
}),
|
||||
actions() {
|
||||
return this.quickActions(this.$route.name) || [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
onClickItem(action) {
|
||||
return action;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
159
server/package-lock.json
generated
159
server/package-lock.json
generated
@@ -858,6 +858,42 @@
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.2.2.tgz",
|
||||
"integrity": "sha512-18P3VwngjNEcmvPj1mmiHLPyUPjhPAxIyJKDj4PRIY0F5ac3P0Vd0hkASPyWXHK0rfY3P9N2FoxV8ZuYaRBZ1g=="
|
||||
},
|
||||
"@sinonjs/commons": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.6.0.tgz",
|
||||
"integrity": "sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"@sinonjs/formatio": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz",
|
||||
"integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1",
|
||||
"@sinonjs/samsam": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"@sinonjs/samsam": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz",
|
||||
"integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.3.0",
|
||||
"array-from": "^2.1.1",
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"@sinonjs/text-encoding": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
|
||||
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/chai": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.0.tgz",
|
||||
@@ -1231,6 +1267,12 @@
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||
},
|
||||
"array-from": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
|
||||
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
|
||||
"dev": true
|
||||
},
|
||||
"array-includes": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz",
|
||||
@@ -1370,7 +1412,6 @@
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "^2.4.0",
|
||||
"regenerator-runtime": "^0.11.0"
|
||||
@@ -1379,8 +1420,7 @@
|
||||
"regenerator-runtime": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1505,6 +1545,16 @@
|
||||
"lodash": "^4.17.10"
|
||||
}
|
||||
},
|
||||
"bookshelf-cascade-delete": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bookshelf-cascade-delete/-/bookshelf-cascade-delete-2.0.1.tgz",
|
||||
"integrity": "sha1-QRqD48g4lUuSORS9c/rCEnGjrpw=",
|
||||
"requires": {
|
||||
"babel-runtime": "^6.6.1",
|
||||
"bluebird": "^3.3.5",
|
||||
"lodash": "^4.11.1"
|
||||
}
|
||||
},
|
||||
"bookshelf-json-columns": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bookshelf-json-columns/-/bookshelf-json-columns-2.1.1.tgz",
|
||||
@@ -2202,8 +2252,7 @@
|
||||
"core-js": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
|
||||
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.2.1",
|
||||
@@ -3222,12 +3271,12 @@
|
||||
}
|
||||
},
|
||||
"express-validator": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.1.1.tgz",
|
||||
"integrity": "sha512-AF6YOhdDiCU7tUOO/OHp2W++I3qpYX7EInMmEEcRGOjs+qoubwgc5s6Wo3OQgxwsWRGCxXlrF73SIDEmY4y3wg==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.2.0.tgz",
|
||||
"integrity": "sha512-892cPistoSPzMuoG2p1W+2ZxBi0bAvPaaYgXK1E1C8/QncLo2d1HbiDDWkXUtTthjGEzEmwiELLJHu1Ez2hOEg==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.11",
|
||||
"validator": "^11.0.0"
|
||||
"lodash": "^4.17.15",
|
||||
"validator": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
@@ -3639,13 +3688,11 @@
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -3658,18 +3705,15 @@
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -3772,8 +3816,7 @@
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -3783,7 +3826,6 @@
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -3796,20 +3838,17 @@
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -3826,7 +3865,6 @@
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -3899,8 +3937,7 @@
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -3910,7 +3947,6 @@
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -4016,7 +4052,6 @@
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -5083,6 +5118,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"just-extend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz",
|
||||
"integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
|
||||
"dev": true
|
||||
},
|
||||
"jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
@@ -5376,6 +5417,12 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.result/-/lodash.result-4.5.2.tgz",
|
||||
"integrity": "sha1-y0Wyf7kU6qjY7m8M57KHC4fLcKo="
|
||||
},
|
||||
"lolex": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz",
|
||||
"integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==",
|
||||
"dev": true
|
||||
},
|
||||
"long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
@@ -5473,6 +5520,11 @@
|
||||
"mimic-fn": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"memory-cache": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz",
|
||||
"integrity": "sha1-eJCwHVLADI68nVM+H46xfjA0hxo="
|
||||
},
|
||||
"memory-fs": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz",
|
||||
@@ -5852,6 +5904,36 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"nise": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-1.5.2.tgz",
|
||||
"integrity": "sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/formatio": "^3.2.1",
|
||||
"@sinonjs/text-encoding": "^0.7.1",
|
||||
"just-extend": "^4.0.2",
|
||||
"lolex": "^4.1.0",
|
||||
"path-to-regexp": "^1.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
|
||||
"dev": true
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
|
||||
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-libs-browser": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
||||
@@ -7438,6 +7520,21 @@
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"sinon": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-7.4.2.tgz",
|
||||
"integrity": "sha512-pY5RY99DKelU3pjNxcWo6XqeB1S118GBcVIIdDi6V+h6hevn1izcg2xv1hTHW/sViRXU7sUOxt4wTUJ3gsW2CQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.4.0",
|
||||
"@sinonjs/formatio": "^3.2.1",
|
||||
"@sinonjs/samsam": "^3.3.3",
|
||||
"diff": "^3.5.0",
|
||||
"lolex": "^4.2.0",
|
||||
"nise": "^1.5.2",
|
||||
"supports-color": "^5.5.0"
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@hapi/boom": "^7.4.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bookshelf": "^0.15.1",
|
||||
"bookshelf-cascade-delete": "^2.0.1",
|
||||
"bookshelf-json-columns": "^2.1.1",
|
||||
"bookshelf-modelbase": "^2.10.4",
|
||||
"bookshelf-paranoia": "^0.13.1",
|
||||
|
||||
@@ -120,4 +120,51 @@ factory.define('resource', 'resources', () => ({
|
||||
name: faker.lorem.word(),
|
||||
}));
|
||||
|
||||
factory.define('view', 'views', async () => {
|
||||
const resource = await factory.create('resource');
|
||||
return {
|
||||
name: faker.lorem.word(),
|
||||
resource_id: resource.id,
|
||||
predefined: false,
|
||||
};
|
||||
});
|
||||
|
||||
factory.define('resource_field', 'resource_fields', async () => {
|
||||
const resource = await factory.create('resource');
|
||||
const dataTypes = ['select', 'date', 'text'];
|
||||
|
||||
return {
|
||||
label_name: faker.lorem.words(),
|
||||
data_type: dataTypes[Math.floor(Math.random() * dataTypes.length)],
|
||||
help_text: faker.lorem.words(),
|
||||
default: faker.lorem.word(),
|
||||
resource_id: resource.id,
|
||||
active: true,
|
||||
predefined: false,
|
||||
};
|
||||
});
|
||||
|
||||
factory.define('view_role', 'view_roles', async () => {
|
||||
const view = await factory.create('view');
|
||||
const field = await factory.create('resource_field');
|
||||
|
||||
return {
|
||||
view_id: view.id,
|
||||
index: faker.random.number(),
|
||||
field_id: field.id,
|
||||
value: '',
|
||||
comparator: '',
|
||||
};
|
||||
});
|
||||
|
||||
factory.define('view_has_columns', 'view_has_columns', async () => {
|
||||
const view = await factory.create('view');
|
||||
const field = await factory.create('resource_field');
|
||||
|
||||
return {
|
||||
field_id: field.id,
|
||||
view_id: view.id,
|
||||
};
|
||||
});
|
||||
|
||||
export default factory;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('resource_fields', (table) => {
|
||||
table.increments();
|
||||
table.string('label_name');
|
||||
table.string('data_type');
|
||||
table.string('help_text');
|
||||
table.string('default');
|
||||
table.boolean('active');
|
||||
table.boolean('predefined');
|
||||
table.json('options');
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('resource_fields');
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('view_has_columns', (table) => {
|
||||
table.increments();
|
||||
table.integer('view_id').unsigned();
|
||||
table.integer('field_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('view_has_columns');
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('view_roles', (table) => {
|
||||
table.increments();
|
||||
table.integer('index');
|
||||
table.integer('field_id').unsigned().references('id').inTable('resource_fields');
|
||||
table.string('comparator');
|
||||
table.string('value');
|
||||
table.integer('view_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('view_roles');
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('views', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
table.boolean('predefined');
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('views');
|
||||
@@ -6,7 +6,9 @@ import Account from '@/models/Account';
|
||||
// import AccountBalance from '@/models/AccountBalance';
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
@@ -17,6 +19,11 @@ export default {
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Opening balance to the given account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
openingBalnace: {
|
||||
validation: [
|
||||
check('accounts').isArray({ min: 1 }),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import { check, validationResult, param } from 'express-validator';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
import Account from '@/models/Account';
|
||||
import AccountBalance from '@/models/AccountBalance';
|
||||
// import AccountBalance from '@/models/AccountBalance';
|
||||
import AccountType from '@/models/AccountType';
|
||||
// import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
|
||||
@@ -22,13 +22,12 @@ export default {
|
||||
this.editAccount.validation,
|
||||
asyncMiddleware(this.editAccount.handler));
|
||||
|
||||
// router.get('/:id',
|
||||
// this.getAccount.validation,
|
||||
// asyncMiddleware(this.getAccount.handler));
|
||||
router.get('/:id',
|
||||
asyncMiddleware(this.getAccount.handler));
|
||||
|
||||
// router.delete('/:id',
|
||||
// this.deleteAccount.validation,
|
||||
// asyncMiddleware(this.deleteAccount.handler));
|
||||
router.delete('/:id',
|
||||
this.deleteAccount.validation,
|
||||
asyncMiddleware(this.deleteAccount.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
@@ -87,6 +86,7 @@ export default {
|
||||
*/
|
||||
editAccount: {
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
check('name').isLength({ min: 3 }).trim().escape(),
|
||||
check('code').isLength({ max: 10 }).trim().escape(),
|
||||
check('account_type_id').isNumeric().toInt(),
|
||||
@@ -142,7 +142,9 @@ export default {
|
||||
* Get details of the given account.
|
||||
*/
|
||||
getAccount: {
|
||||
valiation: [],
|
||||
valiation: [
|
||||
param('id').toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const account = await Account.where('id', id).fetch();
|
||||
@@ -159,7 +161,9 @@ export default {
|
||||
* Delete the given account.
|
||||
*/
|
||||
deleteAccount: {
|
||||
validation: [],
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const account = await Account.where('id', id).fetch();
|
||||
@@ -168,7 +172,6 @@ export default {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
await account.destroy();
|
||||
await AccountBalance.where('account_id', id).destroy({ require: false });
|
||||
|
||||
return res.status(200).send({ id: account.previous('id') });
|
||||
},
|
||||
|
||||
@@ -92,7 +92,7 @@ export default {
|
||||
});
|
||||
}
|
||||
const { email } = req.body;
|
||||
const user = User.where('email', email).fetch();
|
||||
const user = await User.where('email', email).fetch();
|
||||
|
||||
if (!user) {
|
||||
return res.status(422).send();
|
||||
|
||||
246
server/src/http/controllers/Fields.js
Normal file
246
server/src/http/controllers/Fields.js
Normal file
@@ -0,0 +1,246 @@
|
||||
import express from 'express';
|
||||
import { check, param, validationResult } from 'express-validator';
|
||||
import ResourceField from '@/models/ResourceField';
|
||||
import Resource from '@/models/Resource';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
|
||||
/**
|
||||
* Types of the custom fields.
|
||||
*/
|
||||
const TYPES = ['text', 'email', 'number', 'url', 'percentage', 'checkbox', 'radio', 'textarea'];
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/resource/:resource_id',
|
||||
this.addNewField.validation,
|
||||
asyncMiddleware(this.addNewField.handler));
|
||||
|
||||
router.post('/:field_id',
|
||||
this.editField.validation,
|
||||
asyncMiddleware(this.editField.handler));
|
||||
|
||||
router.post('/status/:field_id',
|
||||
this.changeStatus.validation,
|
||||
asyncMiddleware(this.changeStatus.handler));
|
||||
|
||||
router.get('/:field_id',
|
||||
asyncMiddleware(this.getField.handler));
|
||||
|
||||
router.delete('/:field_id',
|
||||
asyncMiddleware(this.deleteField.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new field control to the given resource.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
addNewField: {
|
||||
validation: [
|
||||
param('resource_id').toInt(),
|
||||
check('label').exists().escape().trim(),
|
||||
check('data_type').exists().isIn(TYPES),
|
||||
check('help_text').optional(),
|
||||
check('default').optional(),
|
||||
check('options').optional().isArray(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { resource_id: resourceId } = req.params;
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const resource = await Resource.where('id', resourceId).fetch();
|
||||
|
||||
if (!resource) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
const { label, data_type: dataType, help_text: helpText } = req.body;
|
||||
const { default: defaultValue, options } = req.body;
|
||||
|
||||
const choices = options.map((option, index) => ({ key: index + 1, value: option }));
|
||||
|
||||
const field = ResourceField.forge({
|
||||
data_type: dataType,
|
||||
label_name: label,
|
||||
help_text: helpText,
|
||||
default: defaultValue,
|
||||
resource_id: resource.id,
|
||||
options: choices,
|
||||
});
|
||||
|
||||
await field.save();
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit details of the given field.
|
||||
*/
|
||||
editField: {
|
||||
validation: [
|
||||
param('field_id').toInt(),
|
||||
check('label').exists().escape().trim(),
|
||||
check('data_type').exists(),
|
||||
check('help_text').optional(),
|
||||
check('default').optional(),
|
||||
check('options').optional().isArray(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { field_id: fieldId } = req.params;
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const field = await ResourceField.where('id', fieldId).fetch();
|
||||
|
||||
if (!field) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'FIELD_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
// Sets the default value of optional fields.
|
||||
const form = { options: [], ...req.body };
|
||||
|
||||
const { label, data_type: dataType, help_text: helpText } = form;
|
||||
const { default: defaultValue, options } = form;
|
||||
|
||||
const storedFieldOptions = field.attributes.options || [];
|
||||
let lastChoiceIndex = 0;
|
||||
storedFieldOptions.forEach((option) => {
|
||||
const key = parseInt(option.key, 10);
|
||||
if (key > lastChoiceIndex) {
|
||||
lastChoiceIndex = key;
|
||||
}
|
||||
});
|
||||
const savedOptionKeys = options.filter((op) => typeof op === 'object');
|
||||
const notSavedOptionsKeys = options.filter((op) => typeof op !== 'object');
|
||||
|
||||
const choices = [
|
||||
...savedOptionKeys,
|
||||
...notSavedOptionsKeys.map((option) => {
|
||||
lastChoiceIndex += 1;
|
||||
return { key: lastChoiceIndex, value: option };
|
||||
}),
|
||||
];
|
||||
|
||||
await field.save({
|
||||
data_type: dataType,
|
||||
label_name: label,
|
||||
help_text: helpText,
|
||||
default: defaultValue,
|
||||
options: choices,
|
||||
});
|
||||
|
||||
return res.status(200).send({ id: field.get('id') });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the fields list of the given resource.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
fieldsList: {
|
||||
validation: [
|
||||
param('resource_id').toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { resource_id: resourceId } = req.params;
|
||||
const fields = await ResourceField.where('resource_id', resourceId).fetchAll();
|
||||
|
||||
return res.status(200).send({ fields: fields.toJSON() });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Change status of the given field.
|
||||
*/
|
||||
changeStatus: {
|
||||
validation: [
|
||||
param('field_id').toInt(),
|
||||
check('active').isBoolean().toBoolean(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { field_id: fieldId } = req.params;
|
||||
const field = await ResourceField.where('id', fieldId).fetch();
|
||||
|
||||
if (!field) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'NOT_FOUND_FIELD', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
const { active } = req.body;
|
||||
await field.save({ active });
|
||||
|
||||
return res.status(200).send({ id: field.get('id') });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve details of the given field.
|
||||
*/
|
||||
getField: {
|
||||
validation: [
|
||||
param('field_id').toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { field_id: id } = req.params;
|
||||
const field = await ResourceField.where('id', id).fetch();
|
||||
|
||||
if (!field) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
field: field.toJSON(),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the given field.
|
||||
*/
|
||||
deleteField: {
|
||||
validation: [
|
||||
param('field_id').toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { field_id: id } = req.params;
|
||||
const field = await ResourceField.where('id', id).fetch();
|
||||
|
||||
if (!field) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
if (field.attributes.predefined) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PREDEFINED_FIELD', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
await field.destroy();
|
||||
|
||||
return res.status(200).send({ id: field.get('id') });
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import { check, param, validationResult } from 'express-validator';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
import ItemCategory from '@/models/ItemCategory';
|
||||
// import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
@@ -79,6 +79,7 @@ export default {
|
||||
*/
|
||||
editCategory: {
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
check('name').exists({ checkFalsy: true }).trim().escape(),
|
||||
check('parent_category_id').optional().isNumeric().toInt(),
|
||||
check('description').optional().trim().escape(),
|
||||
@@ -93,13 +94,11 @@ export default {
|
||||
});
|
||||
}
|
||||
const { name, parent_category_id: parentCategoryId, description } = req.body;
|
||||
|
||||
const itemCategory = await ItemCategory.where('id', id).fetch();
|
||||
|
||||
if (!itemCategory) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
|
||||
if (parentCategoryId && parentCategoryId !== itemCategory.attributes.parent_category_id) {
|
||||
const foundParentCategory = await ItemCategory.where('id', parentCategoryId).fetch();
|
||||
|
||||
@@ -109,7 +108,6 @@ export default {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await itemCategory.save({
|
||||
label: name,
|
||||
description,
|
||||
@@ -124,7 +122,9 @@ export default {
|
||||
* Delete the give item category.
|
||||
*/
|
||||
deleteItem: {
|
||||
validation: [],
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const itemCategory = await ItemCategory.where('id', id).fetch();
|
||||
@@ -151,4 +151,22 @@ export default {
|
||||
return res.status(200).send({ items: items.toJSON() });
|
||||
},
|
||||
},
|
||||
|
||||
getCategory: {
|
||||
validation: [
|
||||
param('category_id').toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { category_id: categoryId } = req.params;
|
||||
const item = await ItemCategory.where('id', categoryId).fetch();
|
||||
|
||||
if (!item) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'CATEGORY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({ category: item.toJSON() });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
194
server/src/http/controllers/Views.js
Normal file
194
server/src/http/controllers/Views.js
Normal file
@@ -0,0 +1,194 @@
|
||||
import { difference } from 'lodash';
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Resource from '@/models/Resource';
|
||||
import View from '../../models/View';
|
||||
|
||||
export default {
|
||||
resource: 'items',
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/resource/:resource_id',
|
||||
this.createView.validation,
|
||||
asyncMiddleware(this.createView.handler));
|
||||
|
||||
router.post('/:view_id',
|
||||
this.editView.validation,
|
||||
asyncMiddleware(this.editView.handler));
|
||||
|
||||
router.delete('/:view_id',
|
||||
this.deleteView.validation,
|
||||
asyncMiddleware(this.deleteView.handler));
|
||||
|
||||
router.get('/:view_id',
|
||||
asyncMiddleware(this.getView.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* List all views that associated with the given resource.
|
||||
*/
|
||||
listViews: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { resource_id: resourceId } = req.params;
|
||||
const views = await View.where('resource_id', resourceId).fetchAll();
|
||||
|
||||
return res.status(200).send({ views: views.toJSON() });
|
||||
},
|
||||
},
|
||||
|
||||
getView: {
|
||||
async handler(req, res) {
|
||||
const { view_id: viewId } = req.params;
|
||||
const view = await View.where('id', viewId).fetch({
|
||||
withRelated: ['resource', 'columns', 'viewRoles'],
|
||||
});
|
||||
|
||||
if (!view) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({ ...view.toJSON() });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the given view of the resource.
|
||||
*/
|
||||
deleteView: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { view_id: viewId } = req.params;
|
||||
const view = await View.where('id', viewId).fetch({
|
||||
withRelated: ['viewRoles', 'columns'],
|
||||
});
|
||||
|
||||
if (!view) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
if (view.attributes.predefined) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
|
||||
});
|
||||
}
|
||||
// console.log(view);
|
||||
await view.destroy();
|
||||
|
||||
// await view.columns().destroy({ require: false });
|
||||
|
||||
return res.status(200).send({ id: view.get('id') });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new view.
|
||||
*/
|
||||
createView: {
|
||||
validation: [
|
||||
check('label').exists().escape().trim(),
|
||||
check('columns').isArray({ min: 3 }),
|
||||
check('roles').isArray(),
|
||||
check('roles.*.field').exists().escape().trim(),
|
||||
check('roles.*.comparator').exists(),
|
||||
check('roles.*.value').exists(),
|
||||
check('roles.*.index').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { resource_id: resourceId } = req.params;
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const resource = await Resource.where('id', resourceId).fetch();
|
||||
|
||||
if (!resource) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
const errorReasons = [];
|
||||
const { label, roles, columns } = req.body;
|
||||
|
||||
const fieldsSlugs = roles.map((role) => role.field);
|
||||
|
||||
const resourceFields = await resource.fields().fetch();
|
||||
const resourceFieldsKeys = resourceFields.map((f) => f.get('key'));
|
||||
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
|
||||
|
||||
if (notFoundFields.length > 0) {
|
||||
errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields });
|
||||
}
|
||||
|
||||
const notFoundColumns = difference(columns, resourceFieldsKeys);
|
||||
|
||||
if (notFoundColumns.length > 0) {
|
||||
errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, fields: notFoundColumns });
|
||||
}
|
||||
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
const view = await View.forge({
|
||||
name: label,
|
||||
predefined: false,
|
||||
});
|
||||
|
||||
// Save view details.
|
||||
await view.save();
|
||||
|
||||
// Save view columns.
|
||||
|
||||
// Save view roles.
|
||||
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
editView: {
|
||||
validation: [
|
||||
check('label').exists().escape().trim(),
|
||||
check('columns').isArray({ min: 3 }),
|
||||
check('roles').isArray(),
|
||||
check('roles.*.field').exists().escape().trim(),
|
||||
check('roles.*.comparator').exists(),
|
||||
check('roles.*.value').exists(),
|
||||
check('roles.*.index').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { view_id: viewId } = req.params;
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const view = await View.where('id', viewId).fetch();
|
||||
|
||||
if (!view) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -6,6 +6,8 @@ import Items from '@/http/controllers/Items';
|
||||
import ItemCategories from '@/http/controllers/ItemCategories';
|
||||
import Accounts from '@/http/controllers/Accounts';
|
||||
import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance';
|
||||
import Views from '@/http/controllers/Views';
|
||||
import CustomFields from '@/http/controllers/Fields';
|
||||
|
||||
export default (app) => {
|
||||
// app.use('/api/oauth2', OAuth2.router());
|
||||
@@ -14,6 +16,8 @@ export default (app) => {
|
||||
app.use('/api/roles', Roles.router());
|
||||
app.use('/api/accounts', Accounts.router());
|
||||
app.use('/api/accountOpeningBalance', AccountOpeningBalance.router());
|
||||
app.use('/api/views', Views.router());
|
||||
app.use('/api/fields', CustomFields.router());
|
||||
app.use('/api/items', Items.router());
|
||||
app.use('/api/item_categories', ItemCategories.router());
|
||||
};
|
||||
|
||||
@@ -21,6 +21,11 @@ const Account = bookshelf.Model.extend({
|
||||
balances() {
|
||||
return this.hasMany('AccountBalance', 'accounnt_id');
|
||||
},
|
||||
}, {
|
||||
/**
|
||||
* Cascade delete dependents.
|
||||
*/
|
||||
dependents: ['balances'],
|
||||
});
|
||||
|
||||
export default bookshelf.model('Account', Account);
|
||||
|
||||
@@ -11,10 +11,18 @@ const Resource = bookshelf.Model.extend({
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
|
||||
permissions() {
|
||||
/**
|
||||
* Resource model may has many views.
|
||||
*/
|
||||
views() {
|
||||
return this.hasMany('View', 'resource_id');
|
||||
},
|
||||
|
||||
roles() {
|
||||
/**
|
||||
* Resource model may has many fields.
|
||||
*/
|
||||
fields() {
|
||||
return this.hasMany('ResourceField', 'resource_id');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
37
server/src/models/ResourceField.js
Normal file
37
server/src/models/ResourceField.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { snakeCase } from 'lodash';
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const ResourceField = bookshelf.Model.extend({
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
tableName: 'resource_fields',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
|
||||
virtuals: {
|
||||
/**
|
||||
* Resource field key.
|
||||
*/
|
||||
key() {
|
||||
return snakeCase(this.attributes.label_name);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Resource field may belongs to resource model.
|
||||
*/
|
||||
resource() {
|
||||
return this.belongsTo('Resource', 'resource_id');
|
||||
},
|
||||
}, {
|
||||
/**
|
||||
* JSON Columns.
|
||||
*/
|
||||
jsonColumns: ['options'],
|
||||
});
|
||||
|
||||
export default bookshelf.model('ResourceField', ResourceField);
|
||||
38
server/src/models/View.js
Normal file
38
server/src/models/View.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const View = bookshelf.Model.extend({
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
tableName: 'views',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
|
||||
/**
|
||||
* View model belongs to resource model.
|
||||
*/
|
||||
resource() {
|
||||
return this.belongsTo('Resource', 'resource_id');
|
||||
},
|
||||
|
||||
/**
|
||||
* View model may has many columns.
|
||||
*/
|
||||
columns() {
|
||||
return this.belongsToMany('ResourceField', 'view_has_columns', 'view_id', 'field_id');
|
||||
},
|
||||
|
||||
/**
|
||||
* View model may has many view roles.
|
||||
*/
|
||||
viewRoles() {
|
||||
return this.hasMany('ViewRole', 'view_id');
|
||||
},
|
||||
}, {
|
||||
dependents: ['columns', 'viewRoles'],
|
||||
});
|
||||
|
||||
export default bookshelf.model('View', View);
|
||||
19
server/src/models/ViewColumn.js
Normal file
19
server/src/models/ViewColumn.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const ViewColumn = bookshelf.Model.extend({
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
tableName: 'view_columns',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
|
||||
view() {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
export default bookshelf.model('ViewColumn', ViewColumn);
|
||||
22
server/src/models/ViewRole.js
Normal file
22
server/src/models/ViewRole.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const ViewRole = bookshelf.Model.extend({
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
tableName: 'view_roles',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
|
||||
/**
|
||||
* View role model may belongs to view model.
|
||||
*/
|
||||
view() {
|
||||
return this.belongsTo('View', 'view_id');
|
||||
},
|
||||
});
|
||||
|
||||
export default bookshelf.model('ViewRole', ViewRole);
|
||||
@@ -2,6 +2,7 @@ import Bookshelf from 'bookshelf';
|
||||
import jsonColumns from 'bookshelf-json-columns';
|
||||
import bookshelfParanoia from 'bookshelf-paranoia';
|
||||
import bookshelfModelBase from 'bookshelf-modelbase';
|
||||
import cascadeDelete from 'bookshelf-cascade-delete';
|
||||
import knex from '../database/knex';
|
||||
|
||||
const bookshelf = Bookshelf(knex);
|
||||
@@ -13,5 +14,6 @@ bookshelf.plugin('virtuals');
|
||||
bookshelf.plugin(jsonColumns);
|
||||
bookshelf.plugin(bookshelfParanoia);
|
||||
bookshelf.plugin(bookshelfModelBase.pluggable);
|
||||
bookshelf.plugin(cascadeDelete);
|
||||
|
||||
export default bookshelf;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { create, expect } from '~/testInit';
|
||||
import Resource from '@/models/Resource';
|
||||
import '@/models/View';
|
||||
import '@/models/ResourceField';
|
||||
|
||||
describe('Model: Resource', () => {
|
||||
it('Resource model may has many associated views.', async () => {
|
||||
const view = await create('view');
|
||||
await create('view', { resource_id: view.resource_id });
|
||||
|
||||
const resourceModel = await Resource.where('id', view.resource_id).fetch();
|
||||
const resourceViews = await resourceModel.views().fetch();
|
||||
|
||||
expect(resourceViews).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it('Resource model may has many fields.', async () => {
|
||||
const resourceField = await create('resource_field');
|
||||
|
||||
const resourceModel = await Resource.where('id', resourceField.resource_id).fetch();
|
||||
const resourceFields = await resourceModel.fields().fetch();
|
||||
|
||||
expect(resourceFields).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
18
server/tests/models/ResourceField.test.js
Normal file
18
server/tests/models/ResourceField.test.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { create, expect } from '~/testInit';
|
||||
import Resource from '@/models/Resource';
|
||||
import ResourceField from '@/models/ResourceField';
|
||||
import '@/models/View';
|
||||
|
||||
describe('Model: ResourceField', () => {
|
||||
it('Resource field model may belongs to associated resource.', async () => {
|
||||
const resourceField = await create('resource_field');
|
||||
|
||||
const resourceFieldModel = await ResourceField.where('id', resourceField.id).fetch();
|
||||
const resourceModel = resourceFieldModel.resource().fetch();
|
||||
|
||||
const foundResource = await Resource.where('id', resourceField.resource_id).fetch();
|
||||
|
||||
expect(resourceModel.attributes.id).equals(foundResource.id);
|
||||
expect(resourceModel.attributes.name).equals(foundResource.name);
|
||||
});
|
||||
});
|
||||
42
server/tests/models/View.test.js
Normal file
42
server/tests/models/View.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { create, expect } from '~/testInit';
|
||||
import View from '@/models/View';
|
||||
import Resource from '@/models/Resource';
|
||||
import '@/models/ResourceField';
|
||||
import '@/models/ViewRole';
|
||||
|
||||
|
||||
describe('Model: View', () => {
|
||||
it('View model may has many associated resource.', async () => {
|
||||
const view = await create('view');
|
||||
|
||||
const viewModel = await View.where('id', view.id).fetch();
|
||||
const viewResource = await viewModel.resource().fetch();
|
||||
|
||||
const foundResource = await Resource.where('id', view.resource_id).fetch();
|
||||
|
||||
expect(viewResource.attributes.id).equals(foundResource.id);
|
||||
expect(viewResource.attributes.name).equals(foundResource.attributes.name);
|
||||
});
|
||||
|
||||
it('View model may has many associated view roles.', async () => {
|
||||
const view = await create('view');
|
||||
await create('view_role', { view_id: view.id });
|
||||
await create('view_role', { view_id: view.id });
|
||||
|
||||
const viewModel = await View.where('id', view.id).fetch();
|
||||
const viewRoles = await viewModel.viewRoles().fetch();
|
||||
|
||||
expect(viewRoles).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it('View model may has many associated view columns', async () => {
|
||||
const view = await create('view');
|
||||
await create('view_has_columns', { view_id: view.id });
|
||||
await create('view_has_columns', { view_id: view.id });
|
||||
|
||||
const viewModel = await View.where('id', view.id).fetch();
|
||||
const viewColumns = await viewModel.columns().fetch();
|
||||
|
||||
expect(viewColumns).to.have.lengthOf(2);
|
||||
});
|
||||
});
|
||||
215
server/tests/routes/fields.test.js
Normal file
215
server/tests/routes/fields.test.js
Normal file
@@ -0,0 +1,215 @@
|
||||
import { create, expect, request } from '~/testInit';
|
||||
import knex from '@/database/knex';
|
||||
|
||||
describe('route: `/fields`', () => {
|
||||
describe('POST: `/fields/:resource_id`', () => {
|
||||
it('Should `label` be required.', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/fields/resource/${resource.resource_id}`).send();
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('VALIDATION_ERROR');
|
||||
|
||||
const paramsErrors = res.body.errors.map((er) => er.param);
|
||||
expect(paramsErrors).to.include('label');
|
||||
});
|
||||
|
||||
it('Should `data_type` be required.', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/fields/resource/${resource.resource_id}`);
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('VALIDATION_ERROR');
|
||||
|
||||
const paramsErrors = res.body.errors.map((er) => er.param);
|
||||
expect(paramsErrors).to.include('data_type');
|
||||
});
|
||||
|
||||
it('Should `data_type` be one in the list.', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/fields/resource/${resource.resource_id}`).send({
|
||||
label: 'Field label',
|
||||
data_type: 'invalid_type',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('VALIDATION_ERROR');
|
||||
|
||||
const paramsErrors = res.body.errors.map((er) => er.param);
|
||||
expect(paramsErrors).to.include('data_type');
|
||||
});
|
||||
|
||||
it('Should `value` be boolean valid value in case `data_type` was `boolean`.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `value` be URL valid value in case `data_type` was `url`.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `value` be integer valid value in case `data_type` was `number`.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `value` be decimal valid value in case `data_type` was `decimal`.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `value` be email valid value in case `data_type` was `email`.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `value` be boolean valid value in case `data_type` was `checkbox`.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should response not found in case resource id was not exist.', async () => {
|
||||
const res = await request().post('/api/fields/resource/100').send({
|
||||
label: 'Field label',
|
||||
data_type: 'text',
|
||||
default: 'default value',
|
||||
help_text: 'help text',
|
||||
});
|
||||
|
||||
expect(res.status).equals(404);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'RESOURCE_NOT_FOUND', code: 100,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('Should response success with valid data.', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/fields/resource/${resource.id}`).send({
|
||||
label: 'Field label',
|
||||
data_type: 'text',
|
||||
default: 'default value',
|
||||
help_text: 'help text',
|
||||
});
|
||||
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
|
||||
it('Should store the given field details to the storage.', async () => {
|
||||
const resource = await create('resource');
|
||||
await request().post(`/api/fields/resource/${resource.id}`).send({
|
||||
label: 'Field label',
|
||||
data_type: 'text',
|
||||
default: 'default value',
|
||||
help_text: 'help text',
|
||||
options: ['option 1', 'option 2'],
|
||||
});
|
||||
|
||||
const foundField = await knex('resource_fields').first();
|
||||
|
||||
expect(foundField.label_name).equals('Field label');
|
||||
expect(foundField.data_type).equals('text');
|
||||
expect(foundField.default).equals('default value');
|
||||
expect(foundField.help_text).equals('help text');
|
||||
expect(foundField.options).equals.deep([
|
||||
{ key: 1, value: 'option 1' },
|
||||
{ key: 2, value: 'option 2' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST: `/fields/:field_id`', () => {
|
||||
it('Should `label` be required.', async () => {
|
||||
const field = await create('resource_field');
|
||||
const res = await request().post(`/api/fields/${field.id}`).send();
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('VALIDATION_ERROR');
|
||||
|
||||
const paramsErrors = res.body.errors.map((er) => er.param);
|
||||
expect(paramsErrors).to.include('label');
|
||||
});
|
||||
|
||||
it('Should `data_type` be required.', async () => {
|
||||
const field = await create('resource_field');
|
||||
const res = await request().post(`/api/fields/${field.id}`);
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('VALIDATION_ERROR');
|
||||
|
||||
const paramsErrors = res.body.errors.map((er) => er.param);
|
||||
expect(paramsErrors).to.include('data_type');
|
||||
});
|
||||
|
||||
it('Should `data_type` be one in the list.', async () => {
|
||||
const field = await create('resource_field');
|
||||
const res = await request().post(`/api/fields/${field.id}`).send({
|
||||
label: 'Field label',
|
||||
data_type: 'invalid_type',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('VALIDATION_ERROR');
|
||||
|
||||
const paramsErrors = res.body.errors.map((er) => er.param);
|
||||
expect(paramsErrors).to.include('data_type');
|
||||
});
|
||||
|
||||
it('Should response not found in case resource id was not exist.', async () => {
|
||||
const res = await request().post('/api/fields/100').send({
|
||||
label: 'Field label',
|
||||
data_type: 'text',
|
||||
default: 'default value',
|
||||
help_text: 'help text',
|
||||
});
|
||||
|
||||
expect(res.status).equals(404);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'FIELD_NOT_FOUND', code: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should save the new options of the field in the storage.', async () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST: `/fields/status/:field_id`', () => {
|
||||
it('Should response not found in case field id was not exist.', async () => {
|
||||
const res = await request().post('/api/fields/status/100').send();
|
||||
|
||||
expect(res.status).equals(404);
|
||||
});
|
||||
|
||||
it('Should change status activation of the given field.', async () => {
|
||||
const field = await create('resource_field');
|
||||
await request().post(`/api/fields/status/${field.id}`).send({
|
||||
active: false,
|
||||
});
|
||||
|
||||
const storedField = await knex('resource_fields').where('id', field.id).first();
|
||||
expect(storedField.active).equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('DELETE: `/fields/:field_id`', () => {
|
||||
it('Should response not found in case field id was not exist.', async () => {
|
||||
const res = await request().delete('/api/fields/100').send();
|
||||
|
||||
expect(res.status).equals(404);
|
||||
});
|
||||
|
||||
it('Should not delete predefined field.', async () => {
|
||||
const field = await create('resource_field', { predefined: true });
|
||||
const res = await request().delete(`/api/fields/${field.id}`).send();
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'PREDEFINED_FIELD', code: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should delete the given field from the storage.', async () => {
|
||||
const field = await create('resource_field');
|
||||
const res = await request().delete(`/api/fields/${field.id}`).send();
|
||||
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { request, expect, create } from '~/testInit';
|
||||
import knex from '@/database/knex';
|
||||
|
||||
describe.only('routes: `/roles/`', () => {
|
||||
describe('routes: `/roles/`', () => {
|
||||
describe('POST: `/roles/`', () => {
|
||||
it('Should `name` be required.', async () => {
|
||||
const res = await request().post('/api/roles').send();
|
||||
@@ -237,7 +237,7 @@ describe.only('routes: `/roles/`', () => {
|
||||
expect(storedResources).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it.only('Should save the submit permissions in the storage in case was not exist.', async () => {
|
||||
it('Should save the submit permissions in the storage in case was not exist.', async () => {
|
||||
const role = await create('role');
|
||||
await request().post(`/api/roles/${role.id}`).send({
|
||||
name: 'Role Name',
|
||||
|
||||
310
server/tests/routes/views.test.js
Normal file
310
server/tests/routes/views.test.js
Normal file
@@ -0,0 +1,310 @@
|
||||
import { request, expect, create } from '~/testInit';
|
||||
import View from '@/models/View';
|
||||
import ViewRole from '@/models/ViewRole';
|
||||
import '@/models/ResourceField';
|
||||
|
||||
describe('routes: `/views`', () => {
|
||||
describe('POST: `/views/:resource_id`', () => {
|
||||
it('Should `label` be required.', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/views/resource/${resource.id}`);
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('label');
|
||||
});
|
||||
|
||||
it('Should columns be minimum limited', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/views/resource/${resource.id}`, {
|
||||
label: 'View Label',
|
||||
columns: [],
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('columns');
|
||||
});
|
||||
|
||||
it('Should columns be array.', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/views/resource/${resource.id}`, {
|
||||
label: 'View Label',
|
||||
columns: 'not_array',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('columns');
|
||||
});
|
||||
|
||||
it('Should `roles.*.field` be required.', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/views/resource/${resource.id}`).send({
|
||||
label: 'View Label',
|
||||
roles: [{}],
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('roles[0].field');
|
||||
});
|
||||
|
||||
it('Should `roles.*.comparator` be valid.', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/views/resource/${resource.id}`).send({
|
||||
label: 'View Label',
|
||||
roles: [{}],
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('roles[0].comparator');
|
||||
});
|
||||
|
||||
it('Should `roles.*.index` be number as integer.', async () => {
|
||||
const resource = await create('resource');
|
||||
const res = await request().post(`/api/views/resource/${resource.id}`).send({
|
||||
label: 'View Label',
|
||||
roles: [{ index: 'not_numeric' }],
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('roles[0].index');
|
||||
});
|
||||
|
||||
it('Should response not found in case resource was not exist.', async () => {
|
||||
const res = await request().post('/api/views/resource/100').send({
|
||||
label: 'View Label',
|
||||
columns: ['amount', 'thumbnail', 'status'],
|
||||
roles: [{
|
||||
index: 1,
|
||||
field: 'amount',
|
||||
comparator: 'equals',
|
||||
value: '100',
|
||||
}],
|
||||
});
|
||||
|
||||
expect(res.status).equals(404);
|
||||
});
|
||||
|
||||
it('Should response the roles fields not exist in case role field was not exist.', async () => {
|
||||
const resource = await create('resource');
|
||||
await create('resource_field', {
|
||||
resource_id: resource.id,
|
||||
label_name: 'Amount',
|
||||
});
|
||||
const res = await request().post(`/api/views/resource/${resource.id}`).send({
|
||||
label: 'View Label',
|
||||
columns: ['amount', 'thumbnail', 'status'],
|
||||
roles: [{
|
||||
index: 1,
|
||||
field: 'price',
|
||||
comparator: 'equals',
|
||||
value: '100',
|
||||
}],
|
||||
});
|
||||
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'RESOURCE_FIELDS_NOT_EXIST',
|
||||
code: 100,
|
||||
fields: ['price'],
|
||||
});
|
||||
});
|
||||
|
||||
it('Should response the columns not exists in case column was not exist.', async () => {
|
||||
const resource = await create('resource');
|
||||
await create('resource_field', {
|
||||
resource_id: resource.id,
|
||||
label_name: 'Amount',
|
||||
});
|
||||
const res = await request().post(`/api/views/resource/${resource.id}`).send({
|
||||
label: 'View Label',
|
||||
columns: ['amount', 'thumbnail', 'status'],
|
||||
roles: [{
|
||||
index: 1,
|
||||
field: 'price',
|
||||
comparator: 'equals',
|
||||
value: '100',
|
||||
}],
|
||||
});
|
||||
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'COLUMNS_NOT_EXIST',
|
||||
code: 200,
|
||||
fields: ['thumbnail', 'status'],
|
||||
});
|
||||
});
|
||||
|
||||
it('Should save the given details with associated roles and columns.', async () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe.only('POST: `/views/:view_id`', () => {
|
||||
it('Should `label` be required.', async () => {
|
||||
const view = await create('view');
|
||||
const res = await request().post(`/api/views/${view.id}`);
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('label');
|
||||
});
|
||||
|
||||
it('Should columns be minimum limited', async () => {
|
||||
const view = await create('view');
|
||||
const res = await request().post(`/api/views/${view.id}`, {
|
||||
label: 'View Label',
|
||||
columns: [],
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('columns');
|
||||
});
|
||||
|
||||
it('Should columns be array.', async () => {
|
||||
const view = await create('view');
|
||||
const res = await request().post(`/api/views/${view.id}`, {
|
||||
label: 'View Label',
|
||||
columns: 'not_array',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('columns');
|
||||
});
|
||||
|
||||
it('Should `roles.*.field` be required.', async () => {
|
||||
const view = await create('view');
|
||||
const res = await request().post(`/api/views/${view.id}`).send({
|
||||
label: 'View Label',
|
||||
roles: [{}],
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('roles[0].field');
|
||||
});
|
||||
|
||||
it('Should `roles.*.comparator` be valid.', async () => {
|
||||
const view = await create('view');
|
||||
const res = await request().post(`/api/views/${view.id}`).send({
|
||||
label: 'View Label',
|
||||
roles: [{}],
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('roles[0].comparator');
|
||||
});
|
||||
|
||||
it('Should `roles.*.index` be number as integer.', async () => {
|
||||
const view = await create('view');
|
||||
const res = await request().post(`/api/views/${view.id}`).send({
|
||||
label: 'View Label',
|
||||
roles: [{ index: 'not_numeric' }],
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('roles[0].index');
|
||||
});
|
||||
|
||||
it('Should response not found in case resource was not exist.', async () => {
|
||||
const res = await request().post('/api/views/100').send({
|
||||
label: 'View Label',
|
||||
columns: ['amount', 'thumbnail', 'status'],
|
||||
roles: [{
|
||||
index: 1,
|
||||
field: 'amount',
|
||||
comparator: 'equals',
|
||||
value: '100',
|
||||
}],
|
||||
});
|
||||
|
||||
expect(res.status).equals(404);
|
||||
});
|
||||
|
||||
it.only('Should response the roles fields not exist in case role field was not exist.', async () => {
|
||||
const view = await create('view');
|
||||
await create('resource_field', {
|
||||
resource_id: view.resource_id,
|
||||
label_name: 'Amount',
|
||||
});
|
||||
const res = await request().post(`/api/views/${view.id}`).send({
|
||||
label: 'View Label',
|
||||
columns: ['amount', 'thumbnail', 'status'],
|
||||
roles: [{
|
||||
index: 1,
|
||||
field: 'price',
|
||||
comparator: 'equals',
|
||||
value: '100',
|
||||
}],
|
||||
});
|
||||
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'RESOURCE_FIELDS_NOT_EXIST',
|
||||
code: 100,
|
||||
fields: ['price'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE: `/views/:resource_id`', () => {
|
||||
it('Should not delete predefined view.', async () => {
|
||||
const view = await create('view', { predefined: true });
|
||||
const res = await request().delete(`/api/views/${view.id}`).send();
|
||||
|
||||
expect(res.status).equals(400);
|
||||
});
|
||||
|
||||
it('Should response not found in case view was not exist.', async () => {
|
||||
const res = await request().delete('/api/views/100').send();
|
||||
|
||||
expect(res.status).equals(404);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'VIEW_NOT_FOUND', code: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should delete the given view and associated view columns and roles.', async () => {
|
||||
const view = await create('view', { predefined: false });
|
||||
await create('view_role', { view_id: view.id });
|
||||
await create('view_has_columns', { view_id: view.id });
|
||||
|
||||
await request().delete(`/api/views/${view.id}`).send();
|
||||
|
||||
const foundViews = await View.where('id', view.id).fetchAll();
|
||||
const foundViewRoles = await ViewRole.where('view_id', view.id).fetchAll();
|
||||
|
||||
expect(foundViews).to.have.lengthOf(0);
|
||||
expect(foundViewRoles).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user