Add vue proof of concept courses/course category

pull/3262/head
Julio Montoya 5 years ago
parent a1bcb12f20
commit 053bcad80a
  1. 3
      .jshintrc
  2. 78
      assets/vue/App.vue
  3. 3
      assets/vue/VueConfig.js
  4. 45
      assets/vue/components/ActionCell.vue
  5. 40
      assets/vue/components/Breadcrumb.vue
  6. 45
      assets/vue/components/ConfirmDelete.vue
  7. 40
      assets/vue/components/DataFilter.vue
  8. 52
      assets/vue/components/InputDate.vue
  9. 18
      assets/vue/components/Loading.vue
  10. 32
      assets/vue/components/Snackbar.vue
  11. 137
      assets/vue/components/Toolbar.vue
  12. 76
      assets/vue/components/course/Filter.vue
  13. 173
      assets/vue/components/course/Form.vue
  14. 9
      assets/vue/components/course/Layout.vue
  15. 40
      assets/vue/components/coursecategory/Filter.vue
  16. 113
      assets/vue/components/coursecategory/Form.vue
  17. 9
      assets/vue/components/coursecategory/Layout.vue
  18. 10
      assets/vue/error/SubmissionError.js
  19. 13
      assets/vue/i18n.js
  20. 22
      assets/vue/locales/en.js
  21. 67
      assets/vue/main.js
  22. 41
      assets/vue/mixins/CreateMixin.js
  23. 88
      assets/vue/mixins/ListMixin.js
  24. 37
      assets/vue/mixins/NotificationMixin.js
  25. 47
      assets/vue/mixins/ShowMixin.js
  26. 79
      assets/vue/mixins/UpdateMixin.js
  27. 15
      assets/vue/plugins/vuetify.js
  28. 5
      assets/vue/quasar.js
  29. 28
      assets/vue/router/course.js
  30. 28
      assets/vue/router/coursecategory.js
  31. 18
      assets/vue/router/index.js
  32. 24
      assets/vue/services/api.js
  33. 3
      assets/vue/services/course.js
  34. 3
      assets/vue/services/coursecategory.js
  35. 14
      assets/vue/store/index.js
  36. 273
      assets/vue/store/modules/crud.js
  37. 18
      assets/vue/store/modules/notifications.js
  38. 9
      assets/vue/utils/dates.js
  39. 71
      assets/vue/utils/fetch.js
  40. 19
      assets/vue/utils/hydra.js
  41. 7
      assets/vue/validators/date.js
  42. 46
      assets/vue/views/course/Create.vue
  43. 122
      assets/vue/views/course/List.vue
  44. 119
      assets/vue/views/course/Show.vue
  45. 67
      assets/vue/views/course/Update.vue
  46. 45
      assets/vue/views/coursecategory/Create.vue
  47. 104
      assets/vue/views/coursecategory/List.vue
  48. 87
      assets/vue/views/coursecategory/Show.vue
  49. 61
      assets/vue/views/coursecategory/Update.vue
  50. 11
      assets/vue/vue.config.js
  51. 67
      package.json
  52. 66
      webpack.config.js

@ -0,0 +1,3 @@
{
"esnext": true
}

@ -0,0 +1,78 @@
<template>
<v-app id="inspire">
<snackbar></snackbar>
<v-navigation-drawer v-model="drawer" app>
<v-list dense>
<v-list-item>
<v-list-item-action>
<v-icon>mdi-home</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>Home</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-action>
<v-icon>mdi-book</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
<router-link :to="{ name: 'CourseList' }">Courses</router-link>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-action>
<v-icon>mdi-book</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
<router-link :to="{ name: 'CourseCategoryList' }">Courses category</router-link>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-action>
<v-icon>mdi-comment-quote</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
<router-link :to="{ name: 'ReviewList' }">Reviews</router-link>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar app color="indigo" dark>
<v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>Application</v-toolbar-title>
</v-app-bar>
<v-content>
<Breadcrumb layout-class="pl-3 py-3" />
<router-view></router-view>
</v-content>
<v-footer color="indigo" app>
<span class="white--text">&copy; 2019</span>
</v-footer>
</v-app>
</template>
<script>
import Breadcrumb from './components/Breadcrumb';
import Snackbar from './components/Snackbar';
export default {
name: "App",
components: {
Breadcrumb,
Snackbar
},
data: () => ({
drawer: null
}),
beforeMount() {
}
}
</script>

@ -0,0 +1,3 @@
export const VueConfig = {
delimiters: ['[[', ']]']
};

@ -0,0 +1,45 @@
<template>
<div>
<v-row justify="space-around">
<v-icon v-if="handleShow" small class="mr-2" @click="handleShow">mdi-eye</v-icon>
<v-icon v-if="handleEdit" small class="mr-2" @click="handleEdit">mdi-pencil</v-icon>
<v-icon v-if="handleDelete" small @click="confirmDelete = true">mdi-delete</v-icon>
</v-row>
<ConfirmDelete
v-if="handleDelete"
:visible="confirmDelete"
:handle-delete="handleDelete"
@close="confirmDelete = false"
/>
</div>
</template>
<script>
import ConfirmDelete from './ConfirmDelete';
export default {
name: 'ActionCell',
components: {
ConfirmDelete
},
data() {
return {
confirmDelete: false
};
},
props: {
handleShow: {
type: Function,
required: false
},
handleEdit: {
type: Function,
required: false
},
handleDelete: {
type: Function,
required: false
}
}
};
</script>

@ -0,0 +1,40 @@
<template>
<div>
<v-breadcrumbs :items="items" divider="/" :class="layoutClass" />
</div>
</template>
<script>
export default {
name: 'Breadcrumb',
props: ['layoutClass'],
data() {
return {};
},
computed: {
items() {
const { path, matched } = this.$route;
const items = [
{
text: 'Home',
href: '/'
}
];
const lastItem = matched[matched.length - 1];
for (let i = 0, len = matched.length; i < len; i += 1) {
const route = matched[i];
if (route.path) {
items.push({
text: route.name,
disabled: route.path === path || lastItem.path === route.path,
href: route.path
});
}
}
return items;
}
}
};
</script>

