Skip to content

Commit 38a19bf

Browse files
committed
Add support for AutoViewForm
1 parent a370b4e commit 38a19bf

File tree

6 files changed

+213
-0
lines changed

6 files changed

+213
-0
lines changed

src/components/AutoQueryGrid.vue

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
</template>
3333
</AutoEditForm>
3434
</div>
35+
<div v-else-if="edit">
36+
<slot v-if="slots.viewform" name="viewform" :model="edit" :apis="apis" :done="editDone"></slot>
37+
<AutoViewForm v-else :model="edit" :apis="apis" :done="editDone" />
38+
</div>
3539
<slot v-if="slots.toolbar" name="toolbar"></slot>
3640
<div v-else-if="show('toolbar')">
3741
<QueryPrefs v-if="showQueryPrefs" :columns="viewModelColumns" :prefs="apiPrefs" @done="showQueryPrefs=false" @save="saveApiPrefs" />

src/components/AutoViewForm.vue

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<template>
2+
<div>
3+
<div v-if="!typeName">
4+
<p class="text-red-700">Could not create view for unknown <b>type</b> {{ typeName }}</p>
5+
</div>
6+
<div v-else-if="formStyle=='card'" :class="panelClass">
7+
<div :class="formClass">
8+
<div>
9+
<div v-if="$slots['heading']"><slot name="heading"></slot></div>
10+
<h3 v-else :class="headingClass">{{ title }}</h3>
11+
12+
<div v-if="$slots['subheading']"><slot name="subheading"></slot></div>
13+
<p v-else-if="subHeading" :class="subHeadingClass">{{ subHeading }}</p>
14+
<p v-else-if="metaType?.notes" :class="['notes',subHeadingClass]" v-html="metaType?.notes"></p>
15+
</div>
16+
<MarkupModel :value="model" />
17+
</div>
18+
</div>
19+
<div v-else class="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
20+
<div class="fixed inset-0"></div>
21+
<div class="fixed inset-0 overflow-hidden">
22+
<div @mousedown="close" class="absolute inset-0 overflow-hidden">
23+
<div @mousedown.stop="" class="pointer-events-none fixed inset-y-0 right-0 flex pl-10">
24+
<div :class="['pointer-events-auto w-screen xl:max-w-3xl md:max-w-xl max-w-lg',transition1]">
25+
<div :class="formClass">
26+
<div class="flex min-h-0 flex-1 flex-col overflow-auto">
27+
<div class="flex-1">
28+
<!-- Header -->
29+
<div class="bg-gray-50 dark:bg-gray-900 px-4 py-6 sm:px-6">
30+
<div class="flex items-start justify-between space-x-3">
31+
<div class="space-y-1">
32+
<div v-if="$slots['heading']"><slot name="heading"></slot></div>
33+
<h3 v-else :class="headingClass">{{ title }}</h3>
34+
35+
<div v-if="$slots['subheading']"><slot name="subheading"></slot></div>
36+
<p v-else-if="subHeading" :class="subHeadingClass">{{ subHeading }}</p>
37+
<p v-else-if="metaType?.notes" :class="['notes',subHeadingClass]" v-html="metaType?.notes"></p>
38+
</div>
39+
<div class="flex h-7 items-center">
40+
<CloseButton button-class="bg-gray-50 dark:bg-gray-900" @close="close"/>
41+
</div>
42+
</div>
43+
</div>
44+
<MarkupModel :value="model" />
45+
</div>
46+
</div>
47+
</div>
48+
</div>
49+
</div>
50+
</div>
51+
</div>
52+
</div>
53+
</div>
54+
</template>
55+
56+
<script setup lang="ts">
57+
import { useMetadata, Apis } from '@/use/metadata'
58+
import { form } from './css'
59+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
60+
import { transition } from '@/use/utils'
61+
import { Sole } from '@/use/config'
62+
import { humanize } from '@servicestack/client'
63+
64+
const props = withDefaults(defineProps<{
65+
model: any
66+
apis?: Apis,
67+
typeName?: string,
68+
done?: Function,
69+
formStyle?: "slideOver" | "card"
70+
panelClass?: string
71+
formClass?: string
72+
headingClass?: string
73+
subHeadingClass?: string
74+
heading?: string
75+
subHeading?: string
76+
}>(), {
77+
formStyle: "slideOver",
78+
})
79+
80+
const emit = defineEmits<{
81+
(e:'done'): void
82+
}>()
83+
84+
const { typeOf } = useMetadata()
85+
86+
const typeName = computed(() => props.typeName ?? props.apis!.dataModel!.name)
87+
const metaType = computed(() => typeOf(typeName.value))
88+
const panelClass = computed(() => props.panelClass || form.panelClass(props.formStyle))
89+
const formClass = computed(() => props.formClass || form.formClass(props.formStyle))
90+
const headingClass = computed(() => props.headingClass || form.headingClass(props.formStyle))
91+
const subHeadingClass = computed(() => props.subHeadingClass || form.subHeadingClass(props.formStyle))
92+
93+
const title = computed(() => props.heading || typeOf(typeName.value)?.description ||
94+
(props.model?.id ? `${humanize(typeName.value)} ${props.model.id}` : 'View ' + humanize(typeName.value)))
95+
96+
if (Sole.interceptors.has('AutoViewForm.new')) Sole.interceptors.invoke('AutoViewForm.new', { props })
97+
98+
function done() {
99+
if (props.done) {
100+
props.done()
101+
}
102+
}
103+
104+
/* SlideOver */
105+
const show = ref(false)
106+
const transition1 = ref('')
107+
const rule1 = {
108+
entering: { cls: 'transform transition ease-in-out duration-500 sm:duration-700', from: 'translate-x-full', to: 'translate-x-0' },
109+
leaving: { cls: 'transform transition ease-in-out duration-500 sm:duration-700', from: 'translate-x-0', to: 'translate-x-full' }
110+
}
111+
watch(show, () => {
112+
transition(rule1, transition1, show.value)
113+
if (!show.value) setTimeout(done, 700)
114+
})
115+
show.value = true
116+
function close() {
117+
if (props.formStyle == 'slideOver') {
118+
show.value = false
119+
} else {
120+
done()
121+
}
122+
}
123+
124+
const globalKeyHandler = (e:KeyboardEvent) => { if (e.key === 'Escape') close() }
125+
onMounted(() => window.addEventListener('keydown', globalKeyHandler))
126+
onUnmounted(() => window.removeEventListener('keydown', globalKeyHandler))
127+
</script>

