Skip to content

Commit 30e457c

Browse files
committed
feat(editor): form init
1 parent a607691 commit 30e457c

File tree

7 files changed

+974
-7
lines changed

7 files changed

+974
-7
lines changed

src/app/src/components/content/ContentEditor.vue

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,27 @@ const language = computed(() => {
123123
class="flex-1"
124124
/>
125125
<template v-else>
126-
<ContentEditorTipTap
127-
v-if="preferences.editorMode === 'tiptap' && document.extension === ContentFileExtension.Markdown"
128-
v-model="document"
129-
:draft-item="draftItem"
130-
class="flex-1"
131-
/>
132126
<ContentEditorCode
133-
v-else
127+
v-if="preferences.editorMode === 'code'"
134128
v-model="document"
135129
:draft-item="draftItem"
136130
:read-only="readOnly"
137131
class="flex-1"
138132
/>
133+
<template v-else>
134+
<ContentEditorTipTap
135+
v-if="document.extension === ContentFileExtension.Markdown"
136+
v-model="document"
137+
:draft-item="draftItem"
138+
class="flex-1"
139+
/>
140+
<ContentEditorForm
141+
v-else
142+
v-model="document"
143+
:draft-item="draftItem"
144+
class="flex-1"
145+
/>
146+
</template>
139147
</template>
140148
</template>
141149
</div>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<script setup lang="ts">
2+
import { ref, watch } from 'vue'
3+
import { ContentFileExtension, type DatabasePageItem, type DraftItem } from '../../types'
4+
import type { PropType } from 'vue'
5+
import { jsonToYaml, yamlToJson } from '../../utils/data'
6+
import { useStudio } from '../../composables/useStudio'
7+
8+
const props = defineProps({
9+
draftItem: {
10+
type: Object as PropType<DraftItem>,
11+
required: true,
12+
},
13+
readOnly: {
14+
type: Boolean,
15+
required: false,
16+
default: false,
17+
},
18+
})
19+
20+
const document = defineModel<DatabasePageItem>()
21+
22+
const { host } = useStudio()
23+
// const { t } = useI18n()
24+
25+
const skipFirstUpdate = ref(true)
26+
const contentJSON = ref({})
27+
28+
// Trigger on document changes
29+
watch(() => document.value?.id + '-' + props.draftItem.version, async () => {
30+
if (document.value) {
31+
setJSON(document.value)
32+
}
33+
}, { immediate: true })
34+
35+
// Trigger on action events
36+
watch(() => props.draftItem.status, () => {
37+
setJSON(document.value!)
38+
})
39+
40+
// Trigger on form changes
41+
watch (contentJSON, (json) => {
42+
if (skipFirstUpdate.value) {
43+
skipFirstUpdate.value = false
44+
return
45+
}
46+
47+
if (props.readOnly) {
48+
return
49+
}
50+
51+
// Do not trigger model updates if the document id has changed
52+
// if (currentDocumentId.value !== document.value?.id) {
53+
// return
54+
// }
55+
56+
// if (content.value === newContent) {
57+
// return
58+
// }
59+
60+
// content.value = newContent
61+
62+
let content = ''
63+
switch (document.value?.extension) {
64+
case ContentFileExtension.JSON:
65+
content = JSON.stringify(json)
66+
break
67+
case ContentFileExtension.YAML:
68+
case ContentFileExtension.YML:
69+
content = jsonToYaml(json)
70+
break
71+
}
72+
73+
host.document.generate.documentFromContent(document.value!.id, content).then((doc) => {
74+
document.value = {
75+
...host.document.utils.pickReservedKeys(props.draftItem.modified as DatabasePageItem || document.value!),
76+
...doc,
77+
} as DatabasePageItem
78+
})
79+
})
80+
81+
async function setJSON(document: DatabasePageItem) {
82+
const generateContentFromDocument = host.document.generate.contentFromDocument
83+
const generatedContent = await generateContentFromDocument(document) || ''
84+
85+
switch (document.extension) {
86+
case ContentFileExtension.JSON:
87+
contentJSON.value = JSON.parse(generatedContent)
88+
break
89+
case ContentFileExtension.YAML:
90+
case ContentFileExtension.YML:
91+
contentJSON.value = yamlToJson(generatedContent)!
92+
break
93+
}
94+
}
95+
</script>
96+
97+
<template>
98+
<SchemaBasedForm
99+
v-model="contentJSON"
100+
/>
101+
</template>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
4+
const model = defineModel<Record<string, unknown>>({ required: true })
5+
6+
const jsonString = computed({
7+
get: () => JSON.stringify(model.value, null, 2),
8+
set: (value: string) => {
9+
try {
10+
model.value = JSON.parse(value)
11+
}
12+
catch {
13+
// Invalid JSON, don't update
14+
}
15+
},
16+
})
17+
</script>
18+
19+
<template>
20+
<UTextarea
21+
v-model="jsonString"
22+
autoresize
23+
:rows="10"
24+
class="font-mono text-sm"
25+
/>
26+
</template>

src/app/src/types/form.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { JSType } from 'untyped'
2+
3+
export type FormInputsTypes = JSType | 'icon' | 'media' | 'file' | 'date'
4+
5+
export type FormTree = Record<string, FormItem>
6+
export type FormItem = {
7+
id: string
8+
type: FormInputsTypes
9+
key?: string
10+
value?: unknown
11+
default?: unknown
12+
options?: string[]
13+
title: string
14+
description?: string
15+
icon?: string
16+
children?: FormTree
17+
disabled?: boolean
18+
hidden?: boolean
19+
// If type is combined with boolean
20+
toggleable?: boolean
21+
// Not in schema, created manually by user
22+
custom?: boolean
23+
// Items for array type
24+
items?: FormItem
25+
}

src/app/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export * from './component'
1818
export * from './config'
1919
export * from './media'
2020
export * from './content'
21+
export * from './form'
2122

2223
export interface StudioHost {
2324
meta: {

src/app/src/utils/form.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type { Draft07, Draft07DefinitionProperty, Draft07DefinitionPropertyAnyOf, Draft07DefinitionPropertyAllOf, Draft07DefinitionPropertyOneOf } from '@nuxt/content'
2+
import type { FormTree, FormItem } from '../types'
3+
import { upperFirst } from 'scule'
4+
5+
export const buildFormTreeFromSchema = (treeKey: string, schema: Draft07): FormTree => {
6+
if (!schema || !schema.definitions || !schema.definitions[treeKey]) {
7+
return {}
8+
}
9+
10+
const buildFormTreeItem = (def: Draft07DefinitionProperty, id: string = `#frontmatter/${treeKey}`): FormItem | null => {
11+
const paths = id.split('/')
12+
const itemKey = paths.pop()!
13+
const level = paths.length - 1 // deduce #frontmatter
14+
15+
const editor = def.$content?.editor
16+
if (editor?.hidden) {
17+
return null
18+
}
19+
20+
// Handle two level deep for objects keys
21+
if (level <= 3) {
22+
// Handle `Of` fields
23+
if (
24+
(def as Draft07DefinitionPropertyAllOf).allOf
25+
|| (def as Draft07DefinitionPropertyAnyOf).anyOf
26+
|| (def as Draft07DefinitionPropertyOneOf).oneOf
27+
) {
28+
let item: FormItem | null
29+
const defs = (def as Draft07DefinitionPropertyAllOf).allOf
30+
|| (def as Draft07DefinitionPropertyAnyOf).anyOf
31+
|| (def as Draft07DefinitionPropertyOneOf).oneOf
32+
33+
const objectDef = defs.find(item => item.type === 'object')
34+
const stringDef = defs.find(item => item.type === 'string')
35+
const booleanDef = defs.find(item => item.type === 'boolean')
36+
37+
// Choose object type in priority
38+
if (objectDef) {
39+
item = buildFormTreeItem(objectDef, id)
40+
}
41+
42+
// Then string type
43+
else if (stringDef) {
44+
item = buildFormTreeItem(stringDef, id)
45+
}
46+
47+
// Else select first one
48+
else {
49+
item = buildFormTreeItem(defs[0], id)
50+
}
51+
52+
// Handle multiple types with boolean
53+
if (item?.type !== 'boolean' && booleanDef) {
54+
item!.toggleable = true
55+
}
56+
57+
return item
58+
}
59+
60+
// Handle custom object form
61+
if (def.type === 'object' && def.properties) {
62+
const children = Object.keys(def.properties).reduce((acc, key) => {
63+
// Hide content internal keys
64+
const hiddenKeys = ['id', 'contentId', 'weight', 'stem', 'extension', 'path', 'meta', 'body']
65+
if (hiddenKeys.includes(key) || def.properties![key]!.$content?.editor?.hidden) {
66+
return acc
67+
}
68+
69+
const item = {
70+
...acc,
71+
[key]: buildFormTreeItem(def.properties![key], `${id}/${key}`),
72+
} as FormItem
73+
74+
return item
75+
}, {})
76+
77+
const item: FormItem = {
78+
id,
79+
title: upperFirst(itemKey),
80+
type: editor?.input ?? def.type,
81+
children,
82+
}
83+
84+
if (def.enum && Array.isArray(def.enum) && def.enum.length > 0) {
85+
item.options = def.enum as string[]
86+
}
87+
88+
return item
89+
}
90+
91+
if (def.type === 'array' && def.items) {
92+
return {
93+
id,
94+
title: upperFirst(itemKey),
95+
type: 'array',
96+
items: buildFormTreeItem(def.items, `#${itemKey}/items`)!,
97+
}
98+
}
99+
100+
// Handle primitive types
101+
const editorType = editor?.input
102+
const type = def.type === 'string' && def.format?.includes('date') ? 'date' : editorType ?? def.type as never
103+
104+
const item: FormItem = {
105+
id,
106+
title: upperFirst(itemKey),
107+
type: editorType ?? type,
108+
}
109+
110+
if (def.enum && Array.isArray(def.enum) && def.enum.length > 0) {
111+
item.options = def.enum as string[]
112+
}
113+
114+
return item
115+
}
116+
117+
// Else edit directly as the return type
118+
const editorType = editor?.input
119+
const type = def.type === 'string' && def.format?.includes('date') ? 'date' : editorType ?? def.type
120+
121+
const item: FormItem = {
122+
id,
123+
title: upperFirst(itemKey),
124+
type: editorType ?? type as never,
125+
}
126+
127+
if (type === 'array' && def.items) {
128+
item.items = buildFormTreeItem(def.items, `#${itemKey}/items`)!
129+
}
130+
131+
if (def.enum && Array.isArray(def.enum) && def.enum.length > 0) {
132+
item.options = def.enum as string[]
133+
}
134+
135+
return item
136+
}
137+
138+
return {
139+
[treeKey]: buildFormTreeItem(schema.definitions[treeKey]),
140+
}
141+
}

0 commit comments

Comments
 (0)