first commit
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user