byjove/src/components/model-crud/ByjoveModelCrud.vue

691 lines
20 KiB
Vue
Raw Normal View History

2019-11-06 20:18:07 -06:00
<template>
<div class="model-crud" :class="getModelSlug()">
2019-11-06 20:18:07 -06:00
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="getModelPathPrefix() + '/'">{{ getModelIndexTitle() }}</router-link>
</li>
<li v-if="isRow">
<router-link :to="getParentUrl()">{{ renderParentHeaderLabel(record) }}</router-link>
2019-11-06 20:18:07 -06:00
<!-- &nbsp; {{ renderParentHeaderLabel(record) }} -->
</li>
<li v-if="mode == 'creating'">
&nbsp; New
</li>
<li v-if="mode == 'viewing'">
&nbsp; {{ renderHeaderLabel(record) }}
</li>
<li v-if="mode == 'editing' || mode == 'executing' || mode == 'deleting'">
<router-link :to="getViewURL()">
{{ renderHeaderLabel(record) }}
</router-link>
2019-11-06 20:18:07 -06:00
</li>
<li v-if="mode == 'editing'">
&nbsp; Edit
</li>
<li v-if="mode == 'executing'">
&nbsp; Execute
</li>
2019-11-06 20:18:07 -06:00
<li v-if="mode == 'deleting'">
&nbsp; Delete
</li>
</ul>
</nav>
<slot></slot>
<div v-if="showButtons"
class="buttons" style="margin-top: 1rem;">
<b-button :type="getSaveButtonType()"
v-if="!hideSaveButton"
:icon-left="getSaveButtonIcon()"
2019-11-06 20:18:07 -06:00
:disabled="saveDisabled"
@click="save()">
{{ getSaveButtonText() }}
2019-11-06 20:18:07 -06:00
</b-button>
<b-button tag="router-link"
:to="getCancelURL()">
2019-11-06 20:18:07 -06:00
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 {{ getModelTitle() }}
</b-button>
<b-button v-if="shouldAllowDelete() && !quickDelete"
type="is-danger"
icon-left="trash"
tag="router-link"
:to="getDeleteURL()">
Delete this {{ getModelTitle() }}
</b-button>
<b-button v-if="shouldAllowDelete() && quickDelete"
type="is-danger"
icon-left="trash"
@click="$emit('delete')">
Delete this {{ getModelTitle() }}
</b-button>
</div>
<slot name="quick-entry"></slot>
2019-11-06 20:18:07 -06:00
<div v-if="hasRows && mode == 'viewing'">
<div v-if="rowsTitle && shouldAllowCreateRow()"
style="display: flex;">
<h2 style="flex-grow: 1;">{{ rowsTitle }}</h2>
<b-button type="is-primary"
tag="router-link"
:to="getCreateRowButtonUrl()"
icon-left="plus">
{{ createRowButtonText }}
</b-button>
</div>
<h2 v-if="rowsTitle && !shouldAllowCreateRow()">
{{ rowsTitle }}
</h2>
<b-button v-if="shouldAllowCreateRow() && !rowsTitle"
type="is-primary"
tag="router-link"
:to="getCreateRowButtonUrl()"
icon-left="plus">
{{ createRowButtonText }}
</b-button>
<slot name="row-filters"></slot>
2019-11-06 20:18:07 -06:00
<b-menu>
<b-menu-list>
<b-menu-item v-for="row in rowData.data"
2019-11-06 20:18:07 -06:00
:key="row.uuid"
tag="router-link"
:to="getRowRoute(row)">
2019-11-06 20:18:07 -06:00
<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"
2020-03-20 13:53:11 -05:00
@change="changeRowPagination">
</b-pagination>
2019-11-06 20:18:07 -06:00
</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,
},
rowsTitle: {
type: String,
default: null,
},
allowCreateRow: {
type: Boolean,
default: false,
},
createRowPermission: {
type: String,
default: null,
},
createRowButtonText: {
type: String,
default: "Create New Row",
},
createRowButtonUrl: {
type: String,
default: null,
},
rowsPaginated: {
type: Boolean,
default: true,
},
rowsPerPage: {
type: Number,
default: 20,
},
rowRouteGetter: Function,
2019-11-06 20:18:07 -06:00
apiRowsUrl: String,
isRow: {
type: Boolean,
default: false,
},
rowPathPrefix: String,
rowFilters: {
type: Function,
default: (uuid) => {
return JSON.stringify([
{field: 'batch_uuid', op: 'eq', value: uuid},
{field: 'removed', op: 'eq', value: false},
])
2019-11-06 20:18:07 -06:00
},
},
rowOrderBy: {
type: String,
default: 'modified',
},
rowOrderAscending: {
type: Boolean,
default: false,
},
2019-11-06 20:18:07 -06:00
allowEdit: {
type: Boolean,
default: true,
},
allowDelete: {
type: Boolean,
default: false,
},
quickDelete: {
type: Boolean,
default: false,
},
hideButtons: {
type: Boolean,
default: false,
},
hideSaveButton: {
type: Boolean,
default: false,
},
2019-11-06 20:18:07 -06:00
saveDisabled: {
type: Boolean,
default: false,
},
saveButtonText: {
type: String,
default: null,
},
saveButtonIcon: {
type: String,
default: null,
},
cancelUrl: {
type: String,
default: null,
},
2019-11-06 20:18:07 -06:00
},
data: function() {
return {
record: {},
rowData: {},
rowPage: 1,
2019-11-06 20:18:07 -06:00
}
},
computed: {
showButtons: function() {
if (this.hideButtons) {
return false
}
if (this.mode == 'creating') {
return true
}
if (this.mode == 'editing') {
return true
}
if (this.mode == 'deleting') {
return true
}
return false
},
},
2019-11-06 20:18:07 -06:00
// 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',
})
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',
// })
// this.$router.push(this.getModelPathPrefix() + '/')
// return
// }
2019-11-06 20:18:07 -06:00
// fetch initial page data unless 'creating'
if (this.mode != 'creating') {
this.fetch(this.$route.params.uuid)
}
},
methods: {
clear() {
this.record = {}
this.$emit('refresh', this.record)
},
2019-11-06 20:18:07 -06:00
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'
},
getIndexURL() {
return `${this.getModelPathPrefix()}/`
},
getParentUrl() {
if (this.isRow) {
return `${this.getModelPathPrefix()}/${this.record._parent_uuid}`
}
},
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`
},
2019-11-06 20:18:07 -06:00
getApiIndexUrl() {
if (this.apiIndexUrl) {
return this.apiIndexUrl
}
return '/api/' + this.getModelSlug()
},
getApiObjectUrlBase() {
2019-11-06 20:18:07 -06:00
if (this.apiObjectUrl) {
return this.apiObjectUrl
}
let url = this.getApiIndexUrl()
// drop trailing 's' then add slash
return url.slice(0, -1) + '/'
},
getApiObjectUrl(uuid) {
let url = this.getApiObjectUrlBase()
if (uuid) {
url += uuid
}
return url
},
2019-11-06 20:18:07 -06:00
getApiRowsUrl() {
return this.apiRowsUrl
},
getModelPermissionPrefix() {
if (this.modelPermissionPrefix) {
return this.modelPermissionPrefix
}
return this.getModelSlug()
},
hasModelPerm(perm) {
// do normal check, but first add prefix
let prefix = this.getModelPermissionPrefix()
return this.$byjoveHasPerm(prefix + '.' + perm)
2019-11-06 20:18:07 -06:00
},
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
},
getSaveButtonIcon() {
if (this.saveButtonIcon) {
return this.saveButtonIcon
}
if (this.mode == 'deleting') {
return 'trash'
}
return 'save'
},
getSaveButtonText() {
if (this.saveButtonText) {
return this.saveButtonText
}
if (this.mode == 'deleting') {
return "Delete This Forever!"
}
if (this.mode == 'creating') {
return `Create ${this.getModelTitle()}`
}
return "Save Data"
},
getSaveButtonType() {
if (this.mode == 'deleting') {
return 'is-danger'
}
return 'is-primary'
},
2019-11-06 20:18:07 -06:00
fetch(uuid) {
this.$http.get(this.getApiObjectUrl(uuid)).then(response => {
2019-11-06 20:18:07 -06:00
this.record = response.data.data
this.$emit('refresh', this.record)
if (this.hasRows) {
this.fetchRows(uuid)
}
}, response => {
if (response.status == 403) { // forbidden
2019-11-06 20:18:07 -06:00
this.$buefy.toast.open({
message: "You do not have permission to access that page.",
type: 'is-danger',
})
} else if (response.status == 404) { // notfound
this.$buefy.toast.open({
message: `The requested ${this.getModelTitle()} was not found.`,
type: 'is-danger',
})
} else { // other error
2019-11-06 20:18:07 -06:00
this.$buefy.toast.open({
message: "Failed to fetch page data!",
type: 'is-danger',
})
}
// redirect to model index
this.$router.push(this.getIndexURL())
2019-11-06 20:18:07 -06:00
})
},
fetchRows(uuid) {
let params = {
filters: this.rowFilters(uuid),
orderBy: this.rowOrderBy,
ascending: this.rowOrderAscending ? 1 : 0,
2019-11-06 20:18:07 -06:00
}
if (this.rowsPaginated) {
params.per_page = this.rowsPerPage
params.page = this.rowPage
}
2019-11-06 20:18:07 -06:00
this.$http.get(this.getApiRowsUrl(), {params: params}).then(response => {
this.rowData = response.data
2020-03-20 13:53:11 -05:00
// TODO: for some reason rowPage is getting reset to 0, maybe by
// the b-pagination component? for now this is our workaround
if (!this.rowPage) {
this.rowPage = 1
}
2019-11-06 20:18:07 -06:00
}, 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',
})
this.$router.push('/')
} else {
this.$buefy.toast.open({
message: "Failed to fetch page data!",
type: 'is-danger',
})
}
})
},
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 != 'viewing') {
return false
}
if (!this.hasModelPerm('delete')) {
return false
}
return true
},
getCancelURL() {
if (this.cancelUrl) {
return this.cancelUrl
}
if (this.mode == 'creating') {
return this.getIndexURL()
}
return this.getViewURL()
},
2019-11-06 20:18:07 -06:00
save() {
if (this.mode == 'deleting') {
this.confirmDelete()
return
}
let url
if (this.mode == 'creating') {
url = this.getApiIndexUrl()
} else {
url = this.getApiObjectUrl(this.record.uuid)
2019-11-06 20:18:07 -06:00
}
this.$emit('save', url)
},
confirmDelete() {
let modelTitle = this.getModelTitle()
this.$buefy.dialog.confirm({
title: `Delete ${modelTitle}`,
message: `Are you sure you wish to delete this ${modelTitle}?`,
hasIcon: true,
type: 'is-danger',
confirmText: `Delete ${modelTitle}`,
cancelText: "Whoops, nevermind",
onConfirm: this.deleteObject,
})
},
deleteObject() {
let url = this.getApiObjectUrl(this.record.uuid)
this.$http.delete(url).then(response => {
if (this.isRow) {
this.$router.push(this.getParentUrl())
} else {
this.$router.push(this.getIndexURL())
}
}, response => {
this.$buefy.toast.open({
message: "Failed to delete the record!",
type: 'is-danger',
})
})
},
shouldAllowCreateRow() {
if (!this.allowCreateRow) {
return false
}
if (this.mode != 'viewing') {
return false
}
if (this.createRowPermission) {
if (!this.$byjoveHasPerm(this.createRowPermission)) {
return false
}
} else {
if (!this.hasModelPerm('create_row')) {
return false
}
}
return true
},
getCreateRowButtonUrl() {
if (this.createRowButtonUrl) {
return this.createRowButtonUrl
}
return this.getApiRowsUrl()
},
2019-11-06 20:18:07 -06:00
},
}
</script>