Последняя версия с сервера прошлого разработчика

This commit is contained in:
2025-07-10 04:35:51 +00:00
commit c731570032
1174 changed files with 134314 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
<template>
<div><badge :label="field.label" :extra-classes="field.typeClass" /></div>
</template>
<script>
import Badge from '../Badge'
export default {
components: {
Badge,
},
props: ['resourceName', 'viaResource', 'viaResourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div :class="`text-${field.textAlign}`">
<span>
<span v-if="field.viewable && field.value">
<router-link
:to="{
name: 'detail',
params: {
resourceName: field.resourceName,
resourceId: field.belongsToId,
},
}"
class="no-underline dim text-primary font-bold"
>
{{ field.value }}
</router-link>
</span>
<span v-else-if="field.value">{{ field.value }}</span>
<span v-else>&mdash;</span>
</span>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
}
</script>

View File

@@ -0,0 +1,11 @@
<template>
<div :class="`text-${field.textAlign}`">
<boolean-icon :value="field.value" />
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
}
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div :class="`text-${field.textAlign}`">
<tooltip trigger="click">
<div class="text-primary inline-flex items-center dim cursor-pointer">
<span class="text-primary font-bold">{{ __('View') }}</span>
</div>
<tooltip-content slot="content">
<ul class="list-reset" v-if="value.length > 0">
<li v-for="option in value" class="mb-1">
<span
:class="classes[option.checked]"
class="inline-flex items-center py-1 pl-2 pr-3 rounded-full font-bold text-sm leading-tight"
>
<boolean-icon :value="option.checked" width="20" height="20" />
<span class="ml-1">{{ option.label }}</span>
</span>
</li>
</ul>
<span v-else>{{ this.field.noValueText }}</span>
</tooltip-content>
</tooltip>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
data: () => ({
value: [],
classes: {
true: 'bg-success-light text-success-dark',
false: 'bg-danger-light text-danger-dark',
},
}),
created() {
this.field.value = this.field.value || {}
this.value = _(this.field.options)
.map(o => {
return {
name: o.name,
label: o.label,
checked: this.field.value[o.name] || false,
}
})
.filter(o => {
if (this.field.hideFalseValues === true && o.checked === false) {
return false
} else if (this.field.hideTrueValues === true && o.checked === true) {
return false
}
return true
})
.value()
},
}
</script>

View File

