Pages: Add UI

pull/4020/head^2
Julio 4 years ago
parent acb1032ec3
commit 8144e803e1
  1. 151
      assets/vue/components/page/Form.vue
  2. 9
      assets/vue/components/page/Layout.vue
  3. 17
      assets/vue/main.js
  4. 34
      assets/vue/pages/Home.vue
  5. 4
      assets/vue/router/index.js
  6. 31
      assets/vue/router/page.js
  7. 3
      assets/vue/services/page.js
  8. 3
      assets/vue/services/pagecategory.js
  9. 45
      assets/vue/views/page/Create.vue
  10. 291
      assets/vue/views/page/List.vue
  11. 118
      assets/vue/views/page/Show.vue
  12. 56
      assets/vue/views/page/Update.vue

@ -0,0 +1,151 @@
<template>
<q-form>
<q-input
id="item_title"
v-model="item.title"
:placeholder="$t('Title')"
:error="v$.item.title.$error"
@input="v$.item.title.$touch()"
@blur="v$.item.title.$touch()"
:error-message="titleErrors"
/>
<div class="q-gutter-sm">
<q-checkbox v-model="item.enabled" :label="$t('Enabled')" />
</div>
<q-select v-model="item.category" :options="categories" :label="$t('Category')"
option-value="id"
option-label="title"
/>
<q-select v-model="item.locale" :options="locales" :label="$t('Locale')" />
<TinyEditor
id="item_content"
v-model="item.content"
required
:init="{
skin_url: '/build/libs/tinymce/skins/ui/oxide',
content_css: '/build/libs/tinymce/skins/content/default/content.css',
branding: false,
relative_urls: false,
height: 500,
toolbar_mode: 'sliding',
file_picker_callback : browser,
autosave_ask_before_unload: true,
plugins: [
'fullpage advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste wordcount '
],
toolbar: 'undo redo | bold italic underline strikethrough | insertfile image media template link | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | pagebreak | charmap emoticons | fullscreen preview save print | code codesample | ltr rtl | ' + extraPlugins,
}
"
/>
<slot></slot>
</q-form>
</template>
<script>
import has from 'lodash/has';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import {computed, ref} from "vue";
import {mapGetters, useStore} from "vuex";
import isEmpty from 'lodash/isEmpty';
import Dropdown from "primevue/dropdown";
export default {
name: 'PageForm',
components:{Dropdown},
setup () {
let locales = ref([]);
const store = useStore();
let categories = ref([]);
locales = window.languages.map(locale => locale.isocode);
let allCategories = store.dispatch('pagecategory/findAll');
allCategories.then((response) => {
categories.value = response.map(function(data) {
return data;
})
});
return { v$: useVuelidate(), locales, categories}
},
props: {
values: {
type: Object,
required: true
},
errors: {
type: Object,
default: () => {}
},
initialValues: {
type: Object,
default: () => {}
},
},
data() {
return {
title: null,
content: null,
locale: null,
enabled: null,
};
},
computed: {
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'currentUser': 'security/getUser',
}),
item() {
if (this.values) {
this.values.creator = this.currentUser['@id'];
this.values.url = '/api/access_urls/' + window.access_url_id;
if (!isEmpty(this.values.category)) {
this.values.category = this.values.category['@id'];
}
}
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);
if (this.v$.item.title.required) {
return this.$t('Field is required')
}
return errors;
},
violations() {
return this.errors || {};
}
},
validations: {
item: {
title: {
required,
},
content: {
required,
},
locale: {
required,
},
}
}
};
</script>

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

