first commit

This commit is contained in:
Noor E Ilahi
2026-01-09 12:54:53 +05:30
commit 7ccf44f7da
1070 changed files with 113036 additions and 0 deletions

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

File diff suppressed because it is too large Load Diff

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