@@ -0,0 +1,24 @@
<template>
<input
type="checkbox"
class="checkbox"
:disabled="disabled"
:checked="checked"
@change="$emit('input', $event)"
/>
</template>
<script>
export default {
props: {
disabled: {
type: Boolean,
default: false,
},
checked: {
default: false,
},
},
}
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div>
<template v-if="hasValue">
<div v-if="field.asHtml" v-html="field.value"></div>
<span v-else>{{ field.value }}</span>
</template>
<p v-else>&mdash;</p>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
computed: {
/**
* Determine if the field has a value other than null.
*/
hasValue() {
return this.field.value !== null
},
},
}
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div :class="`text-${field.textAlign}`">
<span v-if="field.value" class="whitespace-no-wrap">{{
formattedDate
}}</span>
<span v-else>&mdash;</span>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
computed: {
formattedDate() {
if (this.field.format) {
return moment(this.field.value).format(this.field.format)
}
return this.field.value
},
},
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div :class="`text-${field.textAlign}`">
<span v-if="field.value" class="whitespace-no-wrap">{{
localizedDateTime
}}</span>
<span v-else class="whitespace-no-wrap">&mdash;</span>
</div>
</template>
<script>
import { InteractsWithDates } from 'laravel-nova'
export default {
mixins: [InteractsWithDates],
props: ['resourceName', 'field'],
computed: {
/**
* Get the localized date time.
*/
localizedDateTime() {
return this.localizeDateTimeField(this.field)
},
},
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<checkbox :checked="checked" :disabled="true" class="pointer-events-none" />
</template>
<script>
export default {
props: {
checked: {
default: false,
},
},
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<p>
<img
v-if="imageUrl"
:src="imageUrl"
style="object-fit: cover"
class="align-bottom w-8 h-8"
:class="{ 'rounded-full': field.rounded, rounded: !field.rounded }"
/>
<span v-else>&mdash;</span>
</p>
</template>
<script>
export default {
props: ['viaResource', 'viaResourceId', 'resourceName', 'field'],
computed: {
imageUrl() {
if (this.field.previewUrl && !this.field.thumbnailUrl) {
return this.field.previewUrl
}
return this.field.thumbnailUrl
},
},
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<span />
</template>
<script>
export default {
props: ['field', 'viaResource', 'viaResourceId', 'resourceName'],
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<div class="hidden" />
</template>
<script>
export default {
props: ['resourceName', 'field'],
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<div :class="`text-${field.textAlign}`">
<router-link
v-if="hasValue"
:to="{
name: 'detail',
params: {
resourceName: resourceName,
resourceId: field.value,
},
}"
class="no-underline dim text-primary font-bold"
>
{{ field.pivotValue || field.value }}
</router-link>
<p v-else>&mdash;</p>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
computed: {
/**
* Determine if the field has a value other than null.
*/
hasValue() {
return this.field.value !== null
},
},
}
</script>

View File

@@ -0,0 +1,95 @@
<template>
<span>
<select
ref="selectBox"
v-if="actions.length > 1"
class="rounded-sm select-box-sm mr-2 h-6 text-xs appearance-none bg-40 pl-2 pr-6 active:outline-none active:shadow-outline focus:outline-none focus:shadow-outline"
style="max-width: 90px"
@change="handleSelectionChange"
dusk="inline-action-select"
>
<option disabled selected>{{ __('Actions') }}</option>
<option
v-for="action in actions"
:key="action.uriKey"
:value="action.uriKey"
>
{{ action.name }}
</option>
</select>
<button
v-else
v-for="action in actions"
:key="action.uriKey"
@click="executeSingleAction(action)"
class="btn btn-xs mr-1"
:class="action.class"
dusk="run-inline-action-button"
:data-testid="action.uriKey"
>
{{ action.name }}
</button>
<!-- Action Confirmation Modal -->
<portal to="modals">
<component
v-if="confirmActionModalOpened"
class="text-left"
:is="selectedAction.component"
:working="working"
:selected-resources="selectedResources"
:resource-name="resourceName"
:action="selectedAction"
:endpoint="actionsEndpoint"
:errors="errors"
@confirm="executeAction"
@close="closeConfirmationModal"
/>
<component
:is="actionResponseData.modal"
@close="closeActionResponseModal"
v-if="showActionResponseModal"
:data="actionResponseData"
/>
</portal>
</span>
</template>
<script>
import HandlesActions from '@/mixins/HandlesActions'
export default {
mixins: [HandlesActions],
props: {
resource: {},
actions: {},
},
data: () => ({
showActionResponseModal: false,
actionResponseData: {},
}),
methods: {
handleSelectionChange(event) {
this.selectedActionKey = event.target.value
this.determineActionStrategy()
this.$refs.selectBox.selectedIndex = 0
},
executeSingleAction(action) {
this.selectedActionKey = action.uriKey
this.determineActionStrategy()
},
},
computed: {
selectedResources() {
return [this.resource.id.value]
},
},
}
</script>

View File

@@ -0,0 +1,26 @@
<template>
<div :class="`text-${field.textAlign}`">
<template v-if="hasValue">
<div v-if="field.asHtml" v-html="field.value"></div>
<span v-else class="whitespace-no-wrap" :class="field.classes">{{
field.value
}}</span>
</template>
<p v-else>&mdash;</p>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
computed: {
/**
* Determine if the field has a value other than null.
*/
hasValue() {
return this.field.value !== null
},
},
}
</script>

View File

@@ -0,0 +1,38 @@
<template>
<router-link
v-if="field.viewable && field.value && !isResourceBeingViewed"
:to="{
name: 'detail',
params: {
resourceName: field.resourceName,
resourceId: field.morphToId,
},
}"
class="dim no-underline text-primary font-bold"
:class="`text-${field.textAlign}`"
>
{{ field.resourceLabel }}: {{ field.value }}
</router-link>
<span v-else-if="field.value">
{{ field.resourceLabel || field.morphToType }}: {{ field.value }}
</span>
<span v-else> - </span>
</template>
<script>
export default {
props: ['resourceName', 'viaResource', 'viaResourceId', 'field'],
computed: {
/**
* Determine if the resource being viewed matches the field's value.
*/
isResourceBeingViewed() {
return (
this.field.morphToType == this.viaResource &&
this.field.morphToId == this.viaResourceId
)
},
},
}
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div :class="`text-${field.textAlign}`">
<span>
<span v-if="field.viewable && field.value">
<router-link
:to="{
name: 'detail',
params: {
resourceName: field.resourceName,
resourceId: field.morphToId,
},
}"
class="no-underline dim text-primary font-bold"
>
{{ field.resourceLabel }}: {{ field.value }}
</router-link>
</span>
<span v-else-if="field.value">
{{ field.resourceLabel || field.morphToType }}: {{ field.value }}
</span>
<span v-else>-</span>
</span>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div :class="`text-${field.textAlign}`">
<span class="font-bold">
&middot; &middot; &middot; &middot; &middot; &middot; &middot; &middot;
</span>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
}
</script>

