first commit
This commit is contained in:
210
resources/frontend/core/views/About.vue
Normal file
210
resources/frontend/core/views/About.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="about content-wrapper">
|
||||
<div class="container">
|
||||
<div class="at-container">
|
||||
<div class="at-container__inner">
|
||||
<div class="row">
|
||||
<div class="col-8 col-offset-8 about__alert-wrapper">
|
||||
<at-alert
|
||||
v-if="updateVersionMsg"
|
||||
:message="updateVersionMsg"
|
||||
class="about__alert"
|
||||
type="success"
|
||||
/>
|
||||
|
||||
<at-alert
|
||||
v-if="knownVulnerableMsg"
|
||||
:message="knownVulnerableMsg"
|
||||
class="about__alert"
|
||||
type="error"
|
||||
/>
|
||||
|
||||
<at-alert v-if="infoMsg" :message="infoMsg" class="about__alert" type="info" />
|
||||
</div>
|
||||
<!-- /.col-8 -->
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<div class="about__logo" />
|
||||
|
||||
<h2>Cattr</h2>
|
||||
<p class="about__version">
|
||||
<skeleton :loading="isLoading" width="80px">{{ appData.version || 'Undefined' }}</skeleton>
|
||||
</p>
|
||||
<p>
|
||||
Instance ID:
|
||||
<strong>
|
||||
<skeleton :loading="isLoading" width="200px"
|
||||
>{{ appData.instance_id || 'Undefined' }}
|
||||
</skeleton>
|
||||
</strong>
|
||||
</p>
|
||||
<at-tabs>
|
||||
<at-tab-pane :label="$t('about.module_versions')" name="about_versions">
|
||||
<p v-if="modulesData.length" class="about__table">
|
||||
<at-table :columns="modulesColumns" :data="modulesData" />
|
||||
</p>
|
||||
<p v-else v-t="'about.no_modules'" />
|
||||
</at-tab-pane>
|
||||
<at-tab-pane
|
||||
v-if="isAdmin"
|
||||
:label="$t('about.module_storage')"
|
||||
class="storage"
|
||||
name="about_storage"
|
||||
>
|
||||
<StorageManagementTab />
|
||||
</at-tab-pane>
|
||||
</at-tabs>
|
||||
<div><a class="about__link" href="https://cattr.app">cattr.app</a></div>
|
||||
<div><a class="about__link" href="https://github.com/orgs/cattr-app/discussions">community</a></div>
|
||||
</div>
|
||||
<!-- /.at-container__inner -->
|
||||
</div>
|
||||
<!-- /.at-container -->
|
||||
</div>
|
||||
<!-- /.container -->
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AboutService from '@/services/resource/about.service';
|
||||
import { hasRole } from '@/utils/user';
|
||||
import semverGt from 'semver/functions/gt';
|
||||
import { Skeleton } from 'vue-loading-skeleton';
|
||||
|
||||
const aboutService = new AboutService();
|
||||
|
||||
export default {
|
||||
name: 'About',
|
||||
components: {
|
||||
StorageManagementTab: () =>
|
||||
import(/* webpackChunkName: "StorageManagementTab" */ '@/components/StorageManagementTab'),
|
||||
Skeleton,
|
||||
},
|
||||
computed: {
|
||||
isAdmin() {
|
||||
return hasRole(this.$store.getters['user/user'], 'admin');
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const { data } = await aboutService.getGeneralInfo();
|
||||
this.appData = data.app;
|
||||
this.modulesData = data.modules;
|
||||
|
||||
if (this.appData.vulnerable) {
|
||||
this.knownVulnerableMsg = `${this.$i18n.t('message.vulnerable_version')} ${
|
||||
this.appData.last_version
|
||||
}`;
|
||||
} else if (this.appData.last_version && this.appData.last_version !== this.appData.version) {
|
||||
if (semverGt(this.appData.last_version, data.app.version)) {
|
||||
this.updateVersionMsg = `${this.$i18n.t('message.update_version')} ${
|
||||
this.appData.last_version
|
||||
}`;
|
||||
} else {
|
||||
this.infoMsg = this.appData.message;
|
||||
}
|
||||
}
|
||||
} catch ({ response }) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(response ? response : 'request to about is canceled');
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
appData: {},
|
||||
modulesData: [],
|
||||
knownVulnerableMsg: null,
|
||||
updateVersionMsg: null,
|
||||
isLoading: false,
|
||||
infoMsg: null,
|
||||
modulesColumns: [
|
||||
{ title: this.$i18n.t('about.modules.name'), key: 'name' },
|
||||
{ title: this.$i18n.t('about.modules.version'), key: 'version' },
|
||||
{
|
||||
title: this.$i18n.t('about.modules.status'),
|
||||
render: (h, params) =>
|
||||
h('AtAlert', {
|
||||
props: {
|
||||
message: Object.prototype.hasOwnProperty.call(params.item, 'lastVersion')
|
||||
? semverGt(params.item.version, params.item.lastVersion)
|
||||
? params.item.flashMessage
|
||||
: params.item.version === params.item.lastVersion
|
||||
? this.$i18n.t('about.modules.ok')
|
||||
: params.item.vulnerable
|
||||
? this.$i18n.t('about.modules.vulnerable')
|
||||
: this.$i18n.t('about.modules.outdated')
|
||||
: this.$i18n.t('about.modules.ok'),
|
||||
type: Object.prototype.hasOwnProperty.call(params.item, 'lastVersion')
|
||||
? semverGt(params.item.version, params.item.lastVersion)
|
||||
? 'info'
|
||||
: params.item.version === params.item.lastVersion
|
||||
? 'success'
|
||||
: params.item.vulnerable
|
||||
? 'error'
|
||||
: 'warning'
|
||||
: 'info',
|
||||
},
|
||||
style: {
|
||||
'text-align': 'center',
|
||||
padding: '4px 8px 4px 16px',
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.about {
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin-bottom: $layout-01;
|
||||
}
|
||||
|
||||
&__alert-wrapper {
|
||||
margin-bottom: $layout-01;
|
||||
}
|
||||
|
||||
&__alert {
|
||||
margin-bottom: $layout-01;
|
||||
}
|
||||
|
||||
&__logo {
|
||||
background-image: url('../assets/logo.svg');
|
||||
background-size: cover;
|
||||
display: block;
|
||||
height: 120px;
|
||||
margin-bottom: $layout-01;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
&__version {
|
||||
color: $gray-3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: $gray-3;
|
||||
}
|
||||
|
||||
&__table {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
& > .at-table {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
resources/frontend/core/views/ApiError.vue
Normal file
39
resources/frontend/core/views/ApiError.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="api-error">
|
||||
<div class="container">
|
||||
<h1 class="api-error__code">500</h1>
|
||||
<h1 class="api-error__title">Whoops!</h1>
|
||||
<p class="api-error__description">{{ $t('message.api_error') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ApiError',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.api-error {
|
||||
text-align: center;
|
||||
|
||||
&__code {
|
||||
color: $red-1;
|
||||
font-size: 8rem;
|
||||
line-height: 1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
76
resources/frontend/core/views/Auth/AuthInput.vue
Normal file
76
resources/frontend/core/views/Auth/AuthInput.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<validation-provider v-slot="{ errors }" rules="required|email" mode="passive" name="E-mail">
|
||||
<div class="input-group">
|
||||
<small>E-Mail</small>
|
||||
<at-input
|
||||
v-model="user.email"
|
||||
name="login"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
placeholder="E-Mail"
|
||||
icon="mail"
|
||||
type="text"
|
||||
required
|
||||
@keydown.native.enter.prevent="submit"
|
||||
></at-input>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</div>
|
||||
<!-- /.input-group -->
|
||||
</validation-provider>
|
||||
<validation-provider v-slot="{ errors }" rules="required" mode="passive" :name="$t('field.password')">
|
||||
<div class="input-group">
|
||||
<small>{{ $t('field.password') }}</small>
|
||||
<at-input
|
||||
v-model="user.password"
|
||||
name="password"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
:placeholder="$t('field.password')"
|
||||
type="password"
|
||||
icon="lock"
|
||||
required
|
||||
@keydown.native.enter.prevent="submit"
|
||||
></at-input>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</div>
|
||||
<!-- /.input-group -->
|
||||
</validation-provider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ValidationProvider } from 'vee-validate';
|
||||
|
||||
export default {
|
||||
name: 'AuthInput',
|
||||
|
||||
components: {
|
||||
ValidationProvider,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
email: null,
|
||||
password: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.$emit('submit');
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'user.email'(value) {
|
||||
// Trim space
|
||||
this.user.email = value.replace(/\s/, '');
|
||||
this.$emit('change', this.user);
|
||||
},
|
||||
'user.password'() {
|
||||
this.$emit('change', this.user);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
178
resources/frontend/core/views/Auth/Desktop.vue
Normal file
178
resources/frontend/core/views/Auth/Desktop.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="login row at-row no-gutter">
|
||||
<div class="login__wrap">
|
||||
<div class="login__form">
|
||||
<div class="box">
|
||||
<div class="top">
|
||||
<div class="static-message">
|
||||
<div class="logo" />
|
||||
</div>
|
||||
<h1 class="login__title">Cattr</h1>
|
||||
</div>
|
||||
<template v-if="error">
|
||||
<div>
|
||||
<at-alert :message="$t('auth.desktop_error')" class="login__error" type="error" />
|
||||
</div>
|
||||
<at-button class="login__btn" type="primary" @click="commonLogin"
|
||||
>{{ $t('auth.switch_to_common') }}
|
||||
</at-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<at-alert :message="$t('auth.desktop_working')" class="login__error" type="info" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<a class="login__slogan" href="https://cattr.app" v-html="slogan" />
|
||||
</div>
|
||||
<div class="hero col-16" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login {
|
||||
flex-wrap: nowrap;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
max-height: 100vh;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&__wrap {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__form {
|
||||
flex: 8;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__slogan {
|
||||
align-content: flex-start;
|
||||
color: $gray-3;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: $black-900;
|
||||
font-size: 1.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__error {
|
||||
margin-bottom: 1rem;
|
||||
overflow: initial;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
padding: 0 $spacing-08;
|
||||
width: 100%;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin-bottom: $layout-01;
|
||||
|
||||
.static-message {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
.logo {
|
||||
align-items: center;
|
||||
background: url('../../assets/logo.svg');
|
||||
background-size: cover;
|
||||
border-radius: 10px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
height: 60px;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $blue-1;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::v-deep .input-group {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: #6159e6 url('../../assets/login.svg') no-repeat;
|
||||
background-size: 100%;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import sloganGenerator from '@/helpers/sloganGenerator';
|
||||
|
||||
export default {
|
||||
name: 'desktop-login',
|
||||
computed: {
|
||||
slogan() {
|
||||
return sloganGenerator();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
commonLogin() {
|
||||
this.$router.replace({ name: 'auth.login' });
|
||||
},
|
||||
finish(error = true) {
|
||||
this.error = error;
|
||||
this.$Loading.error();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$Loading.start();
|
||||
|
||||
if (location.search.length === 0) {
|
||||
this.finish();
|
||||
} else {
|
||||
const query = location.search.substr(1).split('=');
|
||||
|
||||
if (query[0] !== 'token' && query.length !== 2) {
|
||||
this.finish();
|
||||
} else {
|
||||
const apiService = this.$store.getters['user/apiService'];
|
||||
apiService
|
||||
.attemptDesktopLogin(query[1])
|
||||
.then(() => apiService.getCompanyData())
|
||||
.then(() => this.finish(false))
|
||||
.catch(() => this.finish());
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
270
resources/frontend/core/views/Auth/Login.vue
Normal file
270
resources/frontend/core/views/Auth/Login.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="login row at-row no-gutter">
|
||||
<div class="login__wrap">
|
||||
<div class="login__form">
|
||||
<validation-observer ref="observer" class="box" tag="div" @submit.prevent="submit">
|
||||
<div class="top">
|
||||
<div class="static-message">
|
||||
<div class="logo"></div>
|
||||
</div>
|
||||
<h1 class="login__title">Cattr</h1>
|
||||
</div>
|
||||
<div>
|
||||
<at-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
class="login__error"
|
||||
closable
|
||||
:message="error"
|
||||
@on-close="error = null"
|
||||
/>
|
||||
|
||||
<component :is="config.authInput" @change="change" @submit="submit" />
|
||||
|
||||
<vue-recaptcha
|
||||
v-if="recaptchaKey"
|
||||
ref="recaptcha"
|
||||
:loadRecaptchaScript="true"
|
||||
:sitekey="recaptchaKey"
|
||||
class="recaptcha"
|
||||
@verify="onCaptchaVerify"
|
||||
@expired="onCaptchaExpired"
|
||||
></vue-recaptcha>
|
||||
</div>
|
||||
<at-button
|
||||
class="login__btn"
|
||||
native-type="submit"
|
||||
type="primary"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@click="submit"
|
||||
>{{ $t('auth.submit') }}</at-button
|
||||
>
|
||||
<router-link class="link" to="/auth/password/reset">{{ $t('auth.forgot_password') }}</router-link>
|
||||
</validation-observer>
|
||||
</div>
|
||||
<a class="login__slogan" href="https://cattr.app" v-html="slogan" />
|
||||
</div>
|
||||
<div class="hero col-lg-16 col-md-14 col-sm-12"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ValidationObserver } from 'vee-validate';
|
||||
import { VueRecaptcha } from 'vue-recaptcha';
|
||||
import AuthInput from './AuthInput';
|
||||
import sloganGenerator from '@/helpers/sloganGenerator';
|
||||
import has from 'lodash/has';
|
||||
|
||||
export const config = { authInput: AuthInput };
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
|
||||
components: {
|
||||
ValidationObserver,
|
||||
VueRecaptcha,
|
||||
AuthInput,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
email: null,
|
||||
password: null,
|
||||
recaptcha: null,
|
||||
},
|
||||
recaptchaKey: null,
|
||||
error: null,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
config() {
|
||||
return config;
|
||||
},
|
||||
slogan() {
|
||||
return sloganGenerator();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.$store.getters['user/isLoggedIn']) {
|
||||
this.$router.push({ name: 'dashboard' });
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getRandomIntInclusive(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
},
|
||||
|
||||
onCaptchaVerify(response) {
|
||||
this.user.recaptcha = response;
|
||||
},
|
||||
|
||||
onCaptchaExpired() {
|
||||
this.$refs.recaptcha.reset();
|
||||
},
|
||||
|
||||
change(user) {
|
||||
this.user = { ...this.user, ...user };
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const valid = await this.$refs.observer.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$Loading.start();
|
||||
this.isLoading = true;
|
||||
const apiService = this.$store.getters['user/apiService'];
|
||||
|
||||
try {
|
||||
if ('grecaptcha' in window) {
|
||||
this.$refs.recaptcha.reset();
|
||||
}
|
||||
|
||||
await apiService.attemptLogin(this.user);
|
||||
await apiService.getCompanyData();
|
||||
|
||||
this.error = null;
|
||||
this.$Loading.finish();
|
||||
} catch (e) {
|
||||
this.$Loading.error();
|
||||
|
||||
if (has(e, 'response.status')) {
|
||||
if (e.response.status === 429 && this.recaptchaKey === null) {
|
||||
this.recaptchaKey = e.response.data.info.site_key;
|
||||
}
|
||||
|
||||
let message;
|
||||
|
||||
if (e.response.status === 401) {
|
||||
message = this.$t('auth.message.user_not_found');
|
||||
} else if (e.response.status === 429) {
|
||||
message = this.$t('auth.message.solve_captcha');
|
||||
} else if (e.response.status === 503) {
|
||||
message = this.$t('auth.message.data_reset');
|
||||
} else {
|
||||
message = this.$t('auth.message.auth_error');
|
||||
}
|
||||
|
||||
this.error = message;
|
||||
}
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login {
|
||||
flex-wrap: nowrap;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
max-height: 100vh;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&__wrap {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
&__form {
|
||||
width: 100%;
|
||||
flex: 8;
|
||||
}
|
||||
|
||||
&__slogan {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: flex-start;
|
||||
color: $gray-3;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: $black-900;
|
||||
font-size: 1.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__error {
|
||||
margin-bottom: 1rem;
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
padding: 0 $spacing-08;
|
||||
width: 100%;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin-bottom: $layout-01;
|
||||
|
||||
.static-message {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
.logo {
|
||||
align-items: center;
|
||||
background: url('../../assets/logo.svg');
|
||||
background-size: cover;
|
||||
border-radius: 10px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
height: 60px;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recaptcha {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $blue-1;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::v-deep .input-group {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: url('../../assets/login.svg') #6159e6;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
232
resources/frontend/core/views/Auth/Register.vue
Normal file
232
resources/frontend/core/views/Auth/Register.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="content-wrapper">
|
||||
<div v-if="isTokenValid" class="container">
|
||||
<div class="at-container">
|
||||
<div class="at-container__inner">
|
||||
<div v-if="isRegisterSuccess">
|
||||
<div class="header-text">
|
||||
<i class="icon icon-check"></i>
|
||||
<h2 class="header-text__title">
|
||||
{{ $t('register.success_title') }}
|
||||
</h2>
|
||||
<p class="header-text__subtitle">{{ $t('register.success_subtitle') }}</p>
|
||||
<router-link to="/auth/login">{{ $t('reset.go_away') }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="row">
|
||||
<div class="col-6 col-offset-9">
|
||||
<div class="header-text">
|
||||
<h2 class="header-text__title">
|
||||
{{ $t('register.title') }}
|
||||
</h2>
|
||||
<p class="header-text__subtitle">
|
||||
{{ $t('register.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
<at-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
class="alert"
|
||||
closable
|
||||
:message="errorMessage"
|
||||
@on-close="errorMessage = null"
|
||||
/>
|
||||
<validation-observer v-slot="{ invalid }">
|
||||
<validation-provider v-slot="{ errors }" rules="required|email" name="E-mail">
|
||||
<div class="input-group">
|
||||
<small>E-Mail</small>
|
||||
<at-input
|
||||
v-model="email"
|
||||
name="login"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
placeholder="E-Mail"
|
||||
icon="mail"
|
||||
type="text"
|
||||
disabled="true"
|
||||
>
|
||||
</at-input>
|
||||
<p class="error-message">
|
||||
<small>{{ errors[0] }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</validation-provider>
|
||||
<validation-provider v-slot="{ errors }" rules="required" :name="$t('field.full_name')">
|
||||
<div class="input-group">
|
||||
<small>{{ $t('field.full_name') }}</small>
|
||||
<at-input
|
||||
v-model="fullName"
|
||||
name="full_name"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
:placeholder="$t('field.full_name')"
|
||||
icon="user"
|
||||
type="text"
|
||||
>
|
||||
</at-input>
|
||||
<p class="error-message">
|
||||
<small>{{ errors[0] }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required|min:6"
|
||||
vid="password"
|
||||
:name="$t('field.password')"
|
||||
>
|
||||
<div class="input-group">
|
||||
<small>{{ $t('field.password') }}</small>
|
||||
<at-input
|
||||
v-model="password"
|
||||
name="password"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
:placeholder="$t('field.full_name')"
|
||||
icon="lock"
|
||||
type="password"
|
||||
>
|
||||
</at-input>
|
||||
<p class="error-message">
|
||||
<small>{{ errors[0] }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required|min:6|confirmed:password"
|
||||
:name="$t('reset.confirm_password')"
|
||||
>
|
||||
<div class="input-group">
|
||||
<small>{{ $t('reset.confirm_password') }}</small>
|
||||
<at-input
|
||||
v-model="passwordConfirmation"
|
||||
name="passwordConfirmation"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
:placeholder="$t('reset.confirm_password')"
|
||||
icon="lock"
|
||||
type="password"
|
||||
>
|
||||
</at-input>
|
||||
<p class="error-message">
|
||||
<small>{{ errors[0] }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</validation-provider>
|
||||
<at-button
|
||||
class="btn"
|
||||
native-type="submit"
|
||||
type="primary"
|
||||
:disabled="invalid || isLoading"
|
||||
:loading="isLoading"
|
||||
@click="register"
|
||||
>{{ $t('register.register_btn') }}</at-button
|
||||
>
|
||||
</validation-observer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ValidationObserver, ValidationProvider } from 'vee-validate';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'ResetPassword',
|
||||
components: {
|
||||
ValidationProvider,
|
||||
ValidationObserver,
|
||||
},
|
||||
created() {
|
||||
if (this.$route.query.token) {
|
||||
this.token = this.$route.query.token;
|
||||
this.validateToken();
|
||||
} else {
|
||||
this.$router.push({ name: 'not-found' });
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
email: null,
|
||||
password: null,
|
||||
passwordConfirmation: null,
|
||||
fullName: null,
|
||||
token: null,
|
||||
isTokenValid: false,
|
||||
isLoading: false,
|
||||
isRegisterSuccess: false,
|
||||
errorMessage: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async validateToken() {
|
||||
try {
|
||||
const { data } = await axios.get(`/auth/register/${this.token}`);
|
||||
this.email = data.email;
|
||||
this.isTokenValid = true;
|
||||
} catch ({ response }) {
|
||||
if (response.status === 404) {
|
||||
this.$router.replace({ name: 'not-found' });
|
||||
}
|
||||
}
|
||||
},
|
||||
async register() {
|
||||
this.isLoading = true;
|
||||
|
||||
const data = {
|
||||
email: this.email,
|
||||
full_name: this.fullName,
|
||||
password: this.password,
|
||||
password_confirmation: this.passwordConfirmation,
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.post(`auth/register/${this.token}`, data);
|
||||
|
||||
this.isRegisterSuccess = true;
|
||||
} catch ({ response }) {
|
||||
this.errorMessage = response.data.error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-text {
|
||||
text-align: center;
|
||||
|
||||
&__title {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: $layout-01;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 92px;
|
||||
|
||||
&-check {
|
||||
color: $green-1;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-bottom: $layout-01;
|
||||
}
|
||||
</style>
|
||||
281
resources/frontend/core/views/Auth/ResetPassword.vue
Normal file
281
resources/frontend/core/views/Auth/ResetPassword.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div class="content-wrapper">
|
||||
<div class="container">
|
||||
<div class="at-container crud__content">
|
||||
<at-steps :current="currentStep" class="steps col-lg-offset-4">
|
||||
<at-step
|
||||
:title="$t('reset.step', { n: 1 })"
|
||||
:description="$t('reset.step_description.step_1')"
|
||||
></at-step>
|
||||
<at-step
|
||||
:title="$t('reset.step', { n: 2 })"
|
||||
:description="$t('reset.step_description.step_2')"
|
||||
></at-step>
|
||||
<at-step
|
||||
:title="$t('reset.step', { n: 3 })"
|
||||
:description="$t('reset.step_description.step_3')"
|
||||
></at-step>
|
||||
<at-step
|
||||
:title="$t('reset.step', { n: 4 })"
|
||||
:description="$t('reset.step_description.step_4')"
|
||||
></at-step>
|
||||
</at-steps>
|
||||
|
||||
<div class="row">
|
||||
<div v-if="currentStep == 0" class="col-6 col-offset-9">
|
||||
<validation-observer v-slot="{ invalid }">
|
||||
<div class="header-text">
|
||||
<h2 class="header-text__title">
|
||||
{{ $t('reset.tabs.enter_email.title') }}
|
||||
</h2>
|
||||
<p class="header-text__subtitle">
|
||||
{{ $t('reset.tabs.enter_email.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
<validation-provider v-slot="{ errors }" rules="required|email">
|
||||
<small>E-Mail</small>
|
||||
<at-input
|
||||
v-model="email"
|
||||
name="login"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
placeholder="E-Mail"
|
||||
icon="mail"
|
||||
type="text"
|
||||
:disabled="disabledForm"
|
||||
>
|
||||
</at-input>
|
||||
</validation-provider>
|
||||
<at-button
|
||||
class="btn"
|
||||
native-type="submit"
|
||||
type="primary"
|
||||
:disabled="invalid || disabledForm"
|
||||
@click="resetPassword"
|
||||
>{{ $t('reset.reset_password') }}</at-button
|
||||
>
|
||||
</validation-observer>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep == 1" class="col-6 col-offset-9">
|
||||
<div class="header-text">
|
||||
<i class="icon icon-mail"></i>
|
||||
<h2 class="header-text__title">
|
||||
{{ $t('reset.tabs.check_email.title') }}
|
||||
</h2>
|
||||
<p class="header-text__subtitle">
|
||||
{{ $t('reset.tabs.check_email.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep == 2" class="col-6 col-offset-9">
|
||||
<validation-observer v-if="isValidToken" ref="form" v-slot="{ invalid }">
|
||||
<div class="header-text">
|
||||
<h2 class="header-text__title">
|
||||
{{ $t('reset.tabs.new_password.title') }}
|
||||
</h2>
|
||||
<p class="header-text__subtitle">
|
||||
{{ $t('reset.tabs.new_password.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required|min:6"
|
||||
:name="$t('field.password')"
|
||||
vid="password"
|
||||
>
|
||||
<small>{{ $t('field.password') }}</small>
|
||||
<at-input
|
||||
v-model="password"
|
||||
:name="$t('field.password')"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
:placeholder="$t('field.password')"
|
||||
icon="lock"
|
||||
type="password"
|
||||
:disabled="disabledForm"
|
||||
>
|
||||
</at-input>
|
||||
<p class="error-message">
|
||||
<small>{{ errors[0] }}</small>
|
||||
</p>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required|min:6|confirmed:password"
|
||||
:name="$t('reset.confirm_password')"
|
||||
vid="passwordConfirmation"
|
||||
>
|
||||
<small>{{ $t('reset.confirm_password') }}</small>
|
||||
<at-input
|
||||
v-model="passwordConfirmation"
|
||||
name="passwordConfirmation"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
:placeholder="$t('reset.confirm_password')"
|
||||
icon="lock"
|
||||
type="password"
|
||||
:disabled="disabledForm"
|
||||
>
|
||||
</at-input>
|
||||
<p class="error-message">
|
||||
<small>{{ errors[0] }}</small>
|
||||
</p>
|
||||
</validation-provider>
|
||||
<at-button
|
||||
class="btn"
|
||||
native-type="submit"
|
||||
type="primary"
|
||||
:disabled="invalid || disabledForm"
|
||||
@click="submitNewPassword"
|
||||
>{{ $t('control.submit') }}</at-button
|
||||
>
|
||||
</validation-observer>
|
||||
<div v-else>
|
||||
<div class="header-text">
|
||||
<h2 class="header-text__title">
|
||||
{{ $t('reset.page_is_not_available') }}
|
||||
</h2>
|
||||
<router-link to="/auth/login">{{ $t('reset.go_away') }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep == 3" class="col-6 col-offset-9">
|
||||
<div class="header-text">
|
||||
<i class="icon icon-check"></i>
|
||||
<h2 class="header-text__title">
|
||||
{{ $t('reset.tabs.success.title') }}
|
||||
</h2>
|
||||
<p class="header-text__subtitle"></p>
|
||||
<router-link to="/auth/login">{{ $t('reset.go_away') }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ValidationObserver, ValidationProvider } from 'vee-validate';
|
||||
import AuthService from '@/services/auth.service';
|
||||
|
||||
export default {
|
||||
name: 'ResetPassword',
|
||||
components: {
|
||||
ValidationProvider,
|
||||
ValidationObserver,
|
||||
},
|
||||
created() {
|
||||
if (this.$route.query.token && this.$route.query.email) {
|
||||
this.currentStep = 2;
|
||||
this.validateToken();
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
email: null,
|
||||
password: null,
|
||||
passwordConfirmation: null,
|
||||
currentStep: 0,
|
||||
disabledForm: false,
|
||||
isValidToken: true,
|
||||
authService: new AuthService(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async resetPassword() {
|
||||
this.disabledForm = true;
|
||||
|
||||
const payload = {
|
||||
email: this.email,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.authService.resetPasswordRequest(payload);
|
||||
this.currentStep = 1;
|
||||
} catch (e) {
|
||||
//
|
||||
} finally {
|
||||
this.disabledForm = false;
|
||||
}
|
||||
},
|
||||
async validateToken() {
|
||||
const payload = {
|
||||
email: this.$route.query.email,
|
||||
token: this.$route.query.token,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.authService.resetPasswordValidateToken(payload);
|
||||
this.isValidToken = true;
|
||||
} catch (e) {
|
||||
this.isValidToken = false;
|
||||
}
|
||||
},
|
||||
async submitNewPassword() {
|
||||
this.disabledForm = true;
|
||||
|
||||
const payload = {
|
||||
email: this.$route.query.email,
|
||||
token: this.$route.query.token,
|
||||
password: this.password,
|
||||
password_confirmation: this.passwordConfirmation,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.authService.resetPasswordProcess(payload);
|
||||
this.currentStep = 3;
|
||||
this.disabledForm = false;
|
||||
} catch (e) {
|
||||
//
|
||||
} finally {
|
||||
this.disabledForm = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.steps {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
text-align: center;
|
||||
|
||||
&__title {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 92px;
|
||||
|
||||
&-mail {
|
||||
color: $blue-2;
|
||||
}
|
||||
|
||||
&-check {
|
||||
color: $green-1;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.at-input {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
461
resources/frontend/core/views/Crud/EditView.vue
Normal file
461
resources/frontend/core/views/Crud/EditView.vue
Normal file
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<div class="container-fluid crud">
|
||||
<div class="row flex-around">
|
||||
<div class="col-24 col-sm-22 col-lg-20">
|
||||
<div class="at-container crud__content crud__edit-view">
|
||||
<div class="page-controls">
|
||||
<h1 class="control-item title">
|
||||
{{
|
||||
$route.params.id
|
||||
? `${$t(pageData.title)} #${$route.params.id}`
|
||||
: `${$t(pageData.title)}`
|
||||
}}
|
||||
</h1>
|
||||
<div class="control-items">
|
||||
<template v-if="pageData.pageControls && pageData.pageControls.length > 0">
|
||||
<template v-for="(button, key) of pageData.pageControls">
|
||||
<at-button
|
||||
v-if="checkRenderCondition(button)"
|
||||
:key="key"
|
||||
class="control-item"
|
||||
size="large"
|
||||
:type="button.renderType || ''"
|
||||
:icon="button.icon || ''"
|
||||
@click="handleClick(button)"
|
||||
>
|
||||
{{ $t(button.label) }}
|
||||
</at-button>
|
||||
</template>
|
||||
</template>
|
||||
<BackButton size="large" class="control-item">{{ $t('control.back') }}</BackButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<component
|
||||
:is="component"
|
||||
v-for="(component, index) of pageData.topComponents"
|
||||
:key="index"
|
||||
:parent="this"
|
||||
></component>
|
||||
<validation-observer ref="form" v-slot="{ invalid }">
|
||||
<div class="data-entries">
|
||||
<template v-for="(field, key) of fields">
|
||||
<template v-if="isDisplayable(field)">
|
||||
<div :key="key" class="data-entry">
|
||||
<div class="row">
|
||||
<div class="col-6 col-xs-24">
|
||||
<at-tooltip
|
||||
v-if="field.tooltipValue"
|
||||
:content="$t(field.tooltipValue)"
|
||||
placement="top-left"
|
||||
>
|
||||
<u class="label label-tooltip">
|
||||
{{ $t(field.label) }}
|
||||
<span v-if="field.required">*</span>
|
||||
</u>
|
||||
</at-tooltip>
|
||||
<p v-else class="label">
|
||||
{{ $t(field.label) }}
|
||||
<span v-if="field.required">*</span>
|
||||
</p>
|
||||
</div>
|
||||
<at-input
|
||||
v-if="isDataLoading && pageData.type === 'edit'"
|
||||
class="col-18 col-xs-24"
|
||||
disabled
|
||||
/>
|
||||
<div v-else class="col-18 col-xs-24">
|
||||
<validation-provider
|
||||
v-if="typeof field.render !== 'undefined'"
|
||||
v-slot="{ errors }"
|
||||
:rules="
|
||||
typeof field.rules === 'string'
|
||||
? field.rules
|
||||
: field.required
|
||||
? 'required'
|
||||
: ''
|
||||
"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<renderable-field
|
||||
v-model="values[field.key]"
|
||||
:render="field.render"
|
||||
:field="field"
|
||||
:values="values"
|
||||
:setValue="setValue"
|
||||
:class="{
|
||||
'at-select--error at-input--error has-error':
|
||||
errors.length > 0,
|
||||
}"
|
||||
/>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.key === 'email'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.required ? 'required|email' : ''"
|
||||
:name="field.key"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-input
|
||||
v-model="values[field.key]"
|
||||
:placeholder="$t(field.placeholder) || ''"
|
||||
:type="field.frontendType || ''"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
></at-input>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.type === 'input' || field.type === 'text'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.required ? 'required' : ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-input
|
||||
v-model="values[field.key]"
|
||||
:placeholder="$t(field.placeholder) || ''"
|
||||
:type="field.frontendType || ''"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
></at-input>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.type === 'number'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.required ? 'required' : ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-input-number
|
||||
v-model="values[field.key]"
|
||||
:min="field.minValue"
|
||||
:max="field.maxValue"
|
||||
></at-input-number>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.type === 'select'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.required ? 'required' : ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-select
|
||||
v-model="values[field.key]"
|
||||
:class="{
|
||||
'at-select--error': errors.length > 0,
|
||||
}"
|
||||
:placeholder="$t('control.select')"
|
||||
>
|
||||
<at-option
|
||||
v-for="(option, optionKey) of field.options"
|
||||
:key="optionKey"
|
||||
:value="option.value"
|
||||
>{{ ucfirst($t(option.label)) }}
|
||||
</at-option>
|
||||
</at-select>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.type === 'checkbox'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.required ? 'required' : ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-checkbox v-model="values[field.key]" label="" />
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.type === 'resource-select'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.required ? 'required' : ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<resource-select
|
||||
v-model="values[field.key]"
|
||||
:service="field.service"
|
||||
:class="{
|
||||
'at-select--error': errors.length > 0,
|
||||
}"
|
||||
/>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.type === 'textarea'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.required ? 'required' : ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-textarea
|
||||
v-model="values[field.key]"
|
||||
autosize
|
||||
min-rows="2"
|
||||
:class="{
|
||||
'at-textarea--error': errors.length > 0,
|
||||
}"
|
||||
:placeholder="$t(field.placeholder) || ''"
|
||||
/>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<component
|
||||
:is="component"
|
||||
v-for="(component, index) of pageData.bottomComponents.components"
|
||||
:key="index"
|
||||
:parent="this"
|
||||
/>
|
||||
<div class="bottom-control">
|
||||
<at-button
|
||||
type="primary"
|
||||
:disabled="invalid || isLoading"
|
||||
:loading="isLoading"
|
||||
@click="submit"
|
||||
>{{ $t('control.save') }}</at-button
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
pageData.bottomComponents.pageControls &&
|
||||
pageData.bottomComponents.pageControls.length > 0
|
||||
"
|
||||
>
|
||||
<div class="bottom-control__added-items">
|
||||
<template v-for="(button, key) of pageData.bottomComponents.pageControls">
|
||||
<at-button
|
||||
v-if="checkRenderCondition(button)"
|
||||
:key="key"
|
||||
class="bottom-control__added-items__item"
|
||||
:type="button.type || ''"
|
||||
:icon="button.icon || ''"
|
||||
@click="handleClick(button)"
|
||||
>
|
||||
{{ $t(button.label) }}
|
||||
</at-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</validation-observer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BackButton from '@/components/BackButton';
|
||||
import RenderableField from '@/components/RenderableField';
|
||||
import ResourceSelect from '@/components/ResourceSelect';
|
||||
import { ValidationObserver, ValidationProvider } from 'vee-validate';
|
||||
import { ucfirst } from '@/utils/string';
|
||||
|
||||
export default {
|
||||
name: 'EditView',
|
||||
components: {
|
||||
BackButton,
|
||||
RenderableField,
|
||||
ResourceSelect,
|
||||
ValidationProvider,
|
||||
ValidationObserver,
|
||||
},
|
||||
|
||||
data() {
|
||||
const meta = this.$route.meta;
|
||||
const pageData = meta.pageData || {};
|
||||
|
||||
return {
|
||||
service: meta.service,
|
||||
fields: meta.fields || [],
|
||||
values: {},
|
||||
filters: this.$route.meta.filters,
|
||||
|
||||
pageData: {
|
||||
title: pageData.title,
|
||||
topComponents: pageData.topComponents || [],
|
||||
bottomComponents: pageData.bottomComponents || [],
|
||||
type: pageData.type || 'new',
|
||||
routeNamedSection: pageData.editRouteName || '',
|
||||
pageControls: this.$route.meta.pageData.pageControls || [],
|
||||
editRouteName: pageData.editRouteName || '',
|
||||
},
|
||||
|
||||
isLoading: false,
|
||||
isDataLoading: false,
|
||||
afterSubmitCallback: meta.afterSubmitCallback,
|
||||
};
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
if (!Object.values(this.values).length) {
|
||||
await this.fetchData();
|
||||
}
|
||||
},
|
||||
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
next(async vm => {
|
||||
await vm.fetchData();
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
async beforeRouteUpdate(to, from, next) {
|
||||
await this.fetchData();
|
||||
next();
|
||||
},
|
||||
|
||||
methods: {
|
||||
ucfirst,
|
||||
async fetchData() {
|
||||
this.isDataLoading = true;
|
||||
|
||||
if (this.pageData.type === 'edit') {
|
||||
try {
|
||||
const { data } = await this.service.getItem(
|
||||
this.$route.params[this.service.getIdParam()],
|
||||
this.filters,
|
||||
);
|
||||
this.values = { ...this.values, ...data.data };
|
||||
} catch ({ response }) {
|
||||
if (
|
||||
response &&
|
||||
Object.prototype.hasOwnProperty.call(response, 'data') &&
|
||||
response.data.error_type === 'query.item_not_found'
|
||||
) {
|
||||
this.$router.replace({ name: 'forbidden' });
|
||||
}
|
||||
}
|
||||
} else if (this.pageData.type === 'new') {
|
||||
this.fields.forEach(field => {
|
||||
if (field.default !== undefined) {
|
||||
this.values[field.key] =
|
||||
typeof field.default === 'function' ? field.default(this.$store) : field.default;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.isDataLoading = false;
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const valid = await this.$refs.form.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const { data } = (await this.service.save(this.values, this.pageData.type === 'new')).data;
|
||||
|
||||
this.$Notify({
|
||||
type: 'success',
|
||||
title: this.$t('notification.record.save.success.title'),
|
||||
message: this.$t('notification.record.save.success.message'),
|
||||
});
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
if (this.afterSubmitCallback) {
|
||||
this.afterSubmitCallback();
|
||||
} else if (this.pageData.type === 'new') {
|
||||
const result = this.$router.push({
|
||||
name: this.$route.meta.navigation.view ?? this.$route.meta.navigation.edit,
|
||||
params: { id: data[this.service.getIdParam()] },
|
||||
});
|
||||
}
|
||||
} catch ({ message }) {
|
||||
this.isLoading = false;
|
||||
// TODO: fix error printing
|
||||
this.$refs.form.setErrors(message);
|
||||
}
|
||||
},
|
||||
|
||||
handleClick(button) {
|
||||
button.onClick(this, this.values[this.service.getIdParam()]);
|
||||
},
|
||||
|
||||
checkRenderCondition(button) {
|
||||
return typeof button.renderCondition !== 'undefined' ? button.renderCondition(this) : true;
|
||||
},
|
||||
|
||||
setValue(key, value) {
|
||||
this.$set(this.values, key, value);
|
||||
},
|
||||
|
||||
isDisplayable(field) {
|
||||
if (typeof field.displayable === 'function') {
|
||||
return field.displayable(this);
|
||||
}
|
||||
|
||||
if (typeof field.displayable !== 'undefined') {
|
||||
return !!field.displayable;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.crud {
|
||||
&__edit-view {
|
||||
.page-controls {
|
||||
margin-bottom: 1.5em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.control-item {
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.6rem;
|
||||
@media (max-width: 768px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-entries {
|
||||
.data-entry {
|
||||
margin-bottom: $layout-02;
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
@media (max-width: 768px) {
|
||||
font-size: 0.8rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-control {
|
||||
display: flex;
|
||||
|
||||
&__added-items {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1042
resources/frontend/core/views/Crud/GridView.vue
Normal file
1042
resources/frontend/core/views/Crud/GridView.vue
Normal file
File diff suppressed because it is too large
Load Diff
234
resources/frontend/core/views/Crud/ItemView.vue
Normal file
234
resources/frontend/core/views/Crud/ItemView.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div class="container-fluid crud">
|
||||
<div class="row flex-around">
|
||||
<div class="col-24 col-sm-22 col-lg-20">
|
||||
<div class="at-container crud__content crud__item-view">
|
||||
<div class="page-controls">
|
||||
<h1 class="control-item title">
|
||||
<Skeleton :loading="isDataLoading" width="200px">{{ title }}</Skeleton>
|
||||
</h1>
|
||||
|
||||
<div class="control-items">
|
||||
<at-button
|
||||
size="large"
|
||||
class="control-item"
|
||||
@click="$router.go($route.meta.navigation.from)"
|
||||
>{{ $t('control.back') }}
|
||||
</at-button>
|
||||
<template v-if="pageData.pageControls && pageData.pageControls.length > 0">
|
||||
<template v-for="(button, key) of pageData.pageControls">
|
||||
<at-button
|
||||
v-if="checkRenderCondition(button)"
|
||||
:key="key"
|
||||
class="control-item"
|
||||
size="large"
|
||||
:type="button.renderType || ''"
|
||||
:icon="button.icon || ''"
|
||||
@click="handleClick(button)"
|
||||
>{{ $t(button.label) }}
|
||||
</at-button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<component
|
||||
:is="component"
|
||||
v-for="(component, index) of pageData.topComponents"
|
||||
:key="index"
|
||||
:parent="this"
|
||||
></component>
|
||||
<div class="data-entries">
|
||||
<div v-for="(field, key) of fields" v-bind:key="key" class="data-entry">
|
||||
<div class="row">
|
||||
<div class="col-6 col-xs-24 label">{{ $t(field.label) }}:</div>
|
||||
<div class="col">
|
||||
<Skeleton :loading="isDataLoading">
|
||||
<renderable-field
|
||||
v-if="typeof field.render !== 'undefined' && Object.keys(values).length > 0"
|
||||
:render="field.render"
|
||||
:value="values[field.key]"
|
||||
:field="field"
|
||||
:values="values"
|
||||
></renderable-field>
|
||||
<template v-else>{{ values[field.key] }}</template>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<component
|
||||
v-bind:is="component"
|
||||
v-for="(component, index) of pageData.bottomComponents"
|
||||
v-bind:key="index"
|
||||
:parent="this"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.col-24 -->
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import RenderableField from '@/components/RenderableField';
|
||||
import { Skeleton } from 'vue-loading-skeleton';
|
||||
|
||||
export default {
|
||||
name: 'ItemView',
|
||||
|
||||
components: {
|
||||
RenderableField,
|
||||
Skeleton,
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
reload: this.load,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('user', ['user']),
|
||||
title() {
|
||||
const { fields, values, service, filters, pageData } = this;
|
||||
const { titleCallback } = this.$route.meta;
|
||||
if (typeof titleCallback === 'function') {
|
||||
return titleCallback({ fields, values, service, filters, pageData });
|
||||
}
|
||||
|
||||
return this.$t(pageData.title);
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const { fields, service, filters, pageData } = this.$route.meta;
|
||||
|
||||
return {
|
||||
service,
|
||||
filters,
|
||||
values: {},
|
||||
fields: fields || [],
|
||||
isDataLoading: false,
|
||||
pageData: {
|
||||
title: pageData.title || null,
|
||||
topComponents: pageData.topComponents || [],
|
||||
bottomComponents: pageData.bottomComponents || [],
|
||||
pageControls: pageData.pageControls || [],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.load();
|
||||
|
||||
this.websocketEnterChannel = this.$route.meta.pageData.websocketEnterChannel;
|
||||
this.websocketLeaveChannel = this.$route.meta.pageData.websocketLeaveChannel;
|
||||
|
||||
if (typeof this.websocketEnterChannel !== 'undefined') {
|
||||
this.websocketEnterChannel(this.user.id, {
|
||||
edit: data => {
|
||||
const id = this.$route.params[this.service.getIdParam()];
|
||||
if (+id === +data.model.id) {
|
||||
this.values = data.model;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
beforeRouteEnter(to, from, next) {
|
||||
if ('pageData' in from.meta && from.meta.pageData.type === 'new') {
|
||||
to.meta.navigation.from = -2;
|
||||
} else {
|
||||
to.meta.navigation.from = -1;
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (typeof this.websocketLeaveChannel !== 'undefined') {
|
||||
this.websocketLeaveChannel(this.user.id);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async load() {
|
||||
this.isDataLoading = true;
|
||||
const id = this.$route.params[this.service.getIdParam()];
|
||||
|
||||
try {
|
||||
const { data } = (await this.service.getItem(id, this.filters)).data;
|
||||
this.values = data;
|
||||
} catch ({ response }) {
|
||||
if (response.data.error_type === 'query.item_not_found') {
|
||||
this.$router.replace({ name: 'forbidden' });
|
||||
}
|
||||
}
|
||||
|
||||
this.isDataLoading = false;
|
||||
},
|
||||
|
||||
handleClick(button) {
|
||||
button.onClick(this, this.values[this.service.getIdParam()]);
|
||||
},
|
||||
|
||||
checkRenderCondition(button) {
|
||||
return typeof button.renderCondition !== 'undefined' ? button.renderCondition(this) : true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.crud {
|
||||
&__item-view {
|
||||
.page-controls {
|
||||
margin-bottom: 1.5em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.control-items {
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.control-item {
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.6rem;
|
||||
@media (max-width: 768px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-entries {
|
||||
.data-entry {
|
||||
padding-bottom: 1em;
|
||||
margin-bottom: 1em;
|
||||
border-bottom: 1px solid $gray-6;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-right: 1em;
|
||||
font-weight: bold;
|
||||
@media (max-width: 768px) {
|
||||
font-size: 0.8rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
154
resources/frontend/core/views/DesktopLogin.vue
Normal file
154
resources/frontend/core/views/DesktopLogin.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="desktop-key content-wrapper">
|
||||
<div class="container">
|
||||
<div class="at-container crud__content">
|
||||
<div class="at-container__inner">
|
||||
<h2 v-t="'auth.desktop.header'" />
|
||||
</div>
|
||||
<at-steps :current="current" class="col-lg-offset-7">
|
||||
<at-step
|
||||
:icon="transferStatus === 'process' ? 'icon-lock' : undefined"
|
||||
:status="transferStatus"
|
||||
:description="$t('auth.desktop.step1')"
|
||||
:title="$t('auth.desktop.step', { n: 1 })"
|
||||
/>
|
||||
<at-step
|
||||
:icon="openStatus !== 'finish' && openStatus !== 'error' ? 'icon-monitor' : undefined"
|
||||
:status="openStatus"
|
||||
:description="$t('auth.desktop.step2')"
|
||||
:title="$t('auth.desktop.step', { n: 2 })"
|
||||
/>
|
||||
</at-steps>
|
||||
<div class="row">
|
||||
<div v-if="transferStatus === 'finish' && openStatus === 'error'" class="col-10 col-offset-7">
|
||||
<p v-t="'auth.desktop.error'" />
|
||||
<i18n path="auth.desktop.download" tag="p">
|
||||
<a v-t="'auth.desktop.download_button'" href="https://cattr.app/desktop" target="_blank" />
|
||||
</i18n>
|
||||
</div>
|
||||
<div class="col-6 col-offset-9">
|
||||
<at-button
|
||||
:type="
|
||||
transferStatus === 'finish'
|
||||
? openStatus === 'finish'
|
||||
? 'success'
|
||||
: 'primary'
|
||||
: 'error'
|
||||
"
|
||||
@click="click"
|
||||
>
|
||||
{{
|
||||
transferStatus === 'error' || openStatus === 'error'
|
||||
? $t('auth.desktop.retry')
|
||||
: transferStatus === 'finish' && openStatus !== 'finish'
|
||||
? $t('auth.desktop.open')
|
||||
: transferStatus === 'finish' && openStatus === 'finish'
|
||||
? $t('auth.desktop.finish')
|
||||
: $t('auth.desktop.cancel')
|
||||
}}
|
||||
</at-button>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
</div>
|
||||
<!-- /.at-container__inner -->
|
||||
</div>
|
||||
<!-- /.at-container -->
|
||||
</div>
|
||||
<!-- /.container -->
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'DesktopLogin',
|
||||
async mounted() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const { data } = await axios.get('auth/desktop-key');
|
||||
|
||||
this.current += 1;
|
||||
this.transferStatus = 'finish';
|
||||
|
||||
this.token = data.access_token;
|
||||
} catch ({ response }) {
|
||||
this.transferStatus = 'error';
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(response ? response : 'Issuing desktop key has been canceled');
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
current: 0,
|
||||
transferStatus: 'process',
|
||||
openStatus: 'wait',
|
||||
token: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
if (this.transferStatus === 'error' || this.openStatus === 'error') {
|
||||
window.location.reload();
|
||||
} else if (
|
||||
this.transferStatus === 'finish' &&
|
||||
(this.openStatus === 'wait' || this.openStatus === 'process')
|
||||
) {
|
||||
this.current += 1;
|
||||
this.openStatus = 'process';
|
||||
|
||||
const client = window.open(
|
||||
`cattr://authenticate?url=${encodeURIComponent(
|
||||
process.env.VUE_APP_API_URL || `${window.location.origin}/api`,
|
||||
)}&token=${this.token}`,
|
||||
);
|
||||
|
||||
const failFunction = () => {
|
||||
if (this.openStatus !== 'finish') {
|
||||
this.openStatus = 'error';
|
||||
}
|
||||
};
|
||||
|
||||
const timeout = setTimeout(failFunction, 1);
|
||||
|
||||
client.onblur = () => {
|
||||
clearTimeout(timeout);
|
||||
if (this.openStatus !== 'error') {
|
||||
this.openStatus = 'finish';
|
||||
}
|
||||
};
|
||||
|
||||
client.onunload = client.onclose = () => {
|
||||
clearTimeout(timeout);
|
||||
failFunction();
|
||||
};
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.desktop-key {
|
||||
text-align: center;
|
||||
|
||||
.at-steps {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-top: 20px;
|
||||
|
||||
& > .col-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
resources/frontend/core/views/PageForbidden.vue
Normal file
39
resources/frontend/core/views/PageForbidden.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="page-forbidden">
|
||||
<div class="container">
|
||||
<h1 class="page-forbidden__code">403</h1>
|
||||
<h1 class="page-forbidden__title">Whoops!</h1>
|
||||
<p class="page-forbidden__description">{{ $t('message.page_forbidden') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageForbidden',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-forbidden {
|
||||
text-align: center;
|
||||
|
||||
&__code {
|
||||
color: $red-1;
|
||||
font-size: 8rem;
|
||||
line-height: 1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
resources/frontend/core/views/PageNotFound.vue
Normal file
39
resources/frontend/core/views/PageNotFound.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="error-page-system">
|
||||
<h1 class="code">404</h1>
|
||||
<h1 class="title">Whoops!</h1>
|
||||
<p class="description">{{ $t('message.page_not_found') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageNotFound',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.error-page-system {
|
||||
margin-left: 15%;
|
||||
margin-right: 15%;
|
||||
|
||||
.code {
|
||||
text-align: center;
|
||||
color: $red-1;
|
||||
font-size: 10rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 3.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
72
resources/frontend/core/views/Settings/CompanySettings.vue
Normal file
72
resources/frontend/core/views/Settings/CompanySettings.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div :class="containerClass">
|
||||
<h1 class="page-title">{{ $t('navigation.company_settings') }}</h1>
|
||||
<div class="at-container settings">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<at-menu v-if="sections" class="settings__menu" router mode="vertical">
|
||||
<template v-for="(section, key) in sections">
|
||||
<at-menu-item v-if="section.access" :key="key" :to="{ name: section.pathName }">
|
||||
{{ $t(section.label) }}
|
||||
</at-menu-item>
|
||||
</template>
|
||||
</at-menu>
|
||||
</div>
|
||||
<div class="col-19">
|
||||
<div class="settings__content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CompanySettings',
|
||||
computed: {
|
||||
containerClass() {
|
||||
return 'container';
|
||||
},
|
||||
sections() {
|
||||
return this.$store.getters['settings/sections']
|
||||
.filter(section => section.scope === 'company')
|
||||
.sort((a, b) => a.order - b.order);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings {
|
||||
&::v-deep {
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__menu {
|
||||
padding: $layout-01 0;
|
||||
height: 100%;
|
||||
border-top-left-radius: $border-radius-lger;
|
||||
border-bottom-left-radius: $border-radius-lger;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: $spacing-05 $spacing-06 $spacing-07;
|
||||
}
|
||||
}
|
||||
|
||||
.settings__content::v-deep {
|
||||
.at-container,
|
||||
.at-container__inner,
|
||||
.crud {
|
||||
all: unset;
|
||||
|
||||
&__table {
|
||||
margin-bottom: $layout-01;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
376
resources/frontend/core/views/Settings/DynamicSettings.vue
Normal file
376
resources/frontend/core/views/Settings/DynamicSettings.vue
Normal file
@@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 v-if="this.section" class="page-title settings__title">{{ $t(this.section.label) }}</h1>
|
||||
|
||||
<template v-if="this.section && values">
|
||||
<component
|
||||
:is="component"
|
||||
v-for="(component, index) of this.section.topComponents"
|
||||
:key="index"
|
||||
:parent="this"
|
||||
/>
|
||||
<validation-observer ref="form">
|
||||
<div class="data-entries">
|
||||
<template v-for="(fields, groupKey) of this.groups">
|
||||
<template v-for="(field, key) of fields">
|
||||
<template v-if="typeof field.displayable === 'function' ? field.displayable($store) : true">
|
||||
<div :key="key" class="data-entry">
|
||||
<div class="row">
|
||||
<div class="col-6 label">
|
||||
<at-tooltip
|
||||
v-if="field.tooltipValue"
|
||||
:content="$t(field.tooltipValue)"
|
||||
placement="top-right"
|
||||
>
|
||||
<u class="label label-tooltip">
|
||||
{{ $t(field.label) }}
|
||||
<span v-if="field.required">*</span>
|
||||
</u>
|
||||
</at-tooltip>
|
||||
<p v-else class="label">
|
||||
{{ $t(field.label) }}
|
||||
<span v-if="field.required">*</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<validation-provider
|
||||
v-if="typeof field.render === 'function'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.rules || ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<renderable-field
|
||||
v-model="values[field.key]"
|
||||
:render="field.render"
|
||||
:field="field"
|
||||
:values="values"
|
||||
class="with-margin"
|
||||
:class="{
|
||||
'at-select--error at-input--error has-error': errors.length > 0,
|
||||
}"
|
||||
/>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="
|
||||
field.fieldOptions.type === 'input' ||
|
||||
field.fieldOptions.type === 'text'
|
||||
"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.rules || ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-input
|
||||
v-model="values[field.key]"
|
||||
:readonly="field.fieldOptions.disableAutocomplete || false"
|
||||
:placeholder="$t(field.fieldOptions.placeholder) || ''"
|
||||
:type="field.fieldOptions.frontendType || ''"
|
||||
:status="errors.length > 0 ? 'error' : ''"
|
||||
@focus="removeReadonly"
|
||||
/>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.fieldOptions.type === 'number'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.rules || ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-input-number
|
||||
v-model="values[field.key]"
|
||||
:min="field.minValue"
|
||||
:max="field.maxValue"
|
||||
size="large"
|
||||
@blur="handleInputNumber($event, field.key)"
|
||||
/>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.fieldOptions.type === 'select'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.rules || ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-select v-model="values[field.key]" class="with-margin">
|
||||
<at-option
|
||||
v-for="(option, optionKey) of getSelectOptions(field, values)"
|
||||
:key="optionKey"
|
||||
:value="option.value"
|
||||
>{{ $t(option.label) }}
|
||||
</at-option>
|
||||
</at-select>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.fieldOptions.type === 'textarea'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.rules || ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-textarea
|
||||
v-model="values[field.key]"
|
||||
autosize
|
||||
class="with-margin"
|
||||
:class="{
|
||||
'at-textarea--error': errors.length > 0,
|
||||
}"
|
||||
/>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.fieldOptions.type === 'listbox'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.rules || ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<ListBox
|
||||
v-model="values[field.key]"
|
||||
:keyField="field.fieldOptions.keyField"
|
||||
:labelField="field.fieldOptions.labelField"
|
||||
:valueField="field.fieldOptions.valueField"
|
||||
/>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.fieldOptions.type === 'checkbox'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.rules || ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<at-checkbox v-model="values[field.key]" label="" />
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
v-else-if="field.fieldOptions.type === 'switch'"
|
||||
v-slot="{ errors }"
|
||||
:rules="field.rules || ''"
|
||||
:name="$t(field.label)"
|
||||
:vid="field.key"
|
||||
>
|
||||
<span
|
||||
v-if="field.fieldOptions.checkedText"
|
||||
v-html="field.fieldOptions.checkedText"
|
||||
/>
|
||||
<at-switch
|
||||
v-model="values[field.key]"
|
||||
size="large"
|
||||
@change="$set(values, field.key, $event)"
|
||||
>
|
||||
<template
|
||||
v-if="field.fieldOptions.innerCheckedText"
|
||||
v-slot:checkedText
|
||||
>
|
||||
<span v-html="field.fieldOptions.innerCheckedText" />
|
||||
</template>
|
||||
<template
|
||||
v-if="field.fieldOptions.innerUnCheckedText"
|
||||
v-slot:unCheckedText
|
||||
>
|
||||
<span v-html="field.fieldOptions.innerUnCheckedText" />
|
||||
</template>
|
||||
</at-switch>
|
||||
<span
|
||||
v-if="field.fieldOptions.unCheckedText"
|
||||
v-html="field.fieldOptions.unCheckedText"
|
||||
/>
|
||||
<small>{{ errors[0] }}</small>
|
||||
</validation-provider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<hr :key="groupKey" class="group-divider" />
|
||||
</template>
|
||||
</div>
|
||||
<component
|
||||
:is="component"
|
||||
v-for="(component, index) of this.section.bottomComponents"
|
||||
:key="index"
|
||||
:parent="this"
|
||||
></component>
|
||||
<at-button type="primary" :loading="isLoading" :disabled="isLoading" @click="submit"
|
||||
>{{ $t('control.save') }}
|
||||
</at-button>
|
||||
</validation-observer>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListBox from '@/components/ListBox';
|
||||
import RenderableField from '@/components/RenderableField';
|
||||
import { ValidationObserver, ValidationProvider } from 'vee-validate';
|
||||
|
||||
export default {
|
||||
name: 'DynamicSettings',
|
||||
|
||||
components: {
|
||||
RenderableField,
|
||||
ListBox,
|
||||
ValidationObserver,
|
||||
ValidationProvider,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
section: {},
|
||||
values: {},
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchSectionData();
|
||||
},
|
||||
|
||||
watch: {
|
||||
sections() {
|
||||
this.fetchSectionData();
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
sections() {
|
||||
return this.$store.getters['settings/sections'];
|
||||
},
|
||||
|
||||
groups() {
|
||||
if (!this.section) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { fields } = this.section;
|
||||
if (!fields) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.keys(fields)
|
||||
.map(key => ({ key, field: fields[key] }))
|
||||
.reduce((groups, { key, field }) => {
|
||||
const groupKey = field.group || 'default';
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = {};
|
||||
}
|
||||
|
||||
groups[groupKey][key] = field;
|
||||
return groups;
|
||||
}, {});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleInputNumber(ev, key) {
|
||||
let number = ev.target.valueAsNumber;
|
||||
if (ev.target.max && number > ev.target.max) {
|
||||
number = Number(ev.target.max);
|
||||
ev.target.valueAsNumber = number;
|
||||
ev.target.value = String(number);
|
||||
}
|
||||
if (ev.target.min && number < ev.target.min) {
|
||||
number = Number(ev.target.min);
|
||||
ev.target.valueAsNumber = number;
|
||||
ev.target.value = String(number);
|
||||
}
|
||||
|
||||
this.values[key] = number;
|
||||
},
|
||||
fetchSectionData() {
|
||||
const name = this.$route.name;
|
||||
this.section = this.$store.getters['settings/sections'].find(s => s.pathName === name);
|
||||
|
||||
if (this.section) {
|
||||
this.values = { ...this.values, ...this.section.data };
|
||||
}
|
||||
},
|
||||
removeReadonly(el) {
|
||||
if (el.target.getAttribute('readonly') === 'readonly') {
|
||||
el.target.removeAttribute('readonly');
|
||||
}
|
||||
},
|
||||
getSelectOptions(field, values) {
|
||||
const { options } = field.fieldOptions;
|
||||
|
||||
if (typeof options === 'function') {
|
||||
return options({ field, values });
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
async submit() {
|
||||
const valid = await this.$refs.form.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
await this.section.service.save(this.values);
|
||||
|
||||
this.$Notify({
|
||||
type: 'success',
|
||||
title: this.$t('notification.settings.save.success.title'),
|
||||
message: this.$t('notification.settings.save.success.message'),
|
||||
});
|
||||
|
||||
this.$router.go(0);
|
||||
} catch ({ response }) {
|
||||
if (
|
||||
typeof response !== 'undefined' &&
|
||||
Object.prototype.hasOwnProperty.call(response, 'data') &&
|
||||
Object.prototype.hasOwnProperty.call(response.data, 'info')
|
||||
) {
|
||||
this.$refs.form.setErrors(response.data.info);
|
||||
}
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings {
|
||||
&__title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
|
||||
.data-entry {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-divider {
|
||||
border: 0;
|
||||
border-top: 1px solid #eeeef5;
|
||||
|
||||
&:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
69
resources/frontend/core/views/Settings/Settings.vue
Normal file
69
resources/frontend/core/views/Settings/Settings.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1 class="page-title">{{ $t('navigation.settings') }}</h1>
|
||||
<div class="at-container settings">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<at-menu v-if="sections" class="settings__menu" router mode="vertical">
|
||||
<template v-for="(section, key) in sections">
|
||||
<at-menu-item v-if="section.access" :key="key" :to="{ name: section.pathName }">
|
||||
{{ $t(section.label) }}
|
||||
</at-menu-item>
|
||||
</template>
|
||||
</at-menu>
|
||||
</div>
|
||||
<div class="col-19">
|
||||
<div class="settings__content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Settings',
|
||||
computed: {
|
||||
sections() {
|
||||
return this.$store.getters['settings/sections']
|
||||
.filter(section => section.scope === 'settings')
|
||||
.sort((a, b) => a.order - b.order);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings {
|
||||
&::v-deep {
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__menu {
|
||||
padding: $layout-01 0;
|
||||
height: 100%;
|
||||
border-top-left-radius: $border-radius-lger;
|
||||
border-bottom-left-radius: $border-radius-lger;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: $spacing-05 $spacing-06 $spacing-08;
|
||||
|
||||
&::v-deep {
|
||||
.at-container,
|
||||
.at-container__inner,
|
||||
.crud {
|
||||
all: unset;
|
||||
|
||||
&__table {
|
||||
margin-bottom: $layout-01;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user