This commit is contained in:
2024-09-01 18:54:23 +05:00
parent 76d18365a5
commit 061f09eca1
1597 changed files with 109451 additions and 1 deletions

View File

@@ -0,0 +1,188 @@
<template>
<Modal
:show="show"
@close-via-escape="handlePreventModalAbandonmentOnClose"
role="dialog"
:size="action.modalSize"
:modal-style="action.modalStyle"
:use-focus-trap="usesFocusTrap"
>
<form
ref="theForm"
autocomplete="off"
@change="onUpdateFormStatus"
@submit.prevent.stop="$emit('confirm')"
:data-form-unique-id="formUniqueId"
class="bg-white dark:bg-gray-800"
:class="{
'rounded-lg shadow-lg overflow-hidden space-y-6':
action.modalStyle === 'window',
'flex flex-col justify-between h-full':
action.modalStyle === 'fullscreen',
}"
>
<div
class="space-y-6"
:class="{
'overflow-hidden overflow-y-auto': action.modalStyle === 'fullscreen',
}"
>
<ModalHeader v-text="action.name" />
<!-- Confirmation Text -->
<p
v-if="action.confirmText"
class="px-8"
:class="{ 'text-red-500': action.destructive }"
>
{{ action.confirmText }}
</p>
<!-- Action Fields -->
<div v-if="action.fields.length > 0">
<div
class="action"
v-for="field in action.fields"
:key="field.attribute"
>
<component
:is="'form-' + field.component"
:errors="errors"
:resource-name="resourceName"
:field="field"
:show-help-text="true"
:form-unique-id="formUniqueId"
:mode="
action.modalStyle === 'fullscreen'
? 'action-fullscreen'
: 'action-modal'
"
:sync-endpoint="syncEndpoint"
@field-changed="onUpdateFieldStatus"
/>
</div>
</div>
</div>
<ModalFooter>
<div class="flex items-center ml-auto">
<CancelButton
component="button"
type="button"
dusk="cancel-action-button"
class="ml-auto mr-3"
@click="$emit('close')"
>
{{ action.cancelButtonText }}
</CancelButton>
<Button
type="submit"
ref="runButton"
dusk="confirm-action-button"
:loading="working"
variant="solid"
:state="action.destructive ? 'danger' : 'default'"
>
{{ action.confirmButtonText }}
</Button>
</div>
</ModalFooter>
</form>
</Modal>
</template>
<script>
import { PreventsModalAbandonment } from '@/mixins'
import isObject from 'lodash/isObject'
import { uid } from 'uid/single'
import { Button } from 'laravel-nova-ui'
export default {
components: {
Button,
},
emits: ['confirm', 'close'],
mixins: [PreventsModalAbandonment],
props: {
action: { type: Object, required: true },
endpoint: { type: String, required: false },
errors: { type: Object, required: true },
resourceName: { type: String, required: true },
selectedResources: { type: [Array, String], required: true },
show: { type: Boolean, default: false },
working: Boolean,
},
data: () => ({
loading: true,
formUniqueId: uid(),
}),
created() {
document.addEventListener('keydown', this.handleKeydown)
},
mounted() {
this.loading = false
},
beforeUnmount() {
document.removeEventListener('keydown', this.handleKeydown)
},
methods: {
/**
* Prevent accidental abandonment only if form was changed.
*/
onUpdateFormStatus() {
this.updateModalStatus()
},
onUpdateFieldStatus() {
this.onUpdateFormStatus()
},
handlePreventModalAbandonmentOnClose(event) {
this.handlePreventModalAbandonment(
() => {
this.$emit('close')
},
() => {
event.stopPropagation()
}
)
},
},
computed: {
syncEndpoint() {
let searchParams = new URLSearchParams({ action: this.action.uriKey })
if (this.selectedResources === 'all') {
searchParams.append('resources', 'all')
} else {
this.selectedResources.forEach(resource => {
searchParams.append(
'resources[]',
isObject(resource) ? resource.id.value : resource
)
})
}
return (
(this.endpoint || `/nova-api/${this.resourceName}/action`) +
'?' +
searchParams.toString()
)
},
usesFocusTrap() {
return this.loading === false && this.action.fields.length > 0
},
},
}
</script>

View File