@ -0,0 +1,45 @@
<template>
<v-dialog v-model="show" persistent width="300">
<v-card>
<v-card-text>{{ $t('Are you sure you want to delete this item?') }}</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="error darken-1" @click="handleDelete">
{{ $t('Delete') }}
</v-btn>
<v-btn color="secondary darken-1" text @click.stop="show = false">
{{ $t('Cancel') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: 'ConfirmDelete',
props: {
visible: {
type: Boolean,
required: true,
default: () => false
},
handleDelete: {
type: Function,
required: true
}
},
computed: {
show: {
get() {
return this.visible;
},
set(value) {
if (!value) {
this.$emit('close');
}
}
}
}
};
</script>

@ -0,0 +1,40 @@
<template>
<v-expansion-panels v-model="filtersExpanded">
<v-expansion-panel>
<v-expansion-panel-header>
{{ $t('Filters') }}
<template slot="actions">
<v-icon large>mdi-filter-variant</v-icon>
</template>
</v-expansion-panel-header>
<v-expansion-panel-content>
<slot name="filter"></slot>
<v-btn color="primary" @click="handleFilter">{{ $t('Filter')}}</v-btn>
<v-btn color="primary" class="ml-2" text @click="handleReset">{{ $t('Reset') }}</v-btn>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</template>
<script>
export default {
name: 'DataFilter',
props: {
handleReset: {
type: Function,
required: true
},
handleFilter: {
type: Function,
required: true
}
},
data() {
return {
filtersExpanded: false
};
}
};
</script>

@ -0,0 +1,52 @@
<template>
<v-menu
v-model="showMenu"
:close-on-content-click="false"
:nudge-right="40"
transition="scale-transition"
offset-y
min-width="290px"
>
<template v-slot:activator="{ on }">
<v-text-field
v-model="date"
:label="label"
prepend-icon="mdi-calendar"
readonly
v-on="on"
></v-text-field>
</template>
<v-date-picker v-model="date" @input="handleInput"></v-date-picker>
</v-menu>
</template>
<script>
import { formatDateTime } from '../utils/dates';
export default {
props: {
label: {
type: String,
required: false,
default: () => ''
},
value: String
},
created() {
this.date = this.value ? this.value : this.date;
},
data() {
return {
date: this.value ? this.value : new Date().toISOString().substr(0, 10),
showMenu: false
};
},
methods: {
formatDateTime,
handleInput() {
this.showMenu = false;
this.$emit('input', this.date);
}
}
};
</script>

@ -0,0 +1,18 @@
<template>
<div class="text-center">
<v-overlay :value="visible">
<v-progress-circular indeterminate size="64"></v-progress-circular>
</v-overlay>
</div>
</template>
<script>
export default {
props: {
visible: {
type: Boolean,
required: true
}
}
};
</script>

@ -0,0 +1,32 @@
<template>
<v-snackbar
v-model="show"
:color="color"
:multi-line="true"
:timeout="timeout"
right
top
>
{{ text }}
<template v-if="subText">
<p>{{ subText }}</p>
</template>
<v-btn dark text @click.native="close">{{ $t('Close') }}</v-btn>
</v-snackbar>
</template>
<script>
import { mapFields } from 'vuex-map-fields';
export default {
computed: {
...mapFields('notifications', ['color', 'show', 'subText', 'text', 'timeout'])
},
methods: {
close() {
this.show = false;
}
}
};
</script>

@ -0,0 +1,137 @@
<template>
<v-toolbar class="my-md-auto" elevation="0">
<slot name="left"></slot>
<v-spacer />
<div>
<v-btn
v-if="handleList"
:loading="isLoading"
color="primary"
@click="listItem"
>
{{ $t('List') }}
</v-btn>
<v-btn
v-if="handleEdit"
:loading="isLoading"
color="primary"
@click="editItem"
>
{{ $t('Edit') }}
</v-btn>
<v-btn
v-if="handleSubmit"
:loading="isLoading"
color="primary"
@click="submitItem"
>
<v-icon left>mdi-content-save</v-icon>
{{ $t('Submit') }}
</v-btn>
<v-btn
v-if="handleReset"
color="primary"
class="ml-sm-2"
@click="resetItem"
>
{{ $t('Reset') }}
</v-btn>
<v-btn
v-if="handleDelete"
color="error"
class="ml-sm-2"
@click="confirmDelete = true"
>
{{ $t('Delete') }}
</v-btn>
<v-btn v-if="handleAdd" color="primary" rounded @click="addItem">
<v-icon>mdi-plus-circle</v-icon>
</v-btn>
</div>
<ConfirmDelete
v-if="handleDelete"
:visible="confirmDelete"
:handle-delete="handleDelete"
@close="confirmDelete = false"
/>
</v-toolbar>
</template>
<script>
import ConfirmDelete from './ConfirmDelete';
export default {
name: 'Toolbar',
components: {
ConfirmDelete
},
data() {
return {
confirmDelete: false
};
},
props: {
handleList: {
type: Function,
required: false
},
handleEdit: {
type: Function,
required: false
},
handleSubmit: {
type: Function,
required: false
},
handleReset: {
type: Function,
required: false
},
handleDelete: {
type: Function,
required: false
},
handleAdd: {
type: Function,
required: false
},
title: {
type: String,
required: false
},
isLoading: {
type: Boolean,
required: false,
default: () => false
}
},
methods: {
listItem() {
if (this.handleList) {
this.handleList();
}
},
addItem() {
if (this.handleAdd) {
this.handleAdd();
}
},
editItem() {
if (this.handleEdit) {
this.handleEdit();
}
},
submitItem() {
if (this.handleSubmit) {
this.handleSubmit();
}
},
resetItem() {
if (this.handleReset) {
this.handleReset();
}
}
}
};
</script>

@ -0,0 +1,76 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.title"
:label="$t('title')"
type="text"
/>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.code"
:label="$t('code')"
type="text"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-combobox
v-model="item.category"
:items="categorySelectItems"
:no-data-text="$t('No results')"
:label="$t('category')"
item-text="name"
item-value="@id"
chips
/>
</v-col>
<v-row cols="12"></v-row>
</v-row>
</v-container>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
export default {
name: 'CourseFilter',
props: {
values: {
type: Object,
required: true
}
},
data() {
return {};
},
mounted() {
this.categoryGetSelectItems();
},
methods: {
...mapActions({
categoryGetSelectItems: 'coursecategory/fetchSelectItems'
}),
},
computed: {
...mapFields('coursecategory', {
categorySelectItems: 'selectItems'
}),
// eslint-disable-next-line
item() {
return this.initialValues || this.values;
}
}
};
</script>

@ -0,0 +1,173 @@
<template>
<v-form>
<v-container fluid>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.title"
:error-messages="titleErrors"
:label="$t('title')"
required
@input="$v.item.title.$touch()"
@blur="$v.item.title.$touch()"
/>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.code"
:error-messages="codeErrors"
:label="$t('code')"
required
@input="$v.item.code.$touch()"
@blur="$v.item.code.$touch()"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-combobox
v-model="item.category"
:items="categorySelectItems"
:error-messages="categoryErrors"
:no-data-text="$t('No results')"
:label="$t('category')"
item-text="name"
item-value="@id"
/>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model.number="item.visibility"
:error-messages="visibilityErrors"
:label="$t('visibility')"
required
@input="$v.item.visibility.$touch()"
@blur="$v.item.visibility.$touch()"
/>
</v-col>
</v-row>
</v-container>
</v-form>
</template>
<script>
import has from 'lodash/has';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
import { mapActions } from 'vuex';
import { mapFields } from 'vuex-map-fields';
export default {
name: 'CourseForm',
mixins: [validationMixin],
props: {
values: {
type: Object,
required: true
},
errors: {
type: Object,
default: () => {}
},
initialValues: {
type: Object,
default: () => {}
}
},
data() {
return {
title: null,
code: null,
category: null,
visibility: null,
};
},
computed: {
...mapFields('coursecategory', {
categorySelectItems: 'selectItems'
}),
// eslint-disable-next-line
item() {
return this.initialValues || this.values;
},
titleErrors() {
const errors = [];
if (!this.$v.item.title.$dirty) return errors;
has(this.violations, 'title') && errors.push(this.violations.title);
!this.$v.item.title.required && errors.push(this.$t('Field is required'));
return errors;
},
codeErrors() {
const errors = [];
if (!this.$v.item.code.$dirty) return errors;
has(this.violations, 'code') && errors.push(this.violations.code);
!this.$v.item.code.required && errors.push(this.$t('Field is required'));
return errors;
},
categoryErrors() {
const errors = [];
if (!this.$v.item.category.$dirty) return errors;
has(this.violations, 'category') && errors.push(this.violations.category);
return errors;
},
visibilityErrors() {
const errors = [];
if (!this.$v.item.visibility.$dirty) return errors;
has(this.violations, 'visibility') && errors.push(this.violations.visibility);
!this.$v.item.visibility.required && errors.push(this.$t('Field is required'));
return errors;
},
violations() {
return this.errors || {};
}
},
mounted() {
this.categoryGetSelectItems();
},
methods: {
...mapActions({
categoryGetSelectItems: 'coursecategory/fetchSelectItems'
}),
},
validations: {
item: {
title: {
required,
},
code: {
required,
},
category: {
},
visibility: {
required,
},
}
}
};
</script>

@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'CourseLayout'
}
</script>

