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,82 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<audio
v-if="hasPreviewableAudio"
v-bind="defaultAttributes"
class="w-full"
:src="field.previewUrl"
controls
controlslist="nodownload"
/>
<span v-if="!hasPreviewableAudio">&mdash;</span>
<p v-if="shouldShowToolbar" class="flex items-center text-sm mt-3">
<a
v-if="field.downloadable"
:dusk="field.attribute + '-download-link'"
@keydown.enter.prevent="download"
@click.prevent="download"
tabindex="0"
class="cursor-pointer text-gray-500 inline-flex items-center"
>
<Icon
class="mr-2"
type="download"
view-box="0 0 24 24"
width="16"
height="16"
/>
<span class="class mt-1">{{ __('Download') }}</span>
</a>
</p>
</template>
</PanelItem>
</template>
<script>
import isNil from 'lodash/isNil'
import { FieldValue } from '@/mixins'
export default {
mixins: [FieldValue],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
methods: {
/**
* Download the linked file
*/
download() {
const { resourceName, resourceId } = this
const attribute = this.field.attribute
let link = document.createElement('a')
link.href = `/nova-api/${resourceName}/${resourceId}/download/${attribute}`
link.download = 'download'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
},
computed: {
hasPreviewableAudio() {
return !isNil(this.field.previewUrl)
},
shouldShowToolbar() {
return Boolean(this.field.downloadable && this.fieldHasValue)
},
defaultAttributes() {
return {
src: this.field.previewUrl,
autoplay: this.field.autoplay,
preload: this.field.preload,
}
},
},
}
</script>

View File

@@ -0,0 +1,19 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<Badge class="mt-1" :label="field.label" :extra-classes="field.typeClass">
<template #icon>
<span v-if="field.icon" class="mr-1 -ml-1">
<Icon :solid="true" :type="field.icon" />
</span>
</template>
</Badge>
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,40 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<span v-if="field.viewable && field.value">
<RelationPeek
v-if="field.peekable && field.hasFieldsToPeekAt"
:resource-name="field.resourceName"
:resource-id="field.belongsToId"
:resource="resource"
>
<Link
@click.stop
:href="
$url(`/resources/${field.resourceName}/${field.belongsToId}`)
"
class="link-default"
>
{{ field.value }}
</Link>
</RelationPeek>
<Link
v-else
:href="$url(`/resources/${field.resourceName}/${field.belongsToId}`)"
class="link-default"
>
{{ field.value }}
</Link>
</span>
<p v-else-if="field.value">{{ field.value }}</p>
<p v-else>&mdash;</p>
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<ResourceIndex
:field="field"
:resource-name="field.resourceName"
:via-resource="resourceName"
:via-resource-id="resourceId"
:via-relationship="field.belongsToManyRelationship"
:relationship-type="'belongsToMany'"
@actionExecuted="actionExecuted"
:load-cards="false"
:initialPerPage="field.perPage || 5"
:should-override-meta="false"
/>
</template>
<script>
export default {
emits: ['actionExecuted'],
props: ['resourceName', 'resourceId', 'resource', 'field'],
methods: {
/**
* Handle the actionExecuted event and pass it up the chain.
*/
actionExecuted() {
this.$emit('actionExecuted')
},
},
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<Icon
viewBox="0 0 24 24"
width="24"
height="24"
:type="type"
:class="color"
/>
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
computed: {
label() {
return this.field.value == true ? this.__('Yes') : this.__('No')
},
type() {
return this.field.value == true ? 'check-circle' : 'x-circle'
},
color() {
return this.field.value == true ? 'text-green-500' : 'text-red-500'
},
},
}
</script>

View File

@@ -0,0 +1,57 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<ul v-if="value.length > 0" class="space-y-2">
<li
v-for="option in value"
:class="classes[option.checked]"
class="flex items-center rounded-full font-bold text-sm leading-tight space-x-2"
>
<IconBoolean class="flex-none" :value="option.checked" />
<span>{{ option.label }}</span>
</li>
</ul>
<span v-else>{{ this.field.noValueText }}</span>
</template>
</PanelItem>
</template>
<script>
import filter from 'lodash/filter'
import map from 'lodash/map'
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
data: () => ({
value: [],
classes: {
true: 'text-green-500',
false: 'text-red-500',
},
}),
created() {
this.field.value = this.field.value || {}
this.value = filter(
map(this.field.options, o => {
return {
name: o.name,
label: o.label,
checked: this.field.value[o.name] || false,
}
}),
o => {
if (this.field.hideFalseValues === true && o.checked === false) {
return false
} else if (this.field.hideTrueValues === true && o.checked === true) {
return false
}
return true
}
)
},
}
</script>