@@ -0,0 +1,74 @@
<template>
<Modal :show="show" role="alertdialog" size="md">
<div
class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden"
style="width: 460px"
>
<ModalHeader v-text="__('Delete File')" />
<ModalContent>
<p class="leading-tight">
{{ __('Are you sure you want to delete this file?') }}
</p>
</ModalContent>
<ModalFooter>
<div class="ml-auto">
<LinkButton
dusk="cancel-upload-delete-button"
type="button"
@click.prevent="handleClose"
class="mr-3"
>
{{ __('Cancel') }}
</LinkButton>
<Button
@click.prevent="handleConfirm"
ref="confirmButton"
dusk="confirm-upload-delete-button"
:loading="working"
state="danger"
:label="__('Delete')"
/>
</div>
</ModalFooter>
</div>
</Modal>
</template>
<script>
import { Button } from 'laravel-nova-ui'
export default {
components: {
Button,
},
emits: ['confirm', 'close'],
props: {
show: { type: Boolean, default: false },
},
data: () => ({ working: false }),
watch: {
show(showing) {
if (showing === false) {
this.working = false
}
},
},
methods: {
handleClose() {
this.working = false
this.$emit('close')
},
handleConfirm() {
this.working = true
this.$emit('confirm')
},
},
}
</script>

View File

@@ -0,0 +1,75 @@
<template>
<Modal
dusk="new-relation-modal"
:show="show"
@close-via-escape="handlePreventModalAbandonmentOnClose"
:size="size"
:use-focus-trap="!loading"
>
<div
class="bg-gray-100 dark:bg-gray-700 rounded-lg shadow-lg overflow-hidden p-8"
>
<CreateResource
:resource-name="resourceName"
@create-cancelled="handleCreateCancelled"
@finished-loading="() => (loading = false)"
@refresh="handleRefresh"
mode="modal"
resource-id=""
via-relationship=""
via-resource-id=""
via-resource=""
/>
</div>
</Modal>
</template>
<script>
import { PreventsModalAbandonment } from '@/mixins'
import CreateResource from '@/views/Create'
export default {
emits: ['set-resource', 'create-cancelled'],
mixins: [PreventsModalAbandonment],
components: {
CreateResource,
},
props: {
show: { type: Boolean, default: false },
size: { type: String, default: '2xl' },
resourceName: {},
resourceId: {},
viaResource: {},
viaResourceId: {},
viaRelationship: {},
},
data: () => ({
loading: true,
}),
methods: {
handleRefresh(data) {
this.$emit('set-resource', data)
},
handleCreateCancelled() {
return this.$emit('create-cancelled')
},
handlePreventModalAbandonmentOnClose(event) {
this.handlePreventModalAbandonment(
() => {
this.handleCreateCancelled()
},
() => {
event.stopPropagation()
}
)
},
},
}
</script>

View File

@@ -0,0 +1,98 @@
<template>
<Modal :show="show" role="alertdialog" size="sm">
<form
@submit.prevent="handleConfirm"
class="mx-auto bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden"
>
<slot>
<ModalHeader v-text="__(`${uppercaseMode} Resource`)" />
<ModalContent>
<p class="leading-normal">
{{
__(
'Are you sure you want to ' + mode + ' the selected resources?'
)
}}
</p>
</ModalContent>
</slot>
<ModalFooter>
<div class="ml-auto">
<LinkButton
type="button"
dusk="cancel-delete-button"
@click.prevent="handleClose"
class="mr-3"
>
{{ __('Cancel') }}
</LinkButton>
<Button
type="submit"
ref="confirmButton"
dusk="confirm-delete-button"
:loading="working"
state="danger"
:label="__(uppercaseMode)"
/>
</div>
</ModalFooter>
</form>
</Modal>
</template>
<script>
import startCase from 'lodash/startCase'
import { Button } from 'laravel-nova-ui'
export default {
components: {
Button,
},
emits: ['confirm', 'close'],
props: {
show: { type: Boolean, default: false },
mode: {
type: String,
default: 'delete',
validator: function (value) {
return ['force delete', 'delete', 'detach'].indexOf(value) !== -1
},
},
},
data: () => ({
working: false,
}),
watch: {
show(showing) {
if (showing === false) {
this.working = false
}
},
},
methods: {
handleClose() {
this.$emit('close')
this.working = false
},
handleConfirm() {
this.$emit('confirm')
this.working = true
},
},
computed: {
uppercaseMode() {
return startCase(this.mode)
},
},
}
</script>

View File