@ -0,0 +1,40 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.name"
:label="$t('name')"
type="text"
/>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
name: 'CourseCategoryFilter',
props: {
values: {
type: Object,
required: true
}
},
data() {
return {};
},
mounted() {
},
computed: {
// eslint-disable-next-line
item() {
return this.initialValues || this.values;
}
},
methods: {
}
};
</script>

@ -0,0 +1,113 @@
<template>
<v-form>
<v-container fluid>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.name"
:error-messages="nameErrors"
:label="$t('name')"
required
@input="$v.item.name.$touch()"
@blur="$v.item.name.$touch()"
/>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.code"
:error-messages="codeErrors"
:label="$t('code')"
required
@input="$v.item.code.$touch()"
@blur="$v.item.code.$touch()"
/>
</v-col>
</v-row>
</v-container>
</v-form>
</template>
<script>
import has from 'lodash/has';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
import { mapActions } from 'vuex';
import { mapFields } from 'vuex-map-fields';
export default {
name: 'CourseCategoryForm',
mixins: [validationMixin],
props: {
values: {
type: Object,
required: true
},
errors: {
type: Object,
default: () => {}
},
initialValues: {
type: Object,
default: () => {}
}
},
mounted() {
},
data() {
return {
};
},
computed: {
// eslint-disable-next-line
item() {
return this.initialValues || this.values;
},
nameErrors() {
const errors = [];
if (!this.$v.item.name.$dirty) return errors;
has(this.violations, 'name') && errors.push(this.violations.name);
!this.$v.item.name.required && errors.push(this.$t('Field is required'));
return errors;
},
codeErrors() {
const errors = [];
if (!this.$v.item.code.$dirty) return errors;
has(this.violations, 'code') && errors.push(this.violations.code);
!this.$v.item.code.required && errors.push(this.$t('Field is required'));
return errors;
},
violations() {
return this.errors || {};
}
},
methods: {
},
validations: {
item: {
name: {
required,
},
code: {
required,
},
}
}
};
</script>

@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'CourseCategoryLayout'
}
</script>

@ -0,0 +1,10 @@
export default class SubmissionError extends Error {
constructor (errors) {
super('Submit Validation Failed');
this.errors = errors;
//Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
return this;
}
}

@ -0,0 +1,13 @@
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import messages from './locales/en';
Vue.use(VueI18n);
export default new VueI18n({
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
messages: {
en: messages
}
});

@ -0,0 +1,22 @@
export default {
'Submit': 'Submit',
'Reset': 'Reset',
'Delete': 'Delete',
'Edit': 'Edit',
'Are you sure you want to delete this item?': 'Are you sure you want to delete this item?',
'No results': 'No results',
'Close': 'Close',
'Cancel': 'Cancel',
'Updated': 'Updated',
'Field': 'Field',
'Value': 'Value',
'Filters': 'Filters',
'Filter': 'Filter',
'Data unavailable': 'Data unavailable',
'Loading...': 'Loading...',
'Deleted': 'Deleted',
'Please, insert a value bigger than zero!': 'Please, insert a value bigger than zero!',
'Please type something': 'Please type something',
'Field is required': 'Field is required',
'Records per page:': 'Records per page:',
};

@ -0,0 +1,67 @@
import Vue from "vue";
import App from "./App";
import router from "./router";
import store from "./store";
import courseCategoryService from './services/coursecategory';
import courseService from './services/course';
import makeCrudModule from './store/modules/crud';
// import '@mdi/font/css/materialdesignicons.css'
/*router.beforeEach((to, from, next) => {
// hack to allow for forward slashes in path ids
if (to.fullPath.includes('%2F')) {
next(to.fullPath.replace('%2F', '/'));
}
next();
});*/
import vuetify from './plugins/vuetify' // path to vuetify export
import ApolloClient from 'apollo-boost'
const apolloClient = new ApolloClient({
// You should use an absolute URL here
uri: '/api/graphql/'
})
import VueApollo from 'vue-apollo';
Vue.use(VueApollo);
import Vuelidate from 'vuelidate';
import i18n from './i18n';
Vue.config.productionTip = false;
Vue.use(Vuelidate);
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
});
//import './quasar'
store.registerModule(
'course',
makeCrudModule({
service: courseService
})
);
store.registerModule(
'coursecategory',
makeCrudModule({
service: courseCategoryService
})
);
Vue.config.productionTip = false;
new Vue({
vuetify,
i18n,
components: {App},
apolloProvider,
data: {},
store,
router,
render: h => h(App)
}).
$mount("#app");

@ -0,0 +1,41 @@
import NotificationMixin from './NotificationMixin';
import { formatDateTime } from '../utils/dates';
export default {
mixins: [NotificationMixin],
methods: {
formatDateTime,
onCreated(item) {
this.showMessage(`${item['@id']} created`);
this.$router.push({
name: `${this.$options.servicePrefix}Update`,
params: { id: item['@id'] }
});
},
onSendForm() {
const createForm = this.$refs.createForm;
createForm.$v.$touch();
if (!createForm.$v.$invalid) {
this.create(createForm.$v.item.$model);
}
},
resetForm() {
this.$refs.createForm.$v.$reset();
this.item = {};
}
},
watch: {
created(created) {
if (!created) {
return;
}
this.onCreated(created);
},
error(message) {
message && this.showError(message);
}
}
};

@ -0,0 +1,88 @@
import isEmpty from 'lodash/isEmpty';
import { formatDateTime } from '../utils/dates';
import NotificationMixin from './NotificationMixin';
export default {
mixins: [NotificationMixin],
data() {
return {
options: {
sortBy: [],
descending: false,
page: 1,
itemsPerPage: 15
},
filters: {}
};
},
watch: {
deletedItem(item) {
this.showMessage(`${item['@id']} deleted.`);
},
error(message) {
message && this.showError(message);
},
items() {
this.options.totalItems = this.totalItems;
}
},
methods: {
onUpdateOptions(props) {
const { page, itemsPerPage, sortBy, descending, totalItems } = props;
let params = {
...this.filters
};
if (itemsPerPage > 0) {
params = { ...params, itemsPerPage, page };
}
if (!isEmpty(sortBy)) {
params[`order[${sortBy}]`] = descending ? 'desc' : 'asc';
}
this.getPage(params).then(() => {
this.options.sortBy = sortBy;
this.options.descending = descending;
this.options.itemsPerPage = itemsPerPage;
this.options.totalItems = totalItems;
});
},
onSendFilter() {
this.resetList = true;
this.onUpdateOptions(this.options);
},
resetFilter() {
this.filters = {};
},
addHandler() {
this.$router.push({ name: `${this.$options.servicePrefix}Create` });
},
showHandler(item) {
this.$router.push({
name: `${this.$options.servicePrefix}Show`,
params: { id: item['@id'] }
});
},
editHandler(item) {
this.$router.push({
name: `${this.$options.servicePrefix}Update`,
params: { id: item['@id'] }
});
},
deleteHandler(item) {
this.deleteItem(item).then(() => this.onUpdateOptions(this.options));
},
formatDateTime
}
};