View File

@@ -0,0 +1,250 @@
<template>
<tr
:dusk="resource['id'].value + '-row'"
:data-pivot-id="resource['id'].pivotValue"
>
<!-- Resource Selection Checkbox -->
<td class="w-16" v-if="shouldShowCheckboxes">
<checkbox
:data-testid="`${testId}-checkbox`"
:dusk="`${resource['id'].value}-checkbox`"
v-if="shouldShowCheckboxes"
:checked="checked"
@input="toggleSelection"
/>
</td>
<!-- Fields -->
<td v-for="field in resource.fields">
<component
:is="'index-' + field.component"
:class="`text-${field.textAlign}`"
:resource-name="resourceName"
:via-resource="viaResource"
:via-resource-id="viaResourceId"
:field="field"
/>
</td>
<td class="td-fit text-right pr-6 align-middle">
<div class="inline-flex items-center">
<!-- Actions Menu -->
<inline-action-selector
v-if="availableActions.length > 0"
class="mr-3"
:resource="resource"
:resource-name="resourceName"
:actions="availableActions"
:endpoint="actionsEndpoint"
@actionExecuted="$emit('actionExecuted')"
/>
<!-- View Resource Link -->
<span v-if="resource.authorizedToView" class="inline-flex">
<router-link
:data-testid="`${testId}-view-button`"
:dusk="`${resource['id'].value}-view-button`"
class="cursor-pointer text-70 hover:text-primary mr-3 inline-flex items-center"
v-tooltip.click="__('View')"
:to="{
name: 'detail',
params: {
resourceName: resourceName,
resourceId: resource['id'].value,
},
}"
>
<icon type="view" width="22" height="18" view-box="0 0 22 16" />
</router-link>
</span>
<span v-if="resource.authorizedToUpdate" class="inline-flex">
<!-- Edit Pivot Button -->
<router-link
v-if="
relationshipType == 'belongsToMany' ||
relationshipType == 'morphToMany'
"
class="inline-flex cursor-pointer text-70 hover:text-primary mr-3"
:dusk="`${resource['id'].value}-edit-attached-button`"
v-tooltip.click="__('Edit Attached')"
:to="{
name: 'edit-attached',
params: {
resourceName: viaResource,
resourceId: viaResourceId,
relatedResourceName: resourceName,
relatedResourceId: resource['id'].value,
},
query: {
viaRelationship: viaRelationship,
viaPivotId: resource['id'].pivotValue,
},
}"
>
<icon type="edit" />
</router-link>
<!-- Edit Resource Link -->
<router-link
v-else
class="inline-flex cursor-pointer text-70 hover:text-primary mr-3"
:dusk="`${resource['id'].value}-edit-button`"
:to="{
name: 'edit',
params: {
resourceName: resourceName,
resourceId: resource['id'].value,
},
query: {
viaResource: viaResource,
viaResourceId: viaResourceId,
viaRelationship: viaRelationship,
},
}"
v-tooltip.click="__('Edit')"
>
<icon type="edit" />
</router-link>
</span>
<!-- Delete Resource Link -->
<button
:data-testid="`${testId}-delete-button`"
:dusk="`${resource['id'].value}-delete-button`"
class="inline-flex appearance-none cursor-pointer text-70 hover:text-primary mr-3"
v-tooltip.click="__(viaManyToMany ? 'Detach' : 'Delete')"
v-if="
resource.authorizedToDelete &&
(!resource.softDeleted || viaManyToMany)
"
@click.prevent="openDeleteModal"
>
<icon />
</button>
<!-- Restore Resource Link -->
<button
:dusk="`${resource['id'].value}-restore-button`"
class="appearance-none cursor-pointer text-70 hover:text-primary mr-3"
v-if="
resource.authorizedToRestore &&
resource.softDeleted &&
!viaManyToMany
"
v-tooltip.click="__('Restore')"
@click.prevent="openRestoreModal"
>
<icon type="restore" with="20" height="21" />
</button>
<portal
to="modals"
transition="fade-transition"
v-if="deleteModalOpen || restoreModalOpen"
>
<delete-resource-modal
v-if="deleteModalOpen"
@confirm="confirmDelete"
@close="closeDeleteModal"
:mode="viaManyToMany ? 'detach' : 'delete'"
>
<div slot-scope="{ uppercaseMode, mode }" class="p-8">
<heading :level="2" class="mb-6">{{
__(uppercaseMode + ' Resource')
}}</heading>
<p class="text-80 leading-normal">
{{ __('Are you sure you want to ' + mode + ' this resource?') }}
</p>
</div>
</delete-resource-modal>
<restore-resource-modal
v-if="restoreModalOpen"
@confirm="confirmRestore"
@close="closeRestoreModal"
>
<div class="p-8">
<heading :level="2" class="mb-6">{{
__('Restore Resource')
}}</heading>
<p class="text-80 leading-normal">
{{ __('Are you sure you want to restore this resource?') }}
</p>
</div>
</restore-resource-modal>
</portal>
</div>
</td>
</tr>
</template>
<script>
export default {
props: [
'testId',
'deleteResource',
'restoreResource',
'resource',
'resourcesSelected',
'resourceName',
'relationshipType',
'viaRelationship',
'viaResource',
'viaResourceId',
'viaManyToMany',
'checked',
'actionsAreAvailable',
'actionsEndpoint',
'shouldShowCheckboxes',
'updateSelectionStatus',
'queryString',
],
data: () => ({
deleteModalOpen: false,
restoreModalOpen: false,
}),
methods: {
/**
* Select the resource in the parent component
*/
toggleSelection() {
this.updateSelectionStatus(this.resource)
},
openDeleteModal() {
this.deleteModalOpen = true
},
confirmDelete() {
this.deleteResource(this.resource)
this.closeDeleteModal()
},
closeDeleteModal() {
this.deleteModalOpen = false
},
openRestoreModal() {
this.restoreModalOpen = true
},
confirmRestore() {
this.restoreResource(this.resource)
this.closeRestoreModal()
},
closeRestoreModal() {
this.restoreModalOpen = false
},
},
computed: {
availableActions() {
return _.filter(this.resource.actions, a => a.showOnTableRow)
},
},
}
</script>

