/home/bdqbpbxa/api-uniferx.goodface.com.ua/vendor/laravel/nova/resources/js/views/Attach.vue
<template>
<LoadingView :loading="initialLoading">
<template v-if="relatedResourceLabel">
<Head
:title="
__('Attach :resource', {
resource: relatedResourceLabel,
})
"
/>
</template>
<Heading
class="mb-3"
v-text="__('Attach :resource', { resource: relatedResourceLabel })"
/>
<form
v-if="field"
@submit.prevent="attachResource"
@change="onUpdateFormStatus"
:data-form-unique-id="formUniqueId"
autocomplete="off"
>
<Card class="mb-8">
<!-- Related Resource -->
<div
v-if="parentResource"
dusk="via-resource-field"
class="field-wrapper flex flex-col md:flex-row border-b border-gray-100 dark:border-gray-700"
>
<div class="w-1/5 px-8 py-6">
<label
:for="parentResource.name"
class="inline-block text-gray-500 pt-2 leading-tight"
>
{{ parentResource.name }}
</label>
</div>
<div class="py-6 px-8 w-1/2">
<span class="inline-block font-bold text-gray-500 pt-2">
{{ parentResource.display }}
</span>
</div>
</div>
<DefaultField
:field="field"
:errors="validationErrors"
:show-help-text="true"
>
<template #field>
<div class="flex items-center">
<SearchInput
v-if="field.searchable"
:data-testid="`${field.resourceName}-search-input`"
@input="performSearch"
@clear="clearResourceSelection"
@selected="selectResource"
:debounce="field.debounce"
:value="selectedResource"
:data="availableResources"
trackBy="value"
class="w-full"
>
<div v-if="selectedResource" class="flex items-center">
<div v-if="selectedResource.avatar" class="mr-3">
<img
:src="selectedResource.avatar"
class="w-8 h-8 rounded-full block"
/>
</div>
{{ selectedResource.display }}
</div>
<template #option="{ selected, option }">
<div class="flex items-center">
<div v-if="option.avatar" class="flex-none mr-3">
<img
:src="option.avatar"
class="w-8 h-8 rounded-full block"
/>
</div>
<div class="flex-auto">
<div
class="text-sm font-semibold leading-5"
:class="{ 'text-white': selected }"
>
{{ option.display }}
</div>
<div
v-if="field.withSubtitles"
class="mt-1 text-xs font-semibold leading-5 text-gray-500"
:class="{ 'text-white': selected }"
>
<span v-if="option.subtitle">{{
option.subtitle
}}</span>
<span v-else>{{
__('No additional information...')
}}</span>
</div>
</div>
</div>
</template>
</SearchInput>
<SelectControl
v-else
class="w-full"
:class="{
'form-input-border-error': validationErrors.has(
field.attribute
),
}"
:data-testid="field.resourceName"
dusk="attachable-select"
v-model:selected="selectedResourceId"
@change="selectResourceFromSelectControl"
:options="availableResources"
:label="'display'"
>
<option value="" disabled selected>
{{
__('Choose :resource', {
resource: relatedResourceLabel,
})
}}
</option>
</SelectControl>
<CreateRelationButton
v-if="canShowNewRelationModal"
@click="openRelationModal"
class="ml-2"
:dusk="`${field.attribute}-inline-create`"
/>
</div>
<CreateRelationModal
:show="canShowNewRelationModal && relationModalOpen"
@set-resource="handleSetResource"
@create-cancelled="closeRelationModal"
:resource-name="field.resourceName"
:resource-id="resourceId"
:via-relationship="viaRelationship"
:via-resource="viaResource"
:via-resource-id="viaResourceId"
/>
<TrashedCheckbox
v-if="softDeletes"
class="mt-3"
:resource-name="field.resourceName"
:checked="withTrashed"
@input="toggleWithTrashed"
/>
</template>
</DefaultField>
<LoadingView :loading="loading">
<!-- Pivot Fields -->
<div v-for="field in fields" :key="field.uniqueKey">
<component
:is="`form-${field.component}`"
:resource-name="resourceName"
:resource-id="resourceId"
:related-resource-name="relatedResourceName"
:field="field"
:form-unique-id="formUniqueId"
:errors="validationErrors"
:via-resource="viaResource"
:via-resource-id="viaResourceId"
:via-relationship="viaRelationship"
:show-help-text="true"
/>
</div>
</LoadingView>
</Card>
<!-- Attach Button -->
<div
class="flex flex-col md:flex-row md:items-center justify-center md:justify-end space-y-2 md:space-y-0 space-x-3"
>
<CancelButton
dusk="cancel-attach-button"
type="button"
@click="cancelAttachingResource"
/>
<LoadingButton
dusk="attach-and-attach-another-button"
type="button"
@click.native.prevent="attachAndAttachAnother"
:disabled="isWorking"
:processing="submittedViaAttachAndAttachAnother"
>
{{ __('Attach & Attach Another') }}
</LoadingButton>
<LoadingButton
dusk="attach-button"
type="submit"
:disabled="isWorking"
:processing="submittedViaAttachResource"
>
{{
__('Attach :resource', {
resource: relatedResourceLabel,
})
}}
</LoadingButton>
</div>
</form>
</LoadingView>
</template>
<script>
import each from 'lodash/each'
import find from 'lodash/find'
import tap from 'lodash/tap'
import {
PerformsSearches,
TogglesTrashed,
FormEvents,
HandlesFormRequest,
PreventsFormAbandonment,
} from '@/mixins'
import { mapActions } from 'vuex'
export default {
mixins: [
FormEvents,
HandlesFormRequest,
PerformsSearches,
TogglesTrashed,
PreventsFormAbandonment,
],
props: {
resourceName: {
type: String,
required: true,
},
resourceId: {
required: true,
},
relatedResourceName: {
type: String,
required: true,
},
viaResource: {
default: '',
},
viaResourceId: {
default: '',
},
parentResource: {
type: Object,
},
viaRelationship: {
default: '',
},
polymorphic: {
default: false,
},
},
data: () => ({
initialLoading: true,
loading: true,
submittedViaAttachAndAttachAnother: false,
submittedViaAttachResource: false,
field: null,
softDeletes: false,
fields: [],
selectedResource: null,
selectedResourceId: null,
relationModalOpen: false,
initializingWithExistingResource: false,
}),
created() {
if (Nova.missingResource(this.resourceName)) return Nova.visit('/404')
},
/**
* Mount the component.
*/
mounted() {
this.initializeComponent()
},
methods: {
...mapActions(['fetchPolicies']),
/**
* Initialize the component's data.
*/
initializeComponent() {
this.softDeletes = false
this.disableWithTrashed()
this.clearSelection()
this.getField()
this.getPivotFields()
this.resetErrors()
this.allowLeavingForm()
},
/**
* Handle pivot fields loaded event.
*/
handlePivotFieldsLoaded() {
this.loading = false
each(this.fields, field => {
field.fill = () => ''
})
},
/**
* Get the many-to-many relationship field.
*/
getField() {
this.field = null
Nova.request()
.get(
'/nova-api/' + this.resourceName + '/field/' + this.viaRelationship,
{
params: {
relatable: true,
},
}
)
.then(({ data }) => {
this.field = data
this.field.searchable
? this.determineIfSoftDeletes()
: this.getAvailableResources()
this.initialLoading = false
})
},
/**
* Get all of the available pivot fields for the relationship.
*/
getPivotFields() {
this.fields = []
this.loading = true
Nova.request()
.get(
'/nova-api/' +
this.resourceName +
'/' +
this.resourceId +
'/creation-pivot-fields/' +
this.relatedResourceName,
{
params: {
editing: true,
editMode: 'attach',
viaRelationship: this.viaRelationship,
},
}
)
.then(({ data }) => {
this.fields = data
this.handlePivotFieldsLoaded()
})
},
/**
* Get all of the available resources for the current search / trashed state.
*/
getAvailableResources(search = '') {
Nova.$progress.start()
return Nova.request()
.get(
`/nova-api/${this.resourceName}/${this.resourceId}/attachable/${this.relatedResourceName}`,
{
params: {
search,
current: this.selectedResourceId,
first: this.initializingWithExistingResource,
withTrashed: this.withTrashed,
viaRelationship: this.viaRelationship,
},
}
)
.then(response => {
Nova.$progress.done()
if (this.isSearchable) {
this.initializingWithExistingResource = false
}
this.availableResources = response.data.resources
this.withTrashed = response.data.withTrashed
this.softDeletes = response.data.softDeletes
})
.catch(e => {
Nova.$progress.done()
})
},
/**
* Determine if the related resource is soft deleting.
*/
determineIfSoftDeletes() {
Nova.request()
.get('/nova-api/' + this.relatedResourceName + '/soft-deletes')
.then(response => {
this.softDeletes = response.data.softDeletes
})
},
/**
* Attach the selected resource.
*/
async attachResource() {
this.submittedViaAttachResource = true
try {
await this.attachRequest()
this.submittedViaAttachResource = false
this.allowLeavingForm()
await this.fetchPolicies(),
Nova.success(this.__('The resource was attached!'))
Nova.visit(`/resources/${this.resourceName}/${this.resourceId}`)
} catch (error) {
window.scrollTo(0, 0)
this.submittedViaAttachResource = false
this.preventLeavingForm()
this.handleOnCreateResponseError(error)
}
},
/**
* Attach a new resource and reset the form
*/
async attachAndAttachAnother() {
this.submittedViaAttachAndAttachAnother = true
try {
await this.attachRequest()
window.scrollTo(0, 0)
this.disableNavigateBackUsingHistory()
this.allowLeavingForm()
this.submittedViaAttachAndAttachAnother = false
await this.fetchPolicies()
// Reset the form by refetching the fields
this.initializeComponent()
} catch (error) {
this.submittedViaAttachAndAttachAnother = false
this.handleOnCreateResponseError(error)
}
},
cancelAttachingResource() {
this.handleProceedingToPreviousPage()
this.allowLeavingForm()
this.proceedToPreviousPage(
`/resources/${this.resourceName}/${this.resourceId}`
)
},
/**
* Send an attach request for this resource
*/
attachRequest() {
return Nova.request().post(
this.attachmentEndpoint,
this.attachmentFormData(),
{
params: {
editing: true,
editMode: 'attach',
},
}
)
},
/**
* Get the form data for the resource attachment.
*/
attachmentFormData() {
return tap(new FormData(), formData => {
each(this.fields, field => {
field.fill(formData)
})
if (!this.selectedResource) {
formData.append(this.relatedResourceName, '')
} else {
formData.append(this.relatedResourceName, this.selectedResource.value)
}
formData.append(this.relatedResourceName + '_trashed', this.withTrashed)
formData.append('viaRelationship', this.viaRelationship)
})
},
/**
* Select a resource using the <select> control
*/
selectResourceFromSelectControl(value) {
this.selectedResourceId = value
this.selectInitialResource()
if (this.field) {
this.emitFieldValueChange(this.fieldAttribute, this.selectedResourceId)
}
},
/**
* Select the initial selected resource
*/
selectInitialResource() {
this.selectedResource = find(
this.availableResources,
r => r.value == this.selectedResourceId
)
},
/**
* Toggle the trashed state of the search
*/
toggleWithTrashed() {
this.withTrashed = !this.withTrashed
// Reload the data if the component doesn't support searching
if (!this.isSearchable) {
this.getAvailableResources()
}
},
/**
* Prevent accidental abandonment only if form was changed.
*/
onUpdateFormStatus() {
this.updateFormStatus()
},
handleSetResource({ id }) {
this.closeRelationModal()
this.selectedResourceId = id
this.initializingWithExistingResource = true
this.getAvailableResources().then(() => this.selectInitialResource())
},
openRelationModal() {
Nova.$emit('create-relation-modal-opened')
this.relationModalOpen = true
},
closeRelationModal() {
this.relationModalOpen = false
Nova.$emit('create-relation-modal-closed')
},
clearResourceSelection() {
this.clearSelection()
if (!this.isSearchable) {
this.initializingWithExistingResource = false
this.getAvailableResources()
}
},
},
computed: {
/**
* Get the attachment endpoint for the relationship type.
*/
attachmentEndpoint() {
return this.polymorphic
? '/nova-api/' +
this.resourceName +
'/' +
this.resourceId +
'/attach-morphed/' +
this.relatedResourceName
: '/nova-api/' +
this.resourceName +
'/' +
this.resourceId +
'/attach/' +
this.relatedResourceName
},
/**
* Get the label for the related resource.
*/
relatedResourceLabel() {
if (this.field) {
return this.field.singularLabel
}
},
/**
* Determine if the related resources is searchable
*/
isSearchable() {
return this.field.searchable
},
/**
* Determine if the form is being processed
*/
isWorking() {
return (
this.submittedViaAttachResource ||
this.submittedViaAttachAndAttachAnother
)
},
/**
* Return the heading for the view
*/
headingTitle() {
return this.__('Attach :resource', {
resource: this.relatedResourceLabel,
})
},
shouldShowTrashed() {
return Boolean(this.softDeletes)
},
authorizedToCreate() {
return find(Nova.config('resources'), resource => {
return resource.uriKey == this.field.resourceName
}).authorizedToCreate
},
canShowNewRelationModal() {
return this.field.showCreateRelationButton && this.authorizedToCreate
},
},
}
</script>