@ -0,0 +1,37 @@
import { mapFields } from 'vuex-map-fields';
export default {
computed: {
...mapFields('notifications', ['color', 'show', 'subText', 'text', 'timeout'])
},
methods: {
cleanState() {
setTimeout(() => {
this.show = false;
}, this.timeout);
},
showError(error) {
this.showMessage(error, 'danger');
},
showMessage(message, color = 'success') {
this.show = true;
this.color = color;
if (typeof message === 'string') {
this.text = message;
this.cleanState();
return;
}
this.text = message.message;
if (message.response) this.subText = message.response.data.message;
this.cleanState();
}
}
};

@ -0,0 +1,47 @@
import NotificationMixin from './NotificationMixin';
import { formatDateTime } from '../utils/dates';
export default {
mixins: [NotificationMixin],
created() {
this.retrieve(decodeURIComponent(this.$route.params.id));
},
computed: {
item() {
return this.find(decodeURIComponent(this.$route.params.id));
}
},
methods: {
list() {
this.$router
.push({ name: `${this.$options.servicePrefix}List` })
.catch(() => {});
},
del() {
this.deleteItem(this.item).then(() => {
this.showMessage(`${this.item['@id']} deleted.`);
this.$router
.push({ name: `${this.$options.servicePrefix}List` })
.catch(() => {});
});
},
formatDateTime,
editHandler() {
this.$router.push({
name: `${this.$options.servicePrefix}Update`,
params: { id: this.item['@id'] }
});
}
},
watch: {
error(message) {
message && this.showError(message);
},
deleteError(message) {
message && this.showError(message);
}
},
beforeDestroy() {
this.reset();
}
};

@ -0,0 +1,79 @@
import NotificationMixin from './NotificationMixin';
import { formatDateTime } from '../utils/dates';
export default {
mixins: [NotificationMixin],
data() {
return {
item: {}
};
},
created() {
this.retrieve(decodeURIComponent(this.$route.params.id));
},
beforeDestroy() {
this.reset();
},
computed: {
retrieved() {
return this.find(decodeURIComponent(this.$route.params.id));
}
},
methods: {
del() {
this.deleteItem(this.retrieved).then(() => {
this.showMessage(`${this.item['@id']} deleted.`);
this.$router
.push({ name: `${this.$options.servicePrefix}List` })
.catch(() => {});
});
},
formatDateTime,
reset() {
this.$refs.updateForm.$v.$reset();
this.updateReset();
this.delReset();
this.createReset();
},
onSendForm() {
const updateForm = this.$refs.updateForm;
updateForm.$v.$touch();
if (!updateForm.$v.$invalid) {
this.update(updateForm.$v.item.$model);
}
},
resetForm() {
this.$refs.updateForm.$v.$reset();
this.item = { ...this.retrieved };
}
},
watch: {
deleted(deleted) {
if (!deleted) {
return;
}
this.$router
.push({ name: `${this.$options.servicePrefix}List` })
.catch(() => {});
},
error(message) {
message && this.showError(message);
},
deleteError(message) {
message && this.showError(message);
},
updated(val) {
this.showMessage(`${val['@id']} updated.`);
},
retrieved(val) {
this.item = { ...val };
}
}
};

@ -0,0 +1,15 @@
// src/plugins/vuetify.js
import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
Vue.use(Vuetify)
const opts = {
icons: {
iconfont: 'mdi'
}
};
export default new Vuetify(opts)

@ -0,0 +1,5 @@
import Vue from 'vue'
// import './styles/quasar.sass'
import '@quasar/extras/material-icons/material-icons.css'
import Quasar from 'quasar/dist/quasar.umd.js'

@ -0,0 +1,28 @@
export default {
path: '/courses',
name: 'courses',
component: () => import('../components/course/Layout'),
redirect: { name: 'CourseList' },
children: [
{
name: 'CourseList',
path: '',
component: () => import('../views/course/List')
},
{
name: 'CourseCreate',
path: 'new',
component: () => import('../views/course/Create')
},
{
name: 'CourseUpdate',
path: ':id/edit',
component: () => import('../views/course/Update')
},
{
name: 'CourseShow',
path: ':id',
component: () => import('../views/course/Show')
}
]
};

@ -0,0 +1,28 @@
export default {
path: '/course_categories',
name: 'course_categories',
component: () => import('../components/coursecategory/Layout'),
redirect: { name: 'CourseCategoryList' },
children: [
{
name: 'CourseCategoryList',
path: '',
component: () => import('../views/coursecategory/List')
},
{
name: 'CourseCategoryCreate',
path: 'new',
component: () => import('../views/coursecategory/Create')
},
{
name: 'CourseCategoryUpdate',
path: ':id/edit',
component: () => import('../views/coursecategory/Update')
},
{
name: 'CourseCategoryShow',
path: ':id',
component: () => import('../views/coursecategory/Show')
}
]
};

@ -0,0 +1,18 @@
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
import courseRoutes from './course';
import coursecategoryRoutes from './coursecategory';
import sessionRoutes from './../../quasar/router/session';
export default new VueRouter({
mode: "history",
routes: [
courseRoutes,
...sessionRoutes,
coursecategoryRoutes,
// { path: "*", redirect: "/home" }
]
});

@ -0,0 +1,24 @@
import fetch from '../utils/fetch';
export default function makeService(endpoint) {
return {
find(id) {
return fetch(`${id}`);
},
findAll(params) {
return fetch(endpoint, params);
},
create(payload) {
return fetch(endpoint, { method: 'POST', body: JSON.stringify(payload) });
},
del(item) {
return fetch(item['@id'], { method: 'DELETE' });
},
update(payload) {
return fetch(payload['@id'], {
method: 'PUT',
body: JSON.stringify(payload)
});
}
};
}

@ -0,0 +1,3 @@
import makeService from './api';
export default makeService('courses');

@ -0,0 +1,3 @@
import makeService from './api';
export default makeService('course_categories');

@ -0,0 +1,14 @@
import Vue from "vue";
import Vuex from "vuex";
import notifications from './modules/notifications';
//import session from './../../quasar/store/modules/session/';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
notifications,
//session,
}
});

