Skip to content

Commit 3f49405

Browse files
committed
AutoForm fixes
1 parent 32aa165 commit 3f49405

File tree

7 files changed

+102
-38
lines changed

7 files changed

+102
-38
lines changed

src/api.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ export function useConfig() {
320320
Sole.config.value = Object.assign(Sole.config.value, config)
321321
}
322322
function assetsPathResolver(src?:string) {
323+
//console.log('assetsPathResolver', src, Sole.config.value.assetsPathResolver, Sole.config.value.assetsPathResolver(src))
323324
return src && Sole.config.value.assetsPathResolver
324325
? Sole.config.value.assetsPathResolver(src)
325326
: src
@@ -500,16 +501,16 @@ export function useAppMetadata() {
500501
return to
501502
}
502503

503-
function createInput(prop:MetadataPropertyType) {
504+
function createInput(prop:MetadataPropertyType, input?:InputInfo) {
504505
const create = (name:string, type?:string) => {
505-
const ret:InputInfo = {
506+
const ret:InputInfo = Object.assign({
506507
id:name,
507508
name,
508-
type: type || 'text'
509-
}
509+
type
510+
}, input)
510511
return ret
511512
}
512-
const ret = create(prop.name, propInputType(prop))
513+
const ret = create(prop.name, input?.type || propInputType(prop) || 'text')
513514
if (prop.isEnum) {
514515
ret.type = 'select'
515516
ret.allowableEntries = asKvps(propertyOptions(prop))
@@ -525,8 +526,7 @@ export function useAppMetadata() {
525526
const dataModel = typeOfRef(op?.dataModel)
526527
typeProps.forEach(prop => {
527528
if (prop.isPrimaryKey) return //?
528-
const input = prop.input || createInput(prop)
529-
if (!input.name) input.name = input.id
529+
const input = createInput(prop, prop.input)
530530
input.id = toCamelCase(input.id)
531531
if (input.type == 'file' && prop.uploadTo && !input.accept) {
532532
const uploadLocation = Sole.metadata.value?.plugins.filesUpload?.locations.find(x => x.name == prop.uploadTo)

src/components/AutoCreateForm.vue

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div v-if="!metaType">
44
<p class="text-red-700">Could not create form for unknown <b>type</b> {{ typeName }}</p>
55
</div>
6-
<div v-if="formStyle=='card'" :class="panelClass">
6+
<div v-else-if="formStyle=='card'" :class="panelClass">
77
<form @submit.prevent="save">
88
<div :class="formClass">
99
<div>
@@ -105,7 +105,7 @@ const emit = defineEmits<{
105105
function update(value:ApiRequest) {
106106
}
107107
108-
const { typeOf, typeProperties } = useAppMetadata()
108+
const { typeOf, typeProperties, Crud } = useAppMetadata()
109109
110110
const typeName = computed(() => typeof props.type == 'string'
111111
? props.type
@@ -119,7 +119,9 @@ const formClass = computed(() => props.formClass || Css.form.formClass(props.for
119119
const headingClass = computed(() => props.headingClass || Css.form.headingClass(props.formStyle))
120120
const subHeadingClass = computed(() => props.subHeadingClass || Css.form.subHeadingClass(props.formStyle))
121121
122-
const title = computed(() => props.heading || typeOf(typeName.value)?.description || `New ${humanize(typeName.value)}`)
122+
const dataModel = computed(() => Crud.model(metaType.value))
123+
const title = computed(() => props.heading || typeOf(typeName.value)?.description ||
124+
(dataModel.value ? `New ${humanize(dataModel.value)}` : humanize(typeName.value)))
123125
124126
const api = ref<ApiResponse>(new ApiResult<any>())
125127
@@ -139,13 +141,16 @@ async function save(e:Event) {
139141
if (HttpMethods.hasRequestBody(method)) {
140142
let requestDto = new model.value.constructor()
141143
let formData = new FormData(form)
144+
let file = Array.from(form.elements).find(x => (x as any).type == 'file') as HTMLInputElement
145+
console.log('formData', formData, file, file?.files?.length)
142146
if (!returnsVoid) {
143147
api.value = await client.apiForm(requestDto, formData, { jsconfig: 'eccn' })
144148
} else {
145149
api.value = await client.apiFormVoid(requestDto, formData, { jsconfig: 'eccn' })
146150
}
147151
} else {
148152
let fieldValues = formValues(form, typeProperties(metaType.value))
153+
console.log('fieldValues', fieldValues)
149154
let requestDto = new model.value.constructor(fieldValues)
150155
if (!returnsVoid) {
151156
api.value = await client.api(requestDto, { jsconfig: 'eccn' })

src/components/AutoEditForm.vue

+9-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div v-if="!metaType">
44
<p class="text-red-700">Could not create form for unknown <b>type</b> {{ typeName }}</p>
55
</div>
6-
<div v-if="formStyle=='card'" :class="panelClass">
6+
<div v-else-if="formStyle=='card'" :class="panelClass">
77
<form @submit.prevent="save">
88
<div :class="formClass">
99
<div>
@@ -130,7 +130,9 @@
130130
const headingClass = computed(() => props.headingClass || Css.form.headingClass(props.formStyle))
131131
const subHeadingClass = computed(() => props.subHeadingClass || Css.form.subHeadingClass(props.formStyle))
132132
133-
const title = computed(() => props.heading || typeOf(typeName.value)?.description || `New ${humanize(typeName.value)}`)
133+
const dataModel = computed(() => Crud.model(metaType.value))
134+
const title = computed(() => props.heading || typeOf(typeName.value)?.description ||
135+
(dataModel.value ? `New ${humanize(dataModel.value)}` : humanize(typeName.value)))
134136
135137
const api = ref<ApiResponse>(new ApiResult<any>())
136138
@@ -152,7 +154,7 @@
152154
if (HttpMethods.hasRequestBody(method)) {
153155
let requestDto = new model.value.constructor()
154156
let formData = new FormData(form)
155-
if (pk && !formData.has(pk.name)) {
157+
if (pk && !Array.from(formData.keys()).some(k => k.toLowerCase() == pk.name.toLowerCase())) {
156158
formData.append(pk.name, mapGet(props.modelValue, pk.name))
157159
}
158160
@@ -163,7 +165,9 @@
163165
}
164166
} else {
165167
let fieldValues = formValues(form, typeProperties(metaType.value))
166-
if (pk && !fieldValues[pk.name]) fieldValues[pk.name] = mapGet(props.modelValue, pk.name)
168+
if (pk && !mapGet(fieldValues,pk.name)) {
169+
fieldValues[pk.name] = mapGet(props.modelValue, pk.name)
170+
}
167171
168172
let requestDto = new model.value.constructor(fieldValues)
169173
if (!returnsVoid) {
@@ -185,7 +189,7 @@
185189
let pk = getPk()
186190
const id = pk ? mapGet(props.modelValue, pk.name) : null
187191
if (!id) {
188-
console.error(`Could not find Primary Key for Type ${typeName.value} (${dataModel?.name})`)
192+
console.error(`Could not find Primary Key for Type ${typeName.value} (${dataModel.value})`)
189193
return
190194
}
191195
const args = { [pk!.name]: id }

src/components/DynamicInput.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ watch(modelValue, () => {
3434
const files = computed(() => {
3535
const val = props.modelValue[props.input.id]
3636
if (props.input.type !== 'file' || !val) return []
37-
if (typeof val == 'string') return { filePath: val, fileName: lastRightPart(val,'/') }
37+
if (typeof val == 'string') return [{ filePath: val, fileName: lastRightPart(val,'/') }]
3838
if (!Array.isArray(val) && typeof val == 'object') return val
3939
if (Array.isArray(val)) {
4040
const to:UploadedFile[] = []

src/components/FileInput.vue

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div :class="['flex', !multiple ? 'justify-between' : 'flex-col']">
3-
<div>
3+
<div class="relative w-full">
44
<label v-if="useLabel" :for="id" :class="`block text-sm font-medium text-gray-700 dark:text-gray-300 ${labelClass}`">{{ useLabel }}</label>
55
<div class="block mt-2">
66
<span class="sr-only">help ?? useLabel</span>
@@ -10,7 +10,6 @@
1010
:id="id"
1111
:class="cls"
1212
:placeholder="usePlaceholder"
13-
:value="modelValue"
1413
:aria-invalid="errorField != null"
1514
:aria-describedby="`${id}-error`"
1615
v-bind="remaining"
@@ -29,7 +28,7 @@
2928
<div v-if="src" class="shrink-0 cursor-pointer" :title="!isDataUri(src) ? src : ''">
3029
<img @click="openFile" :class="['h-16 w-16', imgCls(src)]" :alt="`Current ${useLabel}`"
3130
:src="fallbackSrc || assetsPathResolver(src)"
32-
@error="fallbackSrc = fallbackPathResolver(src)">
31+
@error="onError">
3332
</div>
3433
</div>
3534
<div v-else class="mt-3">
@@ -84,13 +83,18 @@ const { filePathUri, getMimeType, formatBytes, fileImageUri, flush } = useFiles(
8483
const fallbackSrcMap:{[name:string]:string|undefined} = {}
8584
8685
const fallbackSrc = ref<string|undefined>()
87-
const fileList = ref<UploadedFile[]>(props.files || [])
86+
const fileList = ref<UploadedFile[]>(props.files?.map(toFile) || [])
87+
88+
function toFile(file:UploadedFile) {
89+
file.filePath = assetsPathResolver(file.filePath)
90+
return file
91+
}
8892
8993
if (props.values && props.values.length > 0) {
9094
fileList.value = props.values.map(x => {
9195
let filePath = x.replace(/\\/g,'/')
9296
return { fileName:lastLeftPart(lastRightPart(filePath,'/'),'.'), filePath, contentType:getMimeType(filePath) } as UploadedFile
93-
})
97+
}).map(toFile)
9498
}
9599
96100
const useLabel = computed(() => props.label ?? humanize(toPascalCase(props.id)))
@@ -101,10 +105,10 @@ const remaining = computed(() => omit(useAttrs(), [...Object.keys(props)]))
101105
let ctx: ApiState|undefined = inject('ApiState', undefined)
102106
const errorField = computed(() => errorResponse.call({ responseStatus: props.status ?? ctx?.error.value }, props.id))
103107
104-
const cls = computed(() => ['block w-full sm:text-sm rounded-md dark:text-white dark:bg-gray-900', errorField.value
108+
const cls = computed(() => ['block w-full sm:text-sm rounded-md dark:text-white dark:bg-gray-900 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-violet-50 dark:file:bg-violet-900 file:text-violet-700 dark:file:text-violet-200 hover:file:bg-violet-100 dark:hover:file:bg-violet-800', errorField.value
105109
? 'pr-10 border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500'
106-
: 'block w-full text-sm text-slate-500 dark:text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-violet-50 dark:file:bg-violet-900 file:text-violet-700 dark:file:text-violet-200 hover:file:bg-violet-100 dark:hover:file:bg-violet-800'
107-
,props.inputClass])
110+
: 'text-slate-500 dark:text-slate-400'
111+
, props.inputClass])
108112
109113
const onChange = (e:Event) => {
110114
let f = e.target as HTMLInputElement
@@ -122,6 +126,7 @@ const openFile = () => input.value?.click()
122126
const isDataUri = (src?:string|null) => src == null ? false : src.startsWith("data:") || src.startsWith("blob:")
123127
124128
const src = computed(() => {
129+
125130
if (fileList.value.length > 0)
126131
return fileList.value[0].filePath
127132
let filePath = typeof props.modelValue == 'string' ? props.modelValue : props.values && props.values[0]
@@ -132,6 +137,9 @@ const imgCls = (src?:string|null) => !src || src.startsWith("data:") || src.ends
132137
? ''
133138
: 'rounded-full object-cover'
134139
140+
function onError(e:Event) {
141+
fallbackSrc.value = fallbackPathResolver(src.value)
142+
}
135143
136144
onUnmounted(flush)
137145

src/demo/App.vue

+61-13
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,18 @@
221221

222222
<div v-if="metadataApi" class="mx-auto max-w-4xl">
223223
<h1 class="my-8 text-3xl">AutoCreateForm</h1>
224+
224225
<div class="space-x-2">
225226
<SecondaryButton @click="showCreateBooking=!showCreateBooking">Create Booking</SecondaryButton>
226227
<SecondaryButton @click="showCreateBookingCard=!showCreateBookingCard">Card</SecondaryButton>
227228
</div>
228229

229230
<div>
230231
<h3 class="my-4 text-xl">Bookings</h3>
231-
<DataGrid :items="bookings" :selectedColumns="['id','name','roomType','bookingStartDate','cost','timeAgo']" @rowSelected="selectedBooking = $event" />
232-
<AutoEditForm v-if="selectedBooking" type="UpdateBooking" deleteType="DeleteBooking" v-model="selectedBooking"
233-
@done="selectedBooking=null" @save="refreshBookings" @delete="refreshBookings" />
232+
<DataGrid :items="bookings" :selectedColumns="['id','name','roomType','bookingStartDate','cost','timeAgo']"
233+
@rowSelected="selectBooking($event)" class="mb-4" />
234+
<AutoEditForm v-if="selectedBooking" formStyle="card" type="UpdateBooking" deleteType="DeleteBooking" v-model="selectedBooking"
235+
@done="selectedBooking=null" @save="refreshBookings" @delete="refreshBookings" />
234236
</div>
235237

236238
<div v-if="showCreateBooking">
@@ -239,6 +241,21 @@
239241
<div v-if="showCreateBookingCard">
240242
<AutoCreateForm type="CreateBooking" formStyle="card" @done="showCreateBookingCard=false" @save="refreshBookings" />
241243
</div>
244+
245+
<div class="mt-4 space-x-2">
246+
<SecondaryButton @click="showGameItem=!showGameItem">Create Game Item</SecondaryButton>
247+
</div>
248+
249+
<div>
250+
<h3 class="my-4 text-xl">Game Items</h3>
251+
<DataGrid :items="gameIems" @rowSelected="selectGameItem($event)" class="mb-4" />
252+
<AutoEditForm v-if="selectedGameItem" formStyle="card" type="UpdateGameItem" deleteType="DeleteGameItem" v-model="selectedGameItem"
253+
@done="selectedGameItem=null" @save="refreshGameIems" @delete="refreshGameIems" />
254+
<div v-if="showGameItem">
255+
<AutoCreateForm :type="CreateGameItem" @done="showGameItem=false" @save="refreshGameIems" />
256+
</div>
257+
</div>
258+
242259
</div>
243260

244261
<div v-if="metadataApi" class="mx-auto max-w-4xl">
@@ -321,11 +338,14 @@
321338
<script setup lang="ts">
322339
import type { ApiRequest, ApiResponse } from '../types'
323340
import { inject, onMounted, ref } from 'vue'
324-
import DarkModeToggle from '../components/DarkModeToggle.vue'
325-
import { useClient, dateInputFormat } from './api'
326-
import { useAppMetadata, useFiles } from '../api'
327-
import { AllTypes, Authenticate, Booking, CreateBooking, CreateJobApplication, JobApplicationAttachment, QueryBookings, RoomType } from './dtos'
328341
import { lastRightPart, JsonServiceClient } from '@servicestack/client'
342+
import { dateInputFormat } from './api'
343+
import { useConfig, useAppMetadata, useFiles } from '../api'
344+
import { AllTypes, Authenticate,
345+
Booking, CreateBooking, QueryBookings, RoomType,
346+
CreateJobApplication, JobApplicationAttachment,
347+
GameItem, CreateGameItem, QueryGameItem
348+
} from './dtos'
329349
330350
const emit = defineEmits<{
331351
(e: 'done'): () => void
@@ -354,6 +374,12 @@ if (!metadataApi.value) {
354374
})()
355375
}
356376
377+
378+
const { setConfig } = useConfig()
379+
setConfig({
380+
assetsPathResolver: (src:string) => src.startsWith('/') ? 'http://localhost:5000' + src : src
381+
})
382+
357383
const client = inject('client') as JsonServiceClient
358384
359385
client.api(new Authenticate({ provider:'credentials', userName:'[email protected]', password:'p@55wOrd'}))
@@ -424,8 +450,6 @@ let allContacts = [
424450
425451
const { getMimeType } = useFiles()
426452
427-
const selectedBooking = ref<Booking|null>(null)
428-
const createBookingResponse = ref<any>()
429453
const createBooking = new CreateBooking()
430454
const createBookingApi: ApiResponse|null = null
431455
@@ -435,9 +459,6 @@ const toFile = (filePath:string) => ({
435459
contentType: getMimeType(filePath)
436460
})
437461
438-
const showCreateBooking = ref(false)
439-
const showCreateBookingCard = ref(false)
440-
441462
const createJobApplication = new CreateJobApplication({
442463
//attachments: [new JobApplicationAttachment(toFile('https://cdn.diffusion.works/artifacts/2023/01/26/9060157/output_77487570.png'))]
443464
})
@@ -447,6 +468,14 @@ const allTypes = new AllTypes({ stringList:['red','green'] })
447468
const allTypesApi: ApiResponse|null = null
448469
449470
const bookings = ref<Booking[]>([])
471+
const selectedBooking = ref<Booking|null>(null)
472+
const showCreateBooking = ref(false)
473+
const showCreateBookingCard = ref(false)
474+
475+
function selectBooking(item:Booking|null) {
476+
selectedBooking.value = null
477+
if (item) requestAnimationFrame(() => selectedBooking.value = item)
478+
}
450479
451480
async function refreshBookings(arg?:any) {
452481
if (arg) console.log('refreshBookings', arg)
@@ -458,7 +487,26 @@ async function refreshBookings(arg?:any) {
458487
}
459488
}
460489
461-
onMounted(() => refreshBookings())
490+
const gameIems = ref<GameItem[]>([])
491+
const selectedGameItem = ref<GameItem|null>(null)
492+
const showGameItem = ref(false)
493+
494+
function selectGameItem(item:GameItem|null) {
495+
selectedGameItem.value = null
496+
if (item) requestAnimationFrame(() => selectedGameItem.value = item)
497+
}
498+
499+
async function refreshGameIems(arg?:any) {
500+
if (arg) console.log('refreshGameIems', arg)
501+
showGameItem.value = false
502+
selectedGameItem.value = null
503+
let api = await client.api(new QueryGameItem())
504+
if (api.succeeded) {
505+
gameIems.value = api.response!.results
506+
}
507+
}
508+
509+
onMounted(() => { refreshBookings(); refreshGameIems(); })
462510
463511
</script>
464512

src/demo/api.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { Ref } from "vue"
21
import { ref, provide } from "vue"
32
import type { IReturn, ApiRequest, IReturnVoid, ResponseStatus } from "../types"
43
import { JsonApiClient, dateFmt } from "@servicestack/client"

0 commit comments

Comments
 (0)