Последняя версия с сервера прошлого разработчика
This commit is contained in:
3
nova/resources/js/views/403.vue
Executable file
3
nova/resources/js/views/403.vue
Executable file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<error-403 />
|
||||
</template>
|
||||
3
nova/resources/js/views/404.vue
Executable file
3
nova/resources/js/views/404.vue
Executable file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<error-404 />
|
||||
</template>
|
||||
562
nova/resources/js/views/Attach.vue
Executable file
562
nova/resources/js/views/Attach.vue
Executable file
@@ -0,0 +1,562 @@
|
||||
<template>
|
||||
<loading-view :loading="loading">
|
||||
<custom-attach-header
|
||||
class="mb-3"
|
||||
:resource-name="resourceName"
|
||||
:resource-id="resourceId"
|
||||
/>
|
||||
|
||||
<heading class="mb-3">{{
|
||||
__('Attach :resource', { resource: relatedResourceLabel })
|
||||
}}</heading>
|
||||
|
||||
<form
|
||||
v-if="field"
|
||||
@submit.prevent="attachResource"
|
||||
@change="onUpdateFormStatus"
|
||||
autocomplete="off"
|
||||
>
|
||||
<card class="overflow-hidden mb-8">
|
||||
<!-- Related Resource -->
|
||||
<div
|
||||
v-if="viaResourceField"
|
||||
dusk="via-resource-field"
|
||||
class="flex border-b border-40"
|
||||
>
|
||||
<div class="w-1/5 px-8 py-6">
|
||||
<label
|
||||
:for="viaResourceField.name"
|
||||
class="inline-block text-80 pt-2 leading-tight"
|
||||
>
|
||||
{{ viaResourceField.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="py-6 px-8 w-1/2">
|
||||
<span class="inline-block font-bold text-80 pt-2">
|
||||
{{ viaResourceField.display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<default-field
|
||||
:field="field"
|
||||
:errors="validationErrors"
|
||||
:show-help-text="field.helpText != null"
|
||||
>
|
||||
<template slot="field">
|
||||
<search-input
|
||||
v-if="field.searchable"
|
||||
:data-testid="`${field.resourceName}-search-input`"
|
||||
@input="performSearch"
|
||||
@clear="clearSelection"
|
||||
@selected="selectResource"
|
||||
:debounce="field.debounce"
|
||||
:value="selectedResource"
|
||||
:data="availableResources"
|
||||
trackBy="value"
|
||||
>
|
||||
<div
|
||||
slot="default"
|
||||
v-if="selectedResource"
|
||||
class="flex items-center"
|
||||
>
|
||||
<div v-if="selectedResource.avatar" class="mr-3">
|
||||
<img
|
||||
:src="selectedResource.avatar"
|
||||
class="w-8 h-8 rounded-full block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{ selectedResource.display }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
slot="option"
|
||||
slot-scope="{ option, selected }"
|
||||
class="flex items-center"
|
||||
>
|
||||
<div v-if="option.avatar" class="mr-3">
|
||||
<img
|
||||
:src="option.avatar"
|
||||
class="w-8 h-8 rounded-full block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="text-sm font-semibold leading-5 text-90"
|
||||
:class="{ 'text-white': selected }"
|
||||
>
|
||||
{{ option.display }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="field.withSubtitles"
|
||||
class="mt-1 text-xs font-semibold leading-5 text-80"
|
||||
:class="{ 'text-white': selected }"
|
||||
>
|
||||
<span v-if="option.subtitle">{{ option.subtitle }}</span>
|
||||
<span v-else>{{ __('No additional information...') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</search-input>
|
||||
|
||||
<select-control
|
||||
v-else
|
||||
dusk="attachable-select"
|
||||
class="form-control form-select w-full"
|
||||
:class="{
|
||||
'border-danger': validationErrors.has(field.attribute),
|
||||
}"
|
||||
:data-testid="`${field.resourceName}-select`"
|
||||
@change="selectResourceFromSelectControl"
|
||||
:options="availableResources"
|
||||
:label="'display'"
|
||||
:selected="selectedResourceId"
|
||||
>
|
||||
<option value="" disabled selected>
|
||||
{{
|
||||
__('Choose :resource', {
|
||||
resource: relatedResourceLabel,
|
||||
})
|
||||
}}
|
||||
</option>
|
||||
</select-control>
|
||||
|
||||
<!-- Trashed State -->
|
||||
<div v-if="softDeletes" class="mt-3">
|
||||
<checkbox-with-label
|
||||
:dusk="field.resourceName + '-with-trashed-checkbox'"
|
||||
:checked="withTrashed"
|
||||
@input="toggleWithTrashed"
|
||||
>
|
||||
{{ __('With Trashed') }}
|
||||
</checkbox-with-label>
|
||||
</div>
|
||||
</template>
|
||||
</default-field>
|
||||
|
||||
<!-- Pivot Fields -->
|
||||
<div v-for="field in fields">
|
||||
<component
|
||||
:is="'form-' + field.component"
|
||||
:resource-name="resourceName"
|
||||
:field="field"
|
||||
:errors="validationErrors"
|
||||
:via-resource="viaResource"
|
||||
:via-resource-id="viaResourceId"
|
||||
:via-relationship="viaRelationship"
|
||||
:show-help-text="field.helpText != null"
|
||||
/>
|
||||
</div>
|
||||
</card>
|
||||
|
||||
<!-- Attach Button -->
|
||||
<div class="flex items-center">
|
||||
<cancel-button @click="$router.back()" />
|
||||
|
||||
<progress-button
|
||||
class="mr-3"
|
||||
dusk="attach-and-attach-another-button"
|
||||
@click.native="attachAndAttachAnother"
|
||||
:disabled="isWorking"
|
||||
:processing="submittedViaAttachAndAttachAnother"
|
||||
>
|
||||
{{ __('Attach & Attach Another') }}
|
||||
</progress-button>
|
||||
|
||||
<progress-button
|
||||
dusk="attach-button"
|
||||
type="submit"
|
||||
:disabled="isWorking"
|
||||
:processing="submittedViaAttachResource"
|
||||
>
|
||||
{{
|
||||
__('Attach :resource', {
|
||||
resource: relatedResourceLabel,
|
||||
})
|
||||
}}
|
||||
</progress-button>
|
||||
</div>
|
||||
</form>
|
||||
</loading-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
PerformsSearches,
|
||||
TogglesTrashed,
|
||||
Errors,
|
||||
PreventsFormAbandonment,
|
||||
} from 'laravel-nova'
|
||||
|
||||
export default {
|
||||
mixins: [PerformsSearches, TogglesTrashed, PreventsFormAbandonment],
|
||||
|
||||
metaInfo() {
|
||||
if (this.relatedResourceLabel) {
|
||||
return {
|
||||
title: this.__('Attach :resource', {
|
||||
resource: this.relatedResourceLabel,
|
||||
}),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
resourceName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
resourceId: {
|
||||
required: true,
|
||||
},
|
||||
relatedResourceName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
viaResource: {
|
||||
default: '',
|
||||
},
|
||||
viaResourceId: {
|
||||
default: '',
|
||||
},
|
||||
viaRelationship: {
|
||||
default: '',
|
||||
},
|
||||
polymorphic: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loading: true,
|
||||
submittedViaAttachAndAttachAnother: false,
|
||||
submittedViaAttachResource: false,
|
||||
viaResourceField: null,
|
||||
field: null,
|
||||
softDeletes: false,
|
||||
fields: [],
|
||||
validationErrors: new Errors(),
|
||||
selectedResource: null,
|
||||
selectedResourceId: null,
|
||||
}),
|
||||
|
||||
created() {
|
||||
if (Nova.missingResource(this.resourceName))
|
||||
return this.$router.push({ name: '404' })
|
||||
},
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.initializeComponent()
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Initialize the component's data.
|
||||
*/
|
||||
initializeComponent() {
|
||||
this.softDeletes = false
|
||||
this.disableWithTrashed()
|
||||
this.clearSelection()
|
||||
this.getField()
|
||||
this.getPivotFields()
|
||||
this.resetErrors()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the many-to-many relationship field.
|
||||
*/
|
||||
getField() {
|
||||
this.field = null
|
||||
|
||||
Nova.request()
|
||||
.get(
|
||||
'/nova-api/' + this.resourceName + '/field/' + this.viaRelationship,
|
||||
{
|
||||
params: {
|
||||
relatable: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(({ data }) => {
|
||||
this.field = data
|
||||
this.field.searchable
|
||||
? this.determineIfSoftDeletes()
|
||||
: this.getAvailableResources()
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the available pivot fields for the relationship.
|
||||
*/
|
||||
getPivotFields() {
|
||||
this.fields = []
|
||||
|
||||
Nova.request()
|
||||
.get(
|
||||
'/nova-api/' +
|
||||
this.resourceName +
|
||||
'/' +
|
||||
this.resourceId +
|
||||
'/creation-pivot-fields/' +
|
||||
this.relatedResourceName,
|
||||
{
|
||||
params: {
|
||||
editing: true,
|
||||
editMode: 'attach',
|
||||
viaRelationship: this.viaRelationship,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(({ data }) => {
|
||||
this.fields = data
|
||||
|
||||
_.each(this.fields, field => {
|
||||
field.fill = () => ''
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
resetErrors() {
|
||||
this.validationErrors = new Errors()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the available resources for the current search / trashed state.
|
||||
*/
|
||||
getAvailableResources(search = '') {
|
||||
Nova.request()
|
||||
.get(
|
||||
`/nova-api/${this.resourceName}/${this.resourceId}/attachable/${this.relatedResourceName}`,
|
||||
{
|
||||
params: {
|
||||
search,
|
||||
current: this.selectedResourceId,
|
||||
withTrashed: this.withTrashed,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
this.viaResourceField = response.data.viaResource
|
||||
this.availableResources = response.data.resources
|
||||
this.withTrashed = response.data.withTrashed
|
||||
this.softDeletes = response.data.softDeletes
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the related resource is soft deleting.
|
||||
*/
|
||||
determineIfSoftDeletes() {
|
||||
Nova.request()
|
||||
.get('/nova-api/' + this.relatedResourceName + '/soft-deletes')
|
||||
.then(response => {
|
||||
this.softDeletes = response.data.softDeletes
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Attach the selected resource.
|
||||
*/
|
||||
async attachResource() {
|
||||
this.submittedViaAttachResource = true
|
||||
|
||||
try {
|
||||
await this.attachRequest()
|
||||
|
||||
this.submittedViaAttachResource = false
|
||||
this.canLeave = true
|
||||
|
||||
this.$router.push({
|
||||
name: 'detail',
|
||||
params: {
|
||||
resourceName: this.resourceName,
|
||||
resourceId: this.resourceId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
window.scrollTo(0, 0)
|
||||
|
||||
this.submittedViaAttachResource = false
|
||||
if (
|
||||
this.resourceInformation &&
|
||||
this.resourceInformation.preventFormAbandonment
|
||||
) {
|
||||
this.canLeave = false
|
||||
}
|
||||
|
||||
if (error.response.status == 422) {
|
||||
this.validationErrors = new Errors(error.response.data.errors)
|
||||
Nova.error(this.__('There was a problem submitting the form.'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Attach a new resource and reset the form
|
||||
*/
|
||||
async attachAndAttachAnother() {
|
||||
this.submittedViaAttachAndAttachAnother = true
|
||||
|
||||
try {
|
||||
await this.attachRequest()
|
||||
|
||||
this.submittedViaAttachAndAttachAnother = false
|
||||
|
||||
// Reset the form by refetching the fields
|
||||
this.initializeComponent()
|
||||
} catch (error) {
|
||||
this.submittedViaAttachAndAttachAnother = false
|
||||
|
||||
if (error.response.status == 422) {
|
||||
this.validationErrors = new Errors(error.response.data.errors)
|
||||
Nova.error(this.__('There was a problem submitting the form.'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send an attach request for this resource
|
||||
*/
|
||||
attachRequest() {
|
||||
return Nova.request().post(
|
||||
this.attachmentEndpoint,
|
||||
this.attachmentFormData,
|
||||
{
|
||||
params: {
|
||||
editing: true,
|
||||
editMode: 'attach',
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Select a resource using the <select> control
|
||||
*/
|
||||
selectResourceFromSelectControl(e) {
|
||||
this.selectedResourceId = e.target.value
|
||||
this.selectInitialResource()
|
||||
|
||||
if (this.field) {
|
||||
Nova.$emit(this.field.attribute + '-change', this.selectedResourceId)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Select the initial selected resource
|
||||
*/
|
||||
selectInitialResource() {
|
||||
this.selectedResource = _.find(
|
||||
this.availableResources,
|
||||
r => r.value == this.selectedResourceId
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the trashed state of the search
|
||||
*/
|
||||
toggleWithTrashed() {
|
||||
this.withTrashed = !this.withTrashed
|
||||
|
||||
// Reload the data if the component doesn't support searching
|
||||
if (!this.isSearchable) {
|
||||
this.getAvailableResources()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Prevent accidental abandonment only if form was changed.
|
||||
*/
|
||||
onUpdateFormStatus() {
|
||||
if (
|
||||
this.resourceInformation &&
|
||||
this.resourceInformation.preventFormAbandonment
|
||||
) {
|
||||
this.updateFormStatus()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the attachment endpoint for the relationship type.
|
||||
*/
|
||||
attachmentEndpoint() {
|
||||
return this.polymorphic
|
||||
? '/nova-api/' +
|
||||
this.resourceName +
|
||||
'/' +
|
||||
this.resourceId +
|
||||
'/attach-morphed/' +
|
||||
this.relatedResourceName
|
||||
: '/nova-api/' +
|
||||
this.resourceName +
|
||||
'/' +
|
||||
this.resourceId +
|
||||
'/attach/' +
|
||||
this.relatedResourceName
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the form data for the resource attachment.
|
||||
*/
|
||||
attachmentFormData() {
|
||||
return _.tap(new FormData(), formData => {
|
||||
_.each(this.fields, field => {
|
||||
field.fill(formData)
|
||||
})
|
||||
|
||||
if (!this.selectedResource) {
|
||||
formData.append(this.relatedResourceName, '')
|
||||
} else {
|
||||
formData.append(this.relatedResourceName, this.selectedResource.value)
|
||||
}
|
||||
|
||||
formData.append(this.relatedResourceName + '_trashed', this.withTrashed)
|
||||
formData.append('viaRelationship', this.viaRelationship)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the label for the related resource.
|
||||
*/
|
||||
relatedResourceLabel() {
|
||||
if (this.field) {
|
||||
return this.field.singularLabel
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the related resources is searchable
|
||||
*/
|
||||
isSearchable() {
|
||||
return this.field.searchable
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the form is being processed
|
||||
*/
|
||||
isWorking() {
|
||||
return (
|
||||
this.submittedViaAttachResource ||
|
||||
this.submittedViaAttachAndAttachAnother
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the heading for the view
|
||||
*/
|
||||
headingTitle() {
|
||||
return this.__('Attach :resource', {
|
||||
resource: this.relatedResourceLabel,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
56
nova/resources/js/views/Create.vue
Executable file
56
nova/resources/js/views/Create.vue
Executable file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<create-form
|
||||
@resource-created="handleResourceCreated"
|
||||
@cancelled-create="handleCancelledCreate"
|
||||
:mode="mode"
|
||||
:resource-name="resourceName"
|
||||
:via-resource="viaResource"
|
||||
:via-resource-id="viaResourceId"
|
||||
:via-relationship="viaRelationship"
|
||||
:update-form-status="updateFormStatus"
|
||||
:should-override-meta="mode == 'form' ? true : false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapProps, PreventsFormAbandonment } from 'laravel-nova'
|
||||
|
||||
export default {
|
||||
mixins: [PreventsFormAbandonment],
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'form',
|
||||
validator: val => ['modal', 'form'].includes(val),
|
||||
},
|
||||
|
||||
...mapProps([
|
||||
'resourceName',
|
||||
'viaResource',
|
||||
'viaResourceId',
|
||||
'viaRelationship',
|
||||
]),
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleResourceCreated({ redirect, id }) {
|
||||
this.canLeave = true
|
||||
|
||||
if (this.mode == 'form') {
|
||||
return this.$router.push({ path: redirect })
|
||||
}
|
||||
|
||||
return this.$emit('refresh', { redirect, id })
|
||||
},
|
||||
|
||||
handleCancelledCreate() {
|
||||
if (this.mode == 'form') {
|
||||
return this.$router.back()
|
||||
}
|
||||
|
||||
return this.$emit('cancelled-create')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
100
nova/resources/js/views/Dashboard.vue
Executable file
100
nova/resources/js/views/Dashboard.vue
Executable file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div :dusk="'dashboard-' + this.name">
|
||||
<custom-dashboard-header class="mb-3" :dashboard-name="name" />
|
||||
|
||||
<heading v-if="cards.length > 1" class="mb-6">{{
|
||||
__('Dashboard')
|
||||
}}</heading>
|
||||
|
||||
<div v-if="shouldShowCards">
|
||||
<cards v-if="smallCards.length > 0" :cards="smallCards" class="mb-3" />
|
||||
<cards v-if="largeCards.length > 0" :cards="largeCards" size="large" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CardSizes } from 'laravel-nova'
|
||||
|
||||
export default {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: `${this.label}`,
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({ label: '', cards: '' }),
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'main',
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
name() {
|
||||
this.fetchDashboard()
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetchDashboard()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchDashboard() {
|
||||
const {
|
||||
data: { label, cards },
|
||||
} = await Nova.request()
|
||||
.get(this.dashboardEndpoint, {
|
||||
params: this.extraCardParams,
|
||||
})
|
||||
.catch(e => {
|
||||
this.$router.push({ name: '404' })
|
||||
})
|
||||
|
||||
this.label = label
|
||||
this.cards = cards
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the endpoint for this dashboard.
|
||||
*/
|
||||
dashboardEndpoint() {
|
||||
return `/nova-api/dashboards/${this.name}`
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine whether we have cards to show on the Dashboard
|
||||
*/
|
||||
shouldShowCards() {
|
||||
return this.cards.length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the small cards used for the Dashboard
|
||||
*/
|
||||
smallCards() {
|
||||
return _.filter(this.cards, c => CardSizes.indexOf(c.width) !== -1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the full-width cards used for the Dashboard
|
||||
*/
|
||||
largeCards() {
|
||||
return _.filter(this.cards, c => c.width == 'full')
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the extra card params to pass to the endpoint.
|
||||
*/
|
||||
extraCardParams() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
548
nova/resources/js/views/Detail.vue
Executable file
548
nova/resources/js/views/Detail.vue
Executable file
@@ -0,0 +1,548 @@
|
||||
<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>
|
||||
287
nova/resources/js/views/Error403.vue
Executable file
287
nova/resources/js/views/Error403.vue
Executable file
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed pin bg-40 z-50 flex items-center justify-center min-w-site p-6"
|
||||
>
|
||||
<div class="flex items-center w-error">
|
||||
<div class="flex-no-shrink illustration">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="523"
|
||||
height="541"
|
||||
viewBox="0 0 530 560"
|
||||
>
|
||||
<g fill="none" fill-rule="evenodd" transform="translate(4 10)">
|
||||
<path
|
||||
fill="#DDE4EB"
|
||||
d="M0 185a19.4 19.4 0 0 1 19.4-19.4h37.33a19.4 19.4 0 0 0 0-38.8H45.08a19.4 19.4 0 1 1 0-38.8h170.84a19.4 19.4 0 0 1 0 38.8h-6.87a19.4 19.4 0 0 0 0 38.8h42.55a19.4 19.4 0 0 1 0 38.8H19.4A19.4 19.4 0 0 1 0 185z"
|
||||
/>
|
||||
<g
|
||||
stroke-width="2"
|
||||
transform="rotate(-30 383.9199884 -24.79114317)"
|
||||
>
|
||||
<rect
|
||||
width="32.4"
|
||||
height="9.19"
|
||||
x="12.47"
|
||||
y="3.8"
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
rx="4.6"
|
||||
/>
|
||||
<rect
|
||||
width="32.4"
|
||||
height="14.79"
|
||||
x="1"
|
||||
y="1"
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
rx="7.39"
|
||||
/>
|
||||
<ellipse
|
||||
cx="8.6"
|
||||
cy="8.39"
|
||||
stroke="#4A90E2"
|
||||
rx="7.6"
|
||||
ry="7.39"
|
||||
style="mix-blend-mode: multiply"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
fill="#E0EEFF"
|
||||
d="M94 198.256L106.6 191l22.4 16.744L116.4 215zM48 164.256L60.6 157 83 173.744 70.4 181z"
|
||||
opacity=".58"
|
||||
/>
|
||||
<path
|
||||
stroke="#0D2B3E"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M88 188l9 7-9-7zm-15-11l5 3-5-3z"
|
||||
/>
|
||||
<path
|
||||
stroke="#4A90E2"
|
||||
stroke-width="2"
|
||||
d="M92.82 198.36l20.65 15.44 10.71-6.16-20.65-15.44-10.71 6.16zM119 211l-22-17 22 17zm-72.18-46.64l20.65 15.44 10.71-6.16-20.65-15.44-10.71 6.16zM73 178l-22-17 22 17z"
|
||||
/>
|
||||
<path
|
||||
stroke="#8DDCFF"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M117 176a14 14 0 0 0-14-14m10 15a10 10 0 0 0-10-10"
|
||||
/>
|
||||
<ellipse cx="258" cy="441" fill="#FFF" rx="250" ry="90" />
|
||||
<path
|
||||
fill="#FFF"
|
||||
fill-rule="nonzero"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M195.95992276 433.88207738c-.7613033-1.55811337-1.97677352-5.39619.01107483-6.1324365 1.97685786-.72734656 2.77032762 2.34241006 4.31210683 4.22387675 2.92231431 3.57504952 6.28818967 5.22592295 11.14145652 5.73602185 1.77024897.18606067 3.51532102.0376574 5.19229942-.41955529a3.17 3.17 0 0 1 3.89461497 2.16898002 3.12 3.12 0 0 1-2.19463454 3.85169823c-2.43329264.66931826-4.97971626.88432232-7.54558275.61463889-7.06110546-.7421521-11.79595772-3.81390631-14.81133528-10.04322395z"
|
||||
/>
|
||||
<g stroke="#0D2B3E" stroke-width="2">
|
||||
<path
|
||||
fill="#FFF"
|
||||
fill-rule="nonzero"
|
||||
d="M228.66635404 453.35751889l3.48444585 6.7411525a11.71 11.71 0 0 0-3.36066168 18.19840799l3.157266 3.1573203-8.52104352 8.55618006-.29468882-6.6673277a19.31 19.31 0 0 1 5.53468217-29.98573315z"
|
||||
/>
|
||||
<path d="M221.75370493 481.33823157l5.9097851-4.56727928" />
|
||||
</g>
|
||||
<g stroke="#0D2B3E" stroke-width="2">
|
||||
<path
|
||||
fill="#FFF"
|
||||
fill-rule="nonzero"
|
||||
d="M217.43675157 454.38903415l-.38056208 7.58726384a10.25 10.25 0 0 0-10.62036709 8.5642456l.04580558 4.00318647-11.36366293-.10613565 3.84834642-5.16425501a17.82 17.82 0 0 1 18.46098491-14.88104957z"
|
||||
/>
|
||||
<path d="M199.40986905 468.0735658l7.07551171 1.72015522" />
|
||||
</g>
|
||||
<path
|
||||
fill="#E5F7FF"
|
||||
d="M233.41788355 435.98904264l3.14268919.33030994-3.01041974 28.64223059-3.1426892-.33030995z"
|
||||
/>
|
||||
<path
|
||||
stroke="#7ED7FF"
|
||||
stroke-width="2"
|
||||
d="M218.1633805 433.70198413l13.07796292 1.37454929 1.09127716-10.38280859a6.575 6.575 0 0 0-13.07796293-1.37454929l-1.09127715 10.38280859z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M221.02136188 434.25374714l.64389533-6.12625487a3.59 3.59 0 1 1 7.130722.74946908l-.64389534 6.12625488"
|
||||
/>
|
||||
<path
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M235.80327328 436.92350283l-20.28824667-2.13238065-2.86721575 27.27973559 20.28824667 2.13238065 2.86721575-27.2797356z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M215.51502661 434.79112218l-2.86721575 27.27973559 17.1555027 1.80311599 2.86721575-27.2797356-17.1555027-1.80311598z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M214.36589556 440.07997818l-1.09905036.88999343-1.17489993 11.1784261 11.15853567 1.17280937 1.09905036-.88999344 1.17385464-11.16848088-11.16848088-1.17385464z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
fill-rule="nonzero"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M245.62684398 462.24908175c-.41742893 1.6755456-1.95466376 5.39523768-3.94116941 4.68369338-1.99645087-.71258958-.63076284-3.56546466-.5955535-6.00514913.06313174-4.61870267-1.45795198-8.03642184-4.8492445-11.55015704-1.23234204-1.2858589-2.67505657-2.29217634-4.24858182-3.01059006a3.17 3.17 0 0 1-1.5730205-4.16725407 3.12 3.12 0 0 1 4.14422777-1.54527542c2.29328456 1.04544055 4.3804078 2.52169139 6.1770892 4.36961887 4.93145874 5.12354512 6.58580412 10.52606688 4.87526226 17.2340134z"
|
||||
/>
|
||||
<path
|
||||
stroke="#233242"
|
||||
stroke-width="2"
|
||||
d="M518 372.93A1509.66 1509.66 0 0 0 261 351c-87.62 0-173.5 7.51-257 21.93"
|
||||
/>
|
||||
<circle cx="51" cy="107" r="6" fill="#9AC2F0" />
|
||||
<path
|
||||
stroke="#031836"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M48 116a6 6 0 1 0-6-6"
|
||||
/>
|
||||
<circle cx="501" cy="97" r="6" fill="#9AC2F0" />
|
||||
<path
|
||||
stroke="#031836"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M498 106a6 6 0 1 0-6-6"
|
||||
/>
|
||||
<path
|
||||
fill="#031836"
|
||||
d="M305.75 0h.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-.5a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zM321 14.75v.5a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-.5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zM306.25 30h-.5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zM291 15.25v-.5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v.5a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1z"
|
||||
/>
|
||||
<path
|
||||
fill="#DDE4EB"
|
||||
d="M446 107.5a16.5 16.5 0 0 0 16.5 16.5h44a16.5 16.5 0 0 1 0 33h-143a16.5 16.5 0 0 1 0-33 16.5 16.5 0 0 0 0-33h-66a16.5 16.5 0 0 1 0-33h165a16.5 16.5 0 0 1 0 33 16.5 16.5 0 0 0-16.5 16.5z"
|
||||
/>
|
||||
<circle cx="458" cy="186" r="4" fill="#031836" />
|
||||
<circle cx="138" cy="16" r="4" fill="#031836" />
|
||||
<path
|
||||
stroke="#233242"
|
||||
stroke-width="2"
|
||||
d="M58 364.86l67.93-67.93a10 10 0 0 1 14.14 0L196 352.86m139-18l36.93-36.93a10 10 0 0 1 14.14 0L451 362.86"
|
||||
/>
|
||||
<path
|
||||
stroke="#233242"
|
||||
stroke-width="2"
|
||||
d="M176 332.86l70.93-71.84a10 10 0 0 1 14.19-.05L345 344.86"
|
||||
/>
|
||||
<g stroke-width="2" transform="rotate(-87 355.051 43.529)">
|
||||
<ellipse
|
||||
cx="10.28"
|
||||
cy="27.49"
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
rx="9.21"
|
||||
ry="19.26"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
d="M25.66 54.03c-7.52 0-13.62-12.1-13.62-27.02S18.14 0 25.66 0H96.1c7.22 0 14.15 2.85 19.26 7.91l19.26 19.1-19.26 19.1a27.35 27.35 0 0 1-19.26 7.92H25.66z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#4A90E2"
|
||||
d="M98.09 54.22c-7.52 0-13.62-12.1-13.62-27.02s6.1-27 13.62-27"
|
||||
/>
|
||||
<ellipse
|
||||
cx="59.59"
|
||||
cy="27.27"
|
||||
stroke="#4A90E2"
|
||||
rx="16.34"
|
||||
ry="16.21"
|
||||
/>
|
||||
<ellipse
|
||||
cx="59.59"
|
||||
cy="27.27"
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
rx="12.26"
|
||||
ry="12.16"
|
||||
/>
|
||||
</g>
|
||||
<g stroke="#233242" stroke-width="2" transform="translate(456 396)">
|
||||
<ellipse cx="30" cy="10" rx="20" ry="10" />
|
||||
<path
|
||||
d="M0 15c0 8.28 13.43 15 30 15m12.39-1.33C52.77 26.3 60 21.07 60 15"
|
||||
/>
|
||||
</g>
|
||||
<g stroke="#233242" stroke-width="2" transform="translate(276 520)">
|
||||
<ellipse cx="20" cy="6.67" rx="13.33" ry="6.67" />
|
||||
<path
|
||||
d="M0 10c0 5.52 8.95 10 20 10m8.26-.89C35.18 17.54 40 14.05 40 10"
|
||||
/>
|
||||
</g>
|
||||
<g stroke="#233242" stroke-width="2" transform="translate(186 370)">
|
||||
<ellipse cx="15" cy="5" rx="10" ry="5" />
|
||||
<path
|
||||
d="M0 7.5C0 11.64 6.72 15 15 15m6.2-.67c5.19-1.18 8.8-3.8 8.8-6.83"
|
||||
/>
|
||||
</g>
|
||||
<ellipse cx="58" cy="492" fill="#202C3A" rx="3" ry="2" />
|
||||
<ellipse cx="468" cy="492" fill="#202C3A" rx="3" ry="2" />
|
||||
<ellipse cx="388" cy="392" fill="#202C3A" rx="3" ry="2" />
|
||||
<ellipse cx="338" cy="452" fill="#202C3A" rx="3" ry="2" />
|
||||
<g stroke="#233242" stroke-width="2" transform="translate(46 406)">
|
||||
<ellipse cx="40" cy="13.33" rx="26.67" ry="13.33" />
|
||||
<path
|
||||
d="M0 20c0 11.05 17.9 20 40 20m16.51-1.78C70.37 35.08 80 28.1 80 20"
|
||||
/>
|
||||
</g>
|
||||
<g stroke="#0D2B3E" stroke-width="2">
|
||||
<path
|
||||
d="M299 378l-21 42m35-36l-21 42m4-42l14 6m-17 0l14 6m-17 0l14 6m-17 0l14 6m-17 0l14 6m-17 0l14 6"
|
||||
/>
|
||||
</g>
|
||||
<circle
|
||||
cx="341"
|
||||
cy="155"
|
||||
r="25"
|
||||
stroke="#233242"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<circle cx="342" cy="156" r="20" fill="#FFF" />
|
||||
<path
|
||||
stroke="#233242"
|
||||
stroke-width="2"
|
||||
d="M321.56 140.5c-7.66.32-13 2.37-13.97 6-1.78 6.66 11.9 16.12 30.58 21.13 18.67 5 35.25 3.65 37.04-3.02.96-3.58-2.54-7.96-8.88-12.03"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-error-title font-normal mb-1">403</h1>
|
||||
<p class="text-error-subtitle mb-6">{{ __('Hold Up!') }}</p>
|
||||
<p class="text-error-message mb-8 leading-normal">
|
||||
{{
|
||||
__(
|
||||
"The government won't let us show you what's behind these doors"
|
||||
)
|
||||
}}…
|
||||
</p>
|
||||
<router-link
|
||||
:to="{ name: 'dashboard' }"
|
||||
class="dim btn btn-lg btn-default btn-white text-90 no-text-shadow tracking-wide uppercase"
|
||||
>
|
||||
{{ __('Go Home') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: 'Forbidden',
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
334
nova/resources/js/views/Error404.vue
Executable file
334
nova/resources/js/views/Error404.vue
Executable file
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed pin bg-40 z-50 flex items-center justify-center min-w-site p-6"
|
||||
>
|
||||
<div class="flex items-center w-error">
|
||||
<div class="flex-no-shrink illustration">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="520"
|
||||
height="560"
|
||||
viewBox="0 0 520 560"
|
||||
>
|
||||
<g fill="none" fill-rule="evenodd" transform="translate(3 73)">
|
||||
<g stroke-width="2" transform="rotate(-30 140.579 22.242)">
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
d="M26.03 0h76.9c7.9 0 15.46 3.16 21.04 8.79L145 30l-21.03 21.21A29.62 29.62 0 0 1 102.94 60H26.03V0z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#4A90E2"
|
||||
d="M102.62 60c8.2 0 14.87-13.43 14.87-30s-6.66-30-14.87-30"
|
||||
/>
|
||||
<path
|
||||
stroke="#4A90E2"
|
||||
d="M33.46 60c8.22 0 14.87-13.43 14.87-30S41.68 0 33.46 0"
|
||||
/>
|
||||
<ellipse
|
||||
cx="26.03"
|
||||
cy="30"
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
rx="14.87"
|
||||
ry="30"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
d="M12.15 8.92v42.16c6.35-2.3 10.75-4.58 13.2-6.82l1.53-1.4C30.94 39 32.46 36 32.46 30c0-6.42-1.69-9.3-6.8-13.98l-.31-.28c-2.44-2.24-6.84-4.52-13.2-6.82z"
|
||||
/>
|
||||
<ellipse
|
||||
cx="11.15"
|
||||
cy="30"
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
rx="10.15"
|
||||
ry="21.5"
|
||||
/>
|
||||
<ellipse cx="79.56" cy="30" stroke="#4A90E2" rx="17.85" ry="18" />
|
||||
<ellipse
|
||||
cx="79.56"
|
||||
cy="30"
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
rx="13.38"
|
||||
ry="13.5"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
fill="#DDE4EB"
|
||||
d="M425 74.5A16.5 16.5 0 0 0 441.5 91h44a16.5 16.5 0 0 1 0 33h-143a16.5 16.5 0 0 1 0-33 16.5 16.5 0 0 0 0-33h-66a16.5 16.5 0 0 1 0-33h165a16.5 16.5 0 0 1 0 33A16.5 16.5 0 0 0 425 74.5z"
|
||||
/>
|
||||
<g transform="translate(424 130)">
|
||||
<circle
|
||||
cx="45"
|
||||
cy="45"
|
||||
r="45"
|
||||
stroke="#4A90E2"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<circle cx="45.83" cy="45.83" r="39.17" fill="#FFF" />
|
||||
<circle
|
||||
cx="47.5"
|
||||
cy="15.83"
|
||||
r="4.17"
|
||||
stroke="#4A90E2"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
stroke="#4A90E2"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M48.33 25c4.6 0 8.34-3.73 8.34-8.33"
|
||||
/>
|
||||
<circle
|
||||
cx="70"
|
||||
cy="40"
|
||||
r="6.67"
|
||||
stroke="#4A90E2"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<circle
|
||||
cx="19.17"
|
||||
cy="42.5"
|
||||
r="2.5"
|
||||
stroke="#4A90E2"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
stroke="#4A90E2"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M26.67 42.5a7.5 7.5 0 0 0-7.5-7.5m-7.5 7.5a7.5 7.5 0 0 0 7.5 7.5"
|
||||
/>
|
||||
<circle
|
||||
cx="53.33"
|
||||
cy="66.67"
|
||||
r="3.33"
|
||||
stroke="#4A90E2"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
stroke="#4A90E2"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M45 66.67c0 4.6 3.73 8.33 8.33 8.33"
|
||||
/>
|
||||
<circle cx="19.17" cy="65.83" r="2.5" fill="#4A90E2" />
|
||||
<circle cx="32.5" cy="10.83" r="2.5" fill="#4A90E2" />
|
||||
</g>
|
||||
<path
|
||||
fill="#DDE4EB"
|
||||
d="M309.1 302.8a19.4 19.4 0 0 0-19.4-19.4H177.14a19.4 19.4 0 0 0 0 38.8h11.65a19.4 19.4 0 1 1 0 38.8H63.63a19.4 19.4 0 0 1-19.4-19.4v-.48a18.92 18.92 0 0 1 18.92-18.92 18.92 18.92 0 0 0 18.91-18.92v-.48a19.4 19.4 0 0 0-19.4-19.4H38.4a19.4 19.4 0 0 1 0-38.8h87.33a19.4 19.4 0 0 0 0-38.8h-11.65a19.4 19.4 0 1 1 0-38.8h200.84a19.4 19.4 0 0 1 0 38.8h-36.87a19.4 19.4 0 0 0 0 38.8H390.6a19.4 19.4 0 0 1 0 38.8h-11.65a19.4 19.4 0 0 0-19.4 19.4v.48a18.92 18.92 0 0 0 18.92 18.92 18.92 18.92 0 0 1 18.92 18.92v.48a19.4 19.4 0 0 1-19.4 19.4h-99.94a19.4 19.4 0 0 1 0-38.8h11.65a19.4 19.4 0 0 0 19.4-19.4z"
|
||||
/>
|
||||
<g
|
||||
stroke-width="2"
|
||||
transform="rotate(-30 693.38531629 74.76561894)"
|
||||
>
|
||||
<rect
|
||||
width="72.05"
|
||||
height="22.36"
|
||||
x="25.68"
|
||||
y="7.09"
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
rx="11.18"
|
||||
/>
|
||||
<rect
|
||||
width="72.05"
|
||||
height="34.54"
|
||||
x="1"
|
||||
y="1"
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
rx="17.27"
|
||||
/>
|
||||
<ellipse
|
||||
cx="18.51"
|
||||
cy="18.27"
|
||||
stroke="#4A90E2"
|
||||
rx="17.51"
|
||||
ry="17.27"
|
||||
style="mix-blend-mode: multiply"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
fill="#E0EEFF"
|
||||
d="M120.056 377.079l27.34-15.999L196 398.001 168.66 414zM21.329 303.999L48.669 288l48.604 36.921-27.34 15.999z"
|
||||
opacity=".58"
|
||||
/>
|
||||
<path
|
||||
stroke="#0D2B3E"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M107.4 354.78l20.25 15.12-20.25-15.12zM75.76 332.1l10.12 7.56-10.12-7.56z"
|
||||
/>
|
||||
<path
|
||||
stroke="#4A90E2"
|
||||
stroke-width="2"
|
||||
d="M115.52 377.19l46.88 35.6 25.48-14.9-46.89-35.6-25.47 14.9zm58.96 27.99l-46.83-36.54 46.83 36.54zM16.8 304.11l46.88 35.6 25.47-14.9-46.88-35.6L16.8 304.1zm58.96 29.25l-46.84-36.54 46.84 36.54z"
|
||||
/>
|
||||
<path
|
||||
stroke="#8DDCFF"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M169.42 329.58a30.3 30.3 0 0 0-30.38-30.24m21.52 31.5a21.47 21.47 0 0 0-21.52-21.42"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
fill-rule="nonzero"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M363.8119003 126.44094699c2.51661541-6.60148066 6.09613872-22.6187686-2.2109135-24.895539-8.3168685-2.2748623-10.42540197 10.4614671-16.03584328 18.70341761-10.59995244 15.53804391-23.66683084 23.41606326-43.2306606 27.21888653-7.11679709 1.38336522-14.27332024 1.40937203-21.21489804.130389-6.98465907-1.29098322-13.7264579 3.27938314-15.03870592 10.20705267a12.7 12.7 0 0 0 10.24523714 14.86828894 82.91 82.91 0 0 0 30.8739962-.17423743c28.42792323-5.5258285 46.57886712-19.68941689 56.611788-46.05825832z"
|
||||
/>
|
||||
<path
|
||||
stroke="#7ED7FF"
|
||||
stroke-width="2"
|
||||
d="M361.7030833 130.52842676l-17.8049339-7.7449551"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
fill-rule="nonzero"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M266.54686983 241.69757868l16.77256314 26.0176618a41.79 41.79 0 0 1 54.9786567 8.5873516l8.01291943 14.232555 39.8983953-23.55575834-24.11546673-10.38943521a72.65 72.65 0 0 0-95.54897593-14.90219112z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
fill-rule="nonzero"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M237.8794903 217.16687444l-11.75143341 28.63844816a47.76 47.76 0 0 1 29.336647 40.02772592 47.59 47.59 0 0 1-9.23738334 32.6626813l-11.68590493 13.9867518 37.64817267 31.6885988-1.18280452-27.15319085a78.6 78.6 0 0 0 15.2180959-53.88374576 78.74 78.74 0 0 0-48.34538937-65.96726937z"
|
||||
/>
|
||||
<path
|
||||
fill="#E5F7FF"
|
||||
d="M212.38792337 148.28534705l-12.63354185 2.45571177 22.40860842 115.28229643 12.63354185-2.45571177z"
|
||||
/>
|
||||
<path
|
||||
stroke="#7ED7FF"
|
||||
stroke-width="2"
|
||||
d="M277.14421373 136.00361432l-58.67185675 11.40465366-8.69325783-44.72293448a29.885 29.885 0 1 1 58.67185675-11.40465366l8.69325783 44.72293448z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M266.45521183 136.46158627l-5.90935459-30.40099387c-1.90427377-9.79663929-11.36964991-16.19817461-21.13684039-14.2996251s-16.13355271 11.38765018-14.23118703 21.1744732l5.90935459 30.40099388"
|
||||
/>
|
||||
<path
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M199.41437503 150.51172155l87.71820511-17.05069183 22.52118573 115.86145646-87.71820512 17.05069183-22.52118572-115.86145646z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M287.13258014 133.46102972l22.52118573 115.86145646-75.09447954 14.59688815-22.52118572-115.86145646 75.09447953-14.59688815z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M291.89999763 154.8951691l6.86065974 4.63610266 9.58051965 49.28750088-49.1402568 9.55189831-6.86065974-4.63610267-9.58242774-49.29731715 49.1402568-9.55189831z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M298.87323467 160.1104318l-48.56109676 9.439321 9.46794235 48.70834085 48.56109676-9.439321z"
|
||||
/>
|
||||
<path
|
||||
stroke="#7ED7FF"
|
||||
stroke-width="2"
|
||||
d="M280.23508038 323.43753826l-27.22702477-17.43517197"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
fill-rule="nonzero"
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M172.12419639 259.31801112c2.31538206 6.67076501 9.8636495 21.23813123 17.69252677 17.64835676 7.83678545-3.60149884 1.26419427-14.7216987.27446604-24.63498464-1.87754554-18.72579298 3.06823431-33.16477708 15.6036019-48.65116657a56.95 56.95 0 0 1 16.17568934-13.73888912 12.91 12.91 0 0 0 4.954421-17.48662668 12.7 12.7 0 0 0-17.40510779-4.79708482 82.91 82.91 0 0 0-23.5305555 19.97687308c-18.2294437 22.51194976-23.0325701 45.02200255-13.76504176 71.68352199z"
|
||||
/>
|
||||
<path
|
||||
stroke="#7ED7FF"
|
||||
stroke-width="2"
|
||||
d="M189.24009043 249.42029566l-19.63254367 3.8161799"
|
||||
/>
|
||||
<path
|
||||
stroke="#0D2B3E"
|
||||
stroke-width="2"
|
||||
d="M275.89003883 328.33662123l-25.60278694-16.41637296m107.79044764-58.64485824l-21.48773566 20.47625982"
|
||||
/>
|
||||
<path
|
||||
stroke="#7ED7FF"
|
||||
stroke-width="2"
|
||||
d="M352.8061952 248.16739216l-21.48773565 20.47625982"
|
||||
/>
|
||||
<path
|
||||
stroke="#4A90E2"
|
||||
stroke-width="2"
|
||||
d="M293.81436735 168.98883038l-10.79789902 2.09889895 2.09889895 10.79789902 10.79789901-2.09889895z"
|
||||
/>
|
||||
<path
|
||||
stroke="#7ED7FF"
|
||||
stroke-width="2"
|
||||
d="M297.24892926 186.65811968l-4.90813592.95404498.95404498 4.90813592 4.90813592-.95404498zM285.46940306 188.94782763l-4.90813592.95404498.95404498 4.90813591 4.90813592-.95404497zM265.34717903 174.52229125l-4.90813592.95404498 6.48750584 33.37532423 4.90813592-.95404497zM276.14507804 172.4233923l-3.92650873.76323598.38161799 1.96325437 3.92650873-.76323598z"
|
||||
/>
|
||||
<circle cx="401" cy="44" r="6" fill="#9AC2F0" />
|
||||
<path
|
||||
stroke="#031836"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M398 53a6 6 0 1 0-6-6"
|
||||
/>
|
||||
<circle cx="90" cy="164" r="6" fill="#9AC2F0" />
|
||||
<path
|
||||
stroke="#031836"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M87 173a6 6 0 1 0-6-6"
|
||||
/>
|
||||
<path
|
||||
fill="#031836"
|
||||
d="M400.75 335h.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-.5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1zM416 349.75v.5a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-.5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zM401.25 365h-.5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zM386 350.25v-.5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v.5a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1zM14.75 202h.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-.5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1zM30 216.75v.5a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-.5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zM15.25 232h-.5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zM0 217.25v-.5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v.5a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1zM224.75 7h.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-.5a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1zM240 21.75v.5a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-.5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zM225.25 37h-.5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zM210 22.25v-.5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v.5a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1z"
|
||||
/>
|
||||
<circle cx="377" cy="173" r="4" fill="#031836" />
|
||||
<circle cx="255" cy="403" r="4" fill="#031836" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 class="text-error-title font-normal mb-1">404</h1>
|
||||
<p class="text-error-subtitle mb-6">{{ __('Whoops') }}…</p>
|
||||
<p class="text-error-message mb-8 leading-normal">
|
||||
{{
|
||||
__(
|
||||
"We're lost in space. The page you were trying to view does not exist."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<router-link
|
||||
:to="{ name: 'dashboard' }"
|
||||
class="dim btn btn-lg btn-default btn-white text-90 no-text-shadow tracking-wide uppercase"
|
||||
>
|
||||
{{ __('Go Home') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: 'Page Not Found',
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
1283
nova/resources/js/views/Index.vue
Executable file
1283
nova/resources/js/views/Index.vue
Executable file
File diff suppressed because it is too large
Load Diff
994
nova/resources/js/views/Lens.vue
Executable file
994
nova/resources/js/views/Lens.vue
Executable file
@@ -0,0 +1,994 @@
|
||||
<template>
|
||||
<loading-view :loading="initialLoading" :dusk="lens + '-lens-component'">
|
||||
<custom-lens-header class="mb-3" :resource-name="resourceName" />
|
||||
|
||||
<div v-if="shouldShowCards">
|
||||
<cards
|
||||
v-if="smallCards.length > 0"
|
||||
:cards="smallCards"
|
||||
class="mb-3"
|
||||
:resource-name="resourceName"
|
||||
:lens="lens"
|
||||
/>
|
||||
|
||||
<cards
|
||||
v-if="largeCards.length > 0"
|
||||
:cards="largeCards"
|
||||
size="large"
|
||||
:resource-name="resourceName"
|
||||
:lens="lens"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<heading v-if="resourceResponse" class="mb-3">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'index',
|
||||
params: {
|
||||
resourceName: resourceName,
|
||||
},
|
||||
}"
|
||||
class="no-underline text-primary font-bold dim"
|
||||
data-testid="lens-back"
|
||||
>←</router-link
|
||||
>
|
||||
|
||||
<span class="px-2 text-70">/</span> {{ lenseName }}
|
||||
</heading>
|
||||
|
||||
<card>
|
||||
<div class="py-3 flex items-center border-b border-50">
|
||||
<div class="px-3" v-if="shouldShowCheckBoxes">
|
||||
<!-- Select All -->
|
||||
<dropdown
|
||||
dusk="select-all-dropdown"
|
||||
placement="bottom-end"
|
||||
class="-mx-2"
|
||||
>
|
||||
<dropdown-trigger class="px-2">
|
||||
<fake-checkbox :checked="selectAllChecked" />
|
||||
</dropdown-trigger>
|
||||
|
||||
<dropdown-menu slot="menu" direction="ltr" width="250">
|
||||
<div class="p-4">
|
||||
<ul class="list-reset">
|
||||
<li class="flex items-center mb-4">
|
||||
<checkbox-with-label
|
||||
:checked="selectAllChecked"
|
||||
@input="toggleSelectAll"
|
||||
>
|
||||
{{ __('Select All') }}
|
||||
</checkbox-with-label>
|
||||
</li>
|
||||
<li
|
||||
class="flex items-center"
|
||||
v-if="allMatchingResourceCount > 0"
|
||||
>
|
||||
<checkbox-with-label
|
||||
dusk="select-all-matching-button"
|
||||
:checked="selectAllMatchingChecked"
|
||||
@input="toggleSelectAllMatching"
|
||||
>
|
||||
<template>
|
||||
<span class="mr-1">
|
||||
{{ __('Select All Matching') }} ({{
|
||||
allMatchingResourceCount
|
||||
}})
|
||||
</span>
|
||||
</template>
|
||||
</checkbox-with-label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</dropdown-menu>
|
||||
</dropdown>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center ml-auto px-3">
|
||||
<!-- Action Selector -->
|
||||
<action-selector
|
||||
v-if="selectedResources.length > 0 || haveStandaloneActions"
|
||||
:resource-name="resourceName"
|
||||
:actions="availableActions"
|
||||
:pivot-actions="pivotActions"
|
||||
:pivot-name="pivotName"
|
||||
:selected-resources="selectedResourcesForActionSelector"
|
||||
:endpoint="lensActionEndpoint"
|
||||
:query-string="{
|
||||
currentSearch,
|
||||
encodedFilters,
|
||||
currentTrashed,
|
||||
viaResource,
|
||||
viaResourceId,
|
||||
viaRelationship,
|
||||
}"
|
||||
@actionExecuted="getResources"
|
||||
/>
|
||||
|
||||
<filter-menu
|
||||
:resourceName="resourceName"
|
||||
:soft-deletes="softDeletes"
|
||||
:via-resource="viaResource"
|
||||
:via-has-one="viaHasOne"
|
||||
:trashed="trashed"
|
||||
:per-page="perPage"
|
||||
:per-page-options="
|
||||
perPageOptions || resourceInformation.perPageOptions
|
||||
"
|
||||
:show-trashed-option="
|
||||
authorizedToForceDeleteAnyResources ||
|
||||
authorizedToRestoreAnyResources
|
||||
"
|
||||
:lens="lens"
|
||||
@clear-selected-filters="clearSelectedFilters(lens)"
|
||||
@filter-changed="filterChanged"
|
||||
@trashed-changed="trashedChanged"
|
||||
@per-page-changed="updatePerPageChanged"
|
||||
/>
|
||||
|
||||
<delete-menu
|
||||
v-if="shouldShowDeleteMenu"
|
||||
dusk="delete-menu"
|
||||
:soft-deletes="softDeletes"
|
||||
:resources="resources"
|
||||
:selected-resources="selectedResources"
|
||||
:via-many-to-many="viaManyToMany"
|
||||
:all-matching-resource-count="allMatchingResourceCount"
|
||||
:all-matching-selected="selectAllMatchingChecked"
|
||||
:authorized-to-delete-selected-resources="
|
||||
authorizedToDeleteSelectedResources
|
||||
"
|
||||
:authorized-to-force-delete-selected-resources="
|
||||
authorizedToForceDeleteSelectedResources
|
||||
"
|
||||
:authorized-to-delete-any-resources="authorizedToDeleteAnyResources"
|
||||
:authorized-to-force-delete-any-resources="
|
||||
authorizedToForceDeleteAnyResources
|
||||
"
|
||||
:authorized-to-restore-selected-resources="
|
||||
authorizedToRestoreSelectedResources
|
||||
"
|
||||
:authorized-to-restore-any-resources="
|
||||
authorizedToRestoreAnyResources
|
||||
"
|
||||
@deleteSelected="deleteSelectedResources"
|
||||
@deleteAllMatching="deleteAllMatchingResources"
|
||||
@forceDeleteSelected="forceDeleteSelectedResources"
|
||||
@forceDeleteAllMatching="forceDeleteAllMatchingResources"
|
||||
@restoreSelected="restoreSelectedResources"
|
||||
@restoreAllMatching="restoreAllMatchingResources"
|
||||
@close="deleteModalOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<loading-view :loading="loading">
|
||||
<div
|
||||
v-if="!resources.length"
|
||||
class="flex justify-center items-center px-6 py-8"
|
||||
>
|
||||
<div class="text-center">
|
||||
<svg
|
||||
class="mb-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="65"
|
||||
height="51"
|
||||
viewBox="0 0 65 51"
|
||||
>
|
||||
<path
|
||||
fill="#A8B9C5"
|
||||
d="M56 40h2c.552285 0 1 .447715 1 1s-.447715 1-1 1h-2v2c0 .552285-.447715 1-1 1s-1-.447715-1-1v-2h-2c-.552285 0-1-.447715-1-1s.447715-1 1-1h2v-2c0-.552285.447715-1 1-1s1 .447715 1 1v2zm-5.364125-8H38v8h7.049375c.350333-3.528515 2.534789-6.517471 5.5865-8zm-5.5865 10H6c-3.313708 0-6-2.686292-6-6V6c0-3.313708 2.686292-6 6-6h44c3.313708 0 6 2.686292 6 6v25.049375C61.053323 31.5511 65 35.814652 65 41c0 5.522847-4.477153 10-10 10-5.185348 0-9.4489-3.946677-9.950625-9zM20 30h16v-8H20v8zm0 2v8h16v-8H20zm34-2v-8H38v8h16zM2 30h16v-8H2v8zm0 2v4c0 2.209139 1.790861 4 4 4h12v-8H2zm18-12h16v-8H20v8zm34 0v-8H38v8h16zM2 20h16v-8H2v8zm52-10V6c0-2.209139-1.790861-4-4-4H6C3.790861 2 2 3.790861 2 6v4h52zm1 39c4.418278 0 8-3.581722 8-8s-3.581722-8-8-8-8 3.581722-8 8 3.581722 8 8 8z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h3 class="text-base text-80 font-normal mb-6">
|
||||
{{
|
||||
__('No :resource matched the given criteria.', {
|
||||
resource: resourceInformation.label.toLowerCase(),
|
||||
})
|
||||
}}
|
||||
</h3>
|
||||
|
||||
<create-resource-button
|
||||
classes="btn btn-sm btn-outline"
|
||||
:singular-name="singularName"
|
||||
:resource-name="resourceName"
|
||||
:via-resource="viaResource"
|
||||
:via-resource-id="viaResourceId"
|
||||
:via-relationship="viaRelationship"
|
||||
:relationship-type="relationshipType"
|
||||
:authorized-to-create="authorizedToCreate && !resourceIsFull"
|
||||
:authorized-to-relate="authorizedToRelate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resource Table -->
|
||||
<div class="overflow-hidden overflow-x-auto relative">
|
||||
<resource-table
|
||||
:authorized-to-relate="authorizedToRelate"
|
||||
:resource-name="resourceName"
|
||||
:resources="resources"
|
||||
:singular-name="singularName"
|
||||
:selected-resources="selectedResources"
|
||||
:selected-resource-ids="selectedResourceIds"
|
||||
:actions-are-available="allActions.length > 0"
|
||||
:actions-endpoint="lensActionEndpoint"
|
||||
:should-show-checkboxes="shouldShowCheckBoxes"
|
||||
:via-resource="viaResource"
|
||||
:via-resource-id="viaResourceId"
|
||||
:via-relationship="viaRelationship"
|
||||
:relationship-type="relationshipType"
|
||||
:update-selection-status="updateSelectionStatus"
|
||||
@order="orderByField"
|
||||
@reset-order-by="resetOrderBy"
|
||||
@delete="deleteResources"
|
||||
@restore="restoreResources"
|
||||
@actionExecuted="getResources"
|
||||
ref="resourceTable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<component
|
||||
:is="paginationComponent"
|
||||
v-if="resourceResponse && resources.length > 0"
|
||||
:next="hasNextPage"
|
||||
:previous="hasPreviousPage"
|
||||
@load-more="loadMore"
|
||||
@page="selectPage"
|
||||
:pages="totalPages"
|
||||
:page="currentPage"
|
||||
:per-page="perPage"
|
||||
:resource-count-label="resourceCountLabel"
|
||||
:current-resource-count="resources.length"
|
||||
:all-matching-resource-count="allMatchingResourceCount"
|
||||
>
|
||||
<span
|
||||
v-if="resourceCountLabel"
|
||||
class="text-sm text-80 px-4"
|
||||
:class="{
|
||||
'ml-auto': paginationComponent == 'pagination-links',
|
||||
}"
|
||||
>
|
||||
{{ resourceCountLabel }}
|
||||
</span>
|
||||
</component>
|
||||
</loading-view>
|
||||
</card>
|
||||
</loading-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
HasCards,
|
||||
Deletable,
|
||||
Errors,
|
||||
Filterable,
|
||||
Minimum,
|
||||
Paginatable,
|
||||
PerPageable,
|
||||
InteractsWithQueryString,
|
||||
InteractsWithResourceInformation,
|
||||
} from 'laravel-nova'
|
||||
import HasActions from '@/mixins/HasActions'
|
||||
import { CancelToken, Cancel } from 'axios'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
HasActions,
|
||||
HasCards,
|
||||
Deletable,
|
||||
Filterable,
|
||||
Paginatable,
|
||||
PerPageable,
|
||||
InteractsWithResourceInformation,
|
||||
InteractsWithQueryString,
|
||||
],
|
||||
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.lenseName,
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
resourceName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
viaResource: {
|
||||
default: '',
|
||||
},
|
||||
|
||||
viaResourceId: {
|
||||
default: '',
|
||||
},
|
||||
|
||||
viaRelationship: {
|
||||
default: '',
|
||||
},
|
||||
|
||||
relationshipType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
lens: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
canceller: null,
|
||||
initialLoading: true,
|
||||
loading: true,
|
||||
|
||||
resourceResponse: null,
|
||||
resources: [],
|
||||
softDeletes: false,
|
||||
selectedResources: [],
|
||||
selectAllMatchingResources: false,
|
||||
allMatchingResourceCount: 0,
|
||||
hasId: false,
|
||||
|
||||
deleteModalOpen: false,
|
||||
|
||||
actionValidationErrors: new Errors(),
|
||||
|
||||
authorizedToRelate: false,
|
||||
|
||||
orderBy: '',
|
||||
orderByDirection: '',
|
||||
trashed: '',
|
||||
|
||||
// Load More Pagination
|
||||
currentPageLoadMore: null,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Mount the component and retrieve its initial data.
|
||||
*/
|
||||
async created() {
|
||||
if (Nova.missingResource(this.resourceName))
|
||||
return this.$router.push({ name: '404' })
|
||||
|
||||
this.initializeSearchFromQueryString()
|
||||
this.initializePerPageFromQueryString()
|
||||
this.initializeTrashedFromQueryString()
|
||||
this.initializeOrderingFromQueryString()
|
||||
|
||||
await this.initializeFilters(this.lens)
|
||||
this.getResources()
|
||||
// this.getAuthorizationToRelate()
|
||||
this.getActions()
|
||||
|
||||
this.initialLoading = false
|
||||
|
||||
this.$watch(
|
||||
() => {
|
||||
return (
|
||||
this.lens +
|
||||
this.resourceName +
|
||||
this.encodedFilters +
|
||||
this.currentSearch +
|
||||
this.currentPage +
|
||||
this.currentPerPage +
|
||||
this.currentOrderBy +
|
||||
this.currentOrderByDirection +
|
||||
this.currentTrashed
|
||||
)
|
||||
},
|
||||
() => {
|
||||
if (this.canceller !== null) this.canceller()
|
||||
|
||||
this.getResources()
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (
|
||||
to.params.resourceName === from.params.resourceName &&
|
||||
to.params.lens === from.params.lens
|
||||
) {
|
||||
this.initializeState(this.lens)
|
||||
} else {
|
||||
this.initializeFilters(this.lens)
|
||||
this.getActions()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectAllResources() {
|
||||
this.selectedResources = this.resources.slice(0)
|
||||
},
|
||||
|
||||
toggleSelectAll() {
|
||||
if (this.selectAllChecked) return this.clearResourceSelections()
|
||||
this.selectAllResources()
|
||||
},
|
||||
|
||||
toggleSelectAllMatching() {
|
||||
if (!this.selectAllMatchingResources) {
|
||||
this.selectAllResources()
|
||||
this.selectAllMatchingResources = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.selectAllMatchingResources = false
|
||||
},
|
||||
|
||||
/*
|
||||
* Update the resource selection status
|
||||
*/
|
||||
updateSelectionStatus(resource) {
|
||||
if (!_(this.selectedResources).includes(resource))
|
||||
return this.selectedResources.push(resource)
|
||||
const index = this.selectedResources.indexOf(resource)
|
||||
if (index > -1) return this.selectedResources.splice(index, 1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the resources based on the current page, search, filters, etc.
|
||||
*/
|
||||
getResources() {
|
||||
this.loading = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.clearResourceSelections()
|
||||
|
||||
return Minimum(
|
||||
Nova.request().get(
|
||||
'/nova-api/' + this.resourceName + '/lens/' + this.lens,
|
||||
{
|
||||
params: this.resourceRequestQueryString,
|
||||
cancelToken: new CancelToken(canceller => {
|
||||
this.canceller = canceller
|
||||
}),
|
||||
}
|
||||
),
|
||||
300
|
||||
)
|
||||
.then(({ data }) => {
|
||||
this.resources = []
|
||||
|
||||
this.resourceResponse = data
|
||||
this.resources = data.resources
|
||||
this.softDeletes = data.softDeletes
|
||||
this.perPage = data.per_page
|
||||
this.hasId = data.hasId
|
||||
|
||||
this.loading = false
|
||||
|
||||
this.getAllMatchingResourceCount()
|
||||
|
||||
Nova.$emit('resources-loaded')
|
||||
})
|
||||
.catch(e => {
|
||||
if (e instanceof Cancel) {
|
||||
return
|
||||
}
|
||||
|
||||
throw e
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the actions available for the current resource.
|
||||
*/
|
||||
getActions() {
|
||||
this.actions = []
|
||||
this.pivotActions = null
|
||||
Nova.request()
|
||||
.get(`/nova-api/${this.resourceName}/lens/${this.lens}/actions`, {
|
||||
params: {
|
||||
viaResource: this.viaResource,
|
||||
viaResourceId: this.viaResourceId,
|
||||
viaRelationship: this.viaRelationship,
|
||||
relationshipType: this.relationshipType,
|
||||
display: 'index',
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
this.actions = response.data.actions
|
||||
this.pivotActions = response.data.pivotActions
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the selected resouces and the "select all" states.
|
||||
*/
|
||||
clearResourceSelections() {
|
||||
this.selectAllMatchingResources = false
|
||||
this.selectedResources = []
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the count of all of the matching resources.
|
||||
*/
|
||||
getAllMatchingResourceCount() {
|
||||
Nova.request()
|
||||
.get(
|
||||
'/nova-api/' + this.resourceName + '/lens/' + this.lens + '/count',
|
||||
{
|
||||
params: this.resourceRequestQueryString,
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
this.allMatchingResourceCount = response.data.count
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Sort the resources by the given field.
|
||||
*/
|
||||
orderByField(field) {
|
||||
let direction = this.currentOrderByDirection == 'asc' ? 'desc' : 'asc'
|
||||
|
||||
if (this.currentOrderBy != field.sortableUriKey) {
|
||||
direction = 'asc'
|
||||
}
|
||||
|
||||
this.updateQueryString({
|
||||
[this.orderByParameter]: field.sortableUriKey,
|
||||
[this.orderByDirectionParameter]: direction,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the order by to its default state
|
||||
*/
|
||||
resetOrderBy(field) {
|
||||
this.updateQueryString({
|
||||
[this.orderByParameter]: field.sortableUriKey,
|
||||
[this.orderByDirectionParameter]: null,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync the current search value from the query string.
|
||||
*/
|
||||
initializeSearchFromQueryString() {
|
||||
this.search = this.currentSearch
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync the current order by values from the query string.
|
||||
*/
|
||||
initializeOrderingFromQueryString() {
|
||||
this.orderBy = this.currentOrderBy
|
||||
this.orderByDirection = this.currentOrderByDirection
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync the trashed state values from the query string.
|
||||
*/
|
||||
initializeTrashedFromQueryString() {
|
||||
this.trashed = this.currentTrashed
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the trashed constraint for the resource listing.
|
||||
*/
|
||||
trashedChanged(trashedStatus) {
|
||||
this.trashed = trashedStatus
|
||||
this.updateQueryString({ [this.trashedParameter]: this.trashed })
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the per page parameter in the query string
|
||||
*/
|
||||
updatePerPageChanged(perPage) {
|
||||
this.perPage = perPage
|
||||
this.perPageChanged()
|
||||
},
|
||||
|
||||
/**
|
||||
* Load more resources.
|
||||
*/
|
||||
loadMore() {
|
||||
if (this.currentPageLoadMore === null) {
|
||||
this.currentPageLoadMore = this.currentPage
|
||||
}
|
||||
|
||||
this.currentPageLoadMore = this.currentPageLoadMore + 1
|
||||
|
||||
return Minimum(
|
||||
Nova.request().get(
|
||||
'/nova-api/' + this.resourceName + '/lens/' + this.lens,
|
||||
{
|
||||
params: {
|
||||
...this.resourceRequestQueryString,
|
||||
page: this.currentPageLoadMore, // We do this to override whatever page number is in the URL
|
||||
},
|
||||
}
|
||||
),
|
||||
300
|
||||
).then(({ data }) => {
|
||||
this.resourceResponse = data
|
||||
this.resources = [...this.resources, ...data.resources]
|
||||
|
||||
this.getAllMatchingResourceCount()
|
||||
|
||||
Nova.$emit('resources-loaded')
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Select the next page.
|
||||
*/
|
||||
selectPage(page) {
|
||||
this.updateQueryString({ [this.pageParameter]: page })
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync the per page values from the query string.
|
||||
*/
|
||||
initializePerPageFromQueryString() {
|
||||
this.perPage =
|
||||
this.$route.query[this.perPageParameter] ||
|
||||
this.resourceInformation.perPageOptions[0]
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the endpoint for this resource's actions.
|
||||
*/
|
||||
lensActionEndpoint() {
|
||||
return `/nova-api/${this.resourceName}/lens/${this.lens}/action`
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the name of the search query string variable.
|
||||
*/
|
||||
searchParameter() {
|
||||
return this.resourceName + '_search'
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the name of the order by query string variable.
|
||||
*/
|
||||
orderByParameter() {
|
||||
return this.resourceName + '_order'
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the name of the order by direction query string variable.
|
||||
*/
|
||||
orderByDirectionParameter() {
|
||||
return this.resourceName + '_direction'
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the name of the trashed constraint query string variable.
|
||||
*/
|
||||
trashedParameter() {
|
||||
return this.resourceName + '_trashed'
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the name of the per page query string variable.
|
||||
*/
|
||||
perPageParameter() {
|
||||
return this.resourceName + '_per_page'
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the name of the page query string variable.
|
||||
*/
|
||||
pageParameter() {
|
||||
return this.resourceName + '_page'
|
||||
},
|
||||
|
||||
/**
|
||||
* Build the resource request query string.
|
||||
*/
|
||||
resourceRequestQueryString() {
|
||||
return {
|
||||
search: this.currentSearch,
|
||||
filters: this.encodedFilters,
|
||||
orderBy: this.currentOrderBy,
|
||||
orderByDirection: this.currentOrderByDirection,
|
||||
perPage: this.currentPerPage,
|
||||
page: this.currentPage,
|
||||
viaResource: this.viaResource,
|
||||
viaResourceId: this.viaResourceId,
|
||||
// viaRelationship: this.viaRelationship,
|
||||
viaResourceRelationship: this.viaResourceRelationship,
|
||||
relationshipType: this.relationshipType,
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if all resources are selected.
|
||||
*/
|
||||
selectAllChecked() {
|
||||
return this.selectedResources.length == this.resources.length
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if all matching resources are selected.
|
||||
*/
|
||||
selectAllMatchingChecked() {
|
||||
return (
|
||||
this.selectedResources.length == this.resources.length &&
|
||||
this.selectAllMatchingResources
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the IDs for the selected resources.
|
||||
*/
|
||||
selectedResourceIds() {
|
||||
return _.map(this.selectedResources, resource => resource.id.value)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current search value from the query string.
|
||||
*/
|
||||
currentSearch() {
|
||||
return this.$route.query[this.searchParameter] || ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current order by value from the query string.
|
||||
*/
|
||||
currentOrderBy() {
|
||||
return this.$route.query[this.orderByParameter] || ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current order by direction from the query string.
|
||||
*/
|
||||
currentOrderByDirection() {
|
||||
return this.$route.query[this.orderByDirectionParameter] || 'desc'
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current trashed constraint value from the query string.
|
||||
*/
|
||||
currentTrashed() {
|
||||
return this.$route.query[this.trashedParameter] || ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the current resource listing is via a many-to-many relationship.
|
||||
*/
|
||||
viaManyToMany() {
|
||||
return (
|
||||
this.relationshipType == 'belongsToMany' ||
|
||||
this.relationshipType == 'morphToMany'
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the resource / relationship is "full".
|
||||
*/
|
||||
resourceIsFull() {
|
||||
return this.viaHasOne && this.resources.length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the current resource listing is via a has-one relationship.
|
||||
*/
|
||||
viaHasOne() {
|
||||
return (
|
||||
this.relationshipType == 'hasOne' || this.relationshipType == 'morphOne'
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the singular name for the resource
|
||||
*/
|
||||
singularName() {
|
||||
return this.resourceInformation.singularLabel
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if there are any resources for the view
|
||||
*/
|
||||
hasResources() {
|
||||
return Boolean(this.resources.length > 0)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the resource should show any cards
|
||||
*/
|
||||
shouldShowCards() {
|
||||
return this.cards.length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the endpoint for this resource's metrics.
|
||||
*/
|
||||
cardsEndpoint() {
|
||||
return `/nova-api/${this.resourceName}/lens/${this.lens}/cards`
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine whether to show the selection checkboxes for resources
|
||||
*/
|
||||
shouldShowCheckBoxes() {
|
||||
return (
|
||||
Boolean(this.hasResources && !this.viaHasOne) &&
|
||||
Boolean(
|
||||
this.actionsAreAvailable ||
|
||||
this.authorizedToDeleteAnyResources ||
|
||||
this.canShowDeleteMenu
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine whether the delete menu should be shown to the user
|
||||
*/
|
||||
shouldShowDeleteMenu() {
|
||||
return (
|
||||
Boolean(this.selectedResources.length > 0) && this.canShowDeleteMenu
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if any selected resources may be deleted.
|
||||
*/
|
||||
authorizedToDeleteSelectedResources() {
|
||||
return Boolean(
|
||||
_.find(this.selectedResources, resource => resource.authorizedToDelete)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if any selected resources may be force deleted.
|
||||
*/
|
||||
authorizedToForceDeleteSelectedResources() {
|
||||
return Boolean(
|
||||
_.find(
|
||||
this.selectedResources,
|
||||
resource => resource.authorizedToForceDelete
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to delete any listed resource.
|
||||
*/
|
||||
authorizedToDeleteAnyResources() {
|
||||
return (
|
||||
this.resources.length > 0 &&
|
||||
Boolean(_.find(this.resources, resource => resource.authorizedToDelete))
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to force delete any listed resource.
|
||||
*/
|
||||
authorizedToForceDeleteAnyResources() {
|
||||
return (
|
||||
this.resources.length > 0 &&
|
||||
Boolean(
|
||||
_.find(this.resources, resource => resource.authorizedToForceDelete)
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if any selected resources may be restored.
|
||||
*/
|
||||
authorizedToRestoreSelectedResources() {
|
||||
return Boolean(
|
||||
_.find(this.selectedResources, resource => resource.authorizedToRestore)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to restore any listed resource.
|
||||
*/
|
||||
authorizedToRestoreAnyResources() {
|
||||
return (
|
||||
this.resources.length > 0 &&
|
||||
Boolean(
|
||||
_.find(this.resources, resource => resource.authorizedToRestore)
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine whether the user is authorized to perform actions on the delete menu
|
||||
*/
|
||||
canShowDeleteMenu() {
|
||||
return (
|
||||
this.hasId &&
|
||||
Boolean(
|
||||
this.authorizedToDeleteSelectedResources ||
|
||||
this.authorizedToForceDeleteSelectedResources ||
|
||||
this.authorizedToDeleteAnyResources ||
|
||||
this.authorizedToForceDeleteAnyResources ||
|
||||
this.authorizedToRestoreSelectedResources ||
|
||||
this.authorizedToRestoreAnyResources
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the currently encoded filter string from the store
|
||||
*/
|
||||
encodedFilters() {
|
||||
return this.$store.getters[`${this.resourceName}/currentEncodedFilters`]
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the initial encoded filters from the query string
|
||||
*/
|
||||
initialEncodedFilters() {
|
||||
return this.$route.query[this.filterParameter] || ''
|
||||
},
|
||||
|
||||
paginationComponent() {
|
||||
return `pagination-${Nova.config['pagination'] || 'links'}`
|
||||
},
|
||||
|
||||
hasNextPage() {
|
||||
return Boolean(
|
||||
this.resourceResponse && this.resourceResponse.next_page_url
|
||||
)
|
||||
},
|
||||
|
||||
hasPreviousPage() {
|
||||
return Boolean(
|
||||
this.resourceResponse && this.resourceResponse.prev_page_url
|
||||
)
|
||||
},
|
||||
|
||||
totalPages() {
|
||||
return Math.ceil(this.allMatchingResourceCount / this.currentPerPage)
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the resource count label
|
||||
*/
|
||||
resourceCountLabel() {
|
||||
const first = this.perPage * (this.currentPage - 1)
|
||||
|
||||
return (
|
||||
this.resources.length &&
|
||||
`${first + 1}-${first + this.resources.length} ${this.__('of')} ${
|
||||
this.allMatchingResourceCount
|
||||
}`
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current per page value from the query string.
|
||||
*/
|
||||
currentPerPage() {
|
||||
return this.perPage
|
||||
},
|
||||
|
||||
/**
|
||||
* The per-page options configured for this resource.
|
||||
*/
|
||||
perPageOptions() {
|
||||
if (this.resourceResponse) {
|
||||
return this.resourceResponse.per_page_options
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The Lense name.
|
||||
*/
|
||||
lenseName() {
|
||||
if (this.resourceResponse) {
|
||||
return this.resourceResponse.name
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
357
nova/resources/js/views/Update.vue
Executable file
357
nova/resources/js/views/Update.vue
Executable file
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<loading-view :loading="loading">
|
||||
<custom-update-header
|
||||
class="mb-3"
|
||||
:resource-name="resourceName"
|
||||
:resource-id="resourceId"
|
||||
/>
|
||||
|
||||
<form
|
||||
v-if="panels"
|
||||
@submit="submitViaUpdateResource"
|
||||
@change="onUpdateFormStatus"
|
||||
autocomplete="off"
|
||||
ref="form"
|
||||
>
|
||||
<form-panel
|
||||
v-for="panel in panelsWithFields"
|
||||
@update-last-retrieved-at-timestamp="updateLastRetrievedAtTimestamp"
|
||||
@field-changed="onUpdateFormStatus"
|
||||
@file-upload-started="handleFileUploadStarted"
|
||||
@file-upload-finished="handleFileUploadFinished"
|
||||
:panel="panel"
|
||||
:name="panel.name"
|
||||
:key="panel.name"
|
||||
:resource-id="resourceId"
|
||||
:resource-name="resourceName"
|
||||
:fields="panel.fields"
|
||||
mode="form"
|
||||
class="mb-8"
|
||||
:validation-errors="validationErrors"
|
||||
:via-resource="viaResource"
|
||||
:via-resource-id="viaResourceId"
|
||||
:via-relationship="viaRelationship"
|
||||
/>
|
||||
|
||||
<!-- Update Button -->
|
||||
<div class="flex items-center">
|
||||
<cancel-button @click="$router.back()" />
|
||||
|
||||
<progress-button
|
||||
class="mr-3"
|
||||
dusk="update-and-continue-editing-button"
|
||||
@click.native="submitViaUpdateResourceAndContinueEditing"
|
||||
:disabled="isWorking"
|
||||
:processing="wasSubmittedViaUpdateResourceAndContinueEditing"
|
||||
>
|
||||
{{ __('Update & Continue Editing') }}
|
||||
</progress-button>
|
||||
|
||||
<progress-button
|
||||
dusk="update-button"
|
||||
type="submit"
|
||||
:disabled="isWorking"
|
||||
:processing="wasSubmittedViaUpdateResource"
|
||||
>
|
||||
{{ updateButtonLabel }}
|
||||
</progress-button>
|
||||
</div>
|
||||
</form>
|
||||
</loading-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapProps,
|
||||
Errors,
|
||||
InteractsWithResourceInformation,
|
||||
PreventsFormAbandonment,
|
||||
} from 'laravel-nova'
|
||||
import HandlesUploads from '@/mixins/HandlesUploads'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
InteractsWithResourceInformation,
|
||||
HandlesUploads,
|
||||
PreventsFormAbandonment,
|
||||
],
|
||||
|
||||
metaInfo() {
|
||||
if (this.resourceInformation && this.title) {
|
||||
return {
|
||||
title: this.__('Update :resource: :title', {
|
||||
resource: this.resourceInformation.singularLabel,
|
||||
title: this.title,
|
||||
}),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
props: mapProps([
|
||||
'resourceName',
|
||||
'resourceId',
|
||||
'viaResource',
|
||||
'viaResourceId',
|
||||
'viaRelationship',
|
||||
]),
|
||||
|
||||
data: () => ({
|
||||
relationResponse: null,
|
||||
loading: true,
|
||||
submittedViaUpdateResourceAndContinueEditing: false,
|
||||
submittedViaUpdateResource: false,
|
||||
title: null,
|
||||
fields: [],
|
||||
panels: [],
|
||||
validationErrors: new Errors(),
|
||||
lastRetrievedAt: null,
|
||||
}),
|
||||
|
||||
async created() {
|
||||
if (Nova.missingResource(this.resourceName))
|
||||
return this.$router.push({ name: '404' })
|
||||
|
||||
// If this update is via a relation index, then let's grab the field
|
||||
// and use the label for that as the one we use for the title and buttons
|
||||
if (this.isRelation) {
|
||||
const { data } = await Nova.request(
|
||||
`/nova-api/${this.viaResource}/field/${this.viaRelationship}`
|
||||
)
|
||||
this.relationResponse = data
|
||||
}
|
||||
|
||||
this.getFields()
|
||||
this.updateLastRetrievedAtTimestamp()
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (
|
||||
from.params.resourceName === to.params.resourceName &&
|
||||
from.params.resourceId !== to.params.resourceId
|
||||
) {
|
||||
this.getFields()
|
||||
this.validationErrors = new Errors()
|
||||
this.submittedViaUpdateResource = false
|
||||
this.submittedViaUpdateResourceAndContinueEditing = false
|
||||
this.isWorking = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the available fields for the resource.
|
||||
*/
|
||||
async getFields() {
|
||||
this.loading = true
|
||||
|
||||
this.panels = []
|
||||
this.fields = []
|
||||
|
||||
const {
|
||||
data: { title, panels, fields },
|
||||
} = await Nova.request()
|
||||
.get(
|
||||
`/nova-api/${this.resourceName}/${this.resourceId}/update-fields`,
|
||||
{
|
||||
params: {
|
||||
editing: true,
|
||||
editMode: 'update',
|
||||
viaResource: this.viaResource,
|
||||
viaResourceId: this.viaResourceId,
|
||||
viaRelationship: this.viaRelationship,
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
if (error.response.status == 404) {
|
||||
this.$router.push({ name: '404' })
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
this.title = title
|
||||
this.panels = panels
|
||||
this.fields = fields
|
||||
this.loading = false
|
||||
|
||||
Nova.$emit('resource-loaded')
|
||||
},
|
||||
|
||||
async submitViaUpdateResource(e) {
|
||||
e.preventDefault()
|
||||
this.submittedViaUpdateResource = true
|
||||
this.submittedViaUpdateResourceAndContinueEditing = false
|
||||
this.canLeave = true
|
||||
await this.updateResource()
|
||||
},
|
||||
|
||||
async submitViaUpdateResourceAndContinueEditing() {
|
||||
this.submittedViaUpdateResourceAndContinueEditing = true
|
||||
this.submittedViaUpdateResource = false
|
||||
this.canLeave = true
|
||||
await this.updateResource()
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the resource using the provided data.
|
||||
*/
|
||||
async updateResource() {
|
||||
this.isWorking = true
|
||||
|
||||
if (this.$refs.form.reportValidity()) {
|
||||
try {
|
||||
const {
|
||||
data: { redirect, id },
|
||||
} = await this.updateRequest()
|
||||
|
||||
Nova.success(
|
||||
this.__('The :resource was updated!', {
|
||||
resource: this.resourceInformation.singularLabel.toLowerCase(),
|
||||
})
|
||||
)
|
||||
|
||||
await this.updateLastRetrievedAtTimestamp()
|
||||
|
||||
if (this.submittedViaUpdateResource) {
|
||||
this.$router.push({ path: redirect }, () => {
|
||||
window.scrollTo(0, 0)
|
||||
})
|
||||
} else {
|
||||
if (id != this.resourceId) {
|
||||
this.$router.push({
|
||||
name: 'edit',
|
||||
params: {
|
||||
resourceId: id,
|
||||
resourceName: this.resourceName,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Reset the form by refetching the fields
|
||||
this.getFields()
|
||||
|
||||
this.validationErrors = new Errors()
|
||||
this.submittedViaUpdateResource = false
|
||||
this.submittedViaUpdateResourceAndContinueEditing = false
|
||||
this.isWorking = false
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
window.scrollTo(0, 0)
|
||||
|
||||
this.submittedViaUpdateResource = false
|
||||
this.submittedViaUpdateResourceAndContinueEditing = false
|
||||
|
||||
if (this.resourceInformation.preventFormAbandonment) {
|
||||
this.canLeave = false
|
||||
}
|
||||
|
||||
if (error.response.status == 422) {
|
||||
this.validationErrors = new Errors(error.response.data.errors)
|
||||
Nova.error(this.__('There was a problem submitting the form.'))
|
||||
}
|
||||
|
||||
if (error.response.status == 409) {
|
||||
Nova.error(
|
||||
this.__(
|
||||
'Another user has updated this resource since this page was loaded. Please refresh the page and try again.'
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.submittedViaUpdateResource = false
|
||||
this.submittedViaUpdateResourceAndContinueEditing = false
|
||||
this.isWorking = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Send an update request for this resource
|
||||
*/
|
||||
updateRequest() {
|
||||
return Nova.request().post(
|
||||
`/nova-api/${this.resourceName}/${this.resourceId}`,
|
||||
this.updateResourceFormData,
|
||||
{
|
||||
params: {
|
||||
viaResource: this.viaResource,
|
||||
viaResourceId: this.viaResourceId,
|
||||
viaRelationship: this.viaRelationship,
|
||||
editing: true,
|
||||
editMode: 'update',
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the last retrieved at timestamp to the current UNIX timestamp.
|
||||
*/
|
||||
updateLastRetrievedAtTimestamp() {
|
||||
this.lastRetrievedAt = Math.floor(new Date().getTime() / 1000)
|
||||
},
|
||||
|
||||
/**
|
||||
* Prevent accidental abandonment only if form was changed.
|
||||
*/
|
||||
onUpdateFormStatus() {
|
||||
if (this.resourceInformation.preventFormAbandonment) {
|
||||
this.updateFormStatus()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
wasSubmittedViaUpdateResourceAndContinueEditing() {
|
||||
return this.isWorking && this.submittedViaUpdateResourceAndContinueEditing
|
||||
},
|
||||
|
||||
wasSubmittedViaUpdateResource() {
|
||||
return this.isWorking && this.submittedViaUpdateResource
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the form data for creating the resource.
|
||||
*/
|
||||
updateResourceFormData() {
|
||||
return _.tap(new FormData(), formData => {
|
||||
_(this.fields).each(field => {
|
||||
field.fill(formData)
|
||||
})
|
||||
|
||||
formData.append('_method', 'PUT')
|
||||
formData.append('_retrieved_at', this.lastRetrievedAt)
|
||||
})
|
||||
},
|
||||
|
||||
singularName() {
|
||||
if (this.relationResponse) {
|
||||
return this.relationResponse.singularLabel
|
||||
}
|
||||
|
||||
return this.resourceInformation.singularLabel
|
||||
},
|
||||
|
||||
updateButtonLabel() {
|
||||
return this.resourceInformation.updateButtonLabel
|
||||
},
|
||||
|
||||
isRelation() {
|
||||
return Boolean(this.viaResourceId && this.viaRelationship)
|
||||
},
|
||||
|
||||
panelsWithFields() {
|
||||
return _.map(this.panels, panel => {
|
||||
return {
|
||||
...panel,
|
||||
fields: _.filter(this.fields, field => field.panel == panel.name),
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
541
nova/resources/js/views/UpdateAttached.vue
Executable file
541
nova/resources/js/views/UpdateAttached.vue
Executable file
@@ -0,0 +1,541 @@
|
||||
<template>
|
||||
<loading-view :loading="loading">
|
||||
<custom-update-attached-header
|
||||
class="mb-3"
|
||||
:resource-name="resourceName"
|
||||
:resource-id="resourceId"
|
||||
/>
|
||||
|
||||
<heading class="mb-3" v-if="relatedResourceLabel && title">
|
||||
{{
|
||||
__('Update attached :resource: :title', {
|
||||
resource: relatedResourceLabel,
|
||||
title: title,
|
||||
})
|
||||
}}
|
||||
</heading>
|
||||
|
||||
<form
|
||||
v-if="field"
|
||||
@submit.prevent="updateAttachedResource"
|
||||
@change="onUpdateFormStatus"
|
||||
autocomplete="off"
|
||||
>
|
||||
<card class="overflow-hidden mb-8">
|
||||
<!-- Related Resource -->
|
||||
<div
|
||||
v-if="viaResourceField"
|
||||
dusk="via-resource-field"
|
||||
class="flex border-b border-40"
|
||||
>
|
||||
<div class="w-1/5 px-8 py-6">
|
||||
<label
|
||||
:for="viaResourceField.name"
|
||||
class="inline-block text-80 pt-2 leading-tight"
|
||||
>
|
||||
{{ viaResourceField.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="py-6 px-8 w-1/2">
|
||||
<span class="inline-block font-bold text-80 pt-2">
|
||||
{{ viaResourceField.display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<default-field
|
||||
:field="field"
|
||||
:errors="validationErrors"
|
||||
:show-help-text="field.helpText != null"
|
||||
>
|
||||
<template slot="field">
|
||||
<select-control
|
||||
class="form-control form-select w-full"
|
||||
dusk="attachable-select"
|
||||
:class="{
|
||||
'border-danger': validationErrors.has(field.attribute),
|
||||
}"
|
||||
:data-testid="`${field.resourceName}-select`"
|
||||
@change="selectResourceFromSelectControl"
|
||||
disabled
|
||||
:options="availableResources"
|
||||
:label="'display'"
|
||||
:selected="selectedResourceId"
|
||||
>
|
||||
<option value="" disabled selected>
|
||||
{{ __('Choose :field', { field: field.name }) }}
|
||||
</option>
|
||||
</select-control>
|
||||
</template>
|
||||
</default-field>
|
||||
|
||||
<!-- Pivot Fields -->
|
||||
<div v-for="field in fields">
|
||||
<component
|
||||
:is="'form-' + field.component"
|
||||
:resource-name="resourceName"
|
||||
:resource-id="resourceId"
|
||||
:field="field"
|
||||
:errors="validationErrors"
|
||||
:related-resource-name="relatedResourceName"
|
||||
:related-resource-id="relatedResourceId"
|
||||
:via-resource="viaResource"
|
||||
:via-resource-id="viaResourceId"
|
||||
:via-relationship="viaRelationship"
|
||||
:show-help-text="field.helpText != null"
|
||||
/>
|
||||
</div>
|
||||
</card>
|
||||
<!-- Attach Button -->
|
||||
<div class="flex items-center">
|
||||
<cancel-button @click="$router.back()" />
|
||||
|
||||
<progress-button
|
||||
class="mr-3"
|
||||
dusk="update-and-continue-editing-button"
|
||||
@click.native="updateAndContinueEditing"
|
||||
:disabled="isWorking"
|
||||
:processing="submittedViaUpdateAndContinueEditing"
|
||||
>
|
||||
{{ __('Update & Continue Editing') }}
|
||||
</progress-button>
|
||||
|
||||
<progress-button
|
||||
dusk="update-button"
|
||||
type="submit"
|
||||
:disabled="isWorking"
|
||||
:processing="submittedViaUpdateAttachedResource"
|
||||
>
|
||||
{{
|
||||
__('Update :resource', {
|
||||
resource: relatedResourceLabel,
|
||||
})
|
||||
}}
|
||||
</progress-button>
|
||||
</div>
|
||||
</form>
|
||||
</loading-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import {
|
||||
PerformsSearches,
|
||||
TogglesTrashed,
|
||||
Errors,
|
||||
PreventsFormAbandonment,
|
||||
} from 'laravel-nova'
|
||||
|
||||
export default {
|
||||
mixins: [PerformsSearches, TogglesTrashed, PreventsFormAbandonment],
|
||||
|
||||
metaInfo() {
|
||||
if (this.relatedResourceLabel && this.title) {
|
||||
return {
|
||||
title: this.__('Update attached :resource: :title', {
|
||||
resource: this.relatedResourceLabel,
|
||||
title: this.title,
|
||||
}),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
resourceName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
resourceId: {
|
||||
required: true,
|
||||
},
|
||||
relatedResourceName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
relatedResourceId: {
|
||||
required: true,
|
||||
},
|
||||
viaResource: {
|
||||
default: '',
|
||||
},
|
||||
viaResourceId: {
|
||||
default: '',
|
||||
},
|
||||
viaRelationship: {
|
||||
default: '',
|
||||
},
|
||||
viaPivotId: {
|
||||
default: null,
|
||||
},
|
||||
polymorphic: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loading: true,
|
||||
submittedViaUpdateAndContinueEditing: false,
|
||||
submittedViaUpdateAttachedResource: false,
|
||||
viaResourceField: null,
|
||||
field: null,
|
||||
softDeletes: false,
|
||||
fields: [],
|
||||
validationErrors: new Errors(),
|
||||
selectedResource: null,
|
||||
selectedResourceId: null,
|
||||
lastRetrievedAt: null,
|
||||
title: null,
|
||||
}),
|
||||
|
||||
created() {
|
||||
if (Nova.missingResource(this.resourceName))
|
||||
return this.$router.push({ name: '404' })
|
||||
},
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.initializeComponent()
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Initialize the component's data.
|
||||
*/
|
||||
async initializeComponent() {
|
||||
this.softDeletes = false
|
||||
this.disableWithTrashed()
|
||||
this.clearSelection()
|
||||
await this.getField()
|
||||
await this.getPivotFields()
|
||||
await this.getAvailableResources()
|
||||
this.resetErrors()
|
||||
|
||||
this.selectedResourceId = this.relatedResourceId
|
||||
|
||||
this.selectInitialResource()
|
||||
|
||||
this.updateLastRetrievedAtTimestamp()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the many-to-many relationship field.
|
||||
*/
|
||||
async getField() {
|
||||
this.field = null
|
||||
|
||||
const { data: field } = await Nova.request().get(
|
||||
'/nova-api/' + this.resourceName + '/field/' + this.viaRelationship,
|
||||
{
|
||||
params: {
|
||||
relatable: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
this.field = field
|
||||
|
||||
if (this.field.searchable) {
|
||||
this.determineIfSoftDeletes()
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the available pivot fields for the relationship.
|
||||
*/
|
||||
async getPivotFields() {
|
||||
this.fields = []
|
||||
|
||||
const {
|
||||
data: { title, fields },
|
||||
} = await Nova.request()
|
||||
.get(
|
||||
`/nova-api/${this.resourceName}/${this.resourceId}/update-pivot-fields/${this.relatedResourceName}/${this.relatedResourceId}`,
|
||||
{
|
||||
params: {
|
||||
editing: true,
|
||||
editMode: 'update-attached',
|
||||
viaRelationship: this.viaRelationship,
|
||||
viaPivotId: this.viaPivotId,
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
if (error.response.status == 404) {
|
||||
this.$router.push({ name: '404' })
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
this.title = title
|
||||
this.fields = fields
|
||||
|
||||
_.each(this.fields, field => {
|
||||
if (field) {
|
||||
field.fill = () => ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
resetErrors() {
|
||||
this.validationErrors = new Errors()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the available resources for the current search / trashed state.
|
||||
*/
|
||||
async getAvailableResources(search = '') {
|
||||
try {
|
||||
const response = await Nova.request().get(
|
||||
`/nova-api/${this.resourceName}/${this.resourceId}/attachable/${this.relatedResourceName}`,
|
||||
{
|
||||
params: {
|
||||
search,
|
||||
current: this.relatedResourceId,
|
||||
first: true,
|
||||
withTrashed: this.withTrashed,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
this.viaResourceField = response.data.viaResource
|
||||
this.availableResources = response.data.resources
|
||||
this.withTrashed = response.data.withTrashed
|
||||
this.softDeletes = response.data.softDeletes
|
||||
} catch (error) {}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the related resource is soft deleting.
|
||||
*/
|
||||
determineIfSoftDeletes() {
|
||||
Nova.request()
|
||||
.get('/nova-api/' + this.relatedResourceName + '/soft-deletes')
|
||||
.then(response => {
|
||||
this.softDeletes = response.data.softDeletes
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the attached resource.
|
||||
*/
|
||||
async updateAttachedResource() {
|
||||
this.submittedViaUpdateAttachedResource = true
|
||||
|
||||
try {
|
||||
await this.updateRequest()
|
||||
|
||||
this.submittedViaUpdateAttachedResource = false
|
||||
this.canLeave = true
|
||||
|
||||
Nova.success(this.__('The resource was updated!'))
|
||||
|
||||
this.$router.push({
|
||||
name: 'detail',
|
||||
params: {
|
||||
resourceName: this.resourceName,
|
||||
resourceId: this.resourceId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
window.scrollTo(0, 0)
|
||||
|
||||
this.submittedViaUpdateAttachedResource = false
|
||||
if (
|
||||
this.resourceInformation &&
|
||||
this.resourceInformation.preventFormAbandonment
|
||||
) {
|
||||
this.canLeave = false
|
||||
}
|
||||
|
||||
if (error.response.status == 422) {
|
||||
this.validationErrors = new Errors(error.response.data.errors)
|
||||
Nova.error(this.__('There was a problem submitting the form.'))
|
||||
}
|
||||
|
||||
if (error.response.status == 409) {
|
||||
Nova.error(
|
||||
this.__(
|
||||
'Another user has updated this resource since this page was loaded. Please refresh the page and try again.'
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the resource and reset the form
|
||||
*/
|
||||
async updateAndContinueEditing() {
|
||||
this.submittedViaUpdateAndContinueEditing = true
|
||||
|
||||
try {
|
||||
await this.updateRequest()
|
||||
|
||||
this.submittedViaUpdateAndContinueEditing = false
|
||||
|
||||
Nova.success(this.__('The resource was updated!'))
|
||||
|
||||
// Reset the form by refetching the fields
|
||||
this.initializeComponent()
|
||||
} catch (error) {
|
||||
this.submittedViaUpdateAndContinueEditing = false
|
||||
|
||||
if (error.response.status == 422) {
|
||||
this.validationErrors = new Errors(error.response.data.errors)
|
||||
Nova.error(this.__('There was a problem submitting the form.'))
|
||||
}
|
||||
|
||||
if (error.response.status == 409) {
|
||||
Nova.error(
|
||||
this.__(
|
||||
'Another user has updated this resource since this page was loaded. Please refresh the page and try again.'
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send an update request for this resource
|
||||
*/
|
||||
updateRequest() {
|
||||
return Nova.request().post(
|
||||
`/nova-api/${this.resourceName}/${this.resourceId}/update-attached/${this.relatedResourceName}/${this.relatedResourceId}`,
|
||||
this.updateAttachmentFormData,
|
||||
{
|
||||
params: {
|
||||
editing: true,
|
||||
editMode: 'update-attached',
|
||||
viaPivotId: this.viaPivotId,
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Select a resource using the <select> control
|
||||
*/
|
||||
selectResourceFromSelectControl(e) {
|
||||
this.selectedResourceId = e.target.value
|
||||
this.selectInitialResource()
|
||||
|
||||
if (this.field) {
|
||||
Nova.$emit(this.field.attribute + '-change', this.selectedResourceId)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the trashed state of the search
|
||||
*/
|
||||
toggleWithTrashed() {
|
||||
this.withTrashed = !this.withTrashed
|
||||
|
||||
// Reload the data if the component doesn't support searching
|
||||
if (!this.isSearchable) {
|
||||
this.getAvailableResources()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Select the initial selected resource
|
||||
*/
|
||||
selectInitialResource() {
|
||||
this.selectedResource = _.find(
|
||||
this.availableResources,
|
||||
r => r.value == this.selectedResourceId
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the last retrieved at timestamp to the current UNIX timestamp.
|
||||
*/
|
||||
updateLastRetrievedAtTimestamp() {
|
||||
this.lastRetrievedAt = Math.floor(new Date().getTime() / 1000)
|
||||
},
|
||||
|
||||
/**
|
||||
* Prevent accidental abandonment only if form was changed.
|
||||
*/
|
||||
onUpdateFormStatus() {
|
||||
if (
|
||||
this.resourceInformation &&
|
||||
this.resourceInformation.preventFormAbandonment
|
||||
) {
|
||||
this.updateFormStatus()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the attachment endpoint for the relationship type.
|
||||
*/
|
||||
attachmentEndpoint() {
|
||||
return this.polymorphic
|
||||
? '/nova-api/' +
|
||||
this.resourceName +
|
||||
'/' +
|
||||
this.resourceId +
|
||||
'/attach-morphed/' +
|
||||
this.relatedResourceName
|
||||
: '/nova-api/' +
|
||||
this.resourceName +
|
||||
'/' +
|
||||
this.resourceId +
|
||||
'/attach/' +
|
||||
this.relatedResourceName
|
||||
},
|
||||
|
||||
/*
|
||||
* Get the form data for the resource attachment update.
|
||||
*/
|
||||
updateAttachmentFormData() {
|
||||
return _.tap(new FormData(), formData => {
|
||||
_.each(this.fields, field => {
|
||||
field.fill(formData)
|
||||
})
|
||||
|
||||
formData.append('viaRelationship', this.viaRelationship)
|
||||
|
||||
if (!this.selectedResource) {
|
||||
formData.append(this.relatedResourceName, '')
|
||||
} else {
|
||||
formData.append(this.relatedResourceName, this.selectedResource.value)
|
||||
}
|
||||
|
||||
formData.append(this.relatedResourceName + '_trashed', this.withTrashed)
|
||||
formData.append('_retrieved_at', this.lastRetrievedAt)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the label for the related resource.
|
||||
*/
|
||||
relatedResourceLabel() {
|
||||
if (this.field) {
|
||||
return this.field.singularLabel
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the related resources is searchable
|
||||
*/
|
||||
isSearchable() {
|
||||
return this.field.searchable
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the form is being processed
|
||||
*/
|
||||
isWorking() {
|
||||
return (
|
||||
this.submittedViaUpdateAttachedResource ||
|
||||
this.submittedViaUpdateAndContinueEditing
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user