Files
site/nova/resources/js/views/Detail.vue

549 lines
13 KiB
Vue
Executable File

<template>
<loading-view :loading="initialLoading">
<custom-detail-header
class="mb-3"
:resource="resource"
:resource-id="resourceId"
:resource-name="resourceName"
/>
<div v-if="shouldShowCards">
<cards
v-if="smallCards.length > 0"
:cards="smallCards"
class="mb-3"
:resource="resource"
:resource-id="resourceId"
:resource-name="resourceName"
:only-on-detail="true"
/>
<cards
v-if="largeCards.length > 0"
:cards="largeCards"
size="large"
:resource="resource"
:resource-id="resourceId"
:resource-name="resourceName"
:only-on-detail="true"
/>
</div>
<!-- Resource Detail -->
<div
v-for="panel in availablePanels"
:dusk="resourceName + '-detail-component'"
class="mb-8"
:key="panel.id"
>
<component
:is="panel.component"
:resource-name="resourceName"
:resource-id="resourceId"
:resource="resource"
:panel="panel"
>
<div v-if="panel.showToolbar" class="flex items-center mb-3">
<heading :level="1" class="flex-auto truncate">{{
panel.name
}}</heading>
<div class="ml-3 flex items-center">
<custom-detail-toolbar
:resource="resource"
:resource-name="resourceName"
:resource-id="resourceId"
/>
<!-- Actions -->
<action-selector
v-if="resource"
:resource-name="resourceName"
:actions="actions"
:pivot-actions="{ actions: [] }"
:selected-resources="selectedResources"
:query-string="{
currentSearch,
encodedFilters,
currentTrashed,
viaResource,
viaResourceId,
viaRelationship,
}"
@actionExecuted="actionExecuted"
class="ml-3"
/>
<button
v-if="resource.authorizedToDelete && !resource.softDeleted"
data-testid="open-delete-modal"
dusk="open-delete-modal-button"
@click="openDeleteModal"
class="btn btn-default btn-icon btn-white mr-3"
:title="__('Delete')"
>
<icon type="delete" class="text-80" />
</button>
<button
v-if="resource.authorizedToRestore && resource.softDeleted"
data-testid="open-restore-modal"
dusk="open-restore-modal-button"
@click="openRestoreModal"
class="btn btn-default btn-icon btn-white mr-3"
:title="__('Restore')"
>
<icon type="restore" class="text-80" />
</button>
<button
v-if="resource.authorizedToForceDelete"
data-testid="open-force-delete-modal"
dusk="open-force-delete-modal-button"
@click="openForceDeleteModal"
class="btn btn-default btn-icon btn-white mr-3"
:title="__('Force Delete')"
>
<icon type="force-delete" class="text-80" />
</button>
<portal
to="modals"
v-if="deleteModalOpen || restoreModalOpen || forceDeleteModalOpen"
>
<delete-resource-modal
v-if="deleteModalOpen"
@confirm="confirmDelete"
@close="closeDeleteModal"
mode="delete"
/>
<restore-resource-modal
v-if="restoreModalOpen"
@confirm="confirmRestore"
@close="closeRestoreModal"
/>
<delete-resource-modal
v-if="forceDeleteModalOpen"
@confirm="confirmForceDelete"
@close="closeForceDeleteModal"
mode="force delete"
/>
</portal>
<router-link
v-if="resource.authorizedToUpdate"
data-testid="edit-resource"
dusk="edit-resource-button"
:to="{ name: 'edit', params: { id: resource.id } }"
class="btn btn-default btn-icon bg-primary"
:title="__('Edit')"
>
<icon
type="edit"
class="text-white"
style="margin-top: -2px; margin-left: 3px"
/>
</router-link>
</div>
</div>
</component>
</div>
</loading-view>
</template>
<script>
import {
InteractsWithResourceInformation,
Errors,
Deletable,
Minimum,
mapProps,
HasCards,
} from 'laravel-nova'
export default {
metaInfo() {
if (this.resourceInformation && this.title) {
return {
title: this.__(':resource Details: :title', {
resource: this.resourceInformation.singularLabel,
title: this.title,
}),
}
}
},
props: mapProps(['resourceName', 'resourceId']),
mixins: [Deletable, HasCards, InteractsWithResourceInformation],
data: () => ({
initialLoading: true,
loading: true,
title: null,
resource: null,
panels: [],
actions: [],
actionValidationErrors: new Errors(),
deleteModalOpen: false,
restoreModalOpen: false,
forceDeleteModalOpen: false,
}),
watch: {
resourceId: function (newResourceId, oldResourceId) {
if (newResourceId != oldResourceId) {
this.initializeComponent()
this.fetchCards()
}
},
},
/**
* Bind the keydown even listener when the component is created
*/
created() {
if (Nova.missingResource(this.resourceName))
return this.$router.push({ name: '404' })
Nova.addShortcut('e', this.handleKeydown)
},
/**
* Unbind the keydown even listener when the before component is destroyed
*/
beforeDestroy() {
Nova.disableShortcut('e')
},
/**
* Mount the component.
*/
mounted() {
this.initializeComponent()
},
methods: {
/**
* Handle the keydown event
*/
handleKeydown(e) {
if (
this.resource.authorizedToUpdate &&
e.target.tagName != 'INPUT' &&
e.target.tagName != 'TEXTAREA' &&
e.target.contentEditable != 'true'
) {
this.$router.push({
name: 'edit',
params: { id: this.resource.id },
})
}
},
/**
* Initialize the compnent's data.
*/
async initializeComponent() {
await this.getResource()
await this.getActions()
this.initialLoading = false
},
/**
* Get the resource information.
*/
getResource() {
this.resource = null
return Minimum(
Nova.request().get(
'/nova-api/' + this.resourceName + '/' + this.resourceId
)
)
.then(({ data: { title, panels, resource } }) => {
this.title = title
this.panels = panels
this.resource = resource
this.loading = false
})
.catch(error => {
if (error.response.status >= 500) {
Nova.$emit('error', error.response.data.message)
return
}
if (error.response.status === 404 && this.initialLoading) {
this.$router.push({ name: '404' })
return
}
if (error.response.status === 403) {
this.$router.push({ name: '403' })
return
}
Nova.error(this.__('This resource no longer exists'))
this.$router.push({
name: 'index',
params: { resourceName: this.resourceName },
})
})
},
/**
* Get the available actions for the resource.
*/
getActions() {
this.actions = []
return Nova.request()
.get('/nova-api/' + this.resourceName + '/actions', {
params: {
resourceId: this.resourceId,
editing: true,
editMode: 'create',
display: 'detail',
},
})
.then(response => {
this.actions = response.data.actions
})
},
/**
* Handle an action executed event.
*/
async actionExecuted() {
await this.getResource()
await this.getActions()
},
/**
* Create a new panel for the given field.
*/
createPanelForField(field) {
return _.tap(
_.find(this.panels, panel => panel.name == field.panel),
panel => {
panel.fields = [field]
}
)
},
/**
* Create a new panel for the given relationship field.
*/
createPanelForRelationship(field) {
return {
component: 'relationship-panel',
prefixComponent: true,
name: field.name,
fields: [field],
}
},
/**
* Show the confirmation modal for deleting or detaching a resource
*/
async confirmDelete() {
this.deleteResources([this.resource], response => {
Nova.success(
this.__('The :resource was deleted!', {
resource: this.resourceInformation.singularLabel.toLowerCase(),
})
)
if (response && response.data && response.data.redirect) {
this.$router.push({ path: response.data.redirect }, () => {
window.scrollTo(0, 0)
})
return
}
if (!this.resource.softDeletes) {
this.$router.push(
{
name: 'index',
params: { resourceName: this.resourceName },
},
() => {
window.scrollTo(0, 0)
}
)
return
}
this.closeDeleteModal()
this.getResource()
})
},
/**
* Open the delete modal
*/
openDeleteModal() {
this.deleteModalOpen = true
},
/**
* Close the delete modal
*/
closeDeleteModal() {
this.deleteModalOpen = false
},
/**
* Show the confirmation modal for restoring a resource
*/
async confirmRestore() {
this.restoreResources([this.resource], () => {
Nova.success(
this.__('The :resource was restored!', {
resource: this.resourceInformation.singularLabel.toLowerCase(),
})
)
this.closeRestoreModal()
this.getResource()
})
},
/**
* Open the restore modal
*/
openRestoreModal() {
this.restoreModalOpen = true
},
/**
* Close the restore modal
*/
closeRestoreModal() {
this.restoreModalOpen = false
},
/**
* Show the confirmation modal for force deleting
*/
async confirmForceDelete() {
this.forceDeleteResources([this.resource], response => {
Nova.success(
this.__('The :resource was deleted!', {
resource: this.resourceInformation.singularLabel.toLowerCase(),
})
)
if (response && response.data && response.data.redirect) {
this.$router.push({ path: response.data.redirect })
return
}
this.$router.push({
name: 'index',
params: { resourceName: this.resourceName },
})
})
},
/**
* Open the force delete modal
*/
openForceDeleteModal() {
this.forceDeleteModalOpen = true
},
/**
* Close the force delete modal
*/
closeForceDeleteModal() {
this.forceDeleteModalOpen = false
},
},
computed: {
/**
* Get the available field panels.
*/
availablePanels() {
if (this.resource) {
var panels = {}
var fields = _.toArray(JSON.parse(JSON.stringify(this.resource.fields)))
fields.forEach(field => {
if (field.listable) {
return (panels[field.name] = this.createPanelForRelationship(field))
} else if (panels[field.panel]) {
return panels[field.panel].fields.push(field)
}
panels[field.panel] = this.createPanelForField(field)
})
return _.toArray(panels)
}
},
/**
* These are here to satisfy the parameter requirements for deleting the resource
*/
currentSearch() {
return ''
},
encodedFilters() {
return []
},
currentTrashed() {
return ''
},
viaResource() {
return ''
},
viaResourceId() {
return ''
},
viaRelationship() {
return ''
},
selectedResources() {
return [this.resourceId]
},
/**
* Determine whether this is a detail view for an Action Event
*/
isActionDetail() {
return this.resourceName == 'action-events'
},
/**
* Get the endpoint for this resource's metrics.
*/
cardsEndpoint() {
return `/nova-api/${this.resourceName}/cards`
},
/**
* Get the extra card params to pass to the endpoint.
*/
extraCardParams() {
return {
resourceId: this.resourceId,
}
},
},
}
</script>