Skip to content

Commit 70268f5

Browse files
committedJan 12, 2025·
Add support for delete on ViewOnly forms
1 parent 23c6c42 commit 70268f5

File tree

2 files changed

+161
-112
lines changed

2 files changed

+161
-112
lines changed
 

‎src/components/AutoQueryGrid.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
</div>
3535
<div v-else-if="show('forms') && edit">
3636
<slot v-if="slots.viewform" name="viewform" :model="edit" :apis="apis" :done="editDone"></slot>
37-
<AutoViewForm v-else :model="edit" :apis="apis" :done="editDone" />
37+
<AutoViewForm v-else :model="edit" :apis="apis" :deleteType="canDelete ? apis.Delete!.request.name : null" :done="editDone" @save="editSave" @delete="editSave" />
3838
</div>
3939
<slot v-if="slots.toolbar" name="toolbar"></slot>
4040
<div v-else-if="show('toolbar')">

‎src/components/AutoViewForm.vue

+160-111
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,176 @@
11
<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" />
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>
1715
</div>
16+
<MarkupModel :value="model" />
1817
</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>
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>
4238
</div>
43-
</div>
44-
<MarkupModel :value="model" />
45-
</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" />
4645
</div>
4746
</div>
47+
<div :class="form.buttonsClass">
48+
<div>
49+
<ConfirmDelete v-if="deleteType" @delete="onDelete" />
50+
</div>
51+
<div>
52+
<FormLoading v-if="showLoading && loading" />
53+
</div>
54+
<div class="flex justify-end"></div>
55+
</div>
4856
</div>
4957
</div>
5058
</div>
5159
</div>
5260
</div>
5361
</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-
}
62+
</div>
63+
</template>
64+
65+
<script setup lang="ts">
66+
import type { ApiResponse, ResponseStatus } from '@/types'
67+
import { useMetadata, toFormValues, Apis } from '@/use/metadata'
68+
import { form } from './css'
69+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
70+
import { transition } from '@/use/utils'
71+
import { Sole } from '@/use/config'
72+
import { useClient } from '@/use/client'
73+
import { ApiResult, humanize, map, mapGet } from '@servicestack/client'
74+
75+
const props = withDefaults(defineProps<{
76+
model: any
77+
apis?: Apis,
78+
typeName?: string,
79+
done?: Function,
80+
formStyle?: "slideOver" | "card"
81+
panelClass?: string
82+
formClass?: string
83+
headingClass?: string
84+
subHeadingClass?: string
85+
heading?: string
86+
subHeading?: string
87+
showLoading?: boolean
88+
deleteType?: string|InstanceType<any>|Function
89+
}>(), {
90+
formStyle: "slideOver",
91+
})
92+
93+
const emit = defineEmits<{
94+
(e:'done'): void
95+
(e:'save', response:any): () => void
96+
(e:'delete', response:any): () => void
97+
(e:'error', status:ResponseStatus): void
98+
}>()
99+
100+
const { typeOf, getPrimaryKey, Crud, createDto } = useMetadata()
101+
102+
const typeName = computed(() => props.typeName ?? props.apis!.dataModel!.name)
103+
const metaType = computed(() => typeOf(typeName.value))
104+
const panelClass = computed(() => props.panelClass || form.panelClass(props.formStyle))
105+
const formClass = computed(() => props.formClass || form.formClass(props.formStyle))
106+
const headingClass = computed(() => props.headingClass || form.headingClass(props.formStyle))
107+
const subHeadingClass = computed(() => props.subHeadingClass || form.subHeadingClass(props.formStyle))
108+
109+
const title = computed(() => props.heading || typeOf(typeName.value)?.description ||
110+
(props.model?.id ? `${humanize(typeName.value)} ${props.model.id}` : 'View ' + humanize(typeName.value)))
111+
112+
const api = ref<ApiResponse>(new ApiResult<any>())
113+
let origModel = Object.assign({}, toFormValues(props.model))
114+
if (Sole.interceptors.has('AutoViewForm.new')) Sole.interceptors.invoke('AutoViewForm.new', { props })
115+
116+
let client = useClient()
117+
let loading = computed(() => client.loading.value)
118+
const getPk = () => map(metaType.value, dataModel => getPrimaryKey(dataModel))
119+
const dataModel = computed(() => metaType.value)
120+
121+
async function onDelete(e:Event) {
122+
let pk = getPk()
123+
const id = pk ? mapGet(props.model, pk.name) : null
124+
if (!id) {
125+
console.error(`Could not find Primary Key for Type ${typeName.value} (${dataModel.value})`)
126+
return
102127
}
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' }
128+
const args = { [pk!.name]: id }
129+
const request = typeof props.deleteType == 'string'
130+
? createDto(props.deleteType, args)
131+
: (props.deleteType ? new props.deleteType(args) : null)
132+
133+
let returnsVoid = map(request['createResponse'], fn => typeof fn == 'function' ? fn() : null) == null
134+
if (!returnsVoid) {
135+
api.value = await client.api(request)
136+
} else {
137+
api.value = await client.apiVoid(request)
110138
}
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-
}
139+
140+
if (api.value.succeeded) {
141+
emit('delete', api.value.response)
142+
} else {
143+
emit('error', api.value.error!)
122144
}
145+
}
123146
124-
const globalKeyHandler = (e:KeyboardEvent) => { if (e.key === 'Escape') close() }
125-
onMounted(() => window.addEventListener('keydown', globalKeyHandler))
126-
onUnmounted(() => window.removeEventListener('keydown', globalKeyHandler))
127-
</script>
147+
function done() {
148+
if (props.done) {
149+
props.done()
150+
}
151+
}
152+
153+
/* SlideOver */
154+
const show = ref(false)
155+
const transition1 = ref('')
156+
const rule1 = {
157+
entering: { cls: 'transform transition ease-in-out duration-500 sm:duration-700', from: 'translate-x-full', to: 'translate-x-0' },
158+
leaving: { cls: 'transform transition ease-in-out duration-500 sm:duration-700', from: 'translate-x-0', to: 'translate-x-full' }
159+
}
160+
watch(show, () => {
161+
transition(rule1, transition1, show.value)
162+
if (!show.value) setTimeout(done, 700)
163+
})
164+
show.value = true
165+
function close() {
166+
if (props.formStyle == 'slideOver') {
167+
show.value = false
168+
} else {
169+
done()
170+
}
171+
}
172+
173+
const globalKeyHandler = (e:KeyboardEvent) => { if (e.key === 'Escape') close() }
174+
onMounted(() => window.addEventListener('keydown', globalKeyHandler))
175+
onUnmounted(() => window.removeEventListener('keydown', globalKeyHandler))
176+
</script>

0 commit comments

Comments
 (0)