@@ -0,0 +1,189 @@
<template>
<teleport to="body">
<template v-if="show">
<div
v-bind="defaultAttributes"
class="modal fixed inset-0 z-[60]"
:class="{
'px-3 md:px-0 py-3 md:py-6 overflow-x-hidden overflow-y-auto':
modalStyle === 'window',
'h-full': modalStyle === 'fullscreen',
}"
:role="role"
:data-modal-open="show"
:aria-modal="show"
>
<div
class="@container/modal relative mx-auto z-20"
:class="contentClasses"
ref="modalContent"
>
<slot />
</div>
</div>
<div
class="fixed inset-0 z-[55] bg-gray-500/75 dark:bg-gray-900/75"
dusk="modal-backdrop"
/>
</template>
</teleport>
</template>
<script setup>
import { useStore } from 'vuex'
import filter from 'lodash/filter'
import omit from 'lodash/omit'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import { useEventListener } from '@vueuse/core'
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
useAttrs,
watch,
} from 'vue'
const modalContent = ref(null)
const attrs = useAttrs()
const emit = defineEmits(['showing', 'closing', 'close-via-escape'])
defineOptions({ inheritAttrs: false })
const props = defineProps({
show: { type: Boolean, default: false },
size: {
type: String,
default: 'xl',
validator: v =>
[
'sm',
'md',
'lg',
'xl',
'2xl',
'3xl',
'4xl',
'5xl',
'6xl',
'7xl',
].includes(v),
},
modalStyle: { type: String, default: 'window' },
role: { type: String, default: 'dialog' },
useFocusTrap: { type: Boolean, default: true },
})
const usesFocusTrap = ref(true)
const hasTrapFocus = computed(() => {
return props.useFocusTrap && usesFocusTrap.value === true
})
const { activate, deactivate } = useFocusTrap(modalContent, {
immediate: false,
allowOutsideClick: true,
escapeDeactivates: false,
})
watch(
() => props.show,
v => handleVisibilityChange(v)
)
watch(hasTrapFocus, enable => {
try {
if (enable) {
nextTick(() => activate())
} else {
deactivate()
}
} catch (e) {
//
}
})
useEventListener(document, 'keydown', e => {
if (e.key === 'Escape' && props.show === true) {
emit('close-via-escape', e)
}
})
const disableModalFocusTrap = () => {
usesFocusTrap.value = false
}
const enableModalFocusTrap = () => {
usesFocusTrap.value = true
}
onMounted(() => {
Nova.$on('disable-focus-trap', disableModalFocusTrap)
Nova.$on('enable-focus-trap', enableModalFocusTrap)
if (props.show === true) handleVisibilityChange(true)
})
onBeforeUnmount(() => {
document.body.classList.remove('overflow-hidden')
Nova.resumeShortcuts()
Nova.$off('disable-focus-trap', disableModalFocusTrap)
Nova.$off('enable-focus-trap', enableModalFocusTrap)
usesFocusTrap.value = false
})
const store = useStore()
async function handleVisibilityChange(showing) {
if (showing === true) {
emit('showing')
document.body.classList.add('overflow-hidden')
Nova.pauseShortcuts()
usesFocusTrap.value = true
} else {
usesFocusTrap.value = false
emit('closing')
document.body.classList.remove('overflow-hidden')
Nova.resumeShortcuts()
}
store.commit('allowLeavingModal')
}
const defaultAttributes = computed(() => {
return omit(attrs, ['class'])
})
const sizeClasses = computed(() => {
return {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
'3xl': 'max-w-3xl',
'4xl': 'max-w-4xl',
'5xl': 'max-w-5xl',
'6xl': 'max-w-6xl',
'7xl': 'max-w-7xl',
}
})
const contentClasses = computed(() => {
let windowClasses = props.modalStyle === 'window' ? sizeClasses.value : {}
return filter([
windowClasses[props.size] ?? null,
props.modalStyle === 'fullscreen' ? 'h-full' : '',
attrs.class,
])
})
</script>

View File

@@ -0,0 +1,11 @@
<template>
<div class="py-3 px-8">
<slot />
</div>
</template>
<script>
export default {
//
}
</script>

View File

@@ -0,0 +1,11 @@
<template>
<div class="bg-gray-100 dark:bg-gray-700 px-6 py-3 flex">
<slot />
</div>
</template>
<script>
export default {
//
}
</script>

View File