View File

@@ -0,0 +1,131 @@
<template>
<span
@click.prevent="handleClick"
class="cursor-pointer inline-flex items-center"
:dusk="'sort-' + uriKey"
>
<slot />
<svg
class="ml-2 flex-no-shrink"
xmlns="http://www.w3.org/2000/svg"
width="8"
height="14"
viewBox="0 0 8 14"
>
<path
:class="descClass"
d="M1.70710678 4.70710678c-.39052429.39052429-1.02368927.39052429-1.41421356 0-.3905243-.39052429-.3905243-1.02368927 0-1.41421356l3-3c.39052429-.3905243 1.02368927-.3905243 1.41421356 0l3 3c.39052429.39052429.39052429 1.02368927 0 1.41421356-.39052429.39052429-1.02368927.39052429-1.41421356 0L4 2.41421356 1.70710678 4.70710678z"
/>
<path
:class="ascClass"
d="M6.29289322 9.29289322c.39052429-.39052429 1.02368927-.39052429 1.41421356 0 .39052429.39052429.39052429 1.02368928 0 1.41421358l-3 3c-.39052429.3905243-1.02368927.3905243-1.41421356 0l-3-3c-.3905243-.3905243-.3905243-1.02368929 0-1.41421358.3905243-.39052429 1.02368927-.39052429 1.41421356 0L4 11.5857864l2.29289322-2.29289318z"
/>
</svg>
</span>
</template>
<script>
export default {
props: {
resourceName: String,
uriKey: String,
},
methods: {
/**
* Handle the clicke event.
*/
handleClick() {
if (this.isSorted && this.isDescDirection) {
this.$emit('reset')
} else {
this.$emit('sort', {
key: this.uriKey,
direction: this.direction,
})
}
},
},
computed: {
/**
* Determine if the sorting direction is descending.
*/
isDescDirection() {
return this.direction == 'desc'
},
/**
* Determine if the sorting direction is ascending.
*/
isAscDirection() {
return this.direction == 'asc'
},
/**
* The CSS class to apply to the ascending arrow icon
*/
ascClass() {
if (this.isSorted && this.isDescDirection) {
return 'fill-80'
}
return 'fill-60'
},
/**
* The CSS class to apply to the descending arrow icon
*/
descClass() {
if (this.isSorted && this.isAscDirection) {
return 'fill-80'
}
return 'fill-60'
},
/**
* Determine whether this column is being sorted
*/
isSorted() {
return (
this.sortColumn == this.uriKey &&
['asc', 'desc'].includes(this.direction)
)
},
/**
* The current order query parameter for this resource
*/
sortKey() {
return `${this.resourceName}_order`
},
/**
* The current order query parameter value
*/
sortColumn() {
return this.$route.query[this.sortKey]
},
/**
* The current direction query parameter for this resource
*/
directionKey() {
return `${this.resourceName}_direction`
},
/**
* The current direction query parameter value
*/
direction() {
return this.$route.query[this.directionKey]
},
notSorted() {
return !!!this.isSorted
},
},
}
</script>