src/components/MarkupFormat.vue

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<template>
2+
<a v-if="type=='link'" :href="value" class="text-indigo-600">{{value}}</a>
3+
<a v-else-if="type=='image'" :href="value" :title="value" class="inline-block">
4+
<Icon :src="value" :class="imageClass" />
5+
</a>
6+
<HtmlFormat v-else :value="value" />
7+
</template>
8+
<script setup lang="ts">
9+
import { useFiles } from '@/use/files'
10+
11+
const props = withDefaults(defineProps<{
12+
value: any,
13+
imageClass?: string
14+
}>(), {
15+
imageClass: 'w-8 h-8',
16+
})
17+
18+
const { getMimeType } = useFiles()
19+
const v = props.value
20+
21+
let type:string = typeof props.value
22+
const mimeType = type === 'string' && v.length ? getMimeType(v) : null
23+
24+
if (type === 'string' && v.length) {
25+
const url = v.startsWith('https://') || v.startsWith('http://')
26+
const path = url || v[0] === '/'
27+
28+
if (path && mimeType?.startsWith('image/')) {
29+
type = 'image'
30+
} else if (url) {
31+
type = 'link'
32+
}
33+
}
34+
</script>

src/components/MarkupModel.vue

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<template>
2+
<table class="my-2 w-full">
3+
<tr v-for="(v,k) in basic" class="leading-7">
4+
<th class="px-2 text-left align-top">{{humanize(k as string)}}</th>
5+
<td colspan="align-top"><MarkupFormat :value="v" /></td>
6+
</tr>
7+
<template v-for="(v,k) in complex">
8+
<tr class="my-2 leading-7">
9+
<td colspan="2" class="px-2 bg-indigo-700 text-white">{{humanize(k as string)}}</td>
10+
</tr>
11+
<tr class="leading-7">
12+
<td colspan="2" class="px-2 align-top"><MarkupFormat :value="v" /></td>
13+
</tr>
14+
</template>
15+
</table>
16+
</template>
17+
18+
<script setup lang="ts">
19+
import { humanize } from '@servicestack/client'
20+
21+
const props = defineProps<{
22+
value: any,
23+
imageClass?: string
24+
}>()
25+
26+
const fields = Object.keys(props.value)
27+
const basic:{[k:string]:any} = {}
28+
const complex:{[k:string]:any} = {}
29+
fields.forEach(k => {
30+
const v = props.value[k]
31+
const t = typeof v
32+
if (v == null || t === 'function' || t === 'symbol') {
33+
basic[k] = `(${v == null ? 'null' : 't'})`
34+
}
35+
else if (t === 'object') {
36+
complex[k] = v
37+
} else {
38+
basic[k] = v
39+
}
40+
})
41+
</script>

src/components/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,16 @@ import AutoFormFields from './AutoFormFields.vue'
3838
import AutoForm from './AutoForm.vue'
3939
import AutoCreateForm from './AutoCreateForm.vue'
4040
import AutoEditForm from './AutoEditForm.vue'
41+
import AutoViewForm from './AutoViewForm.vue'
4142
import ConfirmDelete from './ConfirmDelete.vue'
4243
import FormLoading from './FormLoading.vue'
4344

4445
import DataGrid from './DataGrid.vue'
4546
import CellFormat from './CellFormat.vue'
4647
import PreviewFormat from './PreviewFormat.vue'
4748
import HtmlFormat from './HtmlFormat.vue'
49+
import MarkupFormat from './MarkupFormat.vue'
50+
import MarkupModel from './MarkupModel.vue'
4851

4952
import CloseButton from './CloseButton.vue'
5053
import SlideOver from './SlideOver.vue'
@@ -98,13 +101,16 @@ export default {
98101
AutoForm,
99102
AutoCreateForm,
100103
AutoEditForm,
104+
AutoViewForm,
101105
ConfirmDelete,
102106
FormLoading,
103107

104108
DataGrid,
105109
CellFormat,
106110
PreviewFormat,
107111
HtmlFormat,
112+
MarkupFormat,
113+
MarkupModel,
108114

109115
CloseButton,
110116
SlideOver,

src/use/metadata.ts

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export class Apis implements AutoQueryApis
9292

9393
get AnyQuery() { return this.Query || this.QueryInto }
9494
get AnyUpdate() { return this.Patch || this.Update }
95+
get dataModel() { return this.AnyQuery?.dataModel }
9596

9697
toArray() {
9798
let to = [this.Query, this.QueryInto, this.Create, this.Update, this.Patch, this.Delete]

0 commit comments

Comments
 (0)