@@ -0,0 +1,14 @@
<template>
<Heading
:level="3"
class="border-b border-gray-100 dark:border-gray-700 py-4 px-8"
>
<slot />
</Heading>
</template>
<script>
export default {
//
}
</script>

View File

@@ -0,0 +1,147 @@
<template>
<Modal
:show="show"
@close-via-escape="$emit('close')"
role="alertdialog"
size="2xl"
:use-focus-trap="false"
>
<LoadingView
:loading="loading"
class="mx-auto bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden"
>
<slot>
<ModalHeader class="flex items-center">
<span>
{{ modalTitle }}
<span
v-if="resource && resource.softDeleted"
class="ml-auto bg-red-50 text-red-500 py-0.5 px-2 rounded-full text-xs"
>
{{ __('Soft Deleted') }}
</span>
</span>
<Link
dusk="detail-preview-button"
:href="$url(`/resources/${resourceName}/${resourceId}`)"
class="ml-auto"
:alt="__('View :resource', { resource: title })"
>
<Icon type="arrow-right" />
</Link>
</ModalHeader>
<ModalContent
class="px-8 divide-y divide-gray-100 dark:divide-gray-800 -mx-3"
>
<template v-if="resource">
<component
:key="index"
v-for="(field, index) in resource.fields"
:index="index"
:is="`detail-${field.component}`"
:resource-name="resourceName"
:resource-id="resourceId"
:resource="resource"
:field="field"
/>
<div v-if="resource.fields.length == 0">
{{ __('There are no fields to display.') }}
</div>
</template>
</ModalContent>
</slot>
<ModalFooter>
<div class="ml-auto">
<Button
v-if="resource"
dusk="confirm-preview-button"
@click.prevent="$emit('close')"
:label="__('Close')"
/>
</div>
</ModalFooter>
</LoadingView>
</Modal>
</template>
<script>
import { mapProps } from '@/mixins'
import { minimum } from '@/util'
import { Button } from 'laravel-nova-ui'
export default {
components: {
Button,
},
emits: ['close'],
props: {
show: { type: Boolean, default: false },
...mapProps(['resourceName', 'resourceId']),
},
data: () => ({
loading: true,
title: null,
resource: null,
}),
async created() {
await this.getResource()
},
mounted() {
Nova.$emit('close-dropdowns')
},
methods: {
getResource() {
this.resource = null
return minimum(
Nova.request().get(
`/nova-api/${this.resourceName}/${this.resourceId}/preview`
)
)
.then(({ data: { title, resource } }) => {
this.title = title
this.resource = resource
this.loading = false
})
.catch(error => {
if (error.response.status >= 500) {
Nova.$emit('error', error.response.data.message)
return
}
if (error.response.status === 404) {
Nova.visit('/404')
return
}
if (error.response.status === 403) {
Nova.visit('/403')
return
}
if (error.response.status === 401) return Nova.redirectToLogin()
Nova.error(this.__('This resource no longer exists'))
Nova.visit(`/resources/${this.resourceName}`)
})
},
},
computed: {
modalTitle() {
return `${this.__('Previewing')} ${this.title}`
},
},
}
</script>

View File

@@ -0,0 +1,80 @@
<template>
<Modal :show="show" size="sm">
<form
@submit.prevent="handleConfirm"
class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden"
style="width: 460px"
>
<slot>
<ModalHeader v-text="__('Restore Resource')" />
<ModalContent>
<p class="leading-normal">
{{ __('Are you sure you want to restore the selected resources?') }}
</p>
</ModalContent>
</slot>
<ModalFooter>
<div class="ml-auto">
<LinkButton
type="button"
dusk="cancel-restore-button"
@click.prevent="handleClose"
class="mr-3"
>
{{ __('Cancel') }}
</LinkButton>
<Button
type="submit"
ref="confirmButton"
dusk="confirm-restore-button"
:loading="working"
>
{{ __('Restore') }}
</Button>
</div>
</ModalFooter>
</form>
</Modal>
</template>
<script>
import { Button } from 'laravel-nova-ui'
export default {
components: {
Button,
},
emits: ['confirm', 'close'],
props: {
show: { type: Boolean, default: false },
},
data: () => ({
working: false,
}),
watch: {
show(showing) {
if (showing === false) {
this.working = false
}
},
},
methods: {
handleClose() {
this.$emit('close')
this.working = false
},
handleConfirm() {
this.$emit('confirm')
this.working = true
},
},
}
</script>