<template> <div class="model-crud" :class="getModelSlug()"> <nav class="breadcrumb" aria-label="breadcrumbs"> <ul> <li> <router-link :to="getModelPathPrefix() + '/'">{{ getModelIndexTitle() }}</router-link> </li> <li v-if="isRow"> <router-link :to="getModelPathPrefix() + '/' + record._parent_uuid">{{ renderParentHeaderLabel(record) }}</router-link> <!-- {{ renderParentHeaderLabel(record) }} --> </li> <li v-if="mode == 'creating'"> New </li> <li v-if="mode == 'viewing'"> {{ renderHeaderLabel(record) }} </li> <li v-if="mode == 'editing' || mode == 'executing' || mode == 'deleting'"> <router-link :to="getViewURL()"> {{ renderHeaderLabel(record) }} </router-link> </li> <li v-if="mode == 'editing'"> Edit </li> <li v-if="mode == 'executing'"> Execute </li> <li v-if="mode == 'deleting'"> Delete </li> </ul> </nav> <slot></slot> <div v-if="showButtons" class="buttons"> <b-button type="is-primary" :icon-left="saveButtonIcon" :disabled="saveDisabled" @click="save()"> {{ saveButtonText }} </b-button> <b-button v-if="mode == 'creating'" tag="router-link" :to="getModelPathPrefix() + '/'"> Cancel </b-button> <b-button v-if="mode == 'editing'" tag="router-link" :to="getViewURL()"> Cancel </b-button> </div> <div v-if="shouldAllowEdit() || shouldAllowDelete()" class="buttons"> <br /><br /> <b-button v-if="shouldAllowEdit()" type="is-primary" icon-left="edit" tag="router-link" :to="getEditURL()"> Edit This </b-button> <b-button v-if="shouldAllowDelete() && !quickDelete" type="is-danger" icon-left="trash" tag="router-link" :to="getDeleteURL()"> Delete This </b-button> <b-button v-if="shouldAllowDelete() && quickDelete" type="is-danger" icon-left="trash" @click="$emit('delete')"> Delete This </b-button> </div> <slot name="quick-entry"></slot> <div v-if="hasRows && mode == 'viewing'"> <slot name="row-filters"></slot> <b-menu> <b-menu-list> <b-menu-item v-for="row in rowData.data" :key="row.uuid" tag="router-link" :to="getRowRoute(row)"> <template slot="label" slot-scope="props"> <span v-html="renderRowLabel(row)"></span> </template> </b-menu-item> </b-menu-list> </b-menu> <b-pagination v-if="rowsPaginated" :total="rowData.total" :current.sync="rowPage" :per-page="rowsPerPage" @change="changeRowPagination" > <!-- icon-pack="fas" --> </b-pagination> </div> <slot name="footer"></slot> </div> </template> <script> export default { name: 'ByjoveModelCrud', props: { mode: String, modelName: String, modelSlug: String, modelTitle: String, modelTitlePlural: String, modelIndexTitle: String, modelPermissionPrefix: String, modelPathPrefix: String, modelRoutePrefix: String, apiIndexUrl: String, apiObjectUrl: String, labelRenderer: Function, parentHeaderLabelRenderer: Function, headerLabelRenderer: Function, rowLabelRenderer: Function, hasRows: { type: Boolean, default: false, }, rowsPaginated: { type: Boolean, default: true, }, rowsPerPage: { type: Number, default: 20, }, rowRouteGetter: Function, apiRowsUrl: String, isRow: { type: Boolean, default: false, }, rowPathPrefix: String, rowFilters: { type: Function, default: (uuid) => { return JSON.stringify([{field: 'batch_uuid', op: 'eq', value: uuid}]) }, }, allowEdit: { type: Boolean, default: true, }, allowDelete: { type: Boolean, default: false, }, quickDelete: { type: Boolean, default: false, }, hideButtons: { type: Boolean, default: false, }, saveDisabled: { type: Boolean, default: false, }, saveButtonText: { type: String, default: "Save Data", }, saveButtonIcon: { type: String, default: 'save', }, }, data: function() { return { record: {}, rowData: {}, rowPage: 1, } }, computed: { showButtons: function() { if (this.hideButtons) { return false } if (this.mode == 'creating') { return true } if (this.mode == 'editing') { return true } return false }, }, // TODO: why doesn't beforeRouteUpdate() work instead? // cf. https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes watch: { '$route' (to, from) { if (to.name == (this.getModelRoutePrefix() + '.edit') && !this.hasModelPerm('edit')) { this.$buefy.toast.open({ message: "You do not have permission to access that page.", type: 'is-danger', position: 'is-bottom', }) this.$router.push(this.getModelPathPrefix() + '/') } // re-fetch record in case it was just changed if (to.name != (this.getModelRoutePrefix() + '.new')) { this.fetch(to.params.uuid) } }, }, // beforeRouteUpdate (to, from, next) { // // re-fetch record in case it was just changed // if (to.name != (this.getModelRoutePrefix() + '.new')) { // this.fetch(to.params.uuid) // } // next() // }, mounted() { // TODO: this seems like a "good" idea, but in practice, when reloading // the page/app via browser (Ctrl+Shift+R), the app must re-fetch the // session details before it knows which user/permissions are in // effect, and that takes "too long" which means these checks fail! // // redirect if user doesn't have permission to be here // if ((this.mode == 'viewing' && !this.hasModelPerm('view')) // || (this.mode == 'creating' && !this.hasModelPerm('create')) // || (this.mode == 'editing' && !this.hasModelPerm('edit')) // || (this.mode == 'deleting' && !this.hasModelPerm('delete'))) { // this.$buefy.toast.open({ // message: "You do not have permission to access that page.", // type: 'is-danger', // position: 'is-bottom', // }) // this.$router.push(this.getModelPathPrefix() + '/') // return // } // fetch initial page data unless 'creating' if (this.mode != 'creating') { this.fetch(this.$route.params.uuid) } }, methods: { getModelSlug() { if (this.modelSlug) { return this.modelSlug } return this.modelName.toLowerCase() + 's' }, getModelTitle() { if (this.modelTitle) { return this.modelTitle } return this.modelName }, getModelTitlePlural() { if (this.modelTitlePlural) { return this.modelTitlePlural } return this.getModelTitle() + 's' }, getModelIndexTitle() { if (this.modelIndexTitle) { return this.modelIndexTitle } return this.getModelTitlePlural() }, getModelPathPrefix() { if (this.modelPathPrefix) { return this.modelPathPrefix } return '/' + this.getModelSlug() }, getModelRoutePrefix() { if (this.modelRoutePrefix) { return this.modelRoutePrefix } return this.getModelSlug() }, getRowPathPrefix() { if (this.rowPathPrefix) { return this.rowPathPrefix } return '/' + this.getModelSlug() + '/rows' }, getViewURL() { if (this.isRow) { return `${this.getRowPathPrefix()}/${this.record.uuid}` } return `${this.getModelPathPrefix()}/${this.record.uuid}` }, getEditURL() { if (this.isRow) { return `${this.getRowPathPrefix()}/${this.record.uuid}/edit` } return `${this.getModelPathPrefix()}/${this.record.uuid}/edit` }, getDeleteURL() { if (this.isRow) { return `${this.getRowPathPrefix()}/${this.record.uuid}/delete` } return `${this.getModelPathPrefix()}/${this.record.uuid}/delete` }, getApiIndexUrl() { if (this.apiIndexUrl) { return this.apiIndexUrl } return '/api/' + this.getModelSlug() }, getApiObjectUrl() { if (this.apiObjectUrl) { return this.apiObjectUrl } let url = this.getApiIndexUrl() // drop trailing 's' then add slash return url.slice(0, -1) + '/' }, getApiRowsUrl() { return this.apiRowsUrl }, getModelPermissionPrefix() { if (this.modelPermissionPrefix) { return this.modelPermissionPrefix } return this.getModelSlug() }, hasPerm(perm) { // if user is root then assume permission if (this.$store.state.user && this.$store.state.user.is_root) { return true } // otherwise do true perm check for user return this.$store.state.permissions.includes(perm) }, hasModelPerm(perm) { // do normal check, but first add prefix let prefix = this.getModelPermissionPrefix() return this.hasPerm(prefix + '.' + perm) }, renderLabel(obj) { if (this.labelRenderer) { return this.labelRenderer(obj) } return obj._str }, renderHeaderLabel(obj) { if (this.headerLabelRenderer) { return this.headerLabelRenderer(obj) } return this.renderLabel(obj) }, renderParentHeaderLabel(obj) { if (this.parentHeaderLabelRenderer) { return this.parentHeaderLabelRenderer(obj) } return this.renderLabel(obj) }, renderRowLabel(row) { if (this.rowLabelRenderer) { return this.rowLabelRenderer(row) } return row._str }, fetch(uuid) { this.$http.get(this.getApiObjectUrl() + uuid).then(response => { this.record = response.data.data this.$emit('refresh', this.record) if (this.hasRows) { this.fetchRows(uuid) } }, response => { if (response.status == 403) { // forbidden; redirect to model index this.$buefy.toast.open({ message: "You do not have permission to access that page.", type: 'is-danger', position: 'is-bottom', }) this.$router.push(this.getModelPathPrefix() + '/') } else { this.$buefy.toast.open({ message: "Failed to fetch page data!", type: 'is-danger', position: 'is-bottom', }) } }) }, fetchRows(uuid) { let params = { filters: this.rowFilters(uuid), orderBy: 'modified', ascending: 0, } if (this.rowsPaginated) { params.per_page = this.rowsPerPage params.page = this.rowPage } this.$http.get(this.getApiRowsUrl(), {params: params}).then(response => { this.rowData = response.data }, response => { if (response.status == 403) { // forbidden; redirect to home page this.$buefy.toast.open({ message: "You do not have permission to access that page.", type: 'is-danger', position: 'is-bottom', }) this.$router.push('/') } else { this.$buefy.toast.open({ message: "Failed to fetch page data!", type: 'is-danger', position: 'is-bottom', }) } }) }, getRowRoute(row) { if (this.rowRouteGetter) { return this.rowRouteGetter(row) } return this.getRowPathPrefix() + '/' + row.uuid }, changeRowPagination(value) { this.fetchRows(this.record.uuid) }, shouldAllowEdit() { if (!this.allowEdit) { return false } if (this.mode != 'viewing') { return false } if (!this.hasModelPerm('edit')) { return false } return true }, shouldAllowDelete() { if (!this.allowDelete) { return false } if (this.mode == 'creating' || this.mode == 'deleting') { return false } if (!this.hasModelPerm('delete')) { return false } return true }, save() { let url = this.getApiIndexUrl() if (this.mode != 'creating') { url = this.getApiObjectUrl() + this.record.uuid } this.$emit('save', url) }, }, } </script>