Skip to content

Commit 01aea24

Browse files
committed
init form v1
1 parent e7a860a commit 01aea24

File tree

11 files changed

+781
-572
lines changed

11 files changed

+781
-572
lines changed

src/app/src/components/content/ContentEditorForm.vue

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, watch } from 'vue'
2+
import { ref, watch, computed } from 'vue'
33
import { ContentFileExtension, type DatabasePageItem, type DraftItem } from '../../types'
44
import type { PropType } from 'vue'
55
import { jsonToYaml, yamlToJson } from '../../utils/data'
@@ -20,11 +20,16 @@ const props = defineProps({
2020
const document = defineModel<DatabasePageItem>()
2121
2222
const { host } = useStudio()
23+
2324
// const { t } = useI18n()
2425
2526
const skipFirstUpdate = ref(true)
2627
const contentJSON = ref({})
2728
29+
const collection = computed(() => {
30+
return host.collection.getByFsPath(document.value!.fsPath!)
31+
})
32+
2833
// Trigger on document changes
2934
watch(() => document.value?.id + '-' + props.draftItem.version, async () => {
3035
if (document.value) {
@@ -95,7 +100,11 @@ async function setJSON(document: DatabasePageItem) {
95100
</script>
96101

97102
<template>
98-
<SchemaBasedForm
99-
v-model="contentJSON"
100-
/>
103+
<div class="p-4">
104+
<FormSchemaBased
105+
v-model="contentJSON"
106+
:collection-name="collection!.name"
107+
:schema="collection!.schema"
108+
/>
109+
</div>
101110
</template>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<template>
2+
<UFormField
3+
:name="formItem.id"
4+
:label="label"
5+
:ui="{
6+
root: 'w-full',
7+
label: 'text-xs font-semibold tracking-tight',
8+
container: 'mt-1',
9+
}"
10+
>
11+
<UInput
12+
:id="formItem.id"
13+
v-model="model"
14+
:placeholder="placeholder"
15+
:type="inputType"
16+
class="w-full"
17+
/>
18+
</UFormField>
19+
</template>
20+
21+
<script setup lang="ts">
22+
import { titleCase } from 'scule'
23+
import type { FormItem, FormTree } from '../../../types'
24+
import type { PropType } from 'vue'
25+
import { computed, ref, watch } from 'vue'
26+
27+
const props = defineProps({
28+
formItem: {
29+
type: Object as PropType<FormItem>,
30+
required: true,
31+
},
32+
})
33+
34+
const form = defineModel({ type: Object as PropType<FormTree>, default: () => ({}) })
35+
36+
const label = computed(() => titleCase(props.formItem.title))
37+
38+
const placeholder = computed(() => {
39+
switch (props.formItem.type) {
40+
case 'string':
41+
return `Enter ${props.formItem.title.toLowerCase()}...`
42+
case 'number':
43+
return '0'
44+
default:
45+
return ''
46+
}
47+
})
48+
49+
const inputType = computed(() => {
50+
switch (props.formItem.type) {
51+
case 'number':
52+
return 'number'
53+
default:
54+
return 'text'
55+
}
56+
})
57+
58+
// Initialize model value
59+
const model = ref(computeValue(props.formItem))
60+
61+
// Sync changes back to parent form
62+
watch(model, (newValue) => {
63+
form.value = applyValueById(form.value, props.formItem.id, newValue)
64+
})
65+
66+
// Watch for external form item changes
67+
watch(() => props.formItem, (newFormItem) => {
68+
model.value = computeValue(newFormItem)
69+
}, { deep: true })
70+
71+
function computeValue(formItem: FormItem): unknown {
72+
if (formItem.value !== undefined) {
73+
return formItem.value
74+
}
75+
76+
switch (formItem.type) {
77+
case 'string':
78+
case 'icon':
79+
case 'media':
80+
case 'file':
81+
return ''
82+
case 'boolean':
83+
return false
84+
case 'number':
85+
return 0
86+
case 'array':
87+
return []
88+
case 'object':
89+
return {}
90+
default:
91+
return null
92+
}
93+
}
94+
95+
function applyValueById(tree: FormTree, id: string, value: unknown): FormTree {
96+
const result = { ...tree }
97+
const paths = id.split('/').filter(Boolean)
98+
99+
let current: Record<string, unknown> = result
100+
for (let i = 0; i < paths.length - 1; i++) {
101+
const key = paths[i]
102+
if (!current[key] || typeof current[key] !== 'object') {
103+
current[key] = {}
104+
}
105+
current = current[key] as Record<string, unknown>
106+
}
107+
108+
const lastKey = paths[paths.length - 1]
109+
current[lastKey] = value
110+
111+
return result
112+
}
113+
</script>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<template>
2+
<UCollapsible
3+
v-if="formItem.children"
4+
class="w-full group/collapsible"
5+
:default-open="true"
6+
>
7+
<div
8+
class="flex items-center gap-2 w-full py-2"
9+
>
10+
<div class="flex items-center justify-center size-4 rounded bg-gray-100 dark:bg-gray-800 transition-colors duration-200 group-hover/collapsible:bg-gray-200 dark:group-hover/collapsible:bg-gray-700">
11+
<UIcon
12+
name="i-lucide-chevron-right"
13+
class="size-2.5 text-gray-500 dark:text-gray-400 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
14+
/>
15+
</div>
16+
<div class="flex gap-2 items-center">
17+
<span class="font-semibold text-gray-900 dark:text-gray-100 tracking-tight">
18+
{{ formItem.title }}
19+
</span>
20+
<UBadge
21+
v-if="childrenCount"
22+
variant="soft"
23+
size="xs"
24+
class="text-muted"
25+
>
26+
{{ childrenCount }} child{{ childrenCount === 1 ? '' : 'ren' }}
27+
</UBadge>
28+
</div>
29+
</div>
30+
31+
<template #content>
32+
<div class="mt-1 ml-5 pl-4 border-l border-gray-200 dark:border-gray-700/50 space-y-0.5">
33+
<FormPanelSection
34+
v-for="childKey in Object.keys(formItem.children)"
35+
:key="formItem.children[childKey].id"
36+
v-model="form"
37+
:form-item="formItem.children[childKey]"
38+
/>
39+
</div>
40+
</template>
41+
</UCollapsible>
42+
43+
<FormPanelInput
44+
v-else
45+
v-model="form"
46+
:form-item="formItem"
47+
/>
48+
</template>
49+
50+
<script lang="ts" setup>
51+
import type { FormItem, FormTree } from '../../../types'
52+
import type { PropType } from 'vue'
53+
import { computed } from 'vue'
54+
55+
const props = defineProps({
56+
formItem: {
57+
type: Object as PropType<FormItem>,
58+
required: true,
59+
},
60+
})
61+
62+
const form = defineModel({ type: Object as PropType<FormTree>, default: () => ({}) })
63+
64+
const childrenCount = computed(() => {
65+
if (!props.formItem.children) return 0
66+
return Object.keys(props.formItem.children).length
67+
})
68+
</script>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import type { FormTree } from '../../../types'
4+
import type { PropType } from 'vue'
5+
import type { Draft07 } from '@nuxt/content'
6+
import { buildFormTreeFromSchema } from '../../../utils/form'
7+
8+
const props = defineProps({
9+
collectionName: {
10+
type: String,
11+
required: true,
12+
},
13+
schema: {
14+
type: Object as PropType<Draft07>,
15+
required: true,
16+
},
17+
})
18+
19+
console.log('props.schema', props.schema)
20+
21+
defineModel<Record<string, unknown>>({ required: true })
22+
23+
const formTree = computed<FormTree>(() => {
24+
return buildFormTreeFromSchema(props.collectionName, props.schema)
25+
})
26+
27+
console.log('formTree', formTree.value)
28+
29+
// const jsonString = computed({
30+
// get: () => JSON.stringify(model.value, null, 2),
31+
// set: (value: string) => {
32+
// try {
33+
// model.value = JSON.parse(value)
34+
// }
35+
// catch {
36+
// // Invalid JSON, don't update
37+
// }
38+
// },
39+
// })
40+
</script>
41+
42+
<template>
43+
<FormPanelSection
44+
v-for="formItem in formTree[collectionName].children"
45+
:key="formItem.id"
46+
v-model="formTree"
47+
:form-item="formItem"
48+
/>
49+
</template>

src/app/src/components/shared/form/SchemaBasedForm.vue

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/app/src/types/form.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export type FormItem = {
1111
default?: unknown
1212
options?: string[]
1313
title: string
14-
description?: string
1514
icon?: string
1615
children?: FormTree
1716
disabled?: boolean

src/app/src/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { MediaItem } from './media'
55
import type { Repository } from './git'
66
import type { ComponentMeta } from './component'
77
import type { MarkdownParsingOptions, SyntaxHighlightTheme } from './content'
8+
import type { CollectionInfo } from '@nuxt/content'
89

910
export * from './file'
1011
export * from './item'
@@ -72,6 +73,9 @@ export interface StudioHost {
7273
upsert: (fsPath: string, media: MediaItem) => Promise<void>
7374
delete: (fsPath: string) => Promise<void>
7475
}
76+
collection: {
77+
getByFsPath: (fsPath: string) => CollectionInfo | undefined
78+
}
7579
user: {
7680
get: () => StudioUser
7781
}

src/app/src/utils/form.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const buildFormTreeFromSchema = (treeKey: string, schema: Draft07): FormT
5757
return item
5858
}
5959

60-
// Handle custom object form
60+
// Object form
6161
if (def.type === 'object' && def.properties) {
6262
const children = Object.keys(def.properties).reduce((acc, key) => {
6363
// Hide content internal keys
@@ -88,6 +88,7 @@ export const buildFormTreeFromSchema = (treeKey: string, schema: Draft07): FormT
8888
return item
8989
}
9090

91+
// Array form
9192
if (def.type === 'array' && def.items) {
9293
return {
9394
id,
@@ -97,7 +98,7 @@ export const buildFormTreeFromSchema = (treeKey: string, schema: Draft07): FormT
9798
}
9899
}
99100

100-
// Handle primitive types
101+
// Primitive form
101102
const editorType = editor?.input
102103
const type = def.type === 'string' && def.format?.includes('date') ? 'date' : editorType ?? def.type as never
103104

@@ -136,6 +137,6 @@ export const buildFormTreeFromSchema = (treeKey: string, schema: Draft07): FormT
136137
}
137138

138139
return {
139-
[treeKey]: buildFormTreeItem(schema.definitions[treeKey]),
140+
[treeKey]: buildFormTreeItem(schema.definitions[treeKey] as Draft07DefinitionProperty) as FormItem,
140141
}
141142
}

src/app/test/mocks/host.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ export const createMockHost = (): StudioHost => ({
141141
name: 'test-repo',
142142
branch: 'main',
143143
},
144+
collection: {
145+
getByFsPath: vi.fn().mockReturnValue(undefined),
146+
},
144147
} as never)
145148

146149
export const clearMockHost = () => {

0 commit comments

Comments
 (0)