1284 lines
34 KiB
Vue
Executable File
1284 lines
34 KiB
Vue
Executable File
<template>
|
|
<loading-view
|
|
:loading="initialLoading"
|
|
:dusk="resourceName + '-index-component'"
|
|
:data-relationship="viaRelationship"
|
|
>
|
|
<custom-index-header
|
|
v-if="!viaResource"
|
|
class="mb-3"
|
|
:resource-name="resourceName"
|
|
/>
|
|
|
|
<div v-if="shouldShowCards">
|
|
<cards
|
|
v-if="smallCards.length > 0"
|
|
:cards="smallCards"
|
|
class="mb-3"
|
|
:resource-name="resourceName"
|
|
/>
|
|
|
|
<cards
|
|
v-if="largeCards.length > 0"
|
|
:cards="largeCards"
|
|
size="large"
|
|
:resource-name="resourceName"
|
|
/>
|
|
</div>
|
|
|
|
<heading :level="1" class="mb-3" v-html="headingTitle" />
|
|
|
|
<div class="flex">
|
|
<!-- Search -->
|
|
<div
|
|
v-if="resourceInformation.searchable && !viaHasOne"
|
|
class="relative h-9 flex-no-shrink"
|
|
:class="{
|
|
'mb-6': resourceInformation.searchable && !viaHasOne,
|
|
}"
|
|
>
|
|
<icon type="search" class="absolute search-icon-center ml-3 text-70" />
|
|
|
|
<input
|
|
data-testid="search-input"
|
|
dusk="search"
|
|
class="appearance-none form-search w-search pl-search shadow"
|
|
:placeholder="__('Search')"
|
|
type="search"
|
|
v-model="search"
|
|
@keydown.stop="performSearch"
|
|
@search="performSearch"
|
|
spellcheck="false"
|
|
/>
|
|
</div>
|
|
|
|
<div class="w-full flex items-center" :class="{ 'mb-6': !viaResource }">
|
|
<custom-index-toolbar
|
|
v-if="!viaResource"
|
|
:resource-name="resourceName"
|
|
/>
|
|
|
|
<!-- Create / Attach Button -->
|
|
<create-resource-button
|
|
:label="createButtonLabel"
|
|
: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"
|
|
class="flex-no-shrink ml-auto"
|
|
:class="{ 'mb-6': viaResource }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<card>
|
|
<div
|
|
class="flex items-center"
|
|
:class="{
|
|
'py-3 border-b border-50':
|
|
shouldShowCheckBoxes ||
|
|
shouldShowDeleteMenu ||
|
|
softDeletes ||
|
|
!viaResource ||
|
|
hasFilters ||
|
|
haveStandaloneActions,
|
|
}"
|
|
>
|
|
<div class="flex items-center">
|
|
<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"
|
|
dusk="select-all-button"
|
|
>
|
|
{{ __('Select All') }}
|
|
</checkbox-with-label>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<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>
|
|
|
|
<div class="flex items-center ml-auto px-3">
|
|
<resource-polling-button
|
|
v-if="shouldShowPollingToggle"
|
|
:currently-polling="currentlyPolling"
|
|
@start-polling="startPolling"
|
|
@stop-polling="stopPolling"
|
|
class="mr-1"
|
|
/>
|
|
|
|
<!-- Action Selector -->
|
|
<action-selector
|
|
v-if="selectedResources.length > 0 || haveStandaloneActions"
|
|
:resource-name="resourceName"
|
|
:actions="availableActions"
|
|
:pivot-actions="pivotActions"
|
|
:pivot-name="pivotName"
|
|
:query-string="{
|
|
currentSearch,
|
|
encodedFilters,
|
|
currentTrashed,
|
|
viaResource,
|
|
viaResourceId,
|
|
viaRelationship,
|
|
}"
|
|
:selected-resources="selectedResourcesForActionSelector"
|
|
@actionExecuted="getResources"
|
|
/>
|
|
|
|
<!-- Lenses -->
|
|
<dropdown
|
|
class="bg-30 hover:bg-40 mr-3 rounded"
|
|
v-if="lenses.length > 0"
|
|
>
|
|
<dropdown-trigger class="px-3">
|
|
<h3
|
|
slot="default"
|
|
class="flex items-center font-normal text-base text-90 h-9"
|
|
>
|
|
{{ __('Lens') }}
|
|
</h3>
|
|
</dropdown-trigger>
|
|
|
|
<dropdown-menu slot="menu" width="240" direction="rtl">
|
|
<lens-selector :resource-name="resourceName" :lenses="lenses" />
|
|
</dropdown-menu>
|
|
</dropdown>
|
|
|
|
<!-- Filters -->
|
|
<filter-menu
|
|
:resource-name="resourceName"
|
|
:soft-deletes="softDeletes"
|
|
:via-resource="viaResource"
|
|
:via-has-one="viaHasOne"
|
|
:trashed="trashed"
|
|
:per-page="perPage"
|
|
:per-page-options="
|
|
perPageOptions || resourceInformation.perPageOptions
|
|
"
|
|
@clear-selected-filters="clearSelectedFilters"
|
|
@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"
|
|
:class="{ 'mb-6': authorizedToCreate && !resourceIsFull }"
|
|
>
|
|
{{
|
|
__('No :resource matched the given criteria.', {
|
|
resource: singularName.toLowerCase(),
|
|
})
|
|
}}
|
|
</h3>
|
|
|
|
<create-resource-button
|
|
classes="btn btn-sm btn-outline inline-flex items-center focus:outline-none focus:shadow-outline active:outline-none active:shadow-outline"
|
|
:label="createButtonLabel"
|
|
: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"
|
|
>
|
|
</create-resource-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-hidden overflow-x-auto relative">
|
|
<!-- Resource Table -->
|
|
<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"
|
|
: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="shouldShowPagination"
|
|
: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 {
|
|
Capitalize,
|
|
Deletable,
|
|
Filterable,
|
|
HasCards,
|
|
InteractsWithQueryString,
|
|
InteractsWithResourceInformation,
|
|
Minimum,
|
|
Paginatable,
|
|
PerPageable,
|
|
mapProps,
|
|
} from 'laravel-nova'
|
|
import HasActions from '@/mixins/HasActions'
|
|
import { CancelToken, Cancel } from 'axios'
|
|
|
|
export default {
|
|
mixins: [
|
|
HasActions,
|
|
Deletable,
|
|
Filterable,
|
|
HasCards,
|
|
Paginatable,
|
|
PerPageable,
|
|
InteractsWithResourceInformation,
|
|
InteractsWithQueryString,
|
|
],
|
|
|
|
metaInfo() {
|
|
if (this.shouldOverrideMeta) {
|
|
return {
|
|
title: this.__(`${this.resourceInformation.label}`),
|
|
}
|
|
}
|
|
},
|
|
|
|
props: {
|
|
shouldOverrideMeta: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
|
|
field: {
|
|
type: Object,
|
|
},
|
|
|
|
...mapProps([
|
|
'resourceName',
|
|
'viaResource',
|
|
'viaResourceId',
|
|
'viaRelationship',
|
|
]),
|
|
|
|
relationshipType: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
|
|
disablePagination: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
|
|
initialPerPage: {
|
|
type: Number,
|
|
required: false,
|
|
},
|
|
},
|
|
|
|
data: () => ({
|
|
debouncer: null,
|
|
canceller: null,
|
|
pollingListener: null,
|
|
initialLoading: true,
|
|
loading: true,
|
|
|
|
resourceResponse: null,
|
|
resources: [],
|
|
softDeletes: false,
|
|
selectedResources: [],
|
|
selectAllMatchingResources: false,
|
|
allMatchingResourceCount: 0,
|
|
|
|
deleteModalOpen: false,
|
|
|
|
search: '',
|
|
lenses: [],
|
|
|
|
authorizedToRelate: false,
|
|
|
|
orderBy: '',
|
|
orderByDirection: '',
|
|
trashed: '',
|
|
|
|
// Load More Pagination
|
|
currentPageLoadMore: null,
|
|
|
|
currentlyPolling: false,
|
|
}),
|
|
|
|
/**
|
|
* Mount the component and retrieve its initial data.
|
|
*/
|
|
async created() {
|
|
if (Nova.missingResource(this.resourceName))
|
|
return this.$router.push({ name: '404' })
|
|
|
|
this.debouncer = _.debounce(
|
|
callback => callback(),
|
|
this.resourceInformation.debounce
|
|
)
|
|
|
|
// Bind the keydown even listener when the router is visited if this
|
|
// component is not a relation on a Detail page
|
|
if (!this.viaResource && !this.viaResourceId) {
|
|
Nova.addShortcut('c', this.handleKeydown)
|
|
}
|
|
|
|
this.initializeSearchFromQueryString()
|
|
this.initializePerPageFromQueryString()
|
|
this.initializeTrashedFromQueryString()
|
|
this.initializeOrderingFromQueryString()
|
|
|
|
this.currentlyPolling = this.resourceInformation.polling
|
|
|
|
await this.initializeFilters()
|
|
await this.getResources()
|
|
await this.getAuthorizationToRelate()
|
|
|
|
this.getLenses()
|
|
this.getActions()
|
|
|
|
this.initialLoading = false
|
|
|
|
this.$watch(
|
|
() => {
|
|
return (
|
|
this.resourceName +
|
|
this.encodedFilters +
|
|
this.currentSearch +
|
|
this.currentPage +
|
|
this.perPage +
|
|
this.currentOrderBy +
|
|
this.currentOrderByDirection +
|
|
this.currentTrashed
|
|
)
|
|
},
|
|
() => {
|
|
if (this.canceller !== null) this.canceller()
|
|
|
|
this.getResources()
|
|
}
|
|
)
|
|
|
|
Nova.$on('refresh-resources', () => {
|
|
this.getResources()
|
|
})
|
|
|
|
if (this.resourceInformation.polling) {
|
|
this.startPolling()
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Unbind the keydown even listener when the before component is destroyed
|
|
*/
|
|
beforeDestroy() {
|
|
if (this.pollingListener) {
|
|
clearInterval(this.pollingListener)
|
|
}
|
|
|
|
if (!this.viaResource && !this.viaResourceId) {
|
|
Nova.disableShortcut('c')
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
$route(to, from) {
|
|
this.initializeSearchFromQueryString()
|
|
this.initializeState(false)
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
/**
|
|
* Handle the keydown event
|
|
*/
|
|
handleKeydown(e) {
|
|
// `c`
|
|
if (
|
|
this.authorizedToCreate &&
|
|
e.target.tagName != 'INPUT' &&
|
|
e.target.tagName != 'TEXTAREA' &&
|
|
e.target.contentEditable != 'true'
|
|
) {
|
|
this.$router.push({
|
|
name: 'create',
|
|
params: { resourceName: this.resourceName },
|
|
})
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Select all of the available resources
|
|
*/
|
|
selectAllResources() {
|
|
this.selectedResources = this.resources.slice(0)
|
|
},
|
|
|
|
/**
|
|
* Toggle the selection of all resources
|
|
*/
|
|
toggleSelectAll(event) {
|
|
if (this.selectAllChecked) return this.clearResourceSelections()
|
|
this.selectAllResources()
|
|
},
|
|
|
|
/**
|
|
* Toggle the selection of all matching resources in the database
|
|
*/
|
|
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, {
|
|
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.allMatchingResourceCount = data.total
|
|
|
|
this.loading = false
|
|
|
|
Nova.$emit('resources-loaded')
|
|
})
|
|
.catch(e => {
|
|
if (e instanceof Cancel) {
|
|
return
|
|
}
|
|
|
|
throw e
|
|
})
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Get the relatable authorization status for the resource.
|
|
*/
|
|
getAuthorizationToRelate() {
|
|
if (
|
|
!this.authorizedToCreate &&
|
|
this.relationshipType != 'belongsToMany' &&
|
|
this.relationshipType != 'morphToMany'
|
|
) {
|
|
return
|
|
}
|
|
|
|
if (!this.viaResource) {
|
|
return (this.authorizedToRelate = true)
|
|
}
|
|
|
|
return Nova.request()
|
|
.get(
|
|
'/nova-api/' +
|
|
this.resourceName +
|
|
'/relate-authorization' +
|
|
'?viaResource=' +
|
|
this.viaResource +
|
|
'&viaResourceId=' +
|
|
this.viaResourceId +
|
|
'&viaRelationship=' +
|
|
this.viaRelationship +
|
|
'&relationshipType=' +
|
|
this.relationshipType
|
|
)
|
|
.then(response => {
|
|
this.authorizedToRelate = response.data.authorized
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Get the lenses available for the current resource.
|
|
*/
|
|
getLenses() {
|
|
this.lenses = []
|
|
|
|
if (this.viaResource) {
|
|
return
|
|
}
|
|
|
|
return Nova.request()
|
|
.get('/nova-api/' + this.resourceName + '/lenses')
|
|
.then(response => {
|
|
this.lenses = response.data
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Get the actions available for the current resource.
|
|
*/
|
|
getActions() {
|
|
this.actions = []
|
|
this.pivotActions = null
|
|
|
|
return Nova.request()
|
|
.get(`/nova-api/${this.resourceName}/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
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Execute a search against the resource.
|
|
*/
|
|
performSearch(event) {
|
|
this.debouncer(() => {
|
|
// Only search if we're not tabbing into the field
|
|
if (event.which != 9) {
|
|
this.updateQueryString({
|
|
[this.pageParameter]: 1,
|
|
[this.searchParameter]: this.search,
|
|
})
|
|
}
|
|
})
|
|
},
|
|
|
|
/**
|
|
* 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 + '/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, {
|
|
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.allMatchingResourceCount = data.total
|
|
|
|
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.initialPerPage ||
|
|
this.resourceInformation.perPageOptions[0]
|
|
},
|
|
|
|
/**
|
|
* Pause polling for new resources.
|
|
*/
|
|
stopPolling() {
|
|
clearInterval(this.pollingListener)
|
|
|
|
this.$nextTick(() => (this.currentlyPolling = false))
|
|
},
|
|
|
|
/**
|
|
* Start polling for new resources.
|
|
*/
|
|
startPolling() {
|
|
this.pollingListener = setInterval(() => {
|
|
if (
|
|
document.hasFocus() &&
|
|
document.querySelectorAll('div.modal').length < 1
|
|
) {
|
|
this.getResources()
|
|
}
|
|
}, this.resourceInformation.pollingInterval)
|
|
|
|
this.$nextTick(() => (this.currentlyPolling = true))
|
|
},
|
|
},
|
|
|
|
computed: {
|
|
/**
|
|
* Determine if the resource has any filters
|
|
*/
|
|
hasFilters() {
|
|
return this.$store.getters[`${this.resourceName}/hasFilters`]
|
|
},
|
|
|
|
/**
|
|
* Determine if the resource should show any cards
|
|
*/
|
|
shouldShowCards() {
|
|
// Don't show cards if this resource is beings shown via a relations
|
|
return (
|
|
this.cards.length > 0 &&
|
|
this.resourceName == this.$route.params.resourceName
|
|
)
|
|
},
|
|
|
|
/**
|
|
* Get the endpoint for this resource's metrics.
|
|
*/
|
|
cardsEndpoint() {
|
|
return `/nova-api/${this.resourceName}/cards`
|
|
},
|
|
|
|
/**
|
|
* Get the name of the search query string variable.
|
|
*/
|
|
searchParameter() {
|
|
return this.viaRelationship
|
|
? this.viaRelationship + '_search'
|
|
: this.resourceName + '_search'
|
|
},
|
|
|
|
/**
|
|
* Get the name of the order by query string variable.
|
|
*/
|
|
orderByParameter() {
|
|
return this.viaRelationship
|
|
? this.viaRelationship + '_order'
|
|
: this.resourceName + '_order'
|
|
},
|
|
|
|
/**
|
|
* Get the name of the order by direction query string variable.
|
|
*/
|
|
orderByDirectionParameter() {
|
|
return this.viaRelationship
|
|
? this.viaRelationship + '_direction'
|
|
: this.resourceName + '_direction'
|
|
},
|
|
|
|
/**
|
|
* Get the name of the trashed constraint query string variable.
|
|
*/
|
|
trashedParameter() {
|
|
return this.viaRelationship
|
|
? this.viaRelationship + '_trashed'
|
|
: this.resourceName + '_trashed'
|
|
},
|
|
|
|
/**
|
|
* Get the name of the per page query string variable.
|
|
*/
|
|
perPageParameter() {
|
|
return this.viaRelationship
|
|
? this.viaRelationship + '_per_page'
|
|
: this.resourceName + '_per_page'
|
|
},
|
|
|
|
/**
|
|
* Get the name of the page query string variable.
|
|
*/
|
|
pageParameter() {
|
|
return this.viaRelationship
|
|
? this.viaRelationship + '_page'
|
|
: 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,
|
|
trashed: this.currentTrashed,
|
|
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] || null
|
|
},
|
|
|
|
/**
|
|
* 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 (
|
|
(Boolean(this.viaHasOne) && this.resources.length > 0) ||
|
|
Boolean(this.viaHasOneThrough && this.resources.length > 0)
|
|
)
|
|
},
|
|
|
|
/**
|
|
* Determine if the current resource listing is via a has-one relationship.
|
|
*/
|
|
viaHasOne() {
|
|
return (
|
|
this.relationshipType == 'hasOne' || this.relationshipType == 'morphOne'
|
|
)
|
|
},
|
|
|
|
viaHasOneThrough() {
|
|
return this.relationshipType == 'hasOneThrough'
|
|
},
|
|
|
|
/**
|
|
* Get the singular name for the resource
|
|
*/
|
|
singularName() {
|
|
if (this.isRelation && this.field) {
|
|
return Capitalize(this.field.singularLabel)
|
|
}
|
|
|
|
return Capitalize(this.resourceInformation.singularLabel)
|
|
},
|
|
|
|
/**
|
|
* Get the default label for the create button
|
|
*/
|
|
createButtonLabel() {
|
|
return this.resourceInformation.createButtonLabel
|
|
},
|
|
|
|
/**
|
|
* Determine if there are any resources for the view
|
|
*/
|
|
hasResources() {
|
|
return Boolean(this.resources.length > 0)
|
|
},
|
|
|
|
/**
|
|
* Determine if there any lenses for this resource
|
|
*/
|
|
hasLenses() {
|
|
return Boolean(this.lenses.length > 0)
|
|
},
|
|
|
|
/**
|
|
* Determine whether to show the selection checkboxes for resources
|
|
*/
|
|
shouldShowCheckBoxes() {
|
|
return (
|
|
Boolean(this.hasResources && !this.viaHasOne) &&
|
|
Boolean(
|
|
this.actionsAreAvailable ||
|
|
this.authorizedToDeleteAnyResources ||
|
|
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 delete menu should be shown to the user
|
|
*/
|
|
shouldShowDeleteMenu() {
|
|
return (
|
|
Boolean(this.selectedResources.length > 0) && this.canShowDeleteMenu
|
|
)
|
|
},
|
|
|
|
/**
|
|
* Determine whether the user is authorized to perform actions on the delete menu
|
|
*/
|
|
canShowDeleteMenu() {
|
|
return Boolean(
|
|
this.authorizedToDeleteSelectedResources ||
|
|
this.authorizedToForceDeleteSelectedResources ||
|
|
this.authorizedToRestoreSelectedResources ||
|
|
this.selectAllMatchingChecked
|
|
)
|
|
},
|
|
|
|
/**
|
|
* Determine if the index is a relation field
|
|
*/
|
|
isRelation() {
|
|
return Boolean(this.viaResourceId && this.viaRelationship)
|
|
},
|
|
|
|
/**
|
|
* Return the heading for the view
|
|
*/
|
|
headingTitle() {
|
|
return this.loading
|
|
? ' '
|
|
: this.isRelation && this.field
|
|
? this.field.name
|
|
: this.resourceResponse.label
|
|
},
|
|
|
|
/**
|
|
* Return the resource count label
|
|
*/
|
|
resourceCountLabel() {
|
|
const first = this.perPage * (this.currentPage - 1)
|
|
|
|
return (
|
|
this.resources.length &&
|
|
`${Nova.formatNumber(first + 1)}-${Nova.formatNumber(
|
|
first + this.resources.length
|
|
)} ${this.__('of')} ${Nova.formatNumber(this.allMatchingResourceCount)}`
|
|
)
|
|
},
|
|
|
|
/**
|
|
* 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)
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Determine whether the pagination component should be shown.
|
|
*/
|
|
shouldShowPagination() {
|
|
return (
|
|
this.disablePagination !== true &&
|
|
this.resourceResponse &&
|
|
this.resources.length > 0
|
|
)
|
|
},
|
|
|
|
/**
|
|
* Determine if the polling toggle button should be shown.
|
|
*/
|
|
shouldShowPollingToggle() {
|
|
return this.resourceInformation.showPollingToggle
|
|
},
|
|
},
|
|
}
|
|
</script>
|