@ -21,6 +21,8 @@ import userGroupService from './services/usergroup';
import userRelUserService from './services/userreluser';
import calendarEventService from './services/ccalendarevent';
import toolIntroService from './services/ctoolintro';
import pageService from './services/page';
import pageCategoryService from './services/pagecategory';
import makeCrudModule from './store/modules/crud';
//import vuetify from './plugins/vuetify' // path to vuetify export
@ -76,6 +78,21 @@ store.registerModule(
})
);
store.registerModule(
'page',
makeCrudModule({
service: pageService
})
);
store.registerModule(
'pagecategory',
makeCrudModule({
service: pageCategoryService
})
);
store.registerModule(
'personalfile',
makeCrudModule({

@ -9,16 +9,35 @@
<p v-html="announcement.content" ></p>
</div>
</div>
<div v-if="pages"
class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mt-2"
>
<div
v-for="page in pages"
:key="page.id"
>
<v-card
elevation="2"
>
<v-card-text>
<p class="text-h5 text--primary">
{{ page.title }}
</p>
<p v-html="page.content"/>
</v-card-text>
</v-card>
</div>
</div>
</q-page>
</template>
<script>
import {useRouter} from "vue-router";
import {useStore} from "vuex";
import axios from "axios";
import {reactive, toRefs} from 'vue'
import {mapGetters} from "vuex";
import {ENTRYPOINT} from "../config/entrypoint";
export default {
@ -26,6 +45,7 @@ export default {
setup() {
const state = reactive({
announcements: [],
pages: [],
});
axios.get('/news/list').then(response => {
@ -36,6 +56,14 @@ export default {
console.log(error);
});
axios.get(ENTRYPOINT + 'pages.json?category.title=home').then(response => {
if (Array.isArray(response.data)) {
state.pages = response.data;
}
}).catch(function (error) {
console.log(error);
});
return toRefs(state);
}
}

@ -8,6 +8,7 @@ import userGroupRoutes from './usergroup';
import userRelUserRoutes from './userreluser';
import calendarEventRoutes from './ccalendarevent';
import toolIntroRoutes from './ctoolintro';
import pageRoutes from './page';
//import courseCategoryRoutes from './coursecategory';
import documents from './documents';
@ -120,7 +121,8 @@ const router = createRouter({
userGroupRoutes,
userRelUserRoutes,
calendarEventRoutes,
toolIntroRoutes
toolIntroRoutes,
pageRoutes
]
});

@ -0,0 +1,31 @@
export default {
path: '/resources/pages',
meta: { requiresAuth: true },
name: 'pages',
component: () => import('../components/page/Layout.vue'),
redirect: { name: 'PageList' },
children: [
{
name: 'PageList',
path: '',
component: () => import('../views/page/List.vue')
},
{
name: 'PageCreate',
path: 'new',
component: () => import('../views/page/Create.vue')
},
{
name: 'PageUpdate',
//path: ':id/edit',
path: 'edit',
component: () => import('../views/page/Update.vue')
},
{
name: 'PageShow',
//path: ':id',
path: 'show',
component: () => import('../views/page/Show.vue')
}
]
};

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

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

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

@ -0,0 +1,291 @@
<template>
<div v-if="isAdmin" class="q-card">
<div class="p-4 flex flex-row gap-1 mb-2">
<div class="flex flex-row gap-2" >
<!-- <Button class="btn btn-primary" @click="openNew">-->
<!-- <v-icon icon="mdi-folder-plus"/>-->
<!-- {{ $t('New category') }}-->
<!-- </Button>-->
<Button label="{{ $t('New page') }}" class="btn btn-primary" @click="addHandler()" >
<v-icon icon="mdi-file-plus"/>
{{ $t('New page') }}
</Button>
<Button label="{{ $t('Delete selected') }}" class="btn btn-danger " @click="confirmDeleteMultiple" :disabled="!selectedItems || !selectedItems.length">
<v-icon icon="mdi-delete"/>
{{ $t('Delete selected') }}
</Button>
</div>
</div>
</div>
<DataTable
v-if="isAdmin"
class="p-datatable-sm"
:value="items"
v-model:selection="selectedItems"
dataKey="iid"
v-model:filters="filters"
filterDisplay="menu"
:lazy="true"
:paginator="true"
:rows="10"
:totalRecords="totalItems"
:loading="isLoading"
@page="onPage($event)"
@sort="sortingChanged($event)"
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
:rowsPerPageOptions="[5, 10, 20, 50]"
responsiveLayout="scroll"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
>
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
<Column field="title" :header="$t('Title')" :sortable="true">
<template #body="slotProps">
<a
v-if="slotProps.data"
@click="handleClick(slotProps.data)"
class="cursor-pointer "
>
{{ slotProps.data.title }}
</a>
</template>
</Column>
<Column field="locale" :header="$t('Locale')" />
<Column field="category.title" :header="$t('Category')" />
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<!-- <Button class="btn btn-primary" @click="showHandler(slotProps.data)">-->
<!-- <v-icon icon="mdi-information"/>-->
<!-- </Button>-->
<Button v-if="isAuthenticated" class="btn btn-primary p-mr-2" @click="editHandler(slotProps.data)">
<v-icon icon="mdi-pencil"/>
</Button>
<Button v-if="isAuthenticated" class="btn btn-danger" @click="confirmDeleteItem(slotProps.data)" >
<v-icon icon="mdi-delete"/>
</Button>
</div>
</template>
</Column>
</DataTable>
<Dialog v-model:visible="itemDialog" :style="{width: '450px'}" :header="$t('New folder')" :modal="true" class="p-fluid">
<div class="p-field">
<label for="name">{{ $t('Name') }}</label>
<InputText
autocomplete="off"
id="title"
v-model.trim="item.title"
required="true"
autofocus
:class="{'p-invalid': submitted && !item.title}"
/>
<small class="p-error" v-if="submitted && !item.title">$t('Title is required')</small>
</div>
<template #footer>
<Button label="Cancel" icon="pi pi-times" class="p-button-text" @click="hideDialog"/>
<Button label="Save" icon="pi pi-check" class="p-button-text" @click="saveItem" />
</template>
</Dialog>
<Dialog v-model:visible="deleteItemDialog" :style="{width: '450px'}" header="Confirm" :modal="true">
<div class="confirmation-content">
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem" />
<span v-if="item">Are you sure you want to delete <b>{{item.title}}</b>?</span>
</div>
<template #footer>
<Button label="No" icon="pi pi-times" class="p-button-text" @click="deleteItemDialog = false"/>
<Button label="Yes" icon="pi pi-check" class="p-button-text" @click="deleteItemButton" />
</template>
</Dialog>
<Dialog v-model:visible="deleteMultipleDialog" :style="{width: '450px'}" header="Confirm" :modal="true">
<div class="confirmation-content">
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem" />
<span v-if="item">{{ $t('Are you sure you want to delete the selected items?') }}</span>
</div>
<template #footer>
<Button label="No" icon="pi pi-times" class="p-button-text" @click="deleteMultipleDialog = false"/>
<Button label="Yes" icon="pi pi-check" class="p-button-text" @click="deleteMultipleItems" />
</template>
</Dialog>
</template>
<script>
import {mapActions, mapGetters, useStore} from 'vuex';
import { mapFields } from 'vuex-map-fields';
import ListMixin from '../../mixins/ListMixin';
import {useRoute, useRouter} from "vue-router";
export default {
name: 'PageList',
servicePrefix: 'Page',
mixins: [ListMixin],
components: {
},
setup() {
const store = useStore();
const route = useRoute();
},
data() {
return {
sortBy: 'title',
sortDesc: false,
columns: [
{ label: this.$i18n.t('Title'), field: 'title', name: 'title', sortable: true},
{ label: this.$i18n.t('Category'), field: 'category.title', name: 'category', sortable: true},
{ label: this.$i18n.t('Locale'), field: 'locale', name: 'locale', sortable: true},
{ label: this.$i18n.t('Modified'), field: 'updatedAt', name: 'updatedAt', sortable: true},
{ label: this.$i18n.t('Actions'), name: 'action', sortable: false}
],
pageOptions: [10, 20, 50, this.$i18n.t('All')],
selected: [],
isBusy: false,
options: [],
selectedItems: [],
// prime vue
itemDialog: false,
deleteItemDialog: false,
deleteMultipleDialog: false,
item: {},
submitted: false,
};
},
mounted() {
this.onUpdateOptions(this.options);
},
deletedPage(item) {
this.showMessage(this.$i18n.t('{resource} deleted', {'resource': item['resourceNode'].title}));
this.onUpdateOptions(this.options);
},
computed: {
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'isAdmin': 'security/isAdmin',
'isCurrentTeacher': 'security/isCurrentTeacher',
}),
...mapGetters('page', {
items: 'list',
}),
//...getters
// From ListMixin
...mapFields('page', {
deletedPage: 'deleted',
error: 'error',
isLoading: 'isLoading',
resetList: 'resetList',
totalItems: 'totalItems',
view: 'view'
}),
},
methods: {
// prime
onPage(event) {
this.options.itemsPerPage = event.rows;
this.options.page = event.page + 1;
this.options.sortBy = event.sortField;
this.options.sortDesc = event.sortOrder === -1;
this.onUpdateOptions(this.options);
},
sortingChanged(event) {
this.options.sortBy = event.sortField;
this.options.sortDesc = event.sortOrder === -1;
this.onUpdateOptions(this.options);
// ctx.sortBy ==> Field key for sorting by (or null for no sorting)
// ctx.sortDesc ==> true if sorting descending, false otherwise
},
openNew() {
this.item = {};
this.submitted = false;
this.itemDialog = true;
},
hideDialog() {
this.itemDialog = false;
this.submitted = false;
},
saveItem() {
this.submitted = true;
if (this.item.title.trim()) {
if (this.item.id) {
} else {
// this.item.creator
this.createCategory(this.item);
this.showMessage('Saved');
}
this.itemDialog = false;
this.item = {};
}
},
editItem(item) {
this.item = {...item};
this.itemDialog = true;
},
confirmDeleteItem(item) {
this.item = item;
this.deleteItemDialog = true;
},
confirmDeleteMultiple() {
this.deleteMultipleDialog = true;
},
deleteMultipleItems() {
console.log('deleteMultipleItems');
console.log(this.selectedItems);
this.deleteMultipleAction(this.selectedItems);
this.onRequest({
pagination: this.pagination,
});
this.deleteMultipleDialog = false;
this.selectedItems = null;
//this.$toast.add({severity:'success', summary: 'Successful', detail: 'Products Deleted', life: 3000});*/
},
deleteItemButton() {
console.log('deleteItem');
this.deleteItem(this.item);
//this.items = this.items.filter(val => val.iid !== this.item.iid);
this.deleteItemDialog = false;
this.item = {};
this.onUpdateOptions(this.options);
},
onRowSelected(items) {
this.selected = items
},
selectAllRows() {
this.$refs.selectableTable.selectAllRows()
},
clearSelected() {
this.$refs.selectableTable.clearSelected()
},
async deleteSelected() {
console.log('deleteSelected');
this.deleteMultipleAction(this.selected);
this.onRequest({
pagination: this.pagination,
});
},
//...actions,
// From ListMixin
...mapActions('page', {
getPage: 'fetchAll',
createWithFormData: 'createWithFormData',
deleteItem: 'del',
deleteMultipleAction: 'delMultiple'
}),
/*...mapActions('pagecategory', {
getCategories: 'fetchAll',
createCategory: 'create',
deleteItem: 'del',
deleteMultipleAction: 'delMultiple'
}),*/
}
};
</script>

@ -0,0 +1,118 @@
<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.vue';
import ShowMixin from '../../mixins/ShowMixin';
import Toolbar from '../../components/Toolbar.vue';
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,56 @@
<template>
<Toolbar :handle-submit="onSendForm" :handle-reset="resetForm"></Toolbar>
<PageForm ref="updateForm" :values="item" :errors="violations" />
<Loading :visible="isLoading" />
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import Loading from '../../components/Loading.vue';
import Toolbar from '../../components/Toolbar.vue';
import UpdateMixin from '../../mixins/UpdateMixin';
const servicePrefix = 'Page';
import PageForm from '../../components/page/Form.vue';
import useVuelidate from "@vuelidate/core";
export default {
name: 'PageUpdate',
servicePrefix,
mixins: [UpdateMixin],
setup () {
return { v$: useVuelidate() }
},
components: {
Loading,
Toolbar,
PageForm
},
computed: {
...mapFields('page', {
deleteLoading: 'isLoading',
isLoading: 'isLoading',
error: 'error',
updated: 'updated',
violations: 'violations'
}),
...mapGetters('page', ['find'])
},
methods: {
...mapActions('page', {
createReset: 'resetCreate',
deleteItem: 'del',
delReset: 'resetDelete',
retrieve: 'load',
update: 'update',
updateReset: 'resetUpdate'
})
}
};
</script>
Loading…
Cancel
Save