@ -0,0 +1,273 @@
import Vue from 'vue';
import { getField, updateField } from 'vuex-map-fields';
import remove from 'lodash/remove';
import SubmissionError from '../../error/SubmissionError';
const initialState = () => ({
allIds: [],
byId: {},
created: null,
deleted: null,
error: "",
isLoading: false,
resetList: false,
selectItems: null,
totalItems: 0,
updated: null,
view: null,
violations: null
});
const handleError = (commit, e) => {
commit(ACTIONS.TOGGLE_LOADING);
if (e instanceof SubmissionError) {
commit(ACTIONS.SET_VIOLATIONS, e.errors);
// eslint-disable-next-line
commit(ACTIONS.SET_ERROR, e.errors._error);
return Promise.reject(e);
}
// eslint-disable-next-line
commit(ACTIONS.SET_ERROR, e.message);
return Promise.reject(e);
};
export const ACTIONS = {
ADD: 'ADD',
RESET_CREATE: 'RESET_CREATE',
RESET_DELETE: 'RESET_DELETE',
RESET_LIST: 'RESET_LIST',
RESET_SHOW: 'RESET_SHOW',
RESET_UPDATE: 'RESET_UPDATE',
SET_CREATED: 'SET_CREATED',
SET_DELETED: 'SET_DELETED',
SET_ERROR: 'SET_ERROR',
SET_SELECT_ITEMS: 'SET_SELECT_ITEMS',
SET_TOTAL_ITEMS: 'SET_TOTAL_ITEMS',
SET_UPDATED: 'SET_UPDATED',
SET_VIEW: 'SET_VIEW',
SET_VIOLATIONS: 'SET_VIOLATIONS',
TOGGLE_LOADING: 'TOGGLE_LOADING'
};
export default function makeCrudModule({
normalizeRelations = x => x,
resolveRelations = x => x,
service
} = {}) {
return {
actions: {
create: ({ commit }, values) => {
commit(ACTIONS.SET_ERROR, '');
commit(ACTIONS.TOGGLE_LOADING);
service
.create(values)
.then(response => response.json())
.then(data => {
commit(ACTIONS.TOGGLE_LOADING);
commit(ACTIONS.ADD, data);
commit(ACTIONS.SET_CREATED, data);
})
.catch(e => handleError(commit, e));
},
del: ({ commit }, item) => {
commit(ACTIONS.TOGGLE_LOADING);
service
.del(item)
.then(() => {
commit(ACTIONS.TOGGLE_LOADING);
commit(ACTIONS.SET_DELETED, item);
})
.catch(e => handleError(commit, e));
},
fetchAll: ({ commit, state }, params) => {
if (!service) throw new Error('No service specified!');
commit(ACTIONS.TOGGLE_LOADING);
service
.findAll({ params })
.then(response => response.json())
.then(retrieved => {
commit(ACTIONS.TOGGLE_LOADING);
commit(
ACTIONS.SET_TOTAL_ITEMS,
retrieved['hydra:totalItems']
);
commit(ACTIONS.SET_VIEW, retrieved['hydra:view']);
if (true === state.resetList) {
commit(ACTIONS.RESET_LIST);
}
retrieved['hydra:member'].forEach(item => {
commit(ACTIONS.ADD, normalizeRelations(item));
});
})
.catch(e => handleError(commit, e));
},
fetchSelectItems: (
{ commit },
{ params = { properties: ['@id', 'name'] } } = {}
) => {
commit(ACTIONS.TOGGLE_LOADING);
if (!service) throw new Error('No service specified!');
service
.findAll({ params })
.then(response => response.json())
.then(retrieved => {
commit(
ACTIONS.SET_SELECT_ITEMS,
retrieved['hydra:member']
);
})
.catch(e => handleError(commit, e));
},
load: ({ commit }, id) => {
if (!service) throw new Error('No service specified!');
commit(ACTIONS.TOGGLE_LOADING);
service
.find(id)
.then(response => response.json())
.then(item => {
commit(ACTIONS.TOGGLE_LOADING);
commit(ACTIONS.ADD, normalizeRelations(item));
})
.catch(e => handleError(commit, e));
},
resetCreate: ({ commit }) => {
commit(ACTIONS.RESET_CREATE);
},
resetDelete: ({ commit }) => {
commit(ACTIONS.RESET_DELETE);
},
resetShow: ({ commit }) => {
commit(ACTIONS.RESET_SHOW);
},
resetUpdate: ({ commit }) => {
commit(ACTIONS.RESET_UPDATE);
},
update: ({ commit }, item) => {
commit(ACTIONS.SET_ERROR, '');
commit(ACTIONS.TOGGLE_LOADING);
service
.update(item)
.then(response => response.json())
.then(data => {
commit(ACTIONS.TOGGLE_LOADING);
commit(ACTIONS.SET_UPDATED, data);
})
.catch(e => handleError(commit, e));
}
},
getters: {
find: state => id => {
return resolveRelations(state.byId[id]);
},
getField,
list: (state, getters) => {
return state.allIds.map(id => getters.find(id));
}
},
mutations: {
updateField,
[ACTIONS.ADD]: (state, item) => {
Vue.set(state.byId, item['@id'], item);
Vue.set(state, 'isLoading', false);
if (state.allIds.includes(item['@id'])) return;
state.allIds.push(item['@id']);
},
[ACTIONS.RESET_CREATE]: state => {
Object.assign(state, {
isLoading: false,
error: '',
created: null,
violations: null
});
},
[ACTIONS.RESET_DELETE]: state => {
Object.assign(state, {
isLoading: false,
error: '',
deleted: null
});
},
[ACTIONS.RESET_LIST]: state => {
Object.assign(state, {
allIds: [],
byId: {},
error: '',
isLoading: false,
resetList: false
});
},
[ACTIONS.RESET_SHOW]: state => {
Object.assign(state, {
error: '',
isLoading: false
});
},
[ACTIONS.RESET_UPDATE]: state => {
Object.assign(state, {
error: '',
isLoading: false,
updated: null,
violations: null
});
},
[ACTIONS.SET_CREATED]: (state, created) => {
Object.assign(state, { created });
},
[ACTIONS.SET_DELETED]: (state, deleted) => {
if (!state.allIds.includes(deleted['@id'])) return;
Object.assign(state, {
allIds: remove(state.allIds, item => item['@id'] === deleted['@id']),
byId: remove(state.byId, id => id === deleted['@id']),
deleted
});
},
[ACTIONS.SET_ERROR]: (state, error) => {
Object.assign(state, { error, isLoading: false });
},
[ACTIONS.SET_SELECT_ITEMS]: (state, selectItems) => {
Object.assign(state, {
error: '',
isLoading: false,
selectItems
});
},
[ACTIONS.SET_TOTAL_ITEMS]: (state, totalItems) => {
Object.assign(state, { totalItems });
},
[ACTIONS.SET_UPDATED]: (state, updated) => {
Object.assign(state, {
byId: {
[updated['@id']]: updated
},
updated
});
},
[ACTIONS.SET_VIEW]: (state, view) => {
Object.assign(state, { view });
},
[ACTIONS.SET_VIOLATIONS]: (state, violations) => {
Object.assign(state, { violations });
},
[ACTIONS.TOGGLE_LOADING]: state => {
Object.assign(state, { error: '', isLoading: !state.isLoading });
}
},
namespaced: true,
state: initialState
};
}

@ -0,0 +1,18 @@
import {getField, updateField} from 'vuex-map-fields';
export default {
namespaced: true,
state: {
show: false,
color: 'error',
text: 'An error occurred',
subText: '',
timeout: 6000
},
getters: {
getField
},
mutations: {
updateField
}
};

@ -0,0 +1,9 @@
import moment from 'moment';
const formatDateTime = function(date) {
if (!date) return null;
return moment(date).format('DD/MM/YYYY');
};
export { formatDateTime };