View File

@@ -0,0 +1,51 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<div
v-if="fieldValue"
class="form-input form-control-bordered px-0 overflow-hidden"
>
<textarea ref="theTextarea" />
</div>
<p v-else>&mdash;</p>
</template>
</PanelItem>
</template>
<script>
import CodeMirror from 'codemirror'
import { FieldValue } from '@/mixins'
import isNull from 'lodash/isNull'
export default {
mixins: [FieldValue],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
codemirror: null,
/**
* Mount the component.
*/
mounted() {
const fieldValue = this.fieldValue
if (!isNull(fieldValue)) {
const config = {
tabSize: 4,
indentWithTabs: true,
lineWrapping: true,
lineNumbers: true,
theme: 'dracula',
...this.field.options,
readOnly: true,
tabindex: '-1', // The editor is for display only and should not be tabbable.
}
this.codemirror = CodeMirror.fromTextArea(this.$refs.theTextarea, config)
this.codemirror?.getDoc().setValue(fieldValue)
this.codemirror?.setSize('100%', this.field.height)
}
},
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<div
class="rounded-lg inline-flex items-center justify-center border border-60 p-1"
>
<span
class="block w-6 h-6"
:style="{ borderRadius: '5px', backgroundColor: field.value }"
/>
</div>
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<PanelItem :index="index" :field="field" />
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<p v-if="fieldHasValue || usesCustomizedDisplay" :title="field.value">
{{ formattedDate }}
</p>
<p v-else>&mdash;</p>
</template>
</PanelItem>
</template>
<script>
import { DateTime } from 'luxon'
import { FieldValue } from '@/mixins'
export default {
mixins: [FieldValue],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
computed: {
formattedDate() {
if (this.field.usesCustomizedDisplay) {
return this.field.displayedAs
}
let isoDate = DateTime.fromISO(this.field.value)
return isoDate.toLocaleString({
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
},
},
}
</script>

View File

@@ -0,0 +1,44 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<p v-if="fieldHasValue || usesCustomizedDisplay" :title="field.value">
{{ formattedDateTime }}
</p>
<p v-else>&mdash;</p>
</template>
</PanelItem>
</template>
<script>
import { DateTime } from 'luxon'
import { FieldValue } from '@/mixins'
export default {
mixins: [FieldValue],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
computed: {
formattedDateTime() {
if (this.usesCustomizedDisplay) {
return this.field.displayedAs
}
return DateTime.fromISO(this.field.value)
.setZone(this.timezone)
.toLocaleString({
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short',
})
},
timezone() {
return Nova.config('userTimezone') || Nova.config('timezone')
},
},
}
</script>

View File

@@ -0,0 +1,35 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<p v-if="fieldHasValue" class="flex items-center">
<a :href="`mailto:${field.value}`" class="link-default">
{{ fieldValue }}
</a>
<CopyButton
v-if="fieldHasValue && field.copyable && !shouldDisplayAsHtml"
@click.prevent.stop="copy"
v-tooltip="__('Copy to clipboard')"
class="mx-0"
/>
</p>
<p v-else>&mdash;</p>
</template>
</PanelItem>
</template>
<script>
import { CopiesToClipboard, FieldValue } from '@/mixins'
export default {
mixins: [CopiesToClipboard, FieldValue],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
methods: {
copy() {
this.copyValueToClipboard(this.field.value)
},
},
}
</script>

View File

@@ -0,0 +1,88 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<ImageLoader
v-if="shouldShowLoader"
:src="imageUrl"
:maxWidth="field.maxWidth || field.detailWidth"
:rounded="field.rounded"
:aspect="field.aspect"
/>
<span v-if="fieldValue && !imageUrl" class="break-words">
{{ fieldValue }}
</span>
<span v-if="!fieldValue && !imageUrl">&mdash;</span>
<p v-if="shouldShowToolbar" class="flex items-center text-sm mt-3">
<a
v-if="field.downloadable"
:dusk="field.attribute + '-download-link'"
@keydown.enter.prevent="download"
@click.prevent="download"
tabindex="0"
class="cursor-pointer text-gray-500 inline-flex items-center"
>
<Icon
class="mr-2"
type="download"
view-box="0 0 24 24"
width="16"
height="16"
/>
<span class="class mt-1">{{ __('Download') }}</span>
</a>
</p>
</template>
</PanelItem>
</template>
<script>
import { FieldValue } from '@/mixins'
export default {
mixins: [FieldValue],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
methods: {
/**
* Download the linked file
*/
download() {
const { resourceName, resourceId } = this
const attribute = this.fieldAttribute
let link = document.createElement('a')
link.href = `/nova-api/${resourceName}/${resourceId}/download/${attribute}`
link.download = 'download'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
},
computed: {
hasValue() {
return Boolean(this.field.value || this.imageUrl)
},
shouldShowLoader() {
return this.imageUrl
},
shouldShowToolbar() {
return Boolean(this.field.downloadable && this.hasValue)
},
imageUrl() {
return this.field.previewUrl || this.field.thumbnailUrl
},
isVaporField() {
return this.field.component === 'vapor-file-field'
},
},
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<ResourceIndex
:field="field"
:resource-name="field.resourceName"
:via-resource="resourceName"
:via-resource-id="resourceId"
:via-relationship="field.hasManyRelationship"
:relationship-type="'hasMany'"
@actionExecuted="actionExecuted"
:load-cards="false"
:initialPerPage="field.perPage || 5"
:should-override-meta="false"
/>
</template>
<script>
import { mapProps } from '@/mixins'
export default {
emits: ['actionExecuted'],
props: {
...mapProps(['resourceId', 'field']),
resourceName: {},
resource: {},
},
methods: {
/**
* Handle the actionExecuted event and pass it up the chain.
*/
actionExecuted() {
this.$emit('actionExecuted')
},
},
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<ResourceIndex
:field="field"
:resource-name="field.resourceName"
:via-resource="resourceName"
:via-resource-id="resourceId"
:via-relationship="field.hasManyThroughRelationship"
:relationship-type="'hasManyThrough'"
@actionExecuted="actionExecuted"
:load-cards="false"
:initialPerPage="field.perPage || 5"
:should-override-meta="false"
/>
</template>
<script>
export default {
emits: ['actionExecuted'],
props: ['resourceName', 'resourceId', 'resource', 'field'],
methods: {
/**
* Handle the actionExecuted event and pass it up the chain.
*/
actionExecuted() {
this.$emit('actionExecuted')
},
},
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<div
v-if="field.authorizedToView"
class="relative"
:dusk="field.resourceName + '-index-component'"
:data-relationship="viaRelationship"
>
<template v-if="!hasRelation">
<Heading :level="1" class="mb-3 flex items-center">{{
field.singularLabel
}}</Heading>
<Card>
<IndexEmptyDialog
:create-button-label="createButtonLabel"
:singular-name="singularName"
:resource-name="field.resourceName"
:via-resource="resourceName"
:via-resource-id="viaResourceId"
:via-relationship="viaRelationship"
:relationship-type="field.relationshipType"
:authorized-to-create="authorizedToCreate"
:authorized-to-relate="true"
/>
</Card>
</template>
<div v-else>
<ResourceDetail
:resource-name="field.resourceName"
:resource-id="field.hasOneId"
:via-resource="resourceName"
:via-resource-id="viaResourceId"
:via-relationship="viaRelationship"
:relationship-type="field.relationshipType"
:show-view-link="true"
/>
</div>
</div>
</template>
<script>
export default {
props: ['resourceName', 'resourceId', 'resource', 'field'],
computed: {
authorizedToCreate() {
return this.field.authorizedToCreate
},
createButtonLabel() {
return this.field.createButtonLabel
},
hasRelation() {
return this.field.hasOneId != null
},
singularName() {
return this.field.singularLabel
},
viaResourceId() {
return this.resource.id.value
},
viaRelationship() {
return this.field.hasOneRelationship
},
},
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<div
v-if="field.authorizedToView"
class="relative"
:dusk="field.resourceName + '-index-component'"
:data-relationship="viaRelationship"
>
<template v-if="!hasRelation">
<Heading :level="1" class="mb-3 flex items-center">{{
field.singularLabel
}}</Heading>
<Card>
<IndexEmptyDialog
:create-button-label="createButtonLabel"
:singular-name="singularName"
:resource-name="field.resourceName"
:via-resource="resourceName"
:via-resource-id="viaResourceId"
:via-relationship="viaRelationship"
:relationship-type="field.relationshipType"
:authorized-to-create="false"
:authorized-to-relate="false"
/>
</Card>
</template>
<div v-else>
<ResourceDetail
:resource-name="field.resourceName"
:resource-id="field.hasOneThroughId"
:via-resource="resourceName"
:via-resource-id="viaResourceId"
:via-relationship="viaRelationship"
:relationship-type="field.relationshipType"
:show-view-link="true"
/>
</div>
</div>
</template>
<script>
export default {
props: ['resourceName', 'resourceId', 'resource', 'field'],
computed: {
authorizedToCreate() {
return this.field.authorizedToCreate
},
createButtonLabel() {
return this.field.createButtonLabel
},
hasRelation() {
return this.field.hasOneThroughId != null
},
singularName() {
return this.field.singularLabel
},
viaResourceId() {
return this.resource.id.value
},
viaRelationship() {
return this.field.hasOneThroughRelationship
},
},
}
</script>

View File

@@ -0,0 +1,44 @@
<template>
<div
class="-mx-6"
:class="{
'border-t border-gray-100 dark:border-gray-700': index !== 0,
'-mt-2': index === 0,
}"
>
<div class="w-full py-4 px-6">
<slot name="value">
<Heading :level="3" v-if="fieldValue && !shouldDisplayAsHtml">
{{ fieldValue }}
</Heading>
<div
v-else-if="fieldValue && shouldDisplayAsHtml"
v-html="field.value"
></div>
<p v-else>&mdash;</p>
</slot>
</div>
</div>
</template>
<script>
import filled from '@/util/filled'
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
computed: {
fieldValue() {
if (!filled(this.field.value)) {
return false
}
return String(this.field.value)
},
shouldDisplayAsHtml() {
return this.field.asHtml
},
},
}
</script>

View File

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

View File

@@ -0,0 +1,9 @@
<template>
<PanelItem :index="index" :field="field" />
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,48 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<FormKeyValueTable
v-if="theData.length > 0"
:edit-mode="false"
class="overflow-hidden"
>
<FormKeyValueHeader
:key-label="field.keyLabel"
:value-label="field.valueLabel"
/>
<div
class="bg-gray-50 dark:bg-gray-700 overflow-hidden key-value-items"
>
<FormKeyValueItem
v-for="(item, index) in theData"
:index="index"
:item="item"
:disabled="true"
:key="item.key"
/>
</div>
</FormKeyValueTable>
</template>
</PanelItem>
</template>
<script>
import map from 'lodash/map'
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
data: () => ({ theData: [] }),
created() {
this.theData = map(
Object.entries(this.field.value || {}),
([key, value]) => ({
key: `${key}`,
value,
})
)
},
}
</script>

View File

@@ -0,0 +1,19 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<Excerpt :content="excerpt" :should-show="field.shouldShow" />
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
computed: {
excerpt() {
return this.field.previewFor
},
},
}
</script>

View File

@@ -0,0 +1,26 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<Link
v-if="field.viewable && field.value"
:href="$url(`/resources/${field.resourceName}/${field.morphToId}`)"
class="no-underline font-bold link-default"
>
{{ field.name }}: {{ field.value }} ({{ field.resourceLabel }})
</Link>
<p v-else-if="field.morphToId && field.resourceLabel !== null">
{{ field.name }}: {{ field.morphToId }} ({{ field.resourceLabel }})
</p>
<p v-else-if="field.morphToId && field.resourceLabel === null">
{{ field.morphToType }}: {{ field.morphToId }}
</p>
<p v-else>&mdash;</p>
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,41 @@
<template>
<PanelItem :index="index" :field="field" :field-name="field.name">
<template #value>
<span v-if="field.viewable && field.value">
<RelationPeek
v-if="field.peekable && field.hasFieldsToPeekAt"
:resource-name="field.resourceName"
:resource-id="field.morphToId"
:resource="resource"
>
<Link
@click.stop
:href="$url(`/resources/${field.resourceName}/${field.morphToId}`)"
class="link-default"
>
{{ field.resourceLabel }}: {{ field.value }}
</Link>
</RelationPeek>
<Link
v-else
:href="$url(`/resources/${field.resourceName}/${field.morphToId}`)"
class="link-default"
>
{{ field.resourceLabel }}: {{ field.value }}
</Link>
</span>
<p v-else-if="field.value">
{{ field.resourceLabel || field.morphToType }}: {{ field.value }}
</p>
<p v-else>&mdash;</p>
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<ResourceIndex
:field="field"
:resource-name="field.resourceName"
:via-resource="resourceName"
:via-resource-id="resourceId"
:via-relationship="field.morphToManyRelationship"
:relationship-type="'morphToMany'"
@actionExecuted="actionExecuted"
:load-cards="false"
:initialPerPage="field.perPage || 5"
:should-override-meta="false"
/>
</template>
<script>
export default {
emits: ['actionExecuted'],
props: ['resourceName', 'resourceId', 'resource', 'field'],
methods: {
/**
* Handle the actionExecuted event and pass it up the chain.
*/
actionExecuted() {
this.$emit('actionExecuted')
},
},
}
</script>

View File

@@ -0,0 +1,36 @@
<template>
<PanelItem :index="index" :field="field">
<template v-slot:value>
<span
v-for="item in fieldValues"
v-text="item"
class="inline-block text-sm mb-1 mr-2 px-2 py-0 bg-primary-500 text-white dark:text-gray-900 rounded"
/>
</template>
</PanelItem>
</template>
<script>
import { FieldValue } from '@/mixins'
import forEach from 'lodash/forEach'
export default {
mixins: [FieldValue],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
computed: {
fieldValues() {
let selected = []
forEach(this.field.options, option => {
if (this.isEqualsToValue(option.value)) {
selected.push(option.label)
}
})
return selected
},
},
}
</script>

View File

@@ -0,0 +1,110 @@
<template>
<div>
<slot>
<div class="flex items-center">
<Heading :level="1" v-text="panel.name" />
<button
v-if="panel.collapsable"
@click="toggleCollapse"
class="rounded border border-transparent h-6 w-6 ml-1 inline-flex items-center justify-center focus:outline-none focus:ring focus:ring-primary-200"
:aria-label="__('Toggle Collapsed')"
:aria-expanded="collapsed === false ? 'true' : 'false'"
>
<CollapseButton :collapsed="collapsed" />
</button>
</div>
<p
v-if="panel.helpText && !collapsed"
class="text-gray-500 text-sm font-semibold italic"
:class="panel.helpText ? 'mt-1' : 'mt-3'"
v-html="panel.helpText"
/>
</slot>
<Card
class="mt-3 py-2 px-6 divide-y divide-gray-100 dark:divide-gray-700"
v-if="!collapsed && fields.length > 0"
>
<component
:key="index"
v-for="(field, index) in fields"
:index="index"
:is="resolveComponentName(field)"
:resource-name="resourceName"
:resource-id="resourceId"
:resource="resource"
:field="field"
@actionExecuted="actionExecuted"
/>
<div
v-if="shouldShowShowAllFieldsButton"
class="-mx-6 border-t border-gray-100 dark:border-gray-700 text-center rounded-b"
>
<button
type="button"
class="block w-full text-sm link-default font-bold py-2 -mb-2"
@click="showAllFields"
>
{{ __('Show All Fields') }}
</button>
</div>
</Card>
</div>
</template>
<script>
import { Collapsable, BehavesAsPanel } from '@/mixins'
export default {
mixins: [Collapsable, BehavesAsPanel],
methods: {
/**
* Resolve the component name.
*/
resolveComponentName(field) {
return field.prefixComponent
? 'detail-' + field.component
: field.component
},
/**
* Show all of the Panel's fields.
*/
showAllFields() {
return (this.panel.limit = 0)
},
},
computed: {
localStorageKey() {
return `nova.panels.${this.panel.attribute}.collapsed`
},
collapsedByDefault() {
return this.panel?.collapsedByDefault ?? false
},
/**
* Limits the visible fields.
*/
fields() {
if (this.panel.limit > 0) {
return this.panel.fields.slice(0, this.panel.limit)
}
return this.panel.fields
},
/**
* Determines if should display the 'Show all fields' button.
*/
shouldShowShowAllFieldsButton() {
return this.panel.limit > 0
},
},
}
</script>

View File

@@ -0,0 +1,15 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<p>
&middot;&middot;&middot;&middot;&middot;&middot;&middot;&middot;&middot;
</p>
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,7 @@
<script>
import TextField from '@/fields/Detail/TextField'
export default {
extends: TextField,
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div>
<component
:key="`${field.attribute}:${resourceId}`"
:is="`detail-${field.component}`"
:resource-name="resourceName"
:resource-id="resourceId"
:resource="resource"
:field="field"
@actionExecuted="actionExecuted"
/>
</div>
</template>
<script>
import { BehavesAsPanel } from '@/mixins'
export default {
mixins: [BehavesAsPanel],
computed: {
field() {
return this.panel.fields[0]
},
},
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<PanelItem :index="index" :field="field" />
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,7 @@
<script>
import TextField from '@/fields/Detail/TextField'
export default {
extends: TextField,
}
</script>

View File

@@ -0,0 +1,91 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<div
ref="chart"
class="ct-chart"
:style="{ width: chartWidth, height: chartHeight }"
/>
</template>
</PanelItem>
</template>
<script>
import Chartist from 'chartist'
import 'chartist/dist/chartist.min.css'
// Default chart diameters.
const defaultHeight = 120
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', '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() {
if (this.field.height) return `${this.field.height}px`
return `${defaultHeight}px`
},
/**
* Determine the chart width.
*/
chartWidth() {
if (this.field.width) return `${this.field.width}px`
},
},
}
</script>

View File

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

View File

@@ -0,0 +1,38 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<Badge
v-if="fieldHasValue"
class="whitespace-nowrap inline-flex items-center"
:class="field.typeClass"
>
<span class="mr-1 -ml-1">
<Loader v-if="field.type == 'loading'" width="20" class="mr-1" />
<Icon
v-if="field.type == 'failed'"
:solid="true"
type="exclamation-circle"
/>
<Icon
v-if="field.type == 'success'"
:solid="true"
type="check-circle"
/>
</span>
{{ fieldValue }}
</Badge>
<span v-else>&mdash;</span>
</template>
</PanelItem>
</template>
<script>
import { FieldValue } from '@/mixins'
export default {
mixins: [FieldValue],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<div v-if="field.value.length > 0">
<TagGroup
v-if="field.style === 'group'"
:tags="field.value"
:resource-name="field.resourceName"
:editable="false"
:with-preview="field.withPreview"
/>
<TagList
v-if="field.style === 'list'"
:tags="field.value"
:resource-name="field.resourceName"
:editable="false"
:with-preview="field.withPreview"
/>
</div>
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<PanelItem :index="index" :field="field" />
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,17 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<Excerpt
:content="field.value"
:plain-text="true"
:should-show="field.shouldShow"
/>
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<PanelItem :index="index" :field="field">
<template #value>
<Excerpt :content="field.value" :should-show="field.shouldShow" />
</template>
</PanelItem>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<PanelItem :index="index" :field="field">
<template v-slot:value>
<p v-if="fieldHasValue && !shouldDisplayAsHtml">
<a
class="link-default"
:href="field.value"
rel="noreferrer noopener"
target="_blank"
>
{{ fieldValue }}
</a>
</p>
<div
v-else-if="fieldValue && shouldDisplayAsHtml"
v-html="fieldValue"
></div>
<p v-else>&mdash;</p>
</template>
</PanelItem>
</template>
<script>
import { FieldValue } from '@/mixins'
export default {
mixins: [FieldValue],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,7 @@
<script>
import AudioField from '@/fields/Detail/AudioField'
export default {
extends: AudioField,
}
</script>

View File

@@ -0,0 +1,7 @@
<script>
import FileField from '@/fields/Detail/FileField'
export default {
extends: FileField,
}
</script>