@swiss-ai-hub/web 0.298.1 → 0.298.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/Agent/CreateModal.vue +3 -6
- package/components/Event/Display/ToolEvent.vue +2 -2
- package/components/FormKit/AgentSelector.vue +2 -2
- package/components/FormKit/DynamicConfiguration.vue +5 -9
- package/components/FormKit/KnowledgeDatabaseSelector.vue +2 -2
- package/components/FormKit/Repeater.vue +25 -4
- package/components/FormKit/VectorStoreInput.vue +3 -3
- package/components/Knowledge/Document/UploadModal.vue +3 -3
- package/components/Knowledge/Namespace/Card.vue +2 -2
- package/components/Knowledge/Namespace/CreateModal.vue +2 -2
- package/components/Process/CreateModal.vue +3 -6
- package/composables/form/useCreateInstanceForm.ts +11 -129
- package/composables/form/useFormKitTransform.ts +116 -44
- package/package.json +1 -1
- package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/configuration.vue +8 -40
- package/pages/[tenant]/service/knowledge/[db]/[namespace].vue +3 -3
- package/pages/[tenant]/service/knowledge.vue +4 -4
- package/pages/[tenant]/service/models.vue +2 -2
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
:label="rep.label"
|
|
106
106
|
:add-label="rep.addLabel"
|
|
107
107
|
:children-schema="rep.childrenSchema"
|
|
108
|
+
:default-item="rep.defaultItem"
|
|
108
109
|
:min="rep.min"
|
|
109
110
|
:max="rep.max"
|
|
110
111
|
@update:model-value="setRepeaterData(rep.path, $event)"
|
|
@@ -135,7 +136,7 @@
|
|
|
135
136
|
</template>
|
|
136
137
|
|
|
137
138
|
<script setup lang="ts">
|
|
138
|
-
import { type FormElement,
|
|
139
|
+
import { type FormElement, serializeFormData } from '@core/composables/form/useFormKitTransform'
|
|
139
140
|
import { getNode } from '@formkit/core'
|
|
140
141
|
|
|
141
142
|
const props = defineProps<{
|
|
@@ -168,8 +169,6 @@ const {
|
|
|
168
169
|
getRepeaterStepIndex,
|
|
169
170
|
getRepeaterData,
|
|
170
171
|
setRepeaterData,
|
|
171
|
-
cleanFormData,
|
|
172
|
-
coerceNullableToggles,
|
|
173
172
|
applyInitialData,
|
|
174
173
|
resetForm,
|
|
175
174
|
} = useCreateInstanceForm({
|
|
@@ -219,9 +218,7 @@ function triggerFormSubmit() {
|
|
|
219
218
|
|
|
220
219
|
async function handleFormSubmit() {
|
|
221
220
|
try {
|
|
222
|
-
const
|
|
223
|
-
const coerced = coerceNullableToggles(cleanedData, configForm.value as FormElement[])
|
|
224
|
-
const normalizedConfig = normalizeFormLocaleStrings(coerced)
|
|
221
|
+
const normalizedConfig = serializeFormData(formData.value, configForm.value as FormElement[])
|
|
225
222
|
const agentId = normalizedConfig.agent_id as string
|
|
226
223
|
await createAgentInstance({
|
|
227
224
|
agentClass: selectedClass.value,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
v-for="(val, key) in event.event.parameters"
|
|
17
17
|
:key="key"
|
|
18
18
|
>
|
|
19
|
-
<Button :label="
|
|
19
|
+
<Button :label="capitalCase(key)" />
|
|
20
20
|
<InputText
|
|
21
21
|
:placeholder="val"
|
|
22
22
|
readonly
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
</template>
|
|
28
28
|
|
|
29
29
|
<script setup lang="ts">
|
|
30
|
-
import {
|
|
30
|
+
import { capitalCase } from 'change-case'
|
|
31
31
|
|
|
32
32
|
import type { ThreadDto, ToolEvent, ContextualizedAgentEvent } from '@core/sdk/client'
|
|
33
33
|
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
|
|
94
94
|
<script setup lang="ts">
|
|
95
95
|
import { getAgentClasses, getAgentClassInstances } from '@core/sdk/client'
|
|
96
|
-
import {
|
|
96
|
+
import { capitalCase } from 'change-case'
|
|
97
97
|
|
|
98
98
|
import type { AgentClassDto, FullAgentInstanceDto, LocaleString } from '@core/sdk/client'
|
|
99
99
|
|
|
@@ -200,7 +200,7 @@ function emitValue(agentClass: string, agentId: string) {
|
|
|
200
200
|
const classOptions = computed<ClassOption[]>(() =>
|
|
201
201
|
agentClasses.value.map(cls => ({
|
|
202
202
|
name: cls.agent_class,
|
|
203
|
-
displayName: getLocalizedValue(cls.name, locale.value) ||
|
|
203
|
+
displayName: getLocalizedValue(cls.name, locale.value) || capitalCase(cls.agent_class),
|
|
204
204
|
icon: cls.icon || 'mage:robot',
|
|
205
205
|
})),
|
|
206
206
|
)
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
:label="rep.label"
|
|
28
28
|
:add-label="rep.addLabel"
|
|
29
29
|
:children-schema="rep.childrenSchema"
|
|
30
|
+
:default-item="rep.defaultItem"
|
|
30
31
|
:min="rep.min"
|
|
31
32
|
:max="rep.max"
|
|
32
33
|
@update:model-value="setRepeaterData(rep.path, $event)"
|
|
@@ -38,12 +39,10 @@
|
|
|
38
39
|
<script setup lang="ts">
|
|
39
40
|
import {
|
|
40
41
|
buildFormKitSchema,
|
|
41
|
-
coerceNullableToggles,
|
|
42
42
|
extractRepeaterConfigs,
|
|
43
43
|
getNestedValue,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
seedNullableToggles,
|
|
44
|
+
hydrateFormData,
|
|
45
|
+
serializeFormData,
|
|
47
46
|
setNestedValue,
|
|
48
47
|
type FormElement,
|
|
49
48
|
type RepeaterConfig,
|
|
@@ -61,8 +60,7 @@ const props = defineProps<{
|
|
|
61
60
|
}>()
|
|
62
61
|
|
|
63
62
|
function hydrate(raw: Record<string, unknown>): Record<string, unknown> {
|
|
64
|
-
|
|
65
|
-
return seedFormDefaults(seeded, props.form as FormElement[])
|
|
63
|
+
return hydrateFormData(raw, props.form as FormElement[])
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
const data = ref<Record<string, unknown>>(hydrate(props.initialData || {}))
|
|
@@ -110,9 +108,7 @@ function setRepeaterData(path: string, value: Record<string, unknown>[]): void {
|
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
async function submitHandler() {
|
|
113
|
-
|
|
114
|
-
const normalizedData = normalizeFormLocaleStrings(coerced)
|
|
115
|
-
emit('submit', normalizedData)
|
|
111
|
+
emit('submit', serializeFormData(data.value, props.form as FormElement[]))
|
|
116
112
|
}
|
|
117
113
|
</script>
|
|
118
114
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
<script setup lang="ts">
|
|
28
28
|
import { getDatabases } from '@core/sdk/client'
|
|
29
|
-
import {
|
|
29
|
+
import { capitalCase } from 'change-case'
|
|
30
30
|
|
|
31
31
|
import type { DatabaseDto } from '@core/sdk/client'
|
|
32
32
|
|
|
@@ -64,7 +64,7 @@ const selectedDatabases = computed({
|
|
|
64
64
|
const databaseOptions = computed<DatabaseOption[]>(() =>
|
|
65
65
|
databases.value.map(db => ({
|
|
66
66
|
name: db.name,
|
|
67
|
-
displayName: db.display_name ||
|
|
67
|
+
displayName: db.display_name || capitalCase(db.name),
|
|
68
68
|
})),
|
|
69
69
|
)
|
|
70
70
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<div class="flex flex-col gap-3">
|
|
8
8
|
<div
|
|
9
9
|
v-for="(item, index) in items"
|
|
10
|
-
:key="index"
|
|
10
|
+
:key="rowKeys[index]"
|
|
11
11
|
class="relative rounded-lg border border-surface-200 p-4 dark:border-surface-700"
|
|
12
12
|
>
|
|
13
13
|
<div class="mb-3 flex items-center justify-between">
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
|
|
28
28
|
<FormKit
|
|
29
29
|
v-if="modelValue"
|
|
30
|
-
:id="`__validate__${name}__${index}`"
|
|
30
|
+
:id="`__validate__${name}__${rowKeys[index]}`"
|
|
31
31
|
v-model="modelValue[index]"
|
|
32
|
-
:name="`__validate__${name}__${index}`"
|
|
32
|
+
:name="`__validate__${name}__${rowKeys[index]}`"
|
|
33
33
|
type="group"
|
|
34
34
|
>
|
|
35
35
|
<FormKitSchema
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
</template>
|
|
54
54
|
|
|
55
55
|
<script setup lang="ts">
|
|
56
|
+
import { cloneDeep } from 'lodash-es'
|
|
57
|
+
|
|
56
58
|
import type { FormKitSchemaNode } from '@formkit/core'
|
|
57
59
|
|
|
58
60
|
const props = defineProps<{
|
|
@@ -60,6 +62,7 @@ const props = defineProps<{
|
|
|
60
62
|
label?: string
|
|
61
63
|
addLabel?: string
|
|
62
64
|
childrenSchema: FormKitSchemaNode[]
|
|
65
|
+
defaultItem?: Record<string, unknown>
|
|
63
66
|
min?: number
|
|
64
67
|
max?: number
|
|
65
68
|
}>()
|
|
@@ -68,6 +71,22 @@ const modelValue = defineModel<Record<string, unknown>[]>({ default: () => [] })
|
|
|
68
71
|
|
|
69
72
|
const items = computed(() => modelValue.value || [])
|
|
70
73
|
|
|
74
|
+
function makeRowKey(): string {
|
|
75
|
+
return crypto.randomUUID()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// One stable key per row, used both as the Vue `:key` and inside the FormKit group id/name.
|
|
79
|
+
// Index-based keys reindex survivors when a non-last row is removed, rebinding each FormKit
|
|
80
|
+
// group to a different row's data and corrupting the form; stable keys avoid that. add/remove
|
|
81
|
+
// keep the array aligned to the rows; the watch only reconciles external model replacement
|
|
82
|
+
// (e.g. form load), preserving existing keys positionally.
|
|
83
|
+
const rowKeys = ref<string[]>(items.value.map(makeRowKey))
|
|
84
|
+
|
|
85
|
+
watch(() => items.value.length, (length) => {
|
|
86
|
+
if (length === rowKeys.value.length) return
|
|
87
|
+
rowKeys.value = Array.from({ length }, (_, index) => rowKeys.value[index] ?? makeRowKey())
|
|
88
|
+
})
|
|
89
|
+
|
|
71
90
|
const isAddDisabled = computed(() => {
|
|
72
91
|
if (typeof props.max !== 'number') return false
|
|
73
92
|
return items.value.length >= props.max
|
|
@@ -83,11 +102,13 @@ function addItem() {
|
|
|
83
102
|
if (!modelValue.value) {
|
|
84
103
|
modelValue.value = []
|
|
85
104
|
}
|
|
86
|
-
|
|
105
|
+
rowKeys.value.push(makeRowKey())
|
|
106
|
+
modelValue.value.push(props.defaultItem ? cloneDeep(props.defaultItem) : {})
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
function removeItem(index: number) {
|
|
90
110
|
if (!modelValue.value || isRemoveDisabled.value) return
|
|
111
|
+
rowKeys.value.splice(index, 1)
|
|
91
112
|
modelValue.value = modelValue.value.filter((_, i) => i !== index)
|
|
92
113
|
}
|
|
93
114
|
</script>
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
<script setup lang="ts">
|
|
97
97
|
import ChipsInput from '@core/components/FormKit/ChipsInput.vue'
|
|
98
98
|
import { getDatabases } from '@core/sdk/client'
|
|
99
|
-
import {
|
|
99
|
+
import { capitalCase } from 'change-case'
|
|
100
100
|
|
|
101
101
|
import type { DatabaseDto } from '@core/sdk/client'
|
|
102
102
|
|
|
@@ -200,7 +200,7 @@ function emitValue(collectionName: string, namespaces: string[], allowedFilterFi
|
|
|
200
200
|
const databaseOptions = computed<DatabaseOption[]>(() =>
|
|
201
201
|
databases.value.map(db => ({
|
|
202
202
|
name: db.name,
|
|
203
|
-
displayName: db.display_name ||
|
|
203
|
+
displayName: db.display_name || capitalCase(db.name),
|
|
204
204
|
})),
|
|
205
205
|
)
|
|
206
206
|
|
|
@@ -213,7 +213,7 @@ const namespaceOptions = computed<NamespaceOption[]>(() => {
|
|
|
213
213
|
return []
|
|
214
214
|
return db.namespaces.map(ns => ({
|
|
215
215
|
name: ns.name,
|
|
216
|
-
displayName: ns.display_name ||
|
|
216
|
+
displayName: ns.display_name || capitalCase(ns.name),
|
|
217
217
|
}))
|
|
218
218
|
})
|
|
219
219
|
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
</template>
|
|
130
130
|
|
|
131
131
|
<script setup lang="ts">
|
|
132
|
-
import {
|
|
132
|
+
import { capitalCase } from 'change-case'
|
|
133
133
|
|
|
134
134
|
interface Props {
|
|
135
135
|
visible: boolean
|
|
@@ -146,11 +146,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
146
146
|
const { t } = useI18n()
|
|
147
147
|
|
|
148
148
|
const databaseDisplayName = computed(() => {
|
|
149
|
-
return props.databaseDisplayName ||
|
|
149
|
+
return props.databaseDisplayName || capitalCase(props.database)
|
|
150
150
|
})
|
|
151
151
|
|
|
152
152
|
const namespaceDisplayName = computed(() => {
|
|
153
|
-
return props.namespaceDisplayName ||
|
|
153
|
+
return props.namespaceDisplayName || capitalCase(props.namespace)
|
|
154
154
|
})
|
|
155
155
|
|
|
156
156
|
const emit = defineEmits<{
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
</template>
|
|
64
64
|
|
|
65
65
|
<script setup lang="ts">
|
|
66
|
-
import {
|
|
66
|
+
import { capitalCase } from 'change-case'
|
|
67
67
|
|
|
68
68
|
import type { NamespaceDto } from '@core/sdk/client'
|
|
69
69
|
|
|
@@ -82,7 +82,7 @@ const { t } = useI18n()
|
|
|
82
82
|
|
|
83
83
|
const displayName = computed(() => {
|
|
84
84
|
// Use display_name if available, otherwise fall back to formatted technical name
|
|
85
|
-
return props.namespace.display_name ||
|
|
85
|
+
return props.namespace.display_name || capitalCase(props.namespace.name)
|
|
86
86
|
})
|
|
87
87
|
|
|
88
88
|
const createdAt = computed(() => {
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
</template>
|
|
115
115
|
|
|
116
116
|
<script setup lang="ts">
|
|
117
|
-
import {
|
|
117
|
+
import { capitalCase } from 'change-case'
|
|
118
118
|
|
|
119
119
|
import type { CreateNamespaceRequest, DatabaseDto } from '@core/sdk/client'
|
|
120
120
|
|
|
@@ -146,7 +146,7 @@ const databaseOptions = computed(() =>
|
|
|
146
146
|
.filter(db => !db.auto_sync)
|
|
147
147
|
.map(db => ({
|
|
148
148
|
name: db.name,
|
|
149
|
-
displayName:
|
|
149
|
+
displayName: capitalCase(db.name),
|
|
150
150
|
})),
|
|
151
151
|
)
|
|
152
152
|
const nameValidationError = computed(() => {
|
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
:label="rep.label"
|
|
105
105
|
:add-label="rep.addLabel"
|
|
106
106
|
:children-schema="rep.childrenSchema"
|
|
107
|
+
:default-item="rep.defaultItem"
|
|
107
108
|
:min="rep.min"
|
|
108
109
|
:max="rep.max"
|
|
109
110
|
@update:model-value="setRepeaterData(rep.path, $event)"
|
|
@@ -134,7 +135,7 @@
|
|
|
134
135
|
</template>
|
|
135
136
|
|
|
136
137
|
<script setup lang="ts">
|
|
137
|
-
import { type FormElement,
|
|
138
|
+
import { type FormElement, serializeFormData } from '@core/composables/form/useFormKitTransform'
|
|
138
139
|
import { getNode } from '@formkit/core'
|
|
139
140
|
|
|
140
141
|
const props = defineProps<{
|
|
@@ -166,8 +167,6 @@ const {
|
|
|
166
167
|
getRepeaterStepIndex,
|
|
167
168
|
getRepeaterData,
|
|
168
169
|
setRepeaterData,
|
|
169
|
-
cleanFormData,
|
|
170
|
-
coerceNullableToggles,
|
|
171
170
|
applyInitialData,
|
|
172
171
|
resetForm,
|
|
173
172
|
} = useCreateInstanceForm({
|
|
@@ -212,9 +211,7 @@ function triggerFormSubmit() {
|
|
|
212
211
|
|
|
213
212
|
async function handleFormSubmit() {
|
|
214
213
|
try {
|
|
215
|
-
const
|
|
216
|
-
const coerced = coerceNullableToggles(cleanedData, configForm.value as FormElement[])
|
|
217
|
-
const normalizedConfig = normalizeFormLocaleStrings(coerced)
|
|
214
|
+
const normalizedConfig = serializeFormData(formData.value, configForm.value as FormElement[])
|
|
218
215
|
const processId = normalizedConfig.process_id as string
|
|
219
216
|
await createProcessInstance({
|
|
220
217
|
processClass: selectedClass.value,
|
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import { merge } from 'lodash-es'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
type FormElement,
|
|
5
3
|
type GroupConfig,
|
|
6
4
|
type RepeaterConfig,
|
|
7
5
|
buildFormKitSchema,
|
|
8
6
|
categorizeFormElements,
|
|
9
|
-
coerceNullableToggles,
|
|
10
7
|
extractGroupConfigs,
|
|
11
8
|
extractRepeaterConfigs,
|
|
12
|
-
getFormkitType,
|
|
13
9
|
getNestedValue,
|
|
14
|
-
|
|
15
|
-
seedNullableToggles,
|
|
10
|
+
hydrateFormData,
|
|
16
11
|
setNestedValue,
|
|
17
12
|
} from './useFormKitTransform'
|
|
18
13
|
|
|
@@ -24,51 +19,6 @@ export interface ClassDataLike {
|
|
|
24
19
|
templates?: Array<Record<string, unknown>>
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
/**
|
|
28
|
-
* Drops keys whose matching form-schema node is a group/repeater and whose incoming
|
|
29
|
-
* value is `null`. FormKit rejects `null` for group values (must be an object) and for
|
|
30
|
-
* repeater values (must be an array), so template payloads that serialise optional
|
|
31
|
-
* nested configs as `null` (Pydantic `Form | None = None`) would otherwise throw during
|
|
32
|
-
* hydration.
|
|
33
|
-
*/
|
|
34
|
-
export function stripNullsForGroups(
|
|
35
|
-
data: Record<string, unknown>,
|
|
36
|
-
elements: FormElement[],
|
|
37
|
-
): Record<string, unknown> {
|
|
38
|
-
const result: Record<string, unknown> = {}
|
|
39
|
-
|
|
40
|
-
for (const [key, value] of Object.entries(data)) {
|
|
41
|
-
const element = elements.find(el => el.name === key)
|
|
42
|
-
if (!element) {
|
|
43
|
-
result[key] = value
|
|
44
|
-
continue
|
|
45
|
-
}
|
|
46
|
-
const formkitType = getFormkitType(element)
|
|
47
|
-
|
|
48
|
-
if ((formkitType === 'group' || formkitType === 'repeater') && value === null) {
|
|
49
|
-
continue
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const children = (element.children as FormElement[] | undefined) ?? []
|
|
53
|
-
|
|
54
|
-
if (formkitType === 'group' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
55
|
-
result[key] = stripNullsForGroups(value as Record<string, unknown>, children)
|
|
56
|
-
}
|
|
57
|
-
else if (formkitType === 'repeater' && Array.isArray(value)) {
|
|
58
|
-
result[key] = value.map(item =>
|
|
59
|
-
item && typeof item === 'object' && !Array.isArray(item)
|
|
60
|
-
? stripNullsForGroups(item as Record<string, unknown>, children)
|
|
61
|
-
: item,
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
result[key] = value
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return result
|
|
70
|
-
}
|
|
71
|
-
|
|
72
22
|
export interface CreateInstanceFormOptions<T extends ClassDataLike> {
|
|
73
23
|
/** Reactive list of available class definitions (agent classes, process classes, etc.) */
|
|
74
24
|
classes: Ref<T[] | undefined>
|
|
@@ -85,9 +35,10 @@ export interface CreateInstanceFormOptions<T extends ClassDataLike> {
|
|
|
85
35
|
/**
|
|
86
36
|
* Shared form logic for creating agent/process instances from class definitions.
|
|
87
37
|
*
|
|
88
|
-
* Handles class selection, FormKit schema generation, stepper navigation,
|
|
89
|
-
*
|
|
90
|
-
*
|
|
38
|
+
* Handles class selection, FormKit schema generation, stepper navigation, and form data
|
|
39
|
+
* lifecycle. Hydration is shared with the edit form via `hydrateFormData` (and submission via
|
|
40
|
+
* `serializeFormData`) so create and edit behave identically. Template/clone pre-filling is
|
|
41
|
+
* done via applyInitialData(); domain-specific submission stays in the calling component.
|
|
91
42
|
*/
|
|
92
43
|
export function useCreateInstanceForm<T extends ClassDataLike>(options: CreateInstanceFormOptions<T>) {
|
|
93
44
|
const { classes, classField, initialClass, locale } = options
|
|
@@ -144,81 +95,15 @@ export function useCreateInstanceForm<T extends ClassDataLike>(options: CreateIn
|
|
|
144
95
|
}
|
|
145
96
|
|
|
146
97
|
watch(selectedClassData, (newClass) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
formData.value = {}
|
|
153
|
-
}
|
|
98
|
+
formData.value = newClass?.form && newClass.form.length > 0
|
|
99
|
+
? hydrateFormData({}, configForm.value as FormElement[])
|
|
100
|
+
: {}
|
|
154
101
|
}, { immediate: true })
|
|
155
102
|
|
|
156
|
-
function initializeElementData(
|
|
157
|
-
element: FormElement,
|
|
158
|
-
result: Record<string, unknown>,
|
|
159
|
-
recursiveFn: (elements: FormElement[], data: Record<string, unknown>) => Record<string, unknown>,
|
|
160
|
-
): void {
|
|
161
|
-
const formkitType = getFormkitType(element)
|
|
162
|
-
const name = element.name as string
|
|
163
|
-
const children = element.children as FormElement[] | undefined
|
|
164
|
-
const hasChildren = children && Array.isArray(children)
|
|
165
|
-
|
|
166
|
-
if (formkitType === 'group') {
|
|
167
|
-
result[name] = result[name] ?? {}
|
|
168
|
-
if (hasChildren) {
|
|
169
|
-
result[name] = recursiveFn(children, result[name] as Record<string, unknown>)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
else if (formkitType === 'repeater') {
|
|
173
|
-
result[name] = result[name] ?? []
|
|
174
|
-
if (Array.isArray(result[name]) && hasChildren) {
|
|
175
|
-
result[name] = (result[name] as Record<string, unknown>[]).map(item => recursiveFn(children, item))
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function initializeGroupData(
|
|
181
|
-
formElements: FormElement[],
|
|
182
|
-
data: Record<string, unknown>,
|
|
183
|
-
): Record<string, unknown> {
|
|
184
|
-
const result = { ...data }
|
|
185
|
-
for (const element of formElements) {
|
|
186
|
-
initializeElementData(element, result, initializeGroupData)
|
|
187
|
-
}
|
|
188
|
-
return result
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function cleanFormData(data: Record<string, unknown>): Record<string, unknown> {
|
|
192
|
-
const result: Record<string, unknown> = {}
|
|
193
|
-
// FormKit artifacts that should be stripped from submissions
|
|
194
|
-
const formkitArtifacts = new Set(['slots'])
|
|
195
|
-
|
|
196
|
-
for (const [key, value] of Object.entries(data)) {
|
|
197
|
-
if (formkitArtifacts.has(key)) continue
|
|
198
|
-
// Strip the internal repeater validation mirror written by FormKit/Repeater.vue.
|
|
199
|
-
if (key.startsWith('__validate__')) continue
|
|
200
|
-
|
|
201
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
202
|
-
result[key] = cleanFormData(value as Record<string, unknown>)
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
result[key] = value
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return result
|
|
210
|
-
}
|
|
211
|
-
|
|
212
103
|
function applyInitialData(data: Record<string, unknown>) {
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
// would look truthy and the toggle would come up enabled.
|
|
217
|
-
const seeded = seedNullableToggles(data, configForm.value as FormElement[])
|
|
218
|
-
const base = initializeGroupData(configForm.value as FormElement[], {})
|
|
219
|
-
const withDefaults = seedFormDefaults(base, configForm.value as FormElement[])
|
|
220
|
-
const sanitized = stripNullsForGroups(seeded, configForm.value as FormElement[])
|
|
221
|
-
formData.value = merge(withDefaults, sanitized)
|
|
104
|
+
// Hydrate from the template/clone data: nullable toggles follow the data's null-ness,
|
|
105
|
+
// missing leaves fall back to their backend defaults — identical to the edit form.
|
|
106
|
+
formData.value = hydrateFormData(data, configForm.value as FormElement[])
|
|
222
107
|
}
|
|
223
108
|
|
|
224
109
|
function resetForm() {
|
|
@@ -242,9 +127,6 @@ export function useCreateInstanceForm<T extends ClassDataLike>(options: CreateIn
|
|
|
242
127
|
getRepeaterStepIndex,
|
|
243
128
|
getRepeaterData,
|
|
244
129
|
setRepeaterData,
|
|
245
|
-
initializeGroupData,
|
|
246
|
-
cleanFormData,
|
|
247
|
-
coerceNullableToggles,
|
|
248
130
|
applyInitialData,
|
|
249
131
|
resetForm,
|
|
250
132
|
}
|
|
@@ -6,6 +6,10 @@ export interface RepeaterConfig {
|
|
|
6
6
|
label?: string
|
|
7
7
|
addLabel?: string
|
|
8
8
|
childrenSchema: FormKitSchemaNode[]
|
|
9
|
+
// Seeded default for a freshly added item. `childrenSchema` is transformed and has its
|
|
10
|
+
// `value` defaults stripped (see EXCLUDED_FIELDS), so a new item must be cloned from this
|
|
11
|
+
// instead of `{}` — otherwise fields like "documents to retrieve" render empty.
|
|
12
|
+
defaultItem: Record<string, unknown>
|
|
9
13
|
min?: number
|
|
10
14
|
max?: number
|
|
11
15
|
}
|
|
@@ -225,6 +229,8 @@ const EXCLUDED_FIELDS = new Set([
|
|
|
225
229
|
'placeholder', // Transformed via getLocalizedString
|
|
226
230
|
'children', // Handled separately for recursion
|
|
227
231
|
'nullable', // Wrapper-level signal for the transform; never a FormKit/PrimeVue prop
|
|
232
|
+
'defaultEnabled', // Wrapper-level signal (initial nullable-toggle state); never a FormKit prop
|
|
233
|
+
'default_enabled', // snake_case form of the above
|
|
228
234
|
// Backend serialises the Pydantic default into element.value (form duality). FormKit pushes
|
|
229
235
|
// schema `value` up to the parent v-model on input registration, which would clobber the
|
|
230
236
|
// loaded data with the backend default. Defaults belong in data, seeded via seedFormDefaults.
|
|
@@ -449,33 +455,42 @@ export function extractRepeaterConfigs(
|
|
|
449
455
|
const formkitType = getFormkitType(element)
|
|
450
456
|
const elementName = element.name as string
|
|
451
457
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
458
|
+
// Isolate each element so one malformed repeater (or a group containing one) is skipped
|
|
459
|
+
// rather than throwing out of the computed and breaking every repeater on the form.
|
|
460
|
+
try {
|
|
461
|
+
if (formkitType === 'repeater') {
|
|
462
|
+
const childrenSchema = (element.children as FormElement[] || []).flatMap(
|
|
463
|
+
child => transformElementForRepeater(child, locale),
|
|
464
|
+
) as FormKitSchemaNode[]
|
|
465
|
+
|
|
466
|
+
// Build full path for nested data access
|
|
467
|
+
const fullPath = parentPath ? `${parentPath}.${elementName}` : elementName
|
|
468
|
+
|
|
469
|
+
const itemChildren = (element.children as FormElement[]) || []
|
|
470
|
+
repeaters.push({
|
|
471
|
+
name: elementName,
|
|
472
|
+
path: fullPath,
|
|
473
|
+
label: getLocalizedString(element.label, locale),
|
|
474
|
+
addLabel: getLocalizedString(element.addLabel || element.add_label, locale),
|
|
475
|
+
childrenSchema,
|
|
476
|
+
defaultItem: seedFormDefaults({}, itemChildren),
|
|
477
|
+
min: element.min as number | undefined,
|
|
478
|
+
max: element.max as number | undefined,
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
else if (formkitType === 'group' && element.children) {
|
|
482
|
+
// Recursively search for repeaters inside groups, passing the current path
|
|
483
|
+
const groupPath = parentPath ? `${parentPath}.${elementName}` : elementName
|
|
484
|
+
const nestedRepeaters = extractRepeaterConfigs(
|
|
485
|
+
element.children as FormElement[],
|
|
486
|
+
locale,
|
|
487
|
+
groupPath,
|
|
488
|
+
)
|
|
489
|
+
repeaters.push(...nestedRepeaters)
|
|
490
|
+
}
|
|
469
491
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const groupPath = parentPath ? `${parentPath}.${elementName}` : elementName
|
|
473
|
-
const nestedRepeaters = extractRepeaterConfigs(
|
|
474
|
-
element.children as FormElement[],
|
|
475
|
-
locale,
|
|
476
|
-
groupPath,
|
|
477
|
-
)
|
|
478
|
-
repeaters.push(...nestedRepeaters)
|
|
492
|
+
catch (error) {
|
|
493
|
+
console.error(`Error extracting repeater "${elementName ?? '<unknown>'}":`, error)
|
|
479
494
|
}
|
|
480
495
|
}
|
|
481
496
|
|
|
@@ -491,13 +506,19 @@ export function buildFormKitSchema(
|
|
|
491
506
|
): FormKitSchemaNode[] {
|
|
492
507
|
if (!formElements || formElements.length === 0) return []
|
|
493
508
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
509
|
+
// Isolate each element: a single malformed element is skipped (and logged) instead of
|
|
510
|
+
// collapsing the whole section to []. A blanket try/catch here meant one throwing input
|
|
511
|
+
// took every sibling down with it — e.g. a bad config field wiped the entire Basic Info
|
|
512
|
+
// step (agent_id, name, …), leaving an unusable form.
|
|
513
|
+
return formElements.flatMap((element) => {
|
|
514
|
+
try {
|
|
515
|
+
return transformElementToSchema(element, options)
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
console.error(`Error transforming form element "${(element?.name as string) ?? '<unknown>'}":`, error)
|
|
519
|
+
return []
|
|
520
|
+
}
|
|
521
|
+
}) as FormKitSchemaNode[]
|
|
501
522
|
}
|
|
502
523
|
|
|
503
524
|
const LOCALE_KEYS = new Set(['de', 'en', 'fr', 'it'])
|
|
@@ -618,13 +639,15 @@ export function coerceNullableToggles(
|
|
|
618
639
|
* process edit forms.
|
|
619
640
|
*/
|
|
620
641
|
/**
|
|
621
|
-
* Seed a group field's value:
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
642
|
+
* Seed a group field's value: always materialise its children's defaults (starting from
|
|
643
|
+
* the existing object when present, `{}` otherwise — including for a saved `null`). A null
|
|
644
|
+
* nullable group must still hold an object so FormKit can mount and render its children
|
|
645
|
+
* when the user flips the "Enable" toggle on. Visibility is governed by the synthetic
|
|
646
|
+
* toggle (seeded earlier from the raw null-ness by `seedNullableToggles`), and
|
|
647
|
+
* `coerceNullableToggles` re-nullifies disabled subtrees at submit time — so a materialised
|
|
648
|
+
* but disabled group is never persisted.
|
|
625
649
|
*/
|
|
626
|
-
function seedGroupDefault(value: unknown, children: FormElement[]): Record<string, unknown>
|
|
627
|
-
if (value === null) return null
|
|
650
|
+
function seedGroupDefault(value: unknown, children: FormElement[]): Record<string, unknown> {
|
|
628
651
|
const groupValue = value && typeof value === 'object' && !Array.isArray(value)
|
|
629
652
|
? value as Record<string, unknown>
|
|
630
653
|
: {}
|
|
@@ -675,8 +698,11 @@ export function seedFormDefaults(
|
|
|
675
698
|
}
|
|
676
699
|
|
|
677
700
|
/**
|
|
678
|
-
* Recursively seeds synthetic toggle values from initial data
|
|
679
|
-
*
|
|
701
|
+
* Recursively seeds synthetic toggle values from initial data. When the field is present in
|
|
702
|
+
* the source data the toggle follows its null-ness (edit/clone). When it is absent — a fresh
|
|
703
|
+
* form — the toggle falls back to the backend's `default_enabled` (the field's data default is
|
|
704
|
+
* non-null), so a nullable field that ships a default (e.g. a prompt, or org_memory) comes up
|
|
705
|
+
* enabled while a `None`-defaulting one (e.g. reranking_config) stays off.
|
|
680
706
|
*/
|
|
681
707
|
export function seedNullableToggles(
|
|
682
708
|
data: Record<string, unknown>,
|
|
@@ -687,7 +713,9 @@ export function seedNullableToggles(
|
|
|
687
713
|
for (const element of elements) {
|
|
688
714
|
const name = element.name as string
|
|
689
715
|
if (element.nullable === true) {
|
|
690
|
-
result[nullableToggleName(name)] =
|
|
716
|
+
result[nullableToggleName(name)] = name in result
|
|
717
|
+
? result[name] !== null && result[name] !== undefined
|
|
718
|
+
: (element.defaultEnabled ?? element.default_enabled) === true
|
|
691
719
|
}
|
|
692
720
|
|
|
693
721
|
const formkitType = getFormkitType(element)
|
|
@@ -709,6 +737,45 @@ export function seedNullableToggles(
|
|
|
709
737
|
return result
|
|
710
738
|
}
|
|
711
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Strips FormKit submission artifacts: the `slots` helper key and the repeater validation
|
|
742
|
+
* mirror keys (`__validate__*`) registered by Repeater.vue. Recurses into nested group objects.
|
|
743
|
+
*/
|
|
744
|
+
export function cleanFormData(data: Record<string, unknown>): Record<string, unknown> {
|
|
745
|
+
const result: Record<string, unknown> = {}
|
|
746
|
+
for (const [key, value] of Object.entries(data)) {
|
|
747
|
+
if (key === 'slots' || key.startsWith('__validate__')) continue
|
|
748
|
+
result[key] = value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
749
|
+
? cleanFormData(value as Record<string, unknown>)
|
|
750
|
+
: value
|
|
751
|
+
}
|
|
752
|
+
return result
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Raw saved/template/empty data → a fully hydrated FormKit model: nullable toggles seeded from
|
|
757
|
+
* the data's null-ness (or `default_enabled` on a fresh form), then groups materialised and
|
|
758
|
+
* leaf defaults filled. Single entry point so create and edit forms hydrate identically.
|
|
759
|
+
*/
|
|
760
|
+
export function hydrateFormData(
|
|
761
|
+
raw: Record<string, unknown>,
|
|
762
|
+
elements: FormElement[],
|
|
763
|
+
): Record<string, unknown> {
|
|
764
|
+
return seedFormDefaults(seedNullableToggles(raw, elements), elements)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* FormKit model → submission payload: disabled nullable subtrees nulled and synthetic toggle
|
|
769
|
+
* keys dropped (coerceNullableToggles), LocaleStrings normalised, FormKit artifacts stripped.
|
|
770
|
+
* Single entry point so create and edit forms serialise identically.
|
|
771
|
+
*/
|
|
772
|
+
export function serializeFormData(
|
|
773
|
+
data: Record<string, unknown>,
|
|
774
|
+
elements: FormElement[],
|
|
775
|
+
): Record<string, unknown> {
|
|
776
|
+
return cleanFormData(normalizeFormLocaleStrings(coerceNullableToggles(data, elements)))
|
|
777
|
+
}
|
|
778
|
+
|
|
712
779
|
/**
|
|
713
780
|
* Categorizes form elements into simple inputs, groups, and repeaters.
|
|
714
781
|
* Used for organizing form elements into stepper steps.
|
|
@@ -752,9 +819,11 @@ export function extractGroupConfigs(
|
|
|
752
819
|
const groups: GroupConfig[] = []
|
|
753
820
|
|
|
754
821
|
for (const element of formElements) {
|
|
755
|
-
|
|
822
|
+
if (getFormkitType(element) !== 'group') continue
|
|
756
823
|
|
|
757
|
-
|
|
824
|
+
// Isolate each group so a single malformed group is skipped rather than throwing out
|
|
825
|
+
// of the computed and blanking the whole step list.
|
|
826
|
+
try {
|
|
758
827
|
const schema = transformElementToSchema(element, { locale })
|
|
759
828
|
const schemaArray = Array.isArray(schema) ? schema : [schema]
|
|
760
829
|
|
|
@@ -764,6 +833,9 @@ export function extractGroupConfigs(
|
|
|
764
833
|
schema: schemaArray,
|
|
765
834
|
})
|
|
766
835
|
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
console.error(`Error transforming group "${(element?.name as string) ?? '<unknown>'}":`, error)
|
|
838
|
+
}
|
|
767
839
|
}
|
|
768
840
|
|
|
769
841
|
return groups
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"license": "AGPL-3.0-or-later",
|
|
4
4
|
"author": "bbv Software Services AG (https://www.bbv.ch)",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"version": "0.298.
|
|
6
|
+
"version": "0.298.3",
|
|
7
7
|
"description": "Swiss AI Hub - Admin & Management UI (Nuxt 3 layer)",
|
|
8
8
|
"main": "./nuxt.config.ts",
|
|
9
9
|
"repository": {
|
|
@@ -34,10 +34,6 @@
|
|
|
34
34
|
</template>
|
|
35
35
|
|
|
36
36
|
<script setup lang="ts">
|
|
37
|
-
import type { AgentConfigDtoReadable } from '@core/sdk/client'
|
|
38
|
-
|
|
39
|
-
type FormElement = NonNullable<AgentConfigDtoReadable['form']>[number]
|
|
40
|
-
|
|
41
37
|
const route = useRoute()
|
|
42
38
|
const { tenantId } = useTenant()
|
|
43
39
|
const { agentInstance, agentInstanceIsLoading } = useAgentInstance()
|
|
@@ -47,42 +43,14 @@ const toast = useToast()
|
|
|
47
43
|
|
|
48
44
|
const configForm = computed(() => agentInstance.value?.agent_config?.form || [])
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
): Record<string, unknown> => {
|
|
59
|
-
const result = { ...data }
|
|
60
|
-
|
|
61
|
-
for (const element of formElements) {
|
|
62
|
-
const elementRecord = element as Record<string, unknown>
|
|
63
|
-
const formkitType = elementRecord.formkit || elementRecord.$formkit
|
|
64
|
-
|
|
65
|
-
if (formkitType === 'group') {
|
|
66
|
-
const name = elementRecord.name as string
|
|
67
|
-
const children = elementRecord.children as FormElement[] | undefined
|
|
68
|
-
|
|
69
|
-
if (result[name] === null || result[name] === undefined) {
|
|
70
|
-
result[name] = {}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (children && Array.isArray(children)) {
|
|
74
|
-
result[name] = initializeGroupData(children, result[name] as Record<string, unknown>)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return result
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const configurationData = computed(() => {
|
|
83
|
-
const rawData = (agentInstance.value?.configuration || {}) as Record<string, unknown>
|
|
84
|
-
return initializeGroupData(configForm.value, rawData)
|
|
85
|
-
})
|
|
46
|
+
// Pass the saved configuration through unchanged. DynamicConfiguration hydrates it
|
|
47
|
+
// (seedNullableToggles then seedFormDefaults): non-nullable groups are materialised to
|
|
48
|
+
// objects, while nullable groups keep their saved `null` so their "Enable" toggle loads
|
|
49
|
+
// off. Pre-filling `null` groups with `{}` here would make every disabled nullable group
|
|
50
|
+
// (e.g. reranking_config, org_memory) load as enabled.
|
|
51
|
+
const configurationData = computed(
|
|
52
|
+
() => (agentInstance.value?.configuration || {}) as Record<string, unknown>,
|
|
53
|
+
)
|
|
86
54
|
|
|
87
55
|
const submitConfiguration = async (formData: Record<string, unknown>) => {
|
|
88
56
|
const agentClass = route.params.agent_class as string
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
<script setup lang="ts">
|
|
61
61
|
import { useDebounceFn } from '@vueuse/core'
|
|
62
|
-
import {
|
|
62
|
+
import { capitalCase } from 'change-case'
|
|
63
63
|
|
|
64
64
|
import type { DocumentDto } from '@core/sdk/client'
|
|
65
65
|
|
|
@@ -114,11 +114,11 @@ const currentNamespace = computed(() => {
|
|
|
114
114
|
})
|
|
115
115
|
|
|
116
116
|
const databaseDisplayName = computed(() => {
|
|
117
|
-
return currentDatabase.value?.display_name ||
|
|
117
|
+
return currentDatabase.value?.display_name || capitalCase(route.params.db as string)
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
const namespaceDisplayName = computed(() => {
|
|
121
|
-
return currentNamespace.value?.display_name ||
|
|
121
|
+
return currentNamespace.value?.display_name || capitalCase(route.params.namespace as string)
|
|
122
122
|
})
|
|
123
123
|
|
|
124
124
|
const toDocument = (document: DocumentDto) => {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
:key="database.name"
|
|
11
11
|
>
|
|
12
12
|
<div class="flex items-center gap-2 pb-2 pl-2">
|
|
13
|
-
<span class="text-sm font-medium">{{ database.display_name ||
|
|
13
|
+
<span class="text-sm font-medium">{{ database.display_name || capitalCase(database.name) }}</span>
|
|
14
14
|
<i
|
|
15
15
|
v-if="database.auto_sync"
|
|
16
16
|
class="pi pi-lock text-surface-400 dark:text-surface-500"
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
</template>
|
|
68
68
|
|
|
69
69
|
<script setup lang="ts">
|
|
70
|
-
import {
|
|
70
|
+
import { capitalCase } from 'change-case'
|
|
71
71
|
|
|
72
72
|
import type { DatabaseDto, NamespaceDto } from '@core/sdk/client'
|
|
73
73
|
|
|
@@ -96,8 +96,8 @@ const toNamespace = (database_name: string, namespace: NamespaceDto) => {
|
|
|
96
96
|
const openUploadModal = (database: DatabaseDto, namespace: NamespaceDto) => {
|
|
97
97
|
selectedDatabaseForUpload.value = database.name
|
|
98
98
|
selectedNamespaceForUpload.value = namespace.name
|
|
99
|
-
selectedDatabaseDisplayNameForUpload.value = database.display_name ||
|
|
100
|
-
selectedNamespaceDisplayNameForUpload.value = namespace.display_name ||
|
|
99
|
+
selectedDatabaseDisplayNameForUpload.value = database.display_name || capitalCase(database.name)
|
|
100
|
+
selectedNamespaceDisplayNameForUpload.value = namespace.display_name || capitalCase(namespace.name)
|
|
101
101
|
uploadModalVisible.value = true
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<div
|
|
25
25
|
class="pb-2 pl-2 text-sm font-medium"
|
|
26
26
|
>
|
|
27
|
-
{{
|
|
27
|
+
{{ capitalCase(modelType.name) }}
|
|
28
28
|
</div>
|
|
29
29
|
<div
|
|
30
30
|
class="grid grid-cols-2 gap-4 2xl:grid-cols-2"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
</template>
|
|
46
46
|
|
|
47
47
|
<script setup lang="ts">
|
|
48
|
-
import {
|
|
48
|
+
import { capitalCase } from 'change-case'
|
|
49
49
|
|
|
50
50
|
import type { ModelDTO } from '@core/sdk/client'
|
|
51
51
|
|