add nova
This commit is contained in:
14
nova/resources/js/mixins/BehavesAsPanel.js
Normal file
14
nova/resources/js/mixins/BehavesAsPanel.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
emits: ['actionExecuted'],
|
||||
|
||||
props: ['resourceName', 'resourceId', 'resource', 'panel'],
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Handle the actionExecuted event and pass it up the chain.
|
||||
*/
|
||||
actionExecuted() {
|
||||
this.$emit('actionExecuted')
|
||||
},
|
||||
},
|
||||
}
|
||||
40
nova/resources/js/mixins/Collapsable.js
Normal file
40
nova/resources/js/mixins/Collapsable.js
Normal file
@@ -0,0 +1,40 @@
|
||||
export default {
|
||||
data: () => ({ collapsed: false }),
|
||||
|
||||
created() {
|
||||
const value = localStorage.getItem(this.localStorageKey)
|
||||
|
||||
if (value !== 'undefined') {
|
||||
this.collapsed = JSON.parse(value) ?? this.collapsedByDefault
|
||||
}
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
localStorage.setItem(this.localStorageKey, this.collapsed)
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleCollapse() {
|
||||
this.collapsed = !this.collapsed
|
||||
localStorage.setItem(this.localStorageKey, this.collapsed)
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
ariaExpanded() {
|
||||
return this.collapsed === false ? 'true' : 'false'
|
||||
},
|
||||
|
||||
shouldBeCollapsed() {
|
||||
return this.collapsed
|
||||
},
|
||||
|
||||
localStorageKey() {
|
||||
return `nova.navigation.${this.item.key}.collapsed`
|
||||
},
|
||||
|
||||
collapsedByDefault() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
33
nova/resources/js/mixins/CopiesToClipboard.js
Normal file
33
nova/resources/js/mixins/CopiesToClipboard.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const mixin = {
|
||||
methods: {
|
||||
copyValueToClipboard(value) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(value)
|
||||
} else if (window.clipboardData) {
|
||||
window.clipboardData.setData('Text', value)
|
||||
} else {
|
||||
let input = document.createElement('input')
|
||||
let [scrollTop, scrollLeft] = [
|
||||
document.documentElement.scrollTop,
|
||||
document.documentElement.scrollLeft,
|
||||
]
|
||||
document.body.appendChild(input)
|
||||
input.value = value
|
||||
input.focus()
|
||||
input.select()
|
||||
document.documentElement.scrollTop = scrollTop
|
||||
document.documentElement.scrollLeft = scrollLeft
|
||||
document.execCommand('copy')
|
||||
input.remove()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function useCopyValueToClipboard() {
|
||||
return {
|
||||
copyValueToClipboard: value => mixin.methods.copyValueToClipboard(value),
|
||||
}
|
||||
}
|
||||
|
||||
export default mixin
|
||||
300
nova/resources/js/mixins/Deletable.js
Normal file
300
nova/resources/js/mixins/Deletable.js
Normal file
@@ -0,0 +1,300 @@
|
||||
import filter from 'lodash/filter'
|
||||
import map from 'lodash/map'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
/**
|
||||
* Open the delete menu modal.
|
||||
*/
|
||||
openDeleteModal() {
|
||||
this.deleteModalOpen = true
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the given resources.
|
||||
*/
|
||||
deleteResources(resources, callback = null) {
|
||||
if (this.viaManyToMany) {
|
||||
return this.detachResources(resources)
|
||||
}
|
||||
|
||||
return Nova.request({
|
||||
url: '/nova-api/' + this.resourceName,
|
||||
method: 'delete',
|
||||
params: {
|
||||
...this.deletableQueryString,
|
||||
...{ resources: mapResources(resources) },
|
||||
},
|
||||
})
|
||||
.then(
|
||||
callback
|
||||
? callback
|
||||
: () => {
|
||||
this.getResources()
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
Nova.$emit('resources-deleted')
|
||||
})
|
||||
.finally(() => {
|
||||
this.deleteModalOpen = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the selected resources.
|
||||
*/
|
||||
deleteSelectedResources() {
|
||||
this.deleteResources(this.selectedResources)
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete all of the matching resources.
|
||||
*/
|
||||
deleteAllMatchingResources() {
|
||||
if (this.viaManyToMany) {
|
||||
return this.detachAllMatchingResources()
|
||||
}
|
||||
|
||||
return Nova.request({
|
||||
url: this.deleteAllMatchingResourcesEndpoint,
|
||||
method: 'delete',
|
||||
params: {
|
||||
...this.deletableQueryString,
|
||||
...{ resources: 'all' },
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.getResources()
|
||||
})
|
||||
.then(() => {
|
||||
Nova.$emit('resources-deleted')
|
||||
})
|
||||
.finally(() => {
|
||||
this.deleteModalOpen = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Detach the given resources.
|
||||
*/
|
||||
detachResources(resources) {
|
||||
return Nova.request({
|
||||
url: '/nova-api/' + this.resourceName + '/detach',
|
||||
method: 'delete',
|
||||
params: {
|
||||
...this.deletableQueryString,
|
||||
...{ resources: mapResources(resources) },
|
||||
...{ pivots: mapPivots(resources) },
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.getResources()
|
||||
})
|
||||
.then(() => {
|
||||
Nova.$emit('resources-detached')
|
||||
})
|
||||
.finally(() => {
|
||||
this.deleteModalOpen = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Detach all of the matching resources.
|
||||
*/
|
||||
detachAllMatchingResources() {
|
||||
return Nova.request({
|
||||
url: '/nova-api/' + this.resourceName + '/detach',
|
||||
method: 'delete',
|
||||
params: {
|
||||
...this.deletableQueryString,
|
||||
...{ resources: 'all' },
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.getResources()
|
||||
})
|
||||
.then(() => {
|
||||
Nova.$emit('resources-detached')
|
||||
})
|
||||
.finally(() => {
|
||||
this.deleteModalOpen = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Force delete the given resources.
|
||||
*/
|
||||
forceDeleteResources(resources, callback = null) {
|
||||
return Nova.request({
|
||||
url: '/nova-api/' + this.resourceName + '/force',
|
||||
method: 'delete',
|
||||
params: {
|
||||
...this.deletableQueryString,
|
||||
...{ resources: mapResources(resources) },
|
||||
},
|
||||
})
|
||||
.then(
|
||||
callback
|
||||
? callback
|
||||
: () => {
|
||||
this.getResources()
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
Nova.$emit('resources-deleted')
|
||||
})
|
||||
.finally(() => {
|
||||
this.deleteModalOpen = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Force delete the selected resources.
|
||||
*/
|
||||
forceDeleteSelectedResources() {
|
||||
this.forceDeleteResources(this.selectedResources)
|
||||
},
|
||||
|
||||
/**
|
||||
* Force delete all of the matching resources.
|
||||
*/
|
||||
forceDeleteAllMatchingResources() {
|
||||
return Nova.request({
|
||||
url: this.forceDeleteSelectedResourcesEndpoint,
|
||||
method: 'delete',
|
||||
params: {
|
||||
...this.deletableQueryString,
|
||||
...{ resources: 'all' },
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.getResources()
|
||||
})
|
||||
.then(() => {
|
||||
Nova.$emit('resources-deleted')
|
||||
})
|
||||
.finally(() => {
|
||||
this.deleteModalOpen = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore the given resources.
|
||||
*/
|
||||
restoreResources(resources, callback = null) {
|
||||
return Nova.request({
|
||||
url: '/nova-api/' + this.resourceName + '/restore',
|
||||
method: 'put',
|
||||
params: {
|
||||
...this.deletableQueryString,
|
||||
...{ resources: mapResources(resources) },
|
||||
},
|
||||
})
|
||||
.then(
|
||||
callback
|
||||
? callback
|
||||
: () => {
|
||||
this.getResources()
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
Nova.$emit('resources-restored')
|
||||
})
|
||||
.finally(() => {
|
||||
this.restoreModalOpen = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore the selected resources.
|
||||
*/
|
||||
restoreSelectedResources() {
|
||||
this.restoreResources(this.selectedResources)
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore all of the matching resources.
|
||||
*/
|
||||
restoreAllMatchingResources() {
|
||||
return Nova.request({
|
||||
url: this.restoreAllMatchingResourcesEndpoint,
|
||||
method: 'put',
|
||||
params: {
|
||||
...this.deletableQueryString,
|
||||
...{ resources: 'all' },
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.getResources()
|
||||
})
|
||||
.then(() => {
|
||||
Nova.$emit('resources-restored')
|
||||
})
|
||||
.finally(() => {
|
||||
this.restoreModalOpen = false
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the delete all matching resources endpoint.
|
||||
*/
|
||||
deleteAllMatchingResourcesEndpoint() {
|
||||
if (this.lens) {
|
||||
return '/nova-api/' + this.resourceName + '/lens/' + this.lens
|
||||
}
|
||||
|
||||
return '/nova-api/' + this.resourceName
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the force delete all of the matching resources endpoint.
|
||||
*/
|
||||
forceDeleteSelectedResourcesEndpoint() {
|
||||
if (this.lens) {
|
||||
return (
|
||||
'/nova-api/' + this.resourceName + '/lens/' + this.lens + '/force'
|
||||
)
|
||||
}
|
||||
|
||||
return '/nova-api/' + this.resourceName + '/force'
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the restore all of the matching resources endpoint.
|
||||
*/
|
||||
restoreAllMatchingResourcesEndpoint() {
|
||||
if (this.lens) {
|
||||
return (
|
||||
'/nova-api/' + this.resourceName + '/lens/' + this.lens + '/restore'
|
||||
)
|
||||
}
|
||||
|
||||
return '/nova-api/' + this.resourceName + '/restore'
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the query string for a deletable resource request.
|
||||
*/
|
||||
deletableQueryString() {
|
||||
return {
|
||||
search: this.currentSearch,
|
||||
filters: this.encodedFilters,
|
||||
trashed: this.currentTrashed,
|
||||
viaResource: this.viaResource,
|
||||
viaResourceId: this.viaResourceId,
|
||||
viaRelationship: this.viaRelationship,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function mapResources(resources) {
|
||||
return map(resources, resource => resource.id.value)
|
||||
}
|
||||
|
||||
function mapPivots(resources) {
|
||||
return filter(map(resources, resource => resource.id.pivotValue))
|
||||
}
|
||||
267
nova/resources/js/mixins/DependentFormField.js
Normal file
267
nova/resources/js/mixins/DependentFormField.js
Normal file
@@ -0,0 +1,267 @@
|
||||
import { CancelToken, isCancel } from 'axios'
|
||||
import debounce from 'lodash/debounce'
|
||||
import forIn from 'lodash/forIn'
|
||||
import get from 'lodash/get'
|
||||
import identity from 'lodash/identity'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isNil from 'lodash/isNil'
|
||||
import pickBy from 'lodash/pickBy'
|
||||
import FormField from './FormField'
|
||||
import { mapProps } from './propTypes'
|
||||
import filled from '../util/filled'
|
||||
import { escapeUnicode } from '../util/escapeUnicode'
|
||||
|
||||
export default {
|
||||
extends: FormField,
|
||||
|
||||
emits: ['field-shown', 'field-hidden'],
|
||||
|
||||
props: {
|
||||
...mapProps([
|
||||
'shownViaNewRelationModal',
|
||||
'field',
|
||||
'viaResource',
|
||||
'viaResourceId',
|
||||
'viaRelationship',
|
||||
'resourceName',
|
||||
'resourceId',
|
||||
'relatedResourceName',
|
||||
'relatedResourceId',
|
||||
]),
|
||||
|
||||
syncEndpoint: { type: String, required: false },
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
dependentFieldDebouncer: null,
|
||||
canceller: null,
|
||||
watchedFields: {},
|
||||
watchedEvents: {},
|
||||
syncedField: null,
|
||||
pivot: false,
|
||||
editMode: 'create',
|
||||
}),
|
||||
|
||||
created() {
|
||||
this.dependentFieldDebouncer = debounce(callback => callback(), 50)
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.relatedResourceName !== '' && !isNil(this.relatedResourceName)) {
|
||||
this.pivot = true
|
||||
|
||||
if (this.relatedResourceId !== '' && !isNil(this.relatedResourceId)) {
|
||||
this.editMode = 'update-attached'
|
||||
} else {
|
||||
this.editMode = 'attach'
|
||||
}
|
||||
} else {
|
||||
if (this.resourceId !== '' && !isNil(this.resourceId)) {
|
||||
this.editMode = 'update'
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(this.dependsOn)) {
|
||||
forIn(this.dependsOn, (defaultValue, dependsOn) => {
|
||||
this.watchedEvents[dependsOn] = value => {
|
||||
this.watchedFields[dependsOn] = value
|
||||
|
||||
this.dependentFieldDebouncer(() => {
|
||||
this.watchedFields[dependsOn] = value
|
||||
|
||||
this.syncField()
|
||||
})
|
||||
}
|
||||
|
||||
this.watchedFields[dependsOn] = defaultValue
|
||||
|
||||
Nova.$on(
|
||||
this.getFieldAttributeChangeEventName(dependsOn),
|
||||
this.watchedEvents[dependsOn]
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
if (this.canceller !== null) this.canceller()
|
||||
|
||||
if (!isEmpty(this.watchedEvents)) {
|
||||
forIn(this.watchedEvents, (event, dependsOn) => {
|
||||
Nova.$off(this.getFieldAttributeChangeEventName(dependsOn), event)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/*
|
||||
* Set the initial value for the field
|
||||
*/
|
||||
setInitialValue() {
|
||||
this.value = !(
|
||||
this.currentField.value === undefined ||
|
||||
this.currentField.value === null
|
||||
)
|
||||
? this.currentField.value
|
||||
: this.value
|
||||
},
|
||||
|
||||
/**
|
||||
* Provide a function to fills FormData when field is visible.
|
||||
*/
|
||||
fillIfVisible(formData, attribute, value) {
|
||||
if (this.currentlyIsVisible) {
|
||||
formData.append(attribute, value)
|
||||
}
|
||||
},
|
||||
|
||||
syncField() {
|
||||
if (this.canceller !== null) this.canceller()
|
||||
|
||||
Nova.request()
|
||||
.patch(
|
||||
this.syncEndpoint || this.syncFieldEndpoint,
|
||||
this.dependentFieldValues,
|
||||
{
|
||||
params: pickBy(
|
||||
{
|
||||
editing: true,
|
||||
editMode: this.editMode,
|
||||
viaResource: this.viaResource,
|
||||
viaResourceId: this.viaResourceId,
|
||||
viaRelationship: this.viaRelationship,
|
||||
field: this.fieldAttribute,
|
||||
component: this.field.dependentComponentKey,
|
||||
},
|
||||
identity
|
||||
),
|
||||
cancelToken: new CancelToken(canceller => {
|
||||
this.canceller = canceller
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
let previousValue = this.currentField.value
|
||||
let wasVisible = this.currentlyIsVisible
|
||||
|
||||
this.syncedField = response.data
|
||||
|
||||
if (this.syncedField.visible !== wasVisible) {
|
||||
this.$emit(
|
||||
this.syncedField.visible === true
|
||||
? 'field-shown'
|
||||
: 'field-hidden',
|
||||
this.fieldAttribute
|
||||
)
|
||||
}
|
||||
|
||||
if (isNil(this.syncedField.value)) {
|
||||
this.syncedField.value = previousValue
|
||||
} else {
|
||||
this.setInitialValue()
|
||||
}
|
||||
|
||||
let emitChangesEvent = !this.syncedFieldValueHasNotChanged()
|
||||
|
||||
this.onSyncedField()
|
||||
|
||||
if (
|
||||
this.syncedField.dependentShouldEmitChangesEvent &&
|
||||
emitChangesEvent
|
||||
) {
|
||||
this.emitOnSyncedFieldValueChange()
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if (isCancel(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
throw e
|
||||
})
|
||||
},
|
||||
|
||||
onSyncedField() {
|
||||
//
|
||||
},
|
||||
|
||||
emitOnSyncedFieldValueChange() {
|
||||
this.emitFieldValueChange(this.field.attribute, this.currentField.value)
|
||||
},
|
||||
|
||||
syncedFieldValueHasNotChanged() {
|
||||
const value = this.currentField.value
|
||||
|
||||
if (filled(value)) {
|
||||
return !filled(this.value)
|
||||
}
|
||||
|
||||
return !isNil(value) && value?.toString() === this.value?.toString()
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Determine the current field
|
||||
*/
|
||||
currentField() {
|
||||
return this.syncedField || this.field
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the field is in visible mode
|
||||
*/
|
||||
currentlyIsVisible() {
|
||||
return this.currentField.visible
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the field is in readonly mode
|
||||
*/
|
||||
currentlyIsReadonly() {
|
||||
if (this.syncedField !== null) {
|
||||
return Boolean(
|
||||
this.syncedField.readonly ||
|
||||
get(this.syncedField, 'extraAttributes.readonly')
|
||||
)
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
this.field.readonly || get(this.field, 'extraAttributes.readonly')
|
||||
)
|
||||
},
|
||||
|
||||
dependsOn() {
|
||||
return this.field.dependsOn || []
|
||||
},
|
||||
|
||||
currentFieldValues() {
|
||||
return {
|
||||
[this.fieldAttribute]: this.value,
|
||||
}
|
||||
},
|
||||
|
||||
dependentFieldValues() {
|
||||
return {
|
||||
...this.currentFieldValues,
|
||||
...this.watchedFields,
|
||||
}
|
||||
},
|
||||
|
||||
encodedDependentFieldValues() {
|
||||
return btoa(escapeUnicode(JSON.stringify(this.dependentFieldValues)))
|
||||
},
|
||||
|
||||
syncFieldEndpoint() {
|
||||
if (this.editMode === 'update-attached') {
|
||||
return `/nova-api/${this.resourceName}/${this.resourceId}/update-pivot-fields/${this.relatedResourceName}/${this.relatedResourceId}`
|
||||
} else if (this.editMode === 'attach') {
|
||||
return `/nova-api/${this.resourceName}/${this.resourceId}/creation-pivot-fields/${this.relatedResourceName}`
|
||||
} else if (this.editMode === 'update') {
|
||||
return `/nova-api/${this.resourceName}/${this.resourceId}/update-fields`
|
||||
}
|
||||
|
||||
return `/nova-api/${this.resourceName}/creation-fields`
|
||||
},
|
||||
},
|
||||
}
|
||||
31
nova/resources/js/mixins/FieldSuggestions.js
Normal file
31
nova/resources/js/mixins/FieldSuggestions.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import isNil from 'lodash/isNil'
|
||||
import omitBy from 'lodash/omitBy'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
suggestionsId() {
|
||||
return `${this.fieldAttribute}-list`
|
||||
},
|
||||
|
||||
suggestions() {
|
||||
let field = !isNil(this.syncedField) ? this.syncedField : this.field
|
||||
|
||||
if (isNil(field.suggestions)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return field.suggestions
|
||||
},
|
||||
|
||||
suggestionsAttributes() {
|
||||
return {
|
||||
...omitBy(
|
||||
{
|
||||
list: this.suggestions.length > 0 ? this.suggestionsId : null,
|
||||
},
|
||||
isNil
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
50
nova/resources/js/mixins/FieldValue.js
Normal file
50
nova/resources/js/mixins/FieldValue.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import filled from '../util/filled'
|
||||
import isArray from 'lodash/isArray'
|
||||
|
||||
export default {
|
||||
props: ['field'],
|
||||
|
||||
methods: {
|
||||
isEqualsToValue(value) {
|
||||
if (isArray(this.field.value) && filled(value)) {
|
||||
return Boolean(
|
||||
this.field.value.includes(value) ||
|
||||
this.field.value.includes(value.toString())
|
||||
)
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
this.field.value === value ||
|
||||
this.field.value?.toString() === value ||
|
||||
this.field.value === value?.toString() ||
|
||||
this.field.value?.toString() === value?.toString()
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
fieldAttribute() {
|
||||
return this.field.attribute
|
||||
},
|
||||
|
||||
fieldHasValue() {
|
||||
return filled(this.field.value)
|
||||
},
|
||||
|
||||
usesCustomizedDisplay() {
|
||||
return this.field.usesCustomizedDisplay && filled(this.field.displayedAs)
|
||||
},
|
||||
|
||||
fieldValue() {
|
||||
if (!this.usesCustomizedDisplay && !this.fieldHasValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
return String(this.field.displayedAs ?? this.field.value)
|
||||
},
|
||||
|
||||
shouldDisplayAsHtml() {
|
||||
return this.field.asHtml
|
||||
},
|
||||
},
|
||||
}
|
||||
115
nova/resources/js/mixins/Filterable.js
Normal file
115
nova/resources/js/mixins/Filterable.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import identity from 'lodash/identity'
|
||||
import pickBy from 'lodash/pickBy'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
filterHasLoaded: false,
|
||||
filterIsActive: false,
|
||||
}),
|
||||
|
||||
watch: {
|
||||
encodedFilters(value) {
|
||||
Nova.$emit('filter-changed', [value])
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Clear filters and reset the resource table
|
||||
*/
|
||||
async clearSelectedFilters(lens) {
|
||||
if (lens) {
|
||||
await this.$store.dispatch(`${this.resourceName}/resetFilterState`, {
|
||||
resourceName: this.resourceName,
|
||||
lens,
|
||||
})
|
||||
} else {
|
||||
await this.$store.dispatch(`${this.resourceName}/resetFilterState`, {
|
||||
resourceName: this.resourceName,
|
||||
})
|
||||
}
|
||||
|
||||
this.updateQueryString({
|
||||
[this.pageParameter]: 1,
|
||||
[this.filterParameter]: '',
|
||||
})
|
||||
|
||||
Nova.$emit('filter-reset')
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a filter state change.
|
||||
*/
|
||||
filterChanged() {
|
||||
let filtersAreApplied =
|
||||
this.$store.getters[`${this.resourceName}/filtersAreApplied`]
|
||||
|
||||
if (filtersAreApplied || this.filterIsActive) {
|
||||
this.filterIsActive = true
|
||||
this.updateQueryString({
|
||||
[this.pageParameter]: 1,
|
||||
[this.filterParameter]: this.encodedFilters,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up filters for the current view
|
||||
*/
|
||||
async initializeFilters(lens) {
|
||||
if (this.filterHasLoaded === true) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear out the filters from the store first
|
||||
this.$store.commit(`${this.resourceName}/clearFilters`)
|
||||
|
||||
await this.$store.dispatch(
|
||||
`${this.resourceName}/fetchFilters`,
|
||||
pickBy(
|
||||
{
|
||||
resourceName: this.resourceName,
|
||||
viaResource: this.viaResource,
|
||||
viaResourceId: this.viaResourceId,
|
||||
viaRelationship: this.viaRelationship,
|
||||
relationshipType: this.relationshipType,
|
||||
lens,
|
||||
},
|
||||
identity
|
||||
)
|
||||
)
|
||||
|
||||
await this.initializeState(lens)
|
||||
|
||||
this.filterHasLoaded = true
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the filter state
|
||||
*/
|
||||
async initializeState(lens) {
|
||||
this.initialEncodedFilters
|
||||
? await this.$store.dispatch(
|
||||
`${this.resourceName}/initializeCurrentFilterValuesFromQueryString`,
|
||||
this.initialEncodedFilters
|
||||
)
|
||||
: await this.$store.dispatch(`${this.resourceName}/resetFilterState`, {
|
||||
resourceName: this.resourceName,
|
||||
lens,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the name of the filter query string variable.
|
||||
*/
|
||||
filterParameter() {
|
||||
return this.resourceName + '_filter'
|
||||
},
|
||||
|
||||
encodedFilters() {
|
||||
return this.$store.getters[`${this.resourceName}/currentEncodedFilters`]
|
||||
},
|
||||
},
|
||||
}
|
||||
75
nova/resources/js/mixins/FormEvents.js
Normal file
75
nova/resources/js/mixins/FormEvents.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import isNil from 'lodash/isNil'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
formUniqueId: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
emitFieldValue(attribute, value) {
|
||||
Nova.$emit(`${attribute}-value`, value)
|
||||
|
||||
if (this.hasFormUniqueId === true) {
|
||||
Nova.$emit(`${this.formUniqueId}-${attribute}-value`, value)
|
||||
}
|
||||
},
|
||||
|
||||
emitFieldValueChange(attribute, value) {
|
||||
Nova.$emit(`${attribute}-change`, value)
|
||||
|
||||
if (this.hasFormUniqueId === true) {
|
||||
Nova.$emit(`${this.formUniqueId}-${attribute}-change`, value)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get field attribute value event name.
|
||||
*/
|
||||
getFieldAttributeValueEventName(attribute) {
|
||||
return this.hasFormUniqueId === true
|
||||
? `${this.formUniqueId}-${attribute}-value`
|
||||
: `${attribute}-value`
|
||||
},
|
||||
|
||||
/**
|
||||
* Get field attribue value event name.
|
||||
*/
|
||||
getFieldAttributeChangeEventName(attribute) {
|
||||
return this.hasFormUniqueId === true
|
||||
? `${this.formUniqueId}-${attribute}-change`
|
||||
: `${attribute}-change`
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Return the field attribute.
|
||||
*/
|
||||
fieldAttribute() {
|
||||
return this.field.attribute
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the field has Form Unique ID.
|
||||
*/
|
||||
hasFormUniqueId() {
|
||||
return !isNil(this.formUniqueId) && this.formUniqueId !== ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Get field attribue value event name.
|
||||
*/
|
||||
fieldAttributeValueEventName() {
|
||||
return this.getFieldAttributeValueEventName(this.fieldAttribute)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get field attribue value event name.
|
||||
*/
|
||||
fieldAttributeChangeEventName() {
|
||||
return this.getFieldAttributeChangeEventName(this.fieldAttribute)
|
||||
},
|
||||
},
|
||||
}
|
||||
152
nova/resources/js/mixins/FormField.js
Normal file
152
nova/resources/js/mixins/FormField.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import get from 'lodash/get'
|
||||
import { mapProps } from './propTypes'
|
||||
import FormEvents from './FormEvents'
|
||||
|
||||
export default {
|
||||
extends: FormEvents,
|
||||
|
||||
props: {
|
||||
...mapProps([
|
||||
'nested',
|
||||
'shownViaNewRelationModal',
|
||||
'field',
|
||||
'viaResource',
|
||||
'viaResourceId',
|
||||
'viaRelationship',
|
||||
'resourceName',
|
||||
'resourceId',
|
||||
'showHelpText',
|
||||
'mode',
|
||||
]),
|
||||
},
|
||||
|
||||
emits: ['field-changed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
value: this.fieldDefaultValue(),
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.setInitialValue()
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Add a default fill method for the field
|
||||
this.field.fill = this.fill
|
||||
|
||||
// Register a global event for setting the field's value
|
||||
Nova.$on(this.fieldAttributeValueEventName, this.listenToValueChanges)
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
Nova.$off(this.fieldAttributeValueEventName, this.listenToValueChanges)
|
||||
},
|
||||
|
||||
methods: {
|
||||
/*
|
||||
* Set the initial value for the field
|
||||
*/
|
||||
setInitialValue() {
|
||||
this.value = !(
|
||||
this.field.value === undefined || this.field.value === null
|
||||
)
|
||||
? this.field.value
|
||||
: this.fieldDefaultValue()
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the field default value.
|
||||
*/
|
||||
fieldDefaultValue() {
|
||||
return ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Provide a function that fills a passed FormData object with the
|
||||
* field's internal value attribute
|
||||
*/
|
||||
fill(formData) {
|
||||
this.fillIfVisible(formData, this.fieldAttribute, String(this.value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Provide a function to fills FormData when field is visible.
|
||||
*/
|
||||
fillIfVisible(formData, attribute, value) {
|
||||
if (this.isVisible) {
|
||||
formData.append(attribute, value)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the field's internal value
|
||||
*/
|
||||
handleChange(event) {
|
||||
this.value = event.target.value
|
||||
|
||||
if (this.field) {
|
||||
this.emitFieldValueChange(this.fieldAttribute, this.value)
|
||||
this.$emit('field-changed')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clean up any side-effects when removing this field dynamically (Repeater).
|
||||
*/
|
||||
beforeRemove() {
|
||||
//
|
||||
},
|
||||
|
||||
listenToValueChanges(value) {
|
||||
this.value = value
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Determine the current field.
|
||||
*/
|
||||
currentField() {
|
||||
return this.field
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the field should use all the available white-space.
|
||||
*/
|
||||
fullWidthContent() {
|
||||
return this.currentField.fullWidth || this.field.fullWidth
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the placeholder text for the field.
|
||||
*/
|
||||
placeholder() {
|
||||
return this.currentField.placeholder || this.field.name
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the field is in visible mode
|
||||
*/
|
||||
isVisible() {
|
||||
return this.field.visible
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the field is in readonly mode
|
||||
*/
|
||||
isReadonly() {
|
||||
return Boolean(
|
||||
this.field.readonly || get(this.field, 'extraAttributes.readonly')
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the field is accessed from Action
|
||||
*/
|
||||
isActionRequest() {
|
||||
return ['action-fullscreen', 'action-modal'].includes(this.mode)
|
||||
},
|
||||
},
|
||||
}
|
||||
164
nova/resources/js/mixins/HandlesFieldAttachments.js
Normal file
164
nova/resources/js/mixins/HandlesFieldAttachments.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Errors } from 'form-backend-validation'
|
||||
import isNil from 'lodash/isNil'
|
||||
import { mapProps } from './propTypes'
|
||||
|
||||
export default {
|
||||
emits: ['file-upload-started', 'file-upload-finished'],
|
||||
|
||||
props: mapProps(['resourceName']),
|
||||
|
||||
async created() {
|
||||
if (this.field.withFiles) {
|
||||
const {
|
||||
data: { draftId },
|
||||
} = await Nova.request().get(
|
||||
`/nova-api/${this.resourceName}/field-attachment/${this.fieldAttribute}/draftId`
|
||||
)
|
||||
|
||||
this.draftId = draftId
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
draftId: null,
|
||||
files: [],
|
||||
filesToRemove: [],
|
||||
}),
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Upload an attachment
|
||||
*/
|
||||
uploadAttachment(file, { onUploadProgress, onCompleted, onFailure }) {
|
||||
const data = new FormData()
|
||||
data.append('Content-Type', file.type)
|
||||
data.append('attachment', file)
|
||||
data.append('draftId', this.draftId)
|
||||
|
||||
if (isNil(onUploadProgress)) {
|
||||
onUploadProgress = () => {}
|
||||
}
|
||||
|
||||
if (isNil(onFailure)) {
|
||||
onFailure = () => {}
|
||||
}
|
||||
|
||||
if (isNil(onCompleted)) {
|
||||
throw 'Missing onCompleted parameter'
|
||||
}
|
||||
|
||||
this.$emit('file-upload-started')
|
||||
|
||||
Nova.request()
|
||||
.post(
|
||||
`/nova-api/${this.resourceName}/field-attachment/${this.fieldAttribute}`,
|
||||
data,
|
||||
{ onUploadProgress }
|
||||
)
|
||||
.then(({ data: { path, url } }) => {
|
||||
this.files.push({ path, url })
|
||||
const response = onCompleted(path, url)
|
||||
|
||||
this.$emit('file-upload-finished')
|
||||
|
||||
return response
|
||||
})
|
||||
.catch(error => {
|
||||
onFailure(error)
|
||||
|
||||
if (error.response.status == 422) {
|
||||
const validationErrors = new Errors(error.response.data.errors)
|
||||
|
||||
Nova.error(
|
||||
this.__('An error occurred while uploading the file: :error', {
|
||||
error: validationErrors.first('attachment'),
|
||||
})
|
||||
)
|
||||
} else {
|
||||
Nova.error(this.__('An error occurred while uploading the file.'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an attachment from the server
|
||||
*/
|
||||
flagFileForRemoval(url) {
|
||||
const fileIndex = this.files.findIndex(file => file.url === url)
|
||||
|
||||
if (fileIndex !== -1) {
|
||||
this.filesToRemove.push(this.files[fileIndex])
|
||||
return
|
||||
}
|
||||
// Case of deleting a file which was added prior to this draft
|
||||
this.filesToRemove.push({ url })
|
||||
},
|
||||
|
||||
unflagFileForRemoval(url) {
|
||||
const fileIndex = this.filesToRemove.findIndex(file => file.url === url)
|
||||
|
||||
if (fileIndex === -1) {
|
||||
return
|
||||
}
|
||||
this.filesToRemove.splice(fileIndex, 1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Purge pending attachments for the draft
|
||||
*/
|
||||
clearAttachments() {
|
||||
if (this.field.withFiles) {
|
||||
Nova.request()
|
||||
.delete(
|
||||
`/nova-api/${this.resourceName}/field-attachment/${this.fieldAttribute}/${this.draftId}`
|
||||
)
|
||||
.then(response => {})
|
||||
.catch(error => {})
|
||||
}
|
||||
},
|
||||
|
||||
clearFilesMarkedForRemoval() {
|
||||
if (this.field.withFiles) {
|
||||
this.filesToRemove.forEach(file => {
|
||||
console.log('deleting', file)
|
||||
Nova.request()
|
||||
.delete(
|
||||
`/nova-api/${this.resourceName}/field-attachment/${this.fieldAttribute}`,
|
||||
{
|
||||
params: {
|
||||
attachment: file.path,
|
||||
attachmentUrl: file.url,
|
||||
draftId: this.draftId,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(response => {})
|
||||
.catch(error => {})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fill draft id for the field
|
||||
*/
|
||||
fillAttachmentDraftId(formData) {
|
||||
let attribute = this.fieldAttribute
|
||||
|
||||
let [name, ...nested] = attribute.split('[')
|
||||
|
||||
if (!isNil(nested) && nested.length > 0) {
|
||||
let last = nested.pop()
|
||||
|
||||
if (nested.length > 0) {
|
||||
attribute = `${name}[${nested.join('[')}[${last.slice(0, -1)}DraftId]`
|
||||
} else {
|
||||
attribute = `${name}[${last.slice(0, -1)}DraftId]`
|
||||
}
|
||||
} else {
|
||||
attribute = `${attribute}DraftId`
|
||||
}
|
||||
|
||||
this.fillIfVisible(formData, attribute, this.draftId)
|
||||
},
|
||||
},
|
||||
}
|
||||
63
nova/resources/js/mixins/HandlesFormRequest.js
Normal file
63
nova/resources/js/mixins/HandlesFormRequest.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Errors } from 'form-backend-validation'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
formUniqueId: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
validationErrors: new Errors(),
|
||||
}),
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Handle all response error.
|
||||
*/
|
||||
handleResponseError(error) {
|
||||
if (error.response === undefined || error.response.status == 500) {
|
||||
Nova.error(this.__('There was a problem submitting the form.'))
|
||||
} else if (error.response.status == 422) {
|
||||
this.validationErrors = new Errors(error.response.data.errors)
|
||||
Nova.error(this.__('There was a problem submitting the form.'))
|
||||
} else {
|
||||
Nova.error(
|
||||
this.__('There was a problem submitting the form.') +
|
||||
' "' +
|
||||
error.response.statusText +
|
||||
'"'
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle creating response error.
|
||||
*/
|
||||
handleOnCreateResponseError(error) {
|
||||
this.handleResponseError(error)
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle updating response error.
|
||||
*/
|
||||
handleOnUpdateResponseError(error) {
|
||||
if (error.response && 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.'
|
||||
)
|
||||
)
|
||||
} else {
|
||||
this.handleResponseError(error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset validation errors.
|
||||
*/
|
||||
resetErrors() {
|
||||
this.validationErrors = new Errors()
|
||||
},
|
||||
},
|
||||
}
|
||||
36
nova/resources/js/mixins/HandlesPanelVisibility.js
Normal file
36
nova/resources/js/mixins/HandlesPanelVisibility.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import each from 'lodash/each'
|
||||
import filter from 'lodash/filter'
|
||||
|
||||
export default {
|
||||
emits: ['field-shown', 'field-hidden'],
|
||||
|
||||
data: () => ({
|
||||
visibleFieldsForPanel: {},
|
||||
}),
|
||||
|
||||
created() {
|
||||
each(this.panel.fields, field => {
|
||||
this.visibleFieldsForPanel[field.attribute] = field.visible
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleFieldShown(field) {
|
||||
this.visibleFieldsForPanel[field] = true
|
||||
this.$emit('field-shown', field)
|
||||
},
|
||||
|
||||
handleFieldHidden(field) {
|
||||
this.visibleFieldsForPanel[field] = false
|
||||
this.$emit('field-hidden', field)
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
visibleFieldsCount() {
|
||||
return Object.entries(
|
||||
filter(this.visibleFieldsForPanel, visible => visible === true)
|
||||
).length
|
||||
},
|
||||
},
|
||||
}
|
||||
25
nova/resources/js/mixins/HandlesUploads.js
Normal file
25
nova/resources/js/mixins/HandlesUploads.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
data: () => ({ isWorking: false, fileUploadsCount: 0 }),
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Handle file upload finishing
|
||||
*/
|
||||
handleFileUploadFinished() {
|
||||
this.fileUploadsCount--
|
||||
|
||||
if (this.fileUploadsCount < 1) {
|
||||
this.fileUploadsCount = 0
|
||||
this.isWorking = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle file upload starting
|
||||
*/
|
||||
handleFileUploadStarted() {
|
||||
this.isWorking = true
|
||||
this.fileUploadsCount++
|
||||
},
|
||||
},
|
||||
}
|
||||
49
nova/resources/js/mixins/HandlesValidationErrors.js
Normal file
49
nova/resources/js/mixins/HandlesValidationErrors.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Errors } from 'form-backend-validation'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
errors: { default: () => new Errors() },
|
||||
},
|
||||
|
||||
inject: { index: { default: null }, viaParent: { default: null } },
|
||||
|
||||
data: () => ({
|
||||
errorClass: 'form-control-bordered-error',
|
||||
}),
|
||||
|
||||
computed: {
|
||||
errorClasses() {
|
||||
return this.hasError ? [this.errorClass] : []
|
||||
},
|
||||
|
||||
fieldAttribute() {
|
||||
return this.field.attribute
|
||||
},
|
||||
|
||||
validationKey() {
|
||||
return this.nestedValidationKey || this.field.validationKey
|
||||
},
|
||||
|
||||
hasError() {
|
||||
return this.errors.has(this.validationKey)
|
||||
},
|
||||
|
||||
firstError() {
|
||||
if (this.hasError) {
|
||||
return this.errors.first(this.validationKey)
|
||||
}
|
||||
},
|
||||
|
||||
nestedAttribute() {
|
||||
if (this.viaParent) {
|
||||
return `${this.viaParent}[${this.index}][${this.field.attribute}]`
|
||||
}
|
||||
},
|
||||
|
||||
nestedValidationKey() {
|
||||
if (this.viaParent) {
|
||||
return `${this.viaParent}.${this.index}.fields.${this.field.attribute}`
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
1
nova/resources/js/mixins/HasActions.js
Normal file
1
nova/resources/js/mixins/HasActions.js
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
61
nova/resources/js/mixins/HasCards.js
Normal file
61
nova/resources/js/mixins/HasCards.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import filter from 'lodash/filter'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
loadCards: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({ cards: [] }),
|
||||
|
||||
/**
|
||||
* Fetch all of the metrics panels for this view
|
||||
*/
|
||||
created() {
|
||||
this.fetchCards()
|
||||
},
|
||||
|
||||
watch: {
|
||||
cardsEndpoint() {
|
||||
this.fetchCards()
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchCards() {
|
||||
// We disable fetching of cards when the component is being show
|
||||
// on a resource detail view to avoid extra network requests
|
||||
if (this.loadCards) {
|
||||
const { data: cards } = await Nova.request().get(this.cardsEndpoint, {
|
||||
params: this.extraCardParams,
|
||||
})
|
||||
this.cards = cards
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Determine whether we have cards to show on the Dashboard.
|
||||
*/
|
||||
shouldShowCards() {
|
||||
return this.cards.length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the cards array contains some detail-only cards.
|
||||
*/
|
||||
hasDetailOnlyCards() {
|
||||
return filter(this.cards, c => c.onlyOnDetail == true).length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the extra card params to pass to the endpoint.
|
||||
*/
|
||||
extraCardParams() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
}
|
||||
846
nova/resources/js/mixins/IndexConcerns.js
Normal file
846
nova/resources/js/mixins/IndexConcerns.js
Normal file
@@ -0,0 +1,846 @@
|
||||
import debounce from 'lodash/debounce'
|
||||
import find from 'lodash/find'
|
||||
import includes from 'lodash/includes'
|
||||
import isNull from 'lodash/isNull'
|
||||
import map from 'lodash/map'
|
||||
import { Filterable, InteractsWithQueryString, mapProps } from './index'
|
||||
import { capitalize } from '@/util'
|
||||
import { computed } from 'vue'
|
||||
import filter from 'lodash/filter'
|
||||
|
||||
export default {
|
||||
mixins: [Filterable, InteractsWithQueryString],
|
||||
|
||||
props: {
|
||||
...mapProps([
|
||||
'resourceName',
|
||||
'viaResource',
|
||||
'viaResourceId',
|
||||
'viaRelationship',
|
||||
'relationshipType',
|
||||
'disablePagination',
|
||||
]),
|
||||
|
||||
field: { type: Object },
|
||||
initialPerPage: { type: Number, required: false },
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
resourceHasId: computed(() => this.resourceHasId),
|
||||
authorizedToViewAnyResources: computed(
|
||||
() => this.authorizedToViewAnyResources
|
||||
),
|
||||
authorizedToUpdateAnyResources: computed(
|
||||
() => this.authorizedToUpdateAnyResources
|
||||
),
|
||||
authorizedToDeleteAnyResources: computed(
|
||||
() => this.authorizedToDeleteAnyResources
|
||||
),
|
||||
authorizedToRestoreAnyResources: computed(
|
||||
() => this.authorizedToRestoreAnyResources
|
||||
),
|
||||
selectedResourcesCount: computed(() => this.selectedResources.length),
|
||||
selectAllChecked: computed(() => this.selectAllChecked),
|
||||
selectAllMatchingChecked: computed(() => this.selectAllMatchingChecked),
|
||||
selectAllOrSelectAllMatchingChecked: computed(
|
||||
() => this.selectAllOrSelectAllMatchingChecked
|
||||
),
|
||||
selectAllAndSelectAllMatchingChecked: computed(
|
||||
() => this.selectAllAndSelectAllMatchingChecked
|
||||
),
|
||||
selectAllIndeterminate: computed(() => this.selectAllIndeterminate),
|
||||
orderByParameter: computed(() => this.orderByParameter),
|
||||
orderByDirectionParameter: computed(() => this.orderByDirectionParameter),
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
actions: [],
|
||||
allMatchingResourceCount: 0,
|
||||
authorizedToRelate: false,
|
||||
canceller: null,
|
||||
currentPageLoadMore: null,
|
||||
deleteModalOpen: false,
|
||||
initialLoading: true,
|
||||
loading: true,
|
||||
orderBy: '',
|
||||
orderByDirection: '',
|
||||
pivotActions: null,
|
||||
resourceHasId: true,
|
||||
resourceHasActions: false,
|
||||
resourceResponse: null,
|
||||
resourceResponseError: null,
|
||||
resources: [],
|
||||
search: '',
|
||||
selectAllMatchingResources: false,
|
||||
selectedResources: [],
|
||||
softDeletes: false,
|
||||
trashed: '',
|
||||
}),
|
||||
|
||||
async created() {
|
||||
if (Nova.missingResource(this.resourceName)) return Nova.visit('/404')
|
||||
|
||||
const debouncer = debounce(
|
||||
callback => callback(),
|
||||
this.resourceInformation.debounce
|
||||
)
|
||||
|
||||
this.initializeSearchFromQueryString()
|
||||
this.initializePerPageFromQueryString()
|
||||
this.initializeTrashedFromQueryString()
|
||||
this.initializeOrderingFromQueryString()
|
||||
|
||||
await this.initializeFilters(this.lens || null)
|
||||
await this.getResources()
|
||||
|
||||
if (!this.isLensView) {
|
||||
await 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()
|
||||
|
||||
if (this.currentPage === 1) {
|
||||
this.currentPageLoadMore = null
|
||||
}
|
||||
|
||||
this.getResources()
|
||||
}
|
||||
)
|
||||
|
||||
this.$watch('search', newValue => {
|
||||
this.search = newValue
|
||||
debouncer(() => this.performSearch())
|
||||
})
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
if (this.canceller !== null) this.canceller()
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Handle resources loaded event.
|
||||
*/
|
||||
handleResourcesLoaded() {
|
||||
this.loading = false
|
||||
|
||||
if (!this.isLensView && this.resourceResponse.total !== null) {
|
||||
this.allMatchingResourceCount = this.resourceResponse.total
|
||||
} else {
|
||||
this.getAllMatchingResourceCount()
|
||||
}
|
||||
|
||||
Nova.$emit(
|
||||
'resources-loaded',
|
||||
this.isLensView
|
||||
? {
|
||||
resourceName: this.resourceName,
|
||||
lens: this.lens,
|
||||
mode: 'lens',
|
||||
}
|
||||
: {
|
||||
resourceName: this.resourceName,
|
||||
mode: this.isRelation ? 'related' : 'index',
|
||||
}
|
||||
)
|
||||
|
||||
this.initializePolling()
|
||||
},
|
||||
|
||||
/**
|
||||
* Select all of the available resources
|
||||
*/
|
||||
selectAllResources() {
|
||||
this.selectedResources = this.resources.slice(0)
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the selection of all resources
|
||||
*/
|
||||
toggleSelectAll(e) {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (this.selectAllChecked) {
|
||||
this.clearResourceSelections()
|
||||
} else {
|
||||
this.selectAllResources()
|
||||
}
|
||||
|
||||
this.getActions()
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the selection of all matching resources in the database
|
||||
*/
|
||||
toggleSelectAllMatching(e) {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (!this.selectAllMatchingResources) {
|
||||
this.selectAllResources()
|
||||
this.selectAllMatchingResources = true
|
||||
} else {
|
||||
this.selectAllMatchingResources = false
|
||||
}
|
||||
|
||||
this.getActions()
|
||||
},
|
||||
|
||||
/*
|
||||
* Update the resource selection status
|
||||
*/
|
||||
updateSelectionStatus(resource) {
|
||||
if (!includes(this.selectedResources, resource)) {
|
||||
this.selectedResources.push(resource)
|
||||
} else {
|
||||
const index = this.selectedResources.indexOf(resource)
|
||||
if (index > -1) this.selectedResources.splice(index, 1)
|
||||
}
|
||||
|
||||
this.selectAllMatchingResources = false
|
||||
|
||||
this.getActions()
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the selected resouces and the "select all" states.
|
||||
*/
|
||||
clearResourceSelections() {
|
||||
this.selectAllMatchingResources = false
|
||||
this.selectedResources = []
|
||||
},
|
||||
|
||||
/**
|
||||
* 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()
|
||||
},
|
||||
|
||||
/**
|
||||
* Select the next page.
|
||||
*/
|
||||
selectPage(page) {
|
||||
this.updateQueryString({ [this.pageParameter]: page })
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync the per page values from the query string.
|
||||
*/
|
||||
initializePerPageFromQueryString() {
|
||||
this.perPage =
|
||||
this.queryStringParams[this.perPageParameter] ||
|
||||
this.initialPerPage ||
|
||||
this.resourceInformation?.perPageOptions[0] ||
|
||||
null
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the delete modal.
|
||||
*/
|
||||
closeDeleteModal() {
|
||||
this.deleteModalOpen = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute a search against the resource.
|
||||
*/
|
||||
performSearch() {
|
||||
this.updateQueryString({
|
||||
[this.pageParameter]: 1,
|
||||
[this.searchParameter]: this.search,
|
||||
})
|
||||
},
|
||||
|
||||
handleActionExecuted() {
|
||||
this.fetchPolicies()
|
||||
this.getResources()
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Determine if the resource has any filters
|
||||
*/
|
||||
hasFilters() {
|
||||
return this.$store.getters[`${this.resourceName}/hasFilters`]
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the name of the page query string variable.
|
||||
*/
|
||||
pageParameter() {
|
||||
return this.viaRelationship
|
||||
? this.viaRelationship + '_page'
|
||||
: this.resourceName + '_page'
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if all resources are selected on the page.
|
||||
*/
|
||||
selectAllChecked() {
|
||||
return this.selectedResources.length == this.resources.length
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if Select All Dropdown state is indeterminate.
|
||||
*/
|
||||
selectAllIndeterminate() {
|
||||
return (
|
||||
Boolean(this.selectAllChecked || this.selectAllMatchingChecked) &&
|
||||
Boolean(!this.selectAllAndSelectAllMatchingChecked)
|
||||
)
|
||||
},
|
||||
|
||||
selectAllAndSelectAllMatchingChecked() {
|
||||
return this.selectAllChecked && this.selectAllMatchingChecked
|
||||
},
|
||||
|
||||
selectAllOrSelectAllMatchingChecked() {
|
||||
return this.selectAllChecked || this.selectAllMatchingChecked
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if all matching resources are selected.
|
||||
*/
|
||||
selectAllMatchingChecked() {
|
||||
return this.selectAllMatchingResources
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the IDs for the selected resources.
|
||||
*/
|
||||
selectedResourceIds() {
|
||||
return map(this.selectedResources, resource => resource.id.value)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the Pivot IDs for the selected resources.
|
||||
*/
|
||||
selectedPivotIds() {
|
||||
return map(
|
||||
this.selectedResources,
|
||||
resource => resource.id.pivotValue ?? null
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current search value from the query string.
|
||||
*/
|
||||
currentSearch() {
|
||||
return this.queryStringParams[this.searchParameter] || ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current order by value from the query string.
|
||||
*/
|
||||
currentOrderBy() {
|
||||
return this.queryStringParams[this.orderByParameter] || ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current order by direction from the query string.
|
||||
*/
|
||||
currentOrderByDirection() {
|
||||
return this.queryStringParams[this.orderByDirectionParameter] || null
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current trashed constraint value from the query string.
|
||||
*/
|
||||
currentTrashed() {
|
||||
return this.queryStringParams[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 index is a relation field
|
||||
*/
|
||||
isRelation() {
|
||||
return Boolean(this.viaResourceId && this.viaRelationship)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the singular name for the resource
|
||||
*/
|
||||
singularName() {
|
||||
if (this.isRelation && this.field) {
|
||||
return capitalize(this.field.singularLabel)
|
||||
}
|
||||
|
||||
if (this.resourceInformation) {
|
||||
return capitalize(this.resourceInformation.singularLabel)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 if the resource should show any cards
|
||||
*/
|
||||
shouldShowCards() {
|
||||
// Don't show cards if this resource is beings shown via a relations
|
||||
return Boolean(this.cards.length > 0 && !this.isRelation)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine whether to show the selection checkboxes for resources
|
||||
*/
|
||||
shouldShowCheckboxes() {
|
||||
return (
|
||||
Boolean(this.hasResources) &&
|
||||
Boolean(this.resourceHasId) &&
|
||||
Boolean(
|
||||
this.resourceHasActions ||
|
||||
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 view any listed resource.
|
||||
*/
|
||||
authorizedToViewAnyResources() {
|
||||
return (
|
||||
this.resources.length > 0 &&
|
||||
Boolean(this.resourceHasId) &&
|
||||
Boolean(find(this.resources, resource => resource.authorizedToView))
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to view any listed resource.
|
||||
*/
|
||||
authorizedToUpdateAnyResources() {
|
||||
return (
|
||||
this.resources.length > 0 &&
|
||||
Boolean(this.resourceHasId) &&
|
||||
Boolean(find(this.resources, resource => resource.authorizedToUpdate))
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to delete any listed resource.
|
||||
*/
|
||||
authorizedToDeleteAnyResources() {
|
||||
return (
|
||||
this.resources.length > 0 &&
|
||||
Boolean(this.resourceHasId) &&
|
||||
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(this.resourceHasId) &&
|
||||
Boolean(
|
||||
find(this.resources, resource => resource.authorizedToForceDelete)
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if any selected resources may be restored.
|
||||
*/
|
||||
authorizedToRestoreSelectedResources() {
|
||||
return (
|
||||
Boolean(this.resourceHasId) &&
|
||||
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(this.resourceHasId) &&
|
||||
Boolean(find(this.resources, resource => resource.authorizedToRestore))
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.queryStringParams[this.filterParameter] || ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the pagination component for the resource.
|
||||
*/
|
||||
paginationComponent() {
|
||||
return `pagination-${Nova.config('pagination') || 'links'}`
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the resources has a next page.
|
||||
*/
|
||||
hasNextPage() {
|
||||
return Boolean(
|
||||
this.resourceResponse && this.resourceResponse.next_page_url
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the resources has a previous page.
|
||||
*/
|
||||
hasPreviousPage() {
|
||||
return Boolean(
|
||||
this.resourceResponse && this.resourceResponse.prev_page_url
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the total pages for the resource.
|
||||
*/
|
||||
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 &&
|
||||
`${Nova.formatNumber(first + 1)}-${Nova.formatNumber(
|
||||
first + this.resources.length
|
||||
)} ${this.__('of')} ${Nova.formatNumber(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
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the default label for the create button
|
||||
*/
|
||||
createButtonLabel() {
|
||||
if (this.resourceInformation)
|
||||
return this.resourceInformation.createButtonLabel
|
||||
|
||||
return this.__('Create')
|
||||
},
|
||||
|
||||
/**
|
||||
* Build the resource request query string.
|
||||
*/
|
||||
resourceRequestQueryString() {
|
||||
const queryString = {
|
||||
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,
|
||||
}
|
||||
|
||||
if (!this.lensName) {
|
||||
queryString['viaRelationship'] = this.viaRelationship
|
||||
}
|
||||
|
||||
return queryString
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the action selector should be shown.
|
||||
*/
|
||||
shouldShowActionSelector() {
|
||||
return this.selectedResources.length > 0 || this.haveStandaloneActions
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the view is a resource index or a lens.
|
||||
*/
|
||||
isLensView() {
|
||||
return this.lens !== '' && this.lens != undefined && this.lens != null
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine whether the pagination component should be shown.
|
||||
*/
|
||||
shouldShowPagination() {
|
||||
return (
|
||||
this.disablePagination !== true &&
|
||||
this.resourceResponse &&
|
||||
(this.hasResources || this.hasPreviousPage)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the current count of all resources
|
||||
*/
|
||||
currentResourceCount() {
|
||||
return this.resources.length
|
||||
},
|
||||
|
||||
/**
|
||||
* 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'
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine whether there are any standalone actions.
|
||||
*/
|
||||
haveStandaloneActions() {
|
||||
return filter(this.allActions, a => a.standalone === true).length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the available actions.
|
||||
*/
|
||||
availableActions() {
|
||||
return this.actions
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the resource has any pivot actions available.
|
||||
*/
|
||||
hasPivotActions() {
|
||||
return this.pivotActions && this.pivotActions.actions.length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the name of the pivot model for the resource.
|
||||
*/
|
||||
pivotName() {
|
||||
return this.pivotActions ? this.pivotActions.name : ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the resource has any actions available.
|
||||
*/
|
||||
actionsAreAvailable() {
|
||||
return this.allActions.length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the actions available to the resource.
|
||||
*/
|
||||
allActions() {
|
||||
return this.hasPivotActions
|
||||
? this.actions.concat(this.pivotActions.actions)
|
||||
: this.actions
|
||||
},
|
||||
|
||||
availableStandaloneActions() {
|
||||
return this.allActions.filter(a => a.standalone === true)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the selected resources for the action selector.
|
||||
*/
|
||||
selectedResourcesForActionSelector() {
|
||||
return this.selectAllMatchingChecked ? 'all' : this.selectedResources
|
||||
},
|
||||
},
|
||||
}
|
||||
21
nova/resources/js/mixins/InteractsWithDates.js
Normal file
21
nova/resources/js/mixins/InteractsWithDates.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { hourCycle } from '@/util'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
/**
|
||||
* Get the user's local timezone.
|
||||
*/
|
||||
userTimezone() {
|
||||
return Nova.config('userTimezone') || Nova.config('timezone')
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the user is used to 12 hour time.
|
||||
*/
|
||||
usesTwelveHourTime() {
|
||||
let locale = new Intl.DateTimeFormat().resolvedOptions().locale
|
||||
|
||||
return hourCycle(locale) === 12
|
||||
},
|
||||
},
|
||||
}
|
||||
10
nova/resources/js/mixins/InteractsWithQueryString.js
Normal file
10
nova/resources/js/mixins/InteractsWithQueryString.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
async created() {
|
||||
this.syncQueryString()
|
||||
},
|
||||
|
||||
methods: mapActions(['syncQueryString', 'updateQueryString']),
|
||||
computed: mapGetters(['queryStringParams']),
|
||||
}
|
||||
40
nova/resources/js/mixins/InteractsWithResourceInformation.js
Normal file
40
nova/resources/js/mixins/InteractsWithResourceInformation.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import find from 'lodash/find'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
/**
|
||||
* Get the resource information object for the current resource.
|
||||
*/
|
||||
resourceInformation() {
|
||||
return find(Nova.config('resources'), resource => {
|
||||
return resource.uriKey === this.resourceName
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the resource information object for the current resource.
|
||||
*/
|
||||
viaResourceInformation() {
|
||||
if (!this.viaResource) {
|
||||
return
|
||||
}
|
||||
|
||||
return find(Nova.config('resources'), resource => {
|
||||
return resource.uriKey === this.viaResource
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to create the current resource.
|
||||
*/
|
||||
authorizedToCreate() {
|
||||
if (
|
||||
['hasOneThrough', 'hasManyThrough'].indexOf(this.relationshipType) >= 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.resourceInformation?.authorizedToCreate || false
|
||||
},
|
||||
},
|
||||
}
|
||||
17
nova/resources/js/mixins/LoadsResources.js
Normal file
17
nova/resources/js/mixins/LoadsResources.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { mapProps } from './propTypes'
|
||||
|
||||
export default {
|
||||
props: mapProps(['resourceName', 'viaRelationship']),
|
||||
|
||||
computed: {
|
||||
localStorageKey() {
|
||||
let name = this.resourceName
|
||||
|
||||
if (this.viaRelationship) {
|
||||
name = `${name}.${this.viaRelationship}`
|
||||
}
|
||||
|
||||
return `nova.resources.${name}.collapsed`
|
||||
},
|
||||
},
|
||||
}
|
||||
12
nova/resources/js/mixins/Localization.js
Normal file
12
nova/resources/js/mixins/Localization.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import __ from '../util/localization'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
/**
|
||||
* Translate the given key.
|
||||
*/
|
||||
__(key, replace) {
|
||||
return __(key, replace)
|
||||
},
|
||||
},
|
||||
}
|
||||
21
nova/resources/js/mixins/MetricBehavior.js
Normal file
21
nova/resources/js/mixins/MetricBehavior.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
created() {
|
||||
Nova.$on('metric-refresh', this.fetch)
|
||||
|
||||
Nova.$on('resources-deleted', this.fetch)
|
||||
Nova.$on('resources-detached', this.fetch)
|
||||
Nova.$on('resources-restored', this.fetch)
|
||||
|
||||
if (this.card.refreshWhenActionRuns) {
|
||||
Nova.$on('action-executed', this.fetch)
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
Nova.$off('metric-refresh', this.fetch)
|
||||
Nova.$off('resources-deleted', this.fetch)
|
||||
Nova.$off('resources-detached', this.fetch)
|
||||
Nova.$off('resources-restored', this.fetch)
|
||||
Nova.$off('action-executed', this.fetch)
|
||||
},
|
||||
}
|
||||
26
nova/resources/js/mixins/Paginatable.js
Normal file
26
nova/resources/js/mixins/Paginatable.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export default {
|
||||
methods: {
|
||||
/**
|
||||
* Select the previous page.
|
||||
*/
|
||||
selectPreviousPage() {
|
||||
this.updateQueryString({ [this.pageParameter]: this.currentPage - 1 })
|
||||
},
|
||||
|
||||
/**
|
||||
* Select the next page.
|
||||
*/
|
||||
selectNextPage() {
|
||||
this.updateQueryString({ [this.pageParameter]: this.currentPage + 1 })
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the current page from the query string.
|
||||
*/
|
||||
currentPage() {
|
||||
return parseInt(this.queryStringParams[this.pageParameter] || 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
28
nova/resources/js/mixins/PerPageable.js
Normal file
28
nova/resources/js/mixins/PerPageable.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export default {
|
||||
data: () => ({ perPage: 25 }),
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Sync the per page values from the query string.
|
||||
*/
|
||||
initializePerPageFromQueryString() {
|
||||
this.perPage = this.currentPerPage
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the desired amount of resources per page.
|
||||
*/
|
||||
perPageChanged() {
|
||||
this.updateQueryString({ [this.perPageParameter]: this.perPage })
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the current per page value from the query string.
|
||||
*/
|
||||
currentPerPage() {
|
||||
return this.queryStringParams[this.perPageParameter] || 25
|
||||
},
|
||||
},
|
||||
}
|
||||
79
nova/resources/js/mixins/PerformsSearches.js
Normal file
79
nova/resources/js/mixins/PerformsSearches.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
search: '',
|
||||
selectedResource: null,
|
||||
selectedResourceId: null,
|
||||
availableResources: [],
|
||||
}),
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Set the currently selected resource
|
||||
*/
|
||||
selectResource(resource) {
|
||||
this.selectedResource = resource
|
||||
this.selectedResourceId = resource.value
|
||||
|
||||
if (this.field) {
|
||||
if (typeof this['emitFieldValueChange'] == 'function') {
|
||||
this.emitFieldValueChange(
|
||||
this.fieldAttribute,
|
||||
this.selectedResourceId
|
||||
)
|
||||
} else {
|
||||
Nova.$emit(this.fieldAttribute + '-change', this.selectedResourceId)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the search box being cleared.
|
||||
*/
|
||||
handleSearchCleared() {
|
||||
this.availableResources = []
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the selected resource and availableResources
|
||||
*/
|
||||
clearSelection() {
|
||||
this.selectedResource = null
|
||||
this.selectedResourceId = null
|
||||
this.availableResources = []
|
||||
|
||||
if (this.field) {
|
||||
if (typeof this['emitFieldValueChange'] == 'function') {
|
||||
this.emitFieldValueChange(this.fieldAttribute, null)
|
||||
} else {
|
||||
Nova.$emit(this.fieldAttribute + '-change', null)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a search to get the relatable resources.
|
||||
*/
|
||||
performSearch(search) {
|
||||
this.search = search
|
||||
|
||||
const trimmedSearch = search.trim()
|
||||
// If the user performs an empty search, it will load all the results
|
||||
// so let's just set the availableResources to an empty array to avoid
|
||||
// loading a huge result set
|
||||
if (trimmedSearch == '') {
|
||||
return
|
||||
}
|
||||
|
||||
this.searchDebouncer(() => {
|
||||
this.getAvailableResources(trimmedSearch)
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/**
|
||||
* Debounce function for the search handler
|
||||
*/
|
||||
searchDebouncer: debounce(callback => callback(), 500),
|
||||
},
|
||||
}
|
||||
161
nova/resources/js/mixins/PreventsFormAbandonment.js
Normal file
161
nova/resources/js/mixins/PreventsFormAbandonment.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { Inertia } from '@inertiajs/inertia'
|
||||
import filled from '../util/filled'
|
||||
|
||||
export default {
|
||||
created() {
|
||||
this.removeOnNavigationChangesEvent = Inertia.on('before', event => {
|
||||
this.removeOnNavigationChangesEvent()
|
||||
this.handlePreventFormAbandonmentOnInertia(event)
|
||||
})
|
||||
|
||||
window.addEventListener(
|
||||
'beforeunload',
|
||||
this.handlePreventFormAbandonmentOnInertia
|
||||
)
|
||||
|
||||
this.removeOnBeforeUnloadEvent = () => {
|
||||
window.removeEventListener(
|
||||
'beforeunload',
|
||||
this.handlePreventFormAbandonmentOnInertia
|
||||
)
|
||||
|
||||
this.removeOnBeforeUnloadEvent = () => {}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.onpopstate = event => {
|
||||
this.handlePreventFormAbandonmentOnPopState(event)
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.removeOnBeforeUnloadEvent()
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
this.removeOnNavigationChangesEvent()
|
||||
this.resetPushState()
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
removeOnNavigationChangesEvent: null,
|
||||
removeOnBeforeUnloadEvent: null,
|
||||
navigateBackUsingHistory: true,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'allowLeavingForm',
|
||||
'preventLeavingForm',
|
||||
'triggerPushState',
|
||||
'resetPushState',
|
||||
]),
|
||||
|
||||
/**
|
||||
* Prevent accidental abandonment only if form was changed.
|
||||
*/
|
||||
updateFormStatus() {
|
||||
if (this.canLeaveForm === true) {
|
||||
this.triggerPushState()
|
||||
}
|
||||
|
||||
this.preventLeavingForm()
|
||||
},
|
||||
|
||||
enableNavigateBackUsingHistory() {
|
||||
this.navigateBackUsingHistory = false
|
||||
},
|
||||
|
||||
disableNavigateBackUsingHistory() {
|
||||
this.navigateBackUsingHistory = false
|
||||
},
|
||||
|
||||
handlePreventFormAbandonment(proceed, revert) {
|
||||
if (this.canLeaveForm) {
|
||||
proceed()
|
||||
return
|
||||
}
|
||||
|
||||
const answer = window.confirm(
|
||||
this.__('Do you really want to leave? You have unsaved changes.')
|
||||
)
|
||||
|
||||
if (answer) {
|
||||
proceed()
|
||||
return
|
||||
}
|
||||
|
||||
revert()
|
||||
},
|
||||
|
||||
handlePreventFormAbandonmentOnInertia(event) {
|
||||
this.handlePreventFormAbandonment(
|
||||
() => {
|
||||
this.handleProceedingToNextPage()
|
||||
this.allowLeavingForm()
|
||||
},
|
||||
() => {
|
||||
Inertia.ignoreHistoryState = true
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
|
||||
this.removeOnNavigationChangesEvent = Inertia.on('before', event => {
|
||||
this.removeOnNavigationChangesEvent()
|
||||
this.handlePreventFormAbandonmentOnInertia(event)
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
handlePreventFormAbandonmentOnPopState(event) {
|
||||
event.stopImmediatePropagation()
|
||||
event.stopPropagation()
|
||||
|
||||
this.handlePreventFormAbandonment(
|
||||
() => {
|
||||
this.handleProceedingToPreviousPage()
|
||||
this.allowLeavingForm()
|
||||
},
|
||||
() => {
|
||||
this.triggerPushState()
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
handleProceedingToPreviousPage() {
|
||||
window.onpopstate = null
|
||||
Inertia.ignoreHistoryState = false
|
||||
|
||||
this.removeOnBeforeUnloadEvent()
|
||||
|
||||
if (!this.canLeaveFormToPreviousPage && this.navigateBackUsingHistory) {
|
||||
window.history.back()
|
||||
}
|
||||
},
|
||||
|
||||
handleProceedingToNextPage() {
|
||||
window.onpopstate = null
|
||||
Inertia.ignoreHistoryState = false
|
||||
|
||||
this.removeOnBeforeUnloadEvent()
|
||||
},
|
||||
|
||||
proceedToPreviousPage(url) {
|
||||
if (this.navigateBackUsingHistory && window.history.length > 1) {
|
||||
window.history.back()
|
||||
} else if (!this.navigateBackUsingHistory && filled(url)) {
|
||||
Nova.visit(url, { replace: true })
|
||||
} else {
|
||||
Nova.visit('/')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['canLeaveForm', 'canLeaveFormToPreviousPage']),
|
||||
},
|
||||
}
|
||||
41
nova/resources/js/mixins/PreventsModalAbandonment.js
Normal file
41
nova/resources/js/mixins/PreventsModalAbandonment.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
show: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations(['allowLeavingModal', 'preventLeavingModal']),
|
||||
|
||||
/**
|
||||
* Prevent accidental abandonment only if form was changed.
|
||||
*/
|
||||
updateModalStatus() {
|
||||
this.preventLeavingModal()
|
||||
},
|
||||
|
||||
handlePreventModalAbandonment(proceed, revert) {
|
||||
if (this.canLeaveModal) {
|
||||
proceed()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
window.confirm(
|
||||
this.__('Do you really want to leave? You have unsaved changes.')
|
||||
)
|
||||
) {
|
||||
this.allowLeavingModal()
|
||||
proceed()
|
||||
return
|
||||
}
|
||||
|
||||
revert()
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['canLeaveModal']),
|
||||
},
|
||||
}
|
||||
95
nova/resources/js/mixins/SupportsPolling.js
Normal file
95
nova/resources/js/mixins/SupportsPolling.js
Normal file
@@ -0,0 +1,95 @@
|
||||
export default {
|
||||
data: () => ({
|
||||
pollingListener: null,
|
||||
currentlyPolling: false,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Unbind the polling listener before the component is destroyed.
|
||||
*/
|
||||
beforeUnmount() {
|
||||
this.stopPolling()
|
||||
},
|
||||
|
||||
methods: {
|
||||
initializePolling() {
|
||||
this.currentlyPolling =
|
||||
this.currentlyPolling || this.resourceResponse.polling
|
||||
|
||||
if (this.currentlyPolling && this.pollingListener === null) {
|
||||
return this.startPolling()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle polling for new resources.
|
||||
*/
|
||||
togglePolling() {
|
||||
if (this.currentlyPolling) {
|
||||
this.stopPolling()
|
||||
} else {
|
||||
this.startPolling()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause polling for new resources.
|
||||
*/
|
||||
stopPolling() {
|
||||
if (this.pollingListener) {
|
||||
clearInterval(this.pollingListener)
|
||||
this.pollingListener = null
|
||||
}
|
||||
|
||||
this.currentlyPolling = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Start polling for new resources.
|
||||
*/
|
||||
startPolling() {
|
||||
this.pollingListener = setInterval(() => {
|
||||
let selectedResources = this.selectedResources ?? []
|
||||
|
||||
if (
|
||||
document.hasFocus() &&
|
||||
document.querySelectorAll('[data-modal-open]').length < 1 &&
|
||||
selectedResources.length < 1
|
||||
) {
|
||||
this.getResources()
|
||||
}
|
||||
}, this.pollingInterval)
|
||||
|
||||
this.currentlyPolling = true
|
||||
},
|
||||
|
||||
/**
|
||||
* Restart polling for the resource.
|
||||
*/
|
||||
restartPolling() {
|
||||
if (this.currentlyPolling === true) {
|
||||
this.stopPolling()
|
||||
this.startPolling()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
initiallyPolling() {
|
||||
return this.resourceResponse.polling
|
||||
},
|
||||
|
||||
pollingInterval() {
|
||||
return this.resourceResponse.pollingInterval
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the polling toggle button should be shown.
|
||||
*/
|
||||
shouldShowPollingToggle() {
|
||||
if (!this.resourceResponse) return false
|
||||
|
||||
return this.resourceResponse.showPollingToggle || false
|
||||
},
|
||||
},
|
||||
}
|
||||
28
nova/resources/js/mixins/TogglesTrashed.js
Normal file
28
nova/resources/js/mixins/TogglesTrashed.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export default {
|
||||
data: () => ({
|
||||
withTrashed: false,
|
||||
}),
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the trashed state of the search
|
||||
*/
|
||||
toggleWithTrashed() {
|
||||
this.withTrashed = !this.withTrashed
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable searching for trashed resources
|
||||
*/
|
||||
enableWithTrashed() {
|
||||
this.withTrashed = true
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable searching for trashed resources
|
||||
*/
|
||||
disableWithTrashed() {
|
||||
this.withTrashed = false
|
||||
},
|
||||
},
|
||||
}
|
||||
32
nova/resources/js/mixins/index.js
Normal file
32
nova/resources/js/mixins/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export { mapProps } from './propTypes'
|
||||
export { default as BehavesAsPanel } from './BehavesAsPanel'
|
||||
export { default as CopiesToClipboard } from './CopiesToClipboard'
|
||||
export { default as PreventsFormAbandonment } from './PreventsFormAbandonment'
|
||||
export { default as PreventsModalAbandonment } from './PreventsModalAbandonment'
|
||||
export { default as Deletable } from './Deletable'
|
||||
export { default as DependentFormField } from './DependentFormField'
|
||||
export { default as HandlesFormRequest } from './HandlesFormRequest'
|
||||
export { default as HandlesUploads } from './HandlesUploads'
|
||||
export { default as InteractsWithDates } from './InteractsWithDates'
|
||||
export { default as InteractsWithQueryString } from './InteractsWithQueryString'
|
||||
export { default as InteractsWithResourceInformation } from './InteractsWithResourceInformation'
|
||||
export { default as Localization } from './Localization'
|
||||
export { default as Collapsable } from './Collapsable'
|
||||
export { default as MetricBehavior } from './MetricBehavior'
|
||||
export { default as FormEvents } from './FormEvents'
|
||||
export { default as FormField } from './FormField'
|
||||
export { default as HandlesFieldAttachments } from './HandlesFieldAttachments'
|
||||
export { default as HandlesValidationErrors } from './HandlesValidationErrors'
|
||||
export { default as LoadsResources } from './LoadsResources'
|
||||
export { default as TogglesTrashed } from './TogglesTrashed'
|
||||
export { default as PerformsSearches } from './PerformsSearches'
|
||||
export { default as HasCards } from './HasCards'
|
||||
export { default as FieldSuggestions } from './FieldSuggestions'
|
||||
export { default as FieldValue } from './FieldValue'
|
||||
export { default as Filterable } from './Filterable'
|
||||
export { default as HandlesPanelVisibility } from './HandlesPanelVisibility'
|
||||
export { default as Paginatable } from './Paginatable'
|
||||
export { default as PerPageable } from './PerPageable'
|
||||
export { default as SupportsPolling } from './SupportsPolling'
|
||||
export { default as IndexConcerns } from './IndexConcerns'
|
||||
export { Errors } from 'form-backend-validation'
|
||||
21
nova/resources/js/mixins/packages.js
Normal file
21
nova/resources/js/mixins/packages.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export { mapProps } from './propTypes'
|
||||
export {
|
||||
default as CopiesToClipboard,
|
||||
useCopyValueToClipboard,
|
||||
} from './CopiesToClipboard'
|
||||
export { default as PreventsFormAbandonment } from './PreventsFormAbandonment'
|
||||
export { default as PreventsModalAbandonment } from './PreventsModalAbandonment'
|
||||
export { default as DependentFormField } from './DependentFormField'
|
||||
export { default as HandlesFormRequest } from './HandlesFormRequest'
|
||||
export { default as HandlesUploads } from './HandlesUploads'
|
||||
export { default as Localization } from './Localization'
|
||||
export { default as MetricBehavior } from './MetricBehavior'
|
||||
export { default as FieldValue } from './FieldValue'
|
||||
export { default as FormEvents } from './FormEvents'
|
||||
export { default as FormField } from './FormField'
|
||||
export { default as HandlesFieldAttachments } from './HandlesFieldAttachments'
|
||||
export { default as HandlesValidationErrors } from './HandlesValidationErrors'
|
||||
export { default as HasCards } from './HasCards'
|
||||
export { default as HandlesPanelVisibility } from './HandlesPanelVisibility'
|
||||
export { Errors } from 'form-backend-validation'
|
||||
export { useLocalization } from '../composables/useLocalization'
|
||||
83
nova/resources/js/mixins/propTypes.js
Normal file
83
nova/resources/js/mixins/propTypes.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import pick from 'lodash/pick'
|
||||
|
||||
const propTypes = {
|
||||
nested: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
preventInitialLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
showHelpText: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
shownViaNewRelationModal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
resourceId: { type: [Number, String] },
|
||||
|
||||
resourceName: { type: String },
|
||||
|
||||
relatedResourceId: { type: [Number, String] },
|
||||
|
||||
relatedResourceName: { type: String },
|
||||
|
||||
field: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
viaResource: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
|
||||
viaResourceId: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
},
|
||||
|
||||
viaRelationship: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
|
||||
relationshipType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
shouldOverrideMeta: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
disablePagination: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
clickAction: {
|
||||
type: String,
|
||||
default: 'view',
|
||||
validator: val => ['edit', 'select', 'ignore', 'detail'].includes(val),
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'form',
|
||||
validator: v =>
|
||||
['form', 'modal', 'action-modal', 'action-fullscreen'].includes(v),
|
||||
},
|
||||
}
|
||||
|
||||
export function mapProps(attributes) {
|
||||
return pick(propTypes, attributes)
|
||||
}
|
||||
Reference in New Issue
Block a user