View File

@@ -0,0 +1,88 @@
<template>
<div v-if="hasData">
<div
ref="chart"
class="ct-chart"
:style="{ width: chartWidth, height: chartHeight }"
/>
</div>
</template>
<script>
import Chartist from 'chartist'
import 'chartist/dist/chartist.min.css'
// Default chart diameters.
const defaultHeight = 50
const defaultWidth = 100
export default {
props: ['resourceName', 'field'],
data: () => ({ chartist: null }),
watch: {
'field.data': function (newData, oldData) {
this.renderChart()
},
},
methods: {
renderChart() {
this.chartist.update(this.field.data)
},
},
mounted() {
this.chartist = new Chartist[this.chartStyle](
this.$refs.chart,
{ series: [this.field.data] },
{
height: this.chartHeight,
width: this.chartWidth,
showPoint: false,
fullWidth: true,
chartPadding: { top: 0, right: 0, bottom: 0, left: 0 },
axisX: { showGrid: false, showLabel: false, offset: 0 },
axisY: { showGrid: false, showLabel: false, offset: 0 },
}
)
},
computed: {
/**
* Determine if the field has a value other than null.
*/
hasData() {
return this.field.data.length > 0
},
/**
* Determine the chart style.
*/
chartStyle() {
const validTypes = ['line', 'bar']
let chartStyle = this.field.chartStyle.toLowerCase()
// Line and Bar are the only valid types.
if (!validTypes.includes(chartStyle)) return 'Line'
return chartStyle.charAt(0).toUpperCase() + chartStyle.slice(1)
},
/**
* Determine the chart height.
*/
chartHeight() {
return this.field.height || defaultHeight
},
/**
* Determine the chart width.
*/
chartWidth() {
return this.field.width || defaultWidth
},
},
}
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div :class="`text-${field.textAlign}`">
<template v-if="hasValue">
<div class="leading-normal my-2">
<component
:key="line.value"
v-for="line in field.lines"
class="whitespace-no-wrap"
:is="`index-${line.component}`"
:field="line"
:resourceName="resourceName"
/>
</div>
</template>
<p v-else>&mdash;</p>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
computed: {
/**
* Determine if the field has a value other than null.
*/
hasValue() {
return this.field.lines
},
},
}
</script>

View File

@@ -0,0 +1,17 @@
<template>
<div class="flex items-center">
<span class="mr-3 text-60" v-if="field.loadingWords.includes(field.value)">
<loader width="30" />
</span>
<span :class="{ 'text-danger': field.failedWords.includes(field.value) }">
{{ field.value }}
</span>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
}
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div :class="`text-${field.textAlign}`">
<template v-if="hasValue">
<div v-if="field.asHtml" v-html="field.value"></div>
<span v-else class="whitespace-no-wrap">{{ field.value }}</span>
</template>
<p v-else>&mdash;</p>
</div>
</template>
<script>
export default {
props: ['resourceName', 'field'],
computed: {
/**
* Determine if the field has a value other than null.
*/
hasValue() {
return this.field.value !== null
},
},
}
</script>