@ -0,0 +1,71 @@
import isObject from 'lodash/isObject';
import { ENTRYPOINT } from '../config/entrypoint';
import SubmissionError from '../error/SubmissionError';
import { normalize } from './hydra';
const MIME_TYPE = 'application/ld+json';
const makeParamArray = (key, arr) =>
arr.map(val => `${key}[]=${val}`).join('&');
export default function(id, options = {}) {
if ('undefined' === typeof options.headers) options.headers = new Headers();
if (null === options.headers.get('Accept'))
options.headers.set('Accept', MIME_TYPE);
if (
'undefined' !== options.body &&
!(options.body instanceof FormData) &&
null === options.headers.get('Content-Type')
)
options.headers.set('Content-Type', MIME_TYPE);
if (options.params) {
const params = normalize(options.params);
let queryString = Object.keys(params)
.map(key =>
Array.isArray(params[key])
? makeParamArray(key, params[key])
: `${key}=${params[key]}`
)
.join('&');
id = `${id}?${queryString}`;
}
const entryPoint = ENTRYPOINT + (ENTRYPOINT.endsWith('/') ? '' : '/');
const payload = options.body && JSON.parse(options.body);
if (isObject(payload) && payload['@id'])
options.body = JSON.stringify(normalize(payload));
//console.log(id); console.log(new URL(id, entryPoint));
return global.fetch(new URL(id, entryPoint), options).then(response => {
if (response.ok) return response;
return response.json().then(
json => {
const error =
json['hydra:description'] ||
json['hydra:title'] ||
'An error occurred.';
if (!json.violations) throw Error(error);
let errors = { _error: error };
json.violations.forEach(violation =>
errors[violation.propertyPath]
? (errors[violation.propertyPath] +=
'\n' + errors[violation.propertyPath])
: (errors[violation.propertyPath] = violation.message)
);
throw new SubmissionError(errors);
},
() => {
throw new Error(response.statusText || 'An error occurred.');
}
);
});
}

@ -0,0 +1,19 @@
import get from 'lodash/get';
import has from 'lodash/has';
import mapValues from 'lodash/mapValues';
export function normalize(data) {
if (has(data, 'hydra:member')) {
// Normalize items in collections
data['hydra:member'] = data['hydra:member'].map(item => normalize(item));
return data;
}
// Flatten nested documents
return mapValues(data, value =>
Array.isArray(value)
? value.map(v => get(v, '@id', v))
: get(value, '@id', value)
);
}

@ -0,0 +1,7 @@
import moment from 'moment';
const date = function(value) {
return moment(value).isValid();
};
export { date };

@ -0,0 +1,46 @@
<template>
<div>
<CourseForm ref="createForm" :values="item" :errors="violations" />
<Loading :visible="isLoading" />
<Toolbar :handle-submit="onSendForm" :handle-reset="resetForm"></Toolbar>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import CourseForm from '../../components/course/Form';
import Loading from '../../components/Loading';
import Toolbar from '../../components/Toolbar';
import CreateMixin from '../../mixins/CreateMixin';
const servicePrefix = 'Course';
const { mapFields } = createHelpers({
getterType: 'course/getField',
mutationType: 'course/updateField'
});
export default {
name: 'CourseCreate',
servicePrefix,
mixins: [CreateMixin],
components: {
Loading,
Toolbar,
CourseForm
},
data() {
return {
item: {}
};
},
computed: {
...mapFields(['error', 'isLoading', 'created', 'violations'])
},
methods: {
...mapActions('course', ['create', 'reset'])
}
};
</script>

@ -0,0 +1,122 @@
<template>
<div class="course-list">
<Toolbar :handle-add="addHandler" />
<v-container grid-list-xl fluid>
<v-layout row wrap>
<!-- <v-flex sm12>-->
<!-- <h1>Course List</h1>-->
<!-- </v-flex>-->
<v-flex lg12>
<DataFilter :handle-filter="onSendFilter" :handle-reset="resetFilter">
<CourseFilterForm
ref="filterForm"
:values="filters"
slot="filter"
/>
</DataFilter>
<br />
<v-data-table
v-model="selected"
:headers="headers"
:items="items"
:items-per-page.sync="options.itemsPerPage"
:loading="isLoading"
:loading-text="$t('Loading...')"
:options.sync="options"
:server-items-length="totalItems"
class="elevation-1"
item-key="@id"
show-select
@update:options="onUpdateOptions"
>
<template slot="item.category" slot-scope="{ item }">
<div v-if="item['category']">
{{ item['category'].name }}
</div>
<div v-else>
-
</div>
</template>
<template slot="item.visibility" slot-scope="{ item }">
{{ $n(item['visibility']) }}
</template>
<template slot="item.expirationDate" slot-scope="{ item }">
{{ formatDateTime(item['expirationDate'], 'long') }}
</template>
<ActionCell
slot="item.action"
slot-scope="props"
:handle-show="() => showHandler(props.item)"
:handle-edit="() => editHandler(props.item)"
:handle-delete="() => deleteHandler(props.item)"
></ActionCell>
</v-data-table>
</v-flex>
</v-layout>
</v-container>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import ListMixin from '../../mixins/ListMixin';
import ActionCell from '../../components/ActionCell';
import CourseFilterForm from '../../components/course/Filter';
import DataFilter from '../../components/DataFilter';
import Toolbar from '../../components/Toolbar';
export default {
name: 'CourseList',
servicePrefix: 'Course',
mixins: [ListMixin],
components: {
Toolbar,
ActionCell,
CourseFilterForm,
DataFilter
},
data() {
return {
headers: [
{ text: 'title', value: 'title' },
{ text: 'code', value: 'code' },
{ text: 'courseLanguage', value: 'Language' },
{ text: 'category', value: 'category' },
{ text: 'visibility', value: 'visibility' },
{
text: 'Actions',
value: 'action',
sortable: false
}
],
selected: []
};
},
computed: {
...mapGetters('course', {
items: 'list'
}),
...mapFields('course', {
deletedItem: 'deleted',
error: 'error',
isLoading: 'isLoading',
resetList: 'resetList',
totalItems: 'totalItems',
view: 'view'
})
},
methods: {
...mapActions('course', {
getPage: 'fetchAll',
deleteItem: 'del'
})
}
};
</script>

@ -0,0 +1,119 @@
<template>
<div>
<Toolbar
:handle-delete="del"
:handle-list="list"
>
<template slot="left">
<v-toolbar-title v-if="item">{{
`${$options.servicePrefix} ${item['@id']}`
}}</v-toolbar-title>
</template>
</Toolbar>
<br />
<div v-if="item" class="table-course-show">
<v-simple-table>
<template slot="default">
<thead>
<tr>
<th>Field</th>
<th>Value</th>
<th>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>{{ $t('title') }}</strong></td>
<td>
{{ item['title'] }}
</td>
<td><strong>{{ $t('code') }}</strong></td>
<td>
{{ item['code'] }}
</td>
</tr>
<tr>
<td><strong>{{ $t('courseLanguage') }}</strong></td>
<td>
{{ item['courseLanguage'] }}
</td>
<td><strong>{{ $t('category') }}</strong></td>
<td>
<div v-if="item['category']">
{{ item['category'].name }}
</div>
<div v-else>
-
</div>
</td>
</tr>
<tr>
<td><strong>{{ $t('visibility') }}</strong></td>
<td>
{{ $n(item['visibility']) }} </td>
<td><strong>{{ $t('departmentName') }}</strong></td>
<td>
{{ item['departmentName'] }}
</td>
</tr>
<tr>
<td><strong>{{ $t('departmentUrl') }}</strong></td>
<td>
{{ item['departmentUrl'] }}
</td>
<td><strong>{{ $t('expirationDate') }}</strong></td>
<td>
{{ formatDateTime(item['expirationDate'], 'long') }} </td>
</tr>
</tbody>
</template>
</v-simple-table>
</div>
<Loading :visible="isLoading" />
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import Loading from '../../components/Loading';
import ShowMixin from '../../mixins/ShowMixin';
import Toolbar from '../../components/Toolbar';
const servicePrefix = 'Course';
export default {
name: 'CourseShow',
servicePrefix,
components: {
Loading,
Toolbar
},
mixins: [ShowMixin],
computed: {
...mapFields('course', {
isLoading: 'isLoading'
}),
...mapGetters('course', ['find'])
},
methods: {
...mapActions('course', {
deleteItem: 'del',
reset: 'resetShow',
retrieve: 'load'
})
}
};
</script>

