Custom fields feature.

This commit is contained in:
Ahmed Bouhuolia
2019-09-13 20:24:09 +02:00
parent cba17739d6
commit ed4d37c8fb
64 changed files with 2307 additions and 121 deletions

View 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>

View File

@@ -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.',
};

View File

@@ -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',

View 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);
},
});

View File

@@ -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>

View 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>

View 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>

View 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>

View File

@@ -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 />

View File

@@ -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;

View File

@@ -0,0 +1,14 @@
<template>
<div class="">
</div>
</template>
<script>
export default {
name: 'account-form',
data() {
},
};
</script>

View File

@@ -1,9 +1,11 @@
<template>
<div>
</div>
</template>
<script>
export default {
name: 'products-list',
}
name: 'accounts-list',
};
</script>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,18 @@
<template>
<div class="">
</div>
</template>
<script>
export default {
name: 'item-category-form',
data() {
return {
form: {
},
};
},
}
</script>

View File

@@ -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>

View File

@@ -1,3 +0,0 @@
<template>
</template>

View 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>

View File

@@ -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}`);
}
};

View 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);

View File

@@ -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'] },
},
],
},
];

View File

@@ -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 };

View File

@@ -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);
},
};

View File

@@ -22,9 +22,7 @@ const actions = {
commit('setItems', data);
if (count) {
commit('setSidebarItemCount', {
name: 'customers', count,
});
commit('setSidebarItemCount', { name: 'customers', count });
}
return data;
},

View 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,
};

View File

@@ -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 };

View File

View 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
View 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)
}

View File

@@ -0,0 +1,14 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'search-dialog',
data() {
return {
current_state: 0,
};
},
};
</script>

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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>