- Complete GGZ Ecademy Nuxt.js user portal - Learning products browser and management - Member management interface - User authentication and roles - Multi-language support (NL/EN) - Vuex store for state management - Component-based architecture
This commit is contained in:
505
components/Learning/CoursesTable.vue
Normal file
505
components/Learning/CoursesTable.vue
Normal file
@@ -0,0 +1,505 @@
|
||||
<template>
|
||||
<div>
|
||||
<a target="_blank" href="/" id="root_url" v-show="false"></a>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:options="coursesTableOptions"
|
||||
:items="products"
|
||||
:footer-props="{ itemsPerPageOptions: [10, 25, 50, 100, -1] }"
|
||||
@update:options="handleChangesOnOptions"
|
||||
class="pa-4 secondary"
|
||||
>
|
||||
<!-- Translates dynamically headers -->
|
||||
<template v-for="h in headers" v-slot:[`header.${h.value}`]="{ header }">
|
||||
{{ $t(h.text) }}
|
||||
|
||||
<div
|
||||
v-if="h.value === 'actions'"
|
||||
:key="`column-browser-${h.text}`"
|
||||
class="text-center"
|
||||
>
|
||||
<browser-columns />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.cover="{ item }">
|
||||
<v-img
|
||||
:alt="item.title"
|
||||
lazy-src="/images/product-placeholder.jpg"
|
||||
:src="computeImage(item)"
|
||||
contain
|
||||
height="60px"
|
||||
width="110px"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template class="d-flex flex-column" v-slot:item.title="{ item }">
|
||||
<div>
|
||||
<span>
|
||||
<span
|
||||
v-if="!item.published && !item.deleted_at"
|
||||
class="info--text font-weight-thin font-italic"
|
||||
>{{ $t('general.draft') | capitalize }}</span
|
||||
>
|
||||
<span v-if="item.deleted_at" class="error--text font-italic">{{
|
||||
$t('general.deleted') | capitalize
|
||||
}}</span>
|
||||
</span>
|
||||
<span>
|
||||
<filter-items-resolver
|
||||
filterTitle="category"
|
||||
:filters="item.filters"
|
||||
/>
|
||||
|
||||
<filter-items-resolver gratisMode :filters="item.filters" />
|
||||
</span>
|
||||
</div>
|
||||
<strong>{{ item.title | truncate(29) }}</strong>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<filter-items-resolver filterTitle="status" :filters="item.filters" />
|
||||
</template>
|
||||
|
||||
<template v-slot:item.product_type="{ item }">
|
||||
<filter-items-resolver
|
||||
filterTitle="product_type"
|
||||
:filters="item.filters"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.course="{ item }">
|
||||
<filter-items-resolver filterTitle="course" :filters="item.filters" />
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
class="mx-4 white--text"
|
||||
style="height: 100%"
|
||||
:color="$vuetify.theme.dark ? 'info' : 'txt'"
|
||||
rounded
|
||||
depressed
|
||||
nuxt
|
||||
small
|
||||
:to="localePath(`/manager/learning/${item.slug}`)"
|
||||
>{{ $t('general.view') }}</v-btn
|
||||
>
|
||||
|
||||
<v-menu
|
||||
offset-y
|
||||
close-on-content-click
|
||||
v-if="$store.getters.isAdmin || $store.getters.isOperator"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-hover v-slot:default="{ hover }">
|
||||
<v-btn
|
||||
:color="hover ? 'info' : ''"
|
||||
:outlined="hover"
|
||||
depressed
|
||||
fab
|
||||
small
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
class="menu-btn"
|
||||
>
|
||||
<v-icon>icon-options</v-icon>
|
||||
</v-btn>
|
||||
</v-hover>
|
||||
</template>
|
||||
<v-list width="200">
|
||||
<v-list-item
|
||||
:to="localePath(`/manager/learning/${item.slug}?edit`)"
|
||||
nuxt
|
||||
>
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>icon-edit</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>{{
|
||||
$t('general.edit') | capitalize
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="clone(item.id)" v-if="publishedSelected">
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>icon-duplicate</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>{{
|
||||
$t('general.duplicate') | capitalize
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<!-- <v-list-item>
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>icon-download</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>{{
|
||||
$t('general.download') | capitalize
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
@click="shareUrl(localePath(`/manager/learning/${item.slug}`))"
|
||||
>
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>icon-share</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>{{
|
||||
$t('general.share') | capitalize
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item> -->
|
||||
|
||||
<v-dialog max-width="740" persistent v-model="dialog">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-list-item v-on="on">
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon small>icon-remove</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>{{
|
||||
$t('general.delete') | capitalize
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-card class="primary pa-10" flat>
|
||||
<v-card-title class="headline">
|
||||
{{
|
||||
$t('learning.product_overview.delete_confirmation', {
|
||||
productName: item.title,
|
||||
})
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<div class="ma-4">
|
||||
<v-btn
|
||||
@click="deleteProduct(item.id)"
|
||||
class="mx-2"
|
||||
color="accent"
|
||||
depressed
|
||||
rounded
|
||||
>{{ $t('general.delete') }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
@click="close"
|
||||
class="mx-2"
|
||||
color="info"
|
||||
depressed
|
||||
rounded
|
||||
>{{ $t('general.cancel') }}</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue2Filters from 'vue2-filters'
|
||||
import FilterItemsResolver from '~/components/Learning/FilterItemsResolver'
|
||||
import BrowserColumns from '~/components/Learning/BrowserColumns'
|
||||
|
||||
export default {
|
||||
mixins: [Vue2Filters.mixin],
|
||||
props: {
|
||||
published: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
drafts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
deleted: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selector: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
FilterItemsResolver,
|
||||
BrowserColumns,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
page: 1,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close()
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
noImage() {
|
||||
return require(`@/assets/img/no_image.png`)
|
||||
},
|
||||
headers() {
|
||||
return this.$store.getters.columnsSortedSubset
|
||||
},
|
||||
columns() {
|
||||
return this.$store.state.learning.columns
|
||||
},
|
||||
products() {
|
||||
if (!['published', 'drafts', 'deleted'].includes(this.selector)) return []
|
||||
return this[this.selector]
|
||||
},
|
||||
publishedSelected() {
|
||||
return this.selector === 'published'
|
||||
},
|
||||
draftsSelected() {
|
||||
return this.selector === 'drafts'
|
||||
},
|
||||
deletedSelected() {
|
||||
return this.selector === 'deleted'
|
||||
},
|
||||
coursesTableOptions() {
|
||||
return this.$store.state.learning.coursesTableOptions
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
// If cols preferences saved in localstorage
|
||||
if (localStorage.getItem('learning_products_cols')) {
|
||||
// Get them
|
||||
const colsLocalPreferences = JSON.parse(
|
||||
localStorage.getItem('learning_products_cols')
|
||||
)
|
||||
|
||||
// Local copy and extract values to sort columns
|
||||
let colsRestored = [...this.columns]
|
||||
const colsLocalPreferencesValues = colsLocalPreferences.map(
|
||||
({ value }) => value
|
||||
)
|
||||
|
||||
// Sort by local preferences
|
||||
colsRestored.sort(
|
||||
(a, b) =>
|
||||
colsLocalPreferencesValues.indexOf(a.value) -
|
||||
colsLocalPreferencesValues.indexOf(b.value)
|
||||
)
|
||||
|
||||
// update visibility by local preferences
|
||||
colsLocalPreferences.forEach((col) => {
|
||||
if (col.hasOwnProperty('display')) {
|
||||
const colIndex = colsRestored.findIndex(
|
||||
(el) => el.value === col.value
|
||||
)
|
||||
|
||||
colsRestored[colIndex] = {
|
||||
...colsRestored[colIndex],
|
||||
display: col.display,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Send to Store
|
||||
await this.$store.dispatch('learning/setColumns', colsRestored)
|
||||
}
|
||||
|
||||
// Initialize columns ordered
|
||||
if (this.$store.state.learning.columnsSorted.length <= 0) {
|
||||
await this.$store.dispatch('learning/setColumns')
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
// Get datatable options from localstorage if present
|
||||
if (
|
||||
localStorage.getItem(
|
||||
`courses_table_options_user${this.$store.getters.loggedInUser.id || ''}`
|
||||
)
|
||||
) {
|
||||
const options = JSON.parse(
|
||||
localStorage.getItem(
|
||||
`courses_table_options_user${
|
||||
this.$store.getters.loggedInUser.id || ''
|
||||
}`
|
||||
)
|
||||
)
|
||||
|
||||
this.$store.commit('learning/SET_COURSES_TABLE_OPTIONS', options)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
save() {
|
||||
this.close()
|
||||
},
|
||||
|
||||
computeImage(item) {
|
||||
if (item.cover.full) return item.cover.full
|
||||
if (item.video) return '/images/video-placeholder.jpg'
|
||||
return '/images/product-placeholder.jpg'
|
||||
},
|
||||
|
||||
async clone(productId) {
|
||||
if (!productId) {
|
||||
this.$notifier.showMessage({
|
||||
content: `No product to clone selected`,
|
||||
color: 'error',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
const response = await this.$axios.post('/learning-products/clone', {
|
||||
product_id: productId,
|
||||
})
|
||||
this.$emit('reload-learning-products')
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Product cloned in Drafts`,
|
||||
color: 'success',
|
||||
icon: 'icon-message',
|
||||
})
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
} catch (error) {
|
||||
console.log('clone -> error', error)
|
||||
this.$nuxt.$loading.finish()
|
||||
this.$notifier.showMessage({
|
||||
content: `Error trying to clone the selected product`,
|
||||
color: 'error',
|
||||
icon: 'mdi-delete',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async deleteProduct(productId) {
|
||||
if (!productId) return
|
||||
this.dialog = false
|
||||
this.$nextTick(() => this.$nuxt.$loading.start())
|
||||
await this.$store.dispatch('learning/deleteProduct', productId)
|
||||
this.$router.push(this.localePath('/manager/learning'))
|
||||
},
|
||||
|
||||
async shareUrl(url) {
|
||||
if (!url) return
|
||||
|
||||
const root_url = document.getElementById('root_url').href
|
||||
|
||||
// Remove last slash from root url
|
||||
const full_url = `${root_url.slice(0, -1)}${url}`
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(full_url)
|
||||
|
||||
this.$notifier.showMessage({
|
||||
content: `Url Copied in clipboard `,
|
||||
color: 'success',
|
||||
icon: 'mdi-check',
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('shareUrl -> error', error)
|
||||
}
|
||||
},
|
||||
|
||||
async handleChangesOnOptions(options) {
|
||||
this.$store.commit('learning/SET_COURSES_TABLE_OPTIONS', options)
|
||||
await localStorage.setItem(
|
||||
`courses_table_options_user${
|
||||
this.$store.getters.loggedInUser.id || ''
|
||||
}`,
|
||||
JSON.stringify(options)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table tr a.v-btn {
|
||||
opacity: 0;
|
||||
}
|
||||
table tr:hover a.v-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
.v-list-item:hover >>> .v-list-item__icon i,
|
||||
.v-list-item:hover >>> .v-list-item__content .v-list-item__subtitle {
|
||||
color: var(--v-info-base) !important;
|
||||
}
|
||||
.v-data-table >>> .v-data-table-header th:last-child,
|
||||
.v-data-table >>> .v-data-table-header th:nth-child(3) {
|
||||
position: relative;
|
||||
}
|
||||
.v-data-table >>> .v-data-table-header th:last-child::before,
|
||||
.v-data-table >>> .v-data-table-header th:nth-child(3)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
height: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
.v-data-table >>> .v-data-table-header tr th {
|
||||
border-bottom: none !important;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.v-data-table td .menu-btn {
|
||||
background-color: unset !important;
|
||||
}
|
||||
.v-data-table >>> td {
|
||||
color: var(--v-txt-base) !important;
|
||||
}
|
||||
.v-data-table >>> thead th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.v-data-table >>> td:first-child {
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
.v-data-table >>> td:last-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.v-data-table >>> td:first-child .v-image {
|
||||
margin-right: -18px;
|
||||
}
|
||||
.v-data-table >>> td:last-child,
|
||||
.v-data-table >>> td:nth-child(3) {
|
||||
position: relative;
|
||||
}
|
||||
.v-data-table >>> td:last-child::before,
|
||||
.v-data-table >>> td:nth-child(3)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
height: 60px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
.v-data-table >>> .v-data-table__divider {
|
||||
border-right: none !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user