@ -0,0 +1,67 @@
<template>
<div>
<v-card
class="mx-auto"
>
<CourseForm
ref="updateForm"
v-if="item"
:values="item"
:errors="violations"
/>
<Loading :visible="isLoading || deleteLoading" />
<v-footer>
<Toolbar
:handle-submit="onSendForm"
:handle-reset="resetForm"
:handle-delete="del"
/>
</v-footer>
</v-card>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import CourseForm from '../../components/course/Form.vue';
import Loading from '../../components/Loading';
import Toolbar from '../../components/Toolbar';
import UpdateMixin from '../../mixins/UpdateMixin';
const servicePrefix = 'Course';
export default {
name: 'CourseUpdate',
servicePrefix,
mixins: [UpdateMixin],
components: {
Loading,
Toolbar,
CourseForm
},
computed: {
...mapFields('course', {
deleteLoading: 'isLoading',
isLoading: 'isLoading',
error: 'error',
updated: 'updated',
violations: 'violations'
}),
...mapGetters('course', ['find'])
},
methods: {
...mapActions('course', {
createReset: 'resetCreate',
deleteItem: 'del',
delReset: 'resetDelete',
retrieve: 'load',
update: 'update',
updateReset: 'resetUpdate'
})
}
};
</script>

@ -0,0 +1,45 @@
<template>
<div>
<Toolbar :handle-submit="onSendForm" :handle-reset="resetForm"></Toolbar>
<CourseCategoryForm ref="createForm" :values="item" :errors="violations" />
<Loading :visible="isLoading" />
</div>
</template>
<script>
import { mapActions } from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import CourseCategoryForm from '../../components/coursecategory/Form';
import Loading from '../../components/Loading';
import Toolbar from '../../components/Toolbar';
import CreateMixin from '../../mixins/CreateMixin';
const servicePrefix = 'CourseCategory';
const { mapFields } = createHelpers({
getterType: 'coursecategory/getField',
mutationType: 'coursecategory/updateField'
});
export default {
name: 'CourseCategoryCreate',
servicePrefix,
mixins: [CreateMixin],
components: {
Loading,
Toolbar,
CourseCategoryForm
},
data() {
return {
item: {}
};
},
computed: {
...mapFields(['error', 'isLoading', 'created', 'violations'])
},
methods: {
...mapActions('coursecategory', ['create', 'reset'])
}
};
</script>

@ -0,0 +1,104 @@
<template>
<div class="coursecategory-list">
<Toolbar :handle-add="addHandler" />
<v-container grid-list-xl fluid>
<v-layout row wrap>
<v-flex sm12>
<h1>CourseCategory List</h1>
</v-flex>
<v-flex lg12>
<DataFilter :handle-filter="onSendFilter" :handle-reset="resetFilter">
<CourseCategoryFilterForm
ref="filterForm"
:values="filters"
slot="filter"
/>
</DataFilter>
<br />
<v-data-table
v-model="selected"
:headers="headers"
:items="items"
:items-per-page.sync="options.itemsPerPage"
:loading="isLoading"
:loading-text="$t('Loading...')"
:options.sync="options"
:server-items-length="totalItems"
class="elevation-1"
item-key="@id"
show-select
@update:options="onUpdateOptions"
>
<ActionCell
slot="item.action"
slot-scope="props"
:handle-show="() => showHandler(props.item)"
:handle-edit="() => editHandler(props.item)"
:handle-delete="() => deleteHandler(props.item)"
></ActionCell>
</v-data-table>
</v-flex>
</v-layout>
</v-container>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import ListMixin from '../../mixins/ListMixin';
import ActionCell from '../../components/ActionCell';
import CourseCategoryFilterForm from '../../components/coursecategory/Filter';
import DataFilter from '../../components/DataFilter';
import Toolbar from '../../components/Toolbar';
export default {
name: 'CourseCategoryList',
servicePrefix: 'CourseCategory',
mixins: [ListMixin],
components: {
Toolbar,
ActionCell,
CourseCategoryFilterForm,
DataFilter
},
data() {
return {
headers: [
{ text: 'name', value: 'name' },
{ text: 'code', value: 'code' },
//{ text: 'description', value: 'description' },
{
text: 'Actions',
value: 'action',
sortable: false
}
],
selected: []
};
},
computed: {
...mapGetters('coursecategory', {
items: 'list'
}),
...mapFields('coursecategory', {
deletedItem: 'deleted',
error: 'error',
isLoading: 'isLoading',
resetList: 'resetList',
totalItems: 'totalItems',
view: 'view'
})
},
methods: {
...mapActions('coursecategory', {
getPage: 'fetchAll',
deleteItem: 'del'
})
}
};
</script>

@ -0,0 +1,87 @@
<template>
<div>
<Toolbar :handle-edit="editHandler" :handle-delete="del">
<template slot="left">
<v-toolbar-title v-if="item">{{
`${$options.servicePrefix} ${item['@id']}`
}}</v-toolbar-title>
</template>
</Toolbar>
<br />
<div v-if="item" class="table-coursecategory-show">
<v-simple-table>
<template slot="default">
<thead>
<tr>
<th>Field</th>
<th>Value</th>
<th>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>{{ $t('name') }}</strong></td>
<td>
{{ item['name'] }}
</td>
<td><strong>{{ $t('code') }}</strong></td>
<td>
{{ item['code'] }}
</td>
</tr>
<tr>
<td><strong>{{ $t('description') }}</strong></td>
<td>
{{ item['description'] }}
</td>
<td></td>
</tr>
</tbody>
</template>
</v-simple-table>
</div>
<Loading :visible="isLoading" />
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import Loading from '../../components/Loading';
import ShowMixin from '../../mixins/ShowMixin';
import Toolbar from '../../components/Toolbar';
const servicePrefix = 'CourseCategory';
export default {
name: 'CourseCategoryShow',
servicePrefix,
components: {
Loading,
Toolbar
},
mixins: [ShowMixin],
computed: {
...mapFields('coursecategory', {
isLoading: 'isLoading'
}),
...mapGetters('coursecategory', ['find'])
},
methods: {
...mapActions('coursecategory', {
deleteItem: 'del',
reset: 'resetShow',
retrieve: 'load'
})
}
};
</script>

@ -0,0 +1,61 @@
<template>
<div>
<Toolbar
:handle-submit="onSendForm"
:handle-reset="resetForm"
:handle-delete="del"
/>
<CourseCategoryForm
ref="updateForm"
v-if="item"
:values="item"
:errors="violations"
/>
<Loading :visible="isLoading || deleteLoading" />
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import CourseCategoryForm from '../../components/coursecategory/Form.vue';
import Loading from '../../components/Loading';
import Toolbar from '../../components/Toolbar';
import UpdateMixin from '../../mixins/UpdateMixin';
const servicePrefix = 'CourseCategory';
export default {
name: 'CourseCategoryUpdate',
servicePrefix,
mixins: [UpdateMixin],
components: {
Loading,
Toolbar,
CourseCategoryForm
},
computed: {
...mapFields('coursecategory', {
deleteLoading: 'isLoading',
isLoading: 'isLoading',
error: 'error',
updated: 'updated',
violations: 'violations'
}),
...mapGetters('coursecategory', ['find'])
},
methods: {
...mapActions('coursecategory', {
createReset: 'resetCreate',
deleteItem: 'del',
delReset: 'resetDelete',
retrieve: 'load',
update: 'update',
updateReset: 'resetUpdate'
})
}
};
</script>

@ -0,0 +1,11 @@
module.exports = {
pluginOptions: {
quasar: {
importStrategy: 'manual',
rtlSupport: false
}
},
transpileDependencies: [
'quasar'
]
}

@ -3,31 +3,13 @@
"version": "2.0.0",
"description": "E-learning and collaboration software",
"license": "GPL-3.0",
"devDependencies": {
"@coreui/coreui": "^2.1.7",
"@fortawesome/fontawesome-free": "^5.11",
"@symfony/webpack-encore": "~0.28",
"babel-eslint": "^10.1.0",
"babel-preset-react": "^6.24.1",
"bootstrap": "^4.4",
"copy-webpack-plugin": "^5.0",
"eslint": "^6.8.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-vue": "^6.2.2",
"filemanager-webpack-plugin": "^2.0.5",
"free-jqgrid": "https://github.com/chamilo/jqGrid.git#bs4",
"jquery": "^3.5",
"node-sass": "^4.9",
"popper.js": "^1.14.7",
"sass-loader": "^7.0",
"vue": "^2.6",
"vue-loader": "^15.7.0",
"vue-template-compiler": "^2.6.10"
},
"dependencies": {
"@fancyapps/fancybox": "^3.5.7",
"apollo-boost": "^0.4.7",
"axios": "^0.19.2",
"babel-polyfill": "^6.26.0",
"babel": "^6.23.0",
"babel-plugin-transform-builtin-extend": "^1.1.2",
"babel-preset-env": "^1.7.0",
"blueimp-file-upload": "^10.0",
"bootstrap-daterangepicker": "^3.0",
"bootstrap-select": "^1.13.6",
@ -42,6 +24,7 @@
"flag-icon-css": "^3.0",
"fs": "0.0.1-security",
"fullcalendar": "^3.0",
"graphql": "^15.0.0",
"highlight.js": "^9.0",
"hljs": "^6.2",
"image-map-resizer": "^1.0.10",
@ -53,10 +36,11 @@
"js-cookie": "^2.2.0",
"jsplumb": "^2.12",
"linkifyjs": "^2.1",
"lodash": "^4.17.15",
"mathjax": "^2.7",
"mediaelement": "^4.2",
"mediaelement-plugins": "https://github.com/chamilo/mediaelement-plugins",
"moment": "^2.22",
"moment": "^2.25.3",
"multiselect-two-sides": "^2.5.5",
"mxgraph": "^4.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
@ -65,17 +49,54 @@
"pretty-checkbox": "^3.0.3",
"pwstrength-bootstrap": "^3.0.5",
"qtip2": "^3.0.3",
"@quasar/extras": "^1.0.0",
"quasar": "^1.10.5",
"readmore-js": "^2.2.1",
"router": "^1.3.5",
"select2": "^4.0",
"sweetalert2": "^9.5",
"textcomplete": "^0.18.1",
"timeago": "^1.6.7",
"timepicker": "^1.11.14",
"video.js": "^7.6.6",
"vue-apollo": "^3.0.3",
"vue-i18n": "^8.17.4",
"vue-router": "^3.1.6",
"vuelidate": "^0.7.5",
"vuetify": "^2.2.27",
"vue": "^2.6",
"vue-loader": "^15.7.0",
"vue-template-compiler": "^2.6.10",
"vuex": "^3.3.0",
"vuex-map-fields": "^1.4.0",
"webcamjs": "^1.0",
"webpack-jquery-ui": "^2.0.1",
"xcolor": "https://github.com/infusion/jQuery-xcolor"
},
"devDependencies": {
"@coreui/coreui": "^2.1.7",
"@fortawesome/fontawesome-free": "^5.11",
"@symfony/webpack-encore": "~0.28",
"babel-eslint": "^10.1.0",
"babel-preset-react": "^6.24.1",
"bootstrap": "^4.4",
"copy-webpack-plugin": "^5.0",
"deepmerge": "^4.2.2",
"eslint": "^6.8.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-vue": "^6.2.2",
"fibers": "^5.0.0",
"filemanager-webpack-plugin": "^2.0.5",
"free-jqgrid": "https://github.com/chamilo/jqGrid.git#bs4",
"jquery": "^3.5",
"node-sass": "^4.9",
"popper.js": "^1.14.7",
"sass": "^1.26.5",
"sass-loader": "^7.0.2",
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-eslint": "~4.3.0",
"@vue/cli-service": "~4.3.0",
"vue-cli-plugin-quasar": "~2.0.2",
"webpack-notifier": "^1.6.0"
}
}

@ -6,16 +6,16 @@ const FileManagerPlugin = require('filemanager-webpack-plugin');
Encore
.setOutputPath('public/build/')
.setManifestKeyPrefix('public/build/')
.setPublicPath('../')
// .setPublicPath('../')
.setPublicPath('/build')
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.addEntry('app', './assets/js/app.js')
.addEntry('vue', './assets/vue/main.js')
.addEntry('bootstrap', './assets/js/bootstrap.js')
.addEntry('exercise', './assets/js/exercise.js')
.addEntry('free-jqgrid', './assets/js/free-jqgrid.js')
// .addEntry('free-jqgrid', './assets/js/free-jqgrid.js')
.addStyleEntry('css/app', './assets/css/app.scss')
.addStyleEntry('css/bootstrap', './assets/css/bootstrap.scss')
@ -29,12 +29,21 @@ Encore
.addStyleEntry('css/scorm', './assets/css/scorm.css')
.enableSingleRuntimeChunk()
.enableIntegrityHashes()
.enableSourceMaps(!Encore.isProduction())
// .enableVersioning(Encore.isProduction())
.enableSassLoader()
.enableVueLoader()
.enableVueLoader(function(options) {
options.pluginOptions = {
quasar: {
importStrategy: 'manual',
rtlSupport: false
}
}
options.transpileDependencies = ['quasar'];
})
.autoProvidejQuery()
.copyFiles([
{
@ -63,6 +72,19 @@ Encore
to: 'libs/mathjax/MathJax.js'
},
])
// enable ESLint
// .addLoader({
// enforce: 'pre',
// test: /\.(js|vue)$/,
// loader: 'eslint-loader',
// exclude: /node_modules/,
// options: {
// fix: true,
// emitError: false,
// emitWarning: true,
//
// },
// })
;
Encore.addPlugin(new CopyWebpackPlugin([
@ -102,18 +124,18 @@ themes.forEach(function (theme) {
});
// Fix free-jqgrid languages files
Encore.addPlugin(new FileManagerPlugin({
onEnd: {
move: [
{
source: './public/public/build/free-jqgrid/',
destination: './public/build/free-jqgrid/'
}
],
delete: [
'./public/public/'
]
}
}));
module.exports = Encore.getWebpackConfig();
// Encore.addPlugin(new FileManagerPlugin({
// onEnd: {
// move: [
// {
// source: './public/public/build/free-jqgrid/',
// destination: './public/build/free-jqgrid/'
// }
// ],
// delete: [
// './public/public/'
// ]
// }
// }));
module.exports = Encore.getWebpackConfig();
Loading…
Cancel
Save