@swiss-ai-hub/web 0.290.11
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/LICENSE +661 -0
- package/README.md +479 -0
- package/app.config.ts +1 -0
- package/app.vue +52 -0
- package/assets/css/main.css +4 -0
- package/assets/images/logo.png +0 -0
- package/components/Agent/Avatar.vue +40 -0
- package/components/Agent/Card.vue +139 -0
- package/components/Agent/Configuration.vue +20 -0
- package/components/Agent/CreateModal.vue +287 -0
- package/components/Agent/EmptyCard.vue +35 -0
- package/components/Agent/List.vue +85 -0
- package/components/Agent/TemplateCard.vue +58 -0
- package/components/AppLoader.vue +55 -0
- package/components/Chat/Message.vue +90 -0
- package/components/Chat/SourceNodes.vue +120 -0
- package/components/Chat/Thread.vue +134 -0
- package/components/Costs/Table.vue +56 -0
- package/components/Dashboard/Component/BarChart.vue +46 -0
- package/components/Dashboard/Component/LineChart.vue +138 -0
- package/components/Dashboard/Component/Number.vue +31 -0
- package/components/Dashboard/Grid.vue +214 -0
- package/components/Dashboard/Item.vue +75 -0
- package/components/Dashboard/Trend.vue +34 -0
- package/components/Display/List/DisplayList.vue +93 -0
- package/components/Evaluation/Dataset/Card.vue +70 -0
- package/components/Evaluation/Dataset/Create.vue +81 -0
- package/components/Evaluation/Dataset/Edit.vue +132 -0
- package/components/Event/Display/AddMemoryToChatHistoryEvent.vue +60 -0
- package/components/Event/Display/AgentInTheLoopRequestEvent.vue +56 -0
- package/components/Event/Display/AgentInTheLoopResponseEvent.vue +17 -0
- package/components/Event/Display/Base.vue +125 -0
- package/components/Event/Display/BaseRetrieveMemoryEvent.vue +101 -0
- package/components/Event/Display/BaseStoreMemoryEvent.vue +182 -0
- package/components/Event/Display/ChunkEvent.vue +40 -0
- package/components/Event/Display/EmbeddingEvent.vue +25 -0
- package/components/Event/Display/ExceptionEvent.vue +21 -0
- package/components/Event/Display/GuardAcceptEvent.vue +25 -0
- package/components/Event/Display/GuardEvent.vue +17 -0
- package/components/Event/Display/GuardRejectionEvent.vue +25 -0
- package/components/Event/Display/HumanInTheLoopRequestEvent.vue +53 -0
- package/components/Event/Display/HumanInTheLoopResponseEvent.vue +40 -0
- package/components/Event/Display/LLMCostEvent.vue +25 -0
- package/components/Event/Display/LLMEvent.vue +92 -0
- package/components/Event/Display/LimitChatHistoryEvent.vue +60 -0
- package/components/Event/Display/RAGFailureStopEvent.vue +28 -0
- package/components/Event/Display/RAGStartEvent.vue +77 -0
- package/components/Event/Display/RAGSuccessStopEvent.vue +16 -0
- package/components/Event/Display/RawDataContent.vue +69 -0
- package/components/Event/Display/RerankerEvent.vue +39 -0
- package/components/Event/Display/RetrieverEvent.vue +22 -0
- package/components/Event/Display/RouterEvent.vue +54 -0
- package/components/Event/Display/StandaloneQuestionCondenserEvent.vue +38 -0
- package/components/Event/Display/StopEvent.vue +16 -0
- package/components/Event/Display/ThoughtEvent.vue +20 -0
- package/components/Event/Display/ToolEvent.vue +38 -0
- package/components/Event/Display/UnknownEvent.vue +17 -0
- package/components/Event/Display/UserMessageEvent.vue +35 -0
- package/components/Event/List/EventList.vue +249 -0
- package/components/Event/Statistics.vue +49 -0
- package/components/Event/Timeseries.vue +224 -0
- package/components/FormKit/AgentSelector.vue +307 -0
- package/components/FormKit/ChipsInput.vue +62 -0
- package/components/FormKit/DynamicConfiguration.vue +155 -0
- package/components/FormKit/IconSelector.vue +72 -0
- package/components/FormKit/KnowledgeDatabaseSelector.vue +92 -0
- package/components/FormKit/LocaleInput.vue +150 -0
- package/components/FormKit/ModelSelect.vue +110 -0
- package/components/FormKit/Repeater.vue +93 -0
- package/components/FormKit/VectorStoreInput.vue +247 -0
- package/components/Knowledge/Document/List.vue +140 -0
- package/components/Knowledge/Document/Overview.vue +28 -0
- package/components/Knowledge/Document/UploadModal.vue +298 -0
- package/components/Knowledge/Document/WithNodes.vue +105 -0
- package/components/Knowledge/Namespace/Card.vue +108 -0
- package/components/Knowledge/Namespace/CreateModal.vue +203 -0
- package/components/Knowledge/Namespace/EditModal.vue +134 -0
- package/components/Knowledge/Namespace/EmptyCard.vue +35 -0
- package/components/Knowledge/Node/Content.vue +71 -0
- package/components/Markdown/Renderer.vue +87 -0
- package/components/Memory/DetailPage.vue +48 -0
- package/components/Memory/Edit.vue +241 -0
- package/components/Memory/Graph.vue +318 -0
- package/components/Memory/List.vue +155 -0
- package/components/Memory/MemoryManagementPage.vue +178 -0
- package/components/Memory/OpenWebUIContent.vue +96 -0
- package/components/Memory/PageLayout.vue +72 -0
- package/components/Models/ModelDetailsPanel.vue +250 -0
- package/components/Models/NamespaceCard.vue +79 -0
- package/components/Navigation/Left.vue +85 -0
- package/components/Navigation/Top.vue +31 -0
- package/components/Notification/Item.vue +88 -0
- package/components/Notification/NotificationsOverlay.vue +164 -0
- package/components/Process/Card.vue +119 -0
- package/components/Process/Configuration.vue +20 -0
- package/components/Process/CreateModal.vue +276 -0
- package/components/Process/EmptyCard.vue +35 -0
- package/components/Process/Form.vue +153 -0
- package/components/Process/Starts.vue +44 -0
- package/components/Process/Walkthrough/List.vue +162 -0
- package/components/Role/AccessRulesEditor.vue +132 -0
- package/components/Role/Card.vue +68 -0
- package/components/Role/Create.vue +55 -0
- package/components/Role/Edit.vue +82 -0
- package/components/Role/UsageLimitsEditor.vue +225 -0
- package/components/Service/Selection.vue +148 -0
- package/components/Structural/Column.vue +74 -0
- package/components/Structural/Screen.vue +10 -0
- package/components/Structural/Substructure.vue +5 -0
- package/components/Tenant/Switcher.vue +102 -0
- package/components/Thread/Details.vue +135 -0
- package/components/Thread/Hierarchy.vue +136 -0
- package/components/Thread/Info.vue +41 -0
- package/components/Thread/List.vue +136 -0
- package/components/User/Bar.vue +74 -0
- package/components/User/List.vue +86 -0
- package/components/User/RoleChips.vue +83 -0
- package/components/User/Settings.vue +79 -0
- package/components/Workflow/Modal.vue +39 -0
- package/components/Workflow/NodeCard.vue +41 -0
- package/components/Workflow/StartNode.vue +24 -0
- package/components/Workflow/StepNode.vue +27 -0
- package/components/Workflow/StopNode.vue +24 -0
- package/components/Workflow/Visualization.vue +265 -0
- package/components/mdc/MarkdownFigure.vue +9 -0
- package/components/mdc/MarkdownTable.vue +9 -0
- package/components/mdc/ResolveImageComponent.vue +58 -0
- package/composables/agent/useAgentClass.ts +27 -0
- package/composables/agent/useAgentClassInstances.ts +27 -0
- package/composables/agent/useAgentClasses.ts +27 -0
- package/composables/agent/useAgentIconFromThread.ts +8 -0
- package/composables/agent/useAgentInstance.ts +28 -0
- package/composables/agent/useAgentInstanceThreads.ts +76 -0
- package/composables/agent/useAgentInstances.ts +25 -0
- package/composables/agent/useAgentNavigation.ts +35 -0
- package/composables/agent/useCreateAgentInstance.ts +33 -0
- package/composables/agent/useDeleteAgentInstance.ts +31 -0
- package/composables/agent/useUpdateAgentInstance.ts +40 -0
- package/composables/auth/useAuth.ts +54 -0
- package/composables/auth/useAuthProviders.ts +14 -0
- package/composables/chat/useChatCompletions.ts +30 -0
- package/composables/dashboard/useAgentNameFromDashboardWidget.ts +27 -0
- package/composables/dashboard/useDashboardComponent.ts +27 -0
- package/composables/dashboard/useSaveDashboard.ts +21 -0
- package/composables/document/useCreateNamespace.ts +26 -0
- package/composables/document/useDatabases.ts +23 -0
- package/composables/document/useDocument.ts +29 -0
- package/composables/document/useDocumentUrl.ts +20 -0
- package/composables/document/useDocuments.ts +107 -0
- package/composables/document/useNodes.ts +29 -0
- package/composables/document/useSummaryNodes.ts +32 -0
- package/composables/document/useUpdateNamespace.ts +22 -0
- package/composables/evaluation/useCreateDataset.ts +19 -0
- package/composables/evaluation/useDataset.ts +26 -0
- package/composables/evaluation/useDatasets.ts +25 -0
- package/composables/evaluation/useUpdateDataset.ts +23 -0
- package/composables/event/useBasicEventStatistics.ts +83 -0
- package/composables/event/useEventColor.ts +25 -0
- package/composables/event/useEventComponent.ts +87 -0
- package/composables/event/useEventTimeseries.ts +39 -0
- package/composables/event/useEventTimeseriesStats.ts +26 -0
- package/composables/file/useFileUpload.ts +91 -0
- package/composables/file/useSupportedFileTypes.ts +22 -0
- package/composables/form/useCreateInstanceForm.ts +251 -0
- package/composables/form/useFormKitTransform.ts +753 -0
- package/composables/memory/useMemoryCRUD.ts +88 -0
- package/composables/memory/useMemoryFactory.ts +319 -0
- package/composables/memory/useMemorySearchFilter.ts +74 -0
- package/composables/models/useModelsList.ts +24 -0
- package/composables/models/useSingleModel.ts +30 -0
- package/composables/notification/useNotificationPoller.ts +58 -0
- package/composables/notification/useNotifications.ts +57 -0
- package/composables/notification/useUpdateMultipleNotifications.ts +17 -0
- package/composables/notification/useUpdateNotification.ts +17 -0
- package/composables/process/useCreateProcessInstance.ts +32 -0
- package/composables/process/useDeleteProcessInstance.ts +31 -0
- package/composables/process/useProcessClasses.ts +27 -0
- package/composables/process/useProcessInstance.ts +28 -0
- package/composables/process/useProcessInstances.ts +27 -0
- package/composables/process/useProcessWalkthroughs.ts +73 -0
- package/composables/process/useSendProcessStartForm.ts +43 -0
- package/composables/process/useUpdateProcessInstance.ts +40 -0
- package/composables/role/useCreateRole.ts +19 -0
- package/composables/role/useDeleteRole.ts +21 -0
- package/composables/role/useRole.ts +30 -0
- package/composables/role/useRoles.ts +25 -0
- package/composables/role/useUpdateRole.ts +22 -0
- package/composables/suite/useApps.ts +31 -0
- package/composables/suite/useSuite.ts +26 -0
- package/composables/tenant/useActiveTenant.ts +27 -0
- package/composables/tenant/useSysadminNavigation.ts +19 -0
- package/composables/tenant/useTenant.ts +38 -0
- package/composables/tenant/useTenantMemberships.ts +15 -0
- package/composables/tenant/useTenantPath.ts +20 -0
- package/composables/tenant/useTenantPolling.ts +30 -0
- package/composables/tenant/useTenantReady.ts +12 -0
- package/composables/theme/useDarkMode.ts +5 -0
- package/composables/thread/useThread.ts +27 -0
- package/composables/thread/useThreadEvents.ts +91 -0
- package/composables/thread/useThreadUtils.ts +49 -0
- package/composables/thread/useThreads.ts +64 -0
- package/composables/thread/useThreadsInfinite.ts +56 -0
- package/composables/translation/useTranslate.ts +20 -0
- package/composables/useRouteReady.ts +21 -0
- package/composables/useTimeAgo.ts +40 -0
- package/composables/user/useAssignRoleToUser.ts +22 -0
- package/composables/user/useMyUser.ts +25 -0
- package/composables/user/useRevokeRoleFromUser.ts +21 -0
- package/composables/user/useUser.ts +30 -0
- package/composables/user/useUsers.ts +63 -0
- package/composables/utils/useJsonTree.ts +138 -0
- package/formkit.config.ts +44 -0
- package/i18n/locales/de.yaml +815 -0
- package/i18n/locales/en.yaml +804 -0
- package/i18n/locales/fr.yaml +812 -0
- package/i18n/locales/it.yaml +808 -0
- package/layouts/anonymous.vue +8 -0
- package/layouts/default.vue +116 -0
- package/middleware/auth.global.ts +62 -0
- package/nuxt.config.ts +145 -0
- package/package.json +114 -0
- package/pages/[tenant]/index.vue +31 -0
- package/pages/[tenant]/notifications/index.vue +235 -0
- package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/chat.vue +67 -0
- package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/configuration.vue +122 -0
- package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/memories/[memory_id].vue +3 -0
- package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/memories.vue +20 -0
- package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/overview.vue +72 -0
- package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/threads.vue +52 -0
- package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/workflow.vue +19 -0
- package/pages/[tenant]/service/agents/[agent_class]-[agent_id].vue +63 -0
- package/pages/[tenant]/service/agents/templates.vue +102 -0
- package/pages/[tenant]/service/agents.vue +185 -0
- package/pages/[tenant]/service/datasets/[dataset_id].vue +81 -0
- package/pages/[tenant]/service/datasets.vue +53 -0
- package/pages/[tenant]/service/health/index.vue +3 -0
- package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id]/nodes.vue +20 -0
- package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id]/overview.vue +40 -0
- package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id]/summary.vue +88 -0
- package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id].vue +48 -0
- package/pages/[tenant]/service/knowledge/[db]/[namespace].vue +144 -0
- package/pages/[tenant]/service/knowledge.vue +126 -0
- package/pages/[tenant]/service/models/[model_name].vue +84 -0
- package/pages/[tenant]/service/models.vue +61 -0
- package/pages/[tenant]/service/my-account.vue +117 -0
- package/pages/[tenant]/service/openai/[thread_id]/[display_id]/memories.vue +66 -0
- package/pages/[tenant]/service/openai/[thread_id]/[display_id]/sources.vue +100 -0
- package/pages/[tenant]/service/openai/[thread_id]/[display_id]/tracing.vue +49 -0
- package/pages/[tenant]/service/openai.vue +101 -0
- package/pages/[tenant]/service/organization-memories/graph.vue +97 -0
- package/pages/[tenant]/service/organization-memories/list/[memory_id].vue +3 -0
- package/pages/[tenant]/service/organization-memories/list.vue +150 -0
- package/pages/[tenant]/service/organization-memories.vue +3 -0
- package/pages/[tenant]/service/processes/[process_class]-[process_id]/[process_walkthrough_id].vue +7 -0
- package/pages/[tenant]/service/processes/[process_class]-[process_id]/configuration.vue +106 -0
- package/pages/[tenant]/service/processes/[process_class]-[process_id]/overview.vue +67 -0
- package/pages/[tenant]/service/processes/[process_class]-[process_id]/start.vue +26 -0
- package/pages/[tenant]/service/processes/[process_class]-[process_id]/walkthroughs/[process_walkthrough_id]/overview.vue +14 -0
- package/pages/[tenant]/service/processes/[process_class]-[process_id]/walkthroughs.vue +54 -0
- package/pages/[tenant]/service/processes/[process_class]-[process_id].vue +60 -0
- package/pages/[tenant]/service/processes.vue +129 -0
- package/pages/[tenant]/service/roles/[role_id].vue +54 -0
- package/pages/[tenant]/service/roles.vue +84 -0
- package/pages/[tenant]/service/threads/[thread_id]/chat.vue +51 -0
- package/pages/[tenant]/service/threads/[thread_id]/display/[display_id].vue +21 -0
- package/pages/[tenant]/service/threads/[thread_id]/display.vue +29 -0
- package/pages/[tenant]/service/threads/[thread_id]/hierarchy.vue +14 -0
- package/pages/[tenant]/service/threads/[thread_id]/memories/[memory_id].vue +3 -0
- package/pages/[tenant]/service/threads/[thread_id]/memories.vue +19 -0
- package/pages/[tenant]/service/threads/[thread_id]/overview.vue +100 -0
- package/pages/[tenant]/service/threads/[thread_id].vue +54 -0
- package/pages/[tenant]/service/threads.vue +52 -0
- package/pages/[tenant]/service/user-memories/graph.vue +97 -0
- package/pages/[tenant]/service/user-memories/list/[memory_id].vue +3 -0
- package/pages/[tenant]/service/user-memories/list.vue +150 -0
- package/pages/[tenant]/service/user-memories.vue +3 -0
- package/pages/[tenant]/service/users/[user_id].vue +117 -0
- package/pages/[tenant]/service/users.vue +88 -0
- package/pages/auth/callback.vue +52 -0
- package/pages/auth/login.vue +80 -0
- package/pages/auth/renew.vue +24 -0
- package/pages/index.vue +59 -0
- package/pages/select-tenant.vue +76 -0
- package/plugins/0.runtime-config.client.ts +55 -0
- package/plugins/apexcharts.client.ts +5 -0
- package/plugins/api-client.client.ts +38 -0
- package/plugins/dark-mode.client.ts +12 -0
- package/plugins/keycloak-client.ts +41 -0
- package/plugins/oidc-client.ts +78 -0
- package/sdk/client/client/client.gen.ts +237 -0
- package/sdk/client/client/index.ts +24 -0
- package/sdk/client/client/types.gen.ts +213 -0
- package/sdk/client/client/utils.gen.ts +407 -0
- package/sdk/client/client.gen.ts +25 -0
- package/sdk/client/core/auth.gen.ts +42 -0
- package/sdk/client/core/bodySerializer.gen.ts +96 -0
- package/sdk/client/core/params.gen.ts +181 -0
- package/sdk/client/core/pathSerializer.gen.ts +180 -0
- package/sdk/client/core/queryKeySerializer.gen.ts +136 -0
- package/sdk/client/core/serverSentEvents.gen.ts +265 -0
- package/sdk/client/core/types.gen.ts +118 -0
- package/sdk/client/core/utils.gen.ts +143 -0
- package/sdk/client/index.ts +1013 -0
- package/sdk/client/schemas.gen.ts +35395 -0
- package/sdk/client/sdk.gen.ts +3438 -0
- package/sdk/client/transformers.gen.ts +143 -0
- package/sdk/client/types.gen.ts +27567 -0
- package/tailwind.config.mjs +27 -0
- package/themes/aihub-theme.ts +125 -0
- package/types/DashboardWidget.ts +13 -0
- package/types/EventChartInput.ts +7 -0
- package/types/NavItem.ts +6 -0
- package/types/TimeseriesInput.ts +7 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import type { FormKitSchemaNode } from '@formkit/core'
|
|
2
|
+
|
|
3
|
+
export interface RepeaterConfig {
|
|
4
|
+
name: string
|
|
5
|
+
path: string // Full path for nested data access (e.g., "few_shot.few_shot_examples")
|
|
6
|
+
label?: string
|
|
7
|
+
addLabel?: string
|
|
8
|
+
childrenSchema: FormKitSchemaNode[]
|
|
9
|
+
min?: number
|
|
10
|
+
max?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GroupConfig {
|
|
14
|
+
name: string
|
|
15
|
+
label?: string
|
|
16
|
+
schema: FormKitSchemaNode[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CategorizedElements {
|
|
20
|
+
simpleElements: FormElement[]
|
|
21
|
+
groupElements: FormElement[]
|
|
22
|
+
repeaterElements: FormElement[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type FormElement = Record<string, unknown>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets a nested value from an object using a dot-separated path.
|
|
29
|
+
* Creates intermediate objects if they don't exist.
|
|
30
|
+
*/
|
|
31
|
+
export function getNestedValue(
|
|
32
|
+
obj: Record<string, unknown>,
|
|
33
|
+
path: string,
|
|
34
|
+
): Record<string, unknown>[] {
|
|
35
|
+
const parts = path.split('.')
|
|
36
|
+
let current: Record<string, unknown> = obj
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
39
|
+
const key = parts[i]
|
|
40
|
+
current[key] ??= {}
|
|
41
|
+
current = current[key] as Record<string, unknown>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lastKey = parts[parts.length - 1]
|
|
45
|
+
if (!Array.isArray(current[lastKey])) {
|
|
46
|
+
current[lastKey] = []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return current[lastKey] as Record<string, unknown>[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sets a nested array value in an object using a dot-separated path.
|
|
54
|
+
* Creates intermediate objects if they don't exist.
|
|
55
|
+
*/
|
|
56
|
+
export function setNestedValue(
|
|
57
|
+
obj: Record<string, unknown>,
|
|
58
|
+
path: string,
|
|
59
|
+
value: Record<string, unknown>[],
|
|
60
|
+
): void {
|
|
61
|
+
const parts = path.split('.')
|
|
62
|
+
let current: Record<string, unknown> = obj
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
65
|
+
const key = parts[i]
|
|
66
|
+
current[key] ??= {}
|
|
67
|
+
current = current[key] as Record<string, unknown>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
current[parts[parts.length - 1]] = value
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gets the FormKit type from an element, checking both 'formkit' and '$formkit' properties.
|
|
75
|
+
*/
|
|
76
|
+
export function getFormkitType(element: FormElement): unknown {
|
|
77
|
+
return element.formkit || element.$formkit
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wraps a FormKit schema node in a fieldset with a legend label.
|
|
82
|
+
* Optionally applies a condition to the fieldset wrapper.
|
|
83
|
+
*/
|
|
84
|
+
export function wrapInFieldset(
|
|
85
|
+
label: string,
|
|
86
|
+
node: FormKitSchemaNode,
|
|
87
|
+
condition?: string,
|
|
88
|
+
key?: string,
|
|
89
|
+
): FormKitSchemaNode[] {
|
|
90
|
+
const fieldset: Record<string, unknown> = {
|
|
91
|
+
$el: 'fieldset',
|
|
92
|
+
attrs: { class: 'formkit-group-fieldset' },
|
|
93
|
+
children: [
|
|
94
|
+
{
|
|
95
|
+
$el: 'legend',
|
|
96
|
+
attrs: { class: 'text-sm font-semibold px-2 text-surface-700 dark:text-surface-300' },
|
|
97
|
+
children: label,
|
|
98
|
+
},
|
|
99
|
+
node,
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Apply condition to fieldset wrapper so the entire section hides
|
|
104
|
+
if (condition) {
|
|
105
|
+
fieldset.if = condition
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add key to prevent Vue from reusing DOM elements between conditional fieldsets
|
|
109
|
+
if (key) {
|
|
110
|
+
fieldset.key = key
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return [fieldset] as FormKitSchemaNode[]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Gets a localized string from a value that may be a string or a locale object.
|
|
118
|
+
*/
|
|
119
|
+
export function getLocalizedString(value: unknown, locale: string): string | undefined {
|
|
120
|
+
if (!value) return undefined
|
|
121
|
+
if (typeof value === 'string') return value
|
|
122
|
+
if (typeof value === 'object') {
|
|
123
|
+
const localeObj = value as Record<string, string>
|
|
124
|
+
return localeObj[locale] || localeObj.en || Object.values(localeObj)[0]
|
|
125
|
+
}
|
|
126
|
+
return String(value)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface TransformOptions {
|
|
130
|
+
locale?: string
|
|
131
|
+
labelTransform?: (label: string) => string
|
|
132
|
+
optionsResolver?: (element: FormElement) => unknown[] | undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createGroupNode(
|
|
136
|
+
element: FormElement,
|
|
137
|
+
children: FormKitSchemaNode[],
|
|
138
|
+
label: string | undefined,
|
|
139
|
+
): FormKitSchemaNode | FormKitSchemaNode[] {
|
|
140
|
+
const groupNode: Record<string, unknown> = {
|
|
141
|
+
$formkit: 'group',
|
|
142
|
+
name: element.name as string,
|
|
143
|
+
children,
|
|
144
|
+
// Keep the group's data in the form context if its `if:` condition (or its
|
|
145
|
+
// wrapping fieldset's) flips false — without preserve, an unmounted group
|
|
146
|
+
// drops every nested value.
|
|
147
|
+
preserve: true,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Preserve id for $get() references in conditionals
|
|
151
|
+
if (element.id) groupNode.id = element.id
|
|
152
|
+
|
|
153
|
+
// Add key for Vue to prevent DOM reuse between conditional groups
|
|
154
|
+
// This is critical when sibling groups have fields with the same names
|
|
155
|
+
groupNode.key = element.id || element.name
|
|
156
|
+
|
|
157
|
+
const condition = element.if as string | undefined
|
|
158
|
+
const key = (element.id || element.name) as string | undefined
|
|
159
|
+
|
|
160
|
+
// When wrapped in fieldset, apply condition to fieldset (outer wrapper)
|
|
161
|
+
// Otherwise apply to the group itself
|
|
162
|
+
if (label) {
|
|
163
|
+
return wrapInFieldset(label, groupNode as FormKitSchemaNode, condition, key)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (condition) groupNode.if = condition
|
|
167
|
+
return groupNode as FormKitSchemaNode
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveElementOptions(
|
|
171
|
+
element: FormElement,
|
|
172
|
+
optionsResolver?: (element: FormElement) => unknown[] | undefined,
|
|
173
|
+
): unknown[] | undefined {
|
|
174
|
+
if (optionsResolver) {
|
|
175
|
+
return optionsResolver(element)
|
|
176
|
+
}
|
|
177
|
+
return element.options as unknown[] | undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildLocaleInputProperties(
|
|
181
|
+
element: FormElement,
|
|
182
|
+
label: string | undefined,
|
|
183
|
+
locale: string,
|
|
184
|
+
): Record<string, unknown> {
|
|
185
|
+
// `preserve: true` keeps the input's value in the form context when an `if:`
|
|
186
|
+
// condition (nullable toggle or backend-supplied `condition_if`) unmounts it.
|
|
187
|
+
const cleanNode: Record<string, unknown> = { $formkit: 'localeInput', preserve: true }
|
|
188
|
+
|
|
189
|
+
if (element.name) cleanNode.name = element.name
|
|
190
|
+
if (label) cleanNode.label = label
|
|
191
|
+
|
|
192
|
+
// Preserve id for $get() references in conditionals
|
|
193
|
+
if (element.id) cleanNode.id = element.id
|
|
194
|
+
|
|
195
|
+
// Stable key prevents Vue from reusing this DOM node across sibling schema entries
|
|
196
|
+
// (critical when conditional `if:` siblings mount/unmount around it). `element.id`
|
|
197
|
+
// is auto-assigned per FormkitElement and `element.name` is FormKit-unique within
|
|
198
|
+
// a group by construction, so collisions cannot occur via this fallback chain.
|
|
199
|
+
cleanNode.key = (element.id as string | undefined) ?? (element.name as string)
|
|
200
|
+
|
|
201
|
+
// Preserve conditional visibility (FormKit uses 'if' for schema conditionals)
|
|
202
|
+
if (element.if) cleanNode.if = element.if
|
|
203
|
+
|
|
204
|
+
const help = getLocalizedString(element.help, locale)
|
|
205
|
+
if (help) cleanNode.help = help
|
|
206
|
+
|
|
207
|
+
// For localeInput, pass placeholder as full LocaleString object (not localized)
|
|
208
|
+
if (element.placeholder) cleanNode.placeholder = element.placeholder
|
|
209
|
+
|
|
210
|
+
// Pass inputType and rows props (support both camelCase and snake_case for backwards compatibility)
|
|
211
|
+
const inputType = element.inputType || element.input_type
|
|
212
|
+
if (inputType) cleanNode.inputType = inputType
|
|
213
|
+
if (element.rows !== undefined) cleanNode.rows = element.rows
|
|
214
|
+
|
|
215
|
+
if (element.validation) cleanNode.validation = element.validation
|
|
216
|
+
|
|
217
|
+
return cleanNode
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Fields that should be excluded from passthrough (internal markers, already handled, or need transformation)
|
|
221
|
+
const EXCLUDED_FIELDS = new Set([
|
|
222
|
+
'is_formkit_element', // Internal marker
|
|
223
|
+
'formkit', // Handled separately as $formkit
|
|
224
|
+
'$formkit', // Handled separately
|
|
225
|
+
'label', // Transformed via getLocalizedString
|
|
226
|
+
'help', // Transformed via getLocalizedString
|
|
227
|
+
'placeholder', // Transformed via getLocalizedString
|
|
228
|
+
'children', // Handled separately for recursion
|
|
229
|
+
'nullable', // Wrapper-level signal for the transform; never a FormKit/PrimeVue prop
|
|
230
|
+
// Backend serialises the Pydantic default into element.value (form duality). FormKit pushes
|
|
231
|
+
// schema `value` up to the parent v-model on input registration, which would clobber the
|
|
232
|
+
// loaded data with the backend default. Defaults belong in data, seeded via seedFormDefaults.
|
|
233
|
+
'value',
|
|
234
|
+
])
|
|
235
|
+
|
|
236
|
+
function buildNodeProperties(
|
|
237
|
+
element: FormElement,
|
|
238
|
+
formkitType: unknown,
|
|
239
|
+
label: string | undefined,
|
|
240
|
+
locale: string,
|
|
241
|
+
optionsResolver?: (element: FormElement) => unknown[] | undefined,
|
|
242
|
+
): Record<string, unknown> {
|
|
243
|
+
// Handle localeInput specially
|
|
244
|
+
if (formkitType === 'localeInput') {
|
|
245
|
+
return buildLocaleInputProperties(element, label, locale)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// `preserve: true` keeps the input's value in the form context when an `if:`
|
|
249
|
+
// condition (nullable toggle or backend-supplied `condition_if`) unmounts it.
|
|
250
|
+
const cleanNode: Record<string, unknown> = { $formkit: formkitType, preserve: true }
|
|
251
|
+
|
|
252
|
+
// Pass through all properties except excluded ones
|
|
253
|
+
for (const [key, value] of Object.entries(element)) {
|
|
254
|
+
if (EXCLUDED_FIELDS.has(key)) continue
|
|
255
|
+
if (value === undefined || value === null) continue
|
|
256
|
+
cleanNode[key] = value
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Apply transformations for localized fields
|
|
260
|
+
if (label) cleanNode.label = label
|
|
261
|
+
|
|
262
|
+
const help = getLocalizedString(element.help, locale)
|
|
263
|
+
if (help) cleanNode.help = help
|
|
264
|
+
|
|
265
|
+
const placeholder = getLocalizedString(element.placeholder, locale)
|
|
266
|
+
if (placeholder) cleanNode.placeholder = placeholder
|
|
267
|
+
|
|
268
|
+
// Resolve options if resolver provided
|
|
269
|
+
const options = resolveElementOptions(element, optionsResolver)
|
|
270
|
+
if (options) cleanNode.options = options
|
|
271
|
+
|
|
272
|
+
// Stable key prevents Vue from reusing this DOM node across sibling schema entries
|
|
273
|
+
// (critical when conditional `if:` siblings mount/unmount around it). `element.id`
|
|
274
|
+
// is auto-assigned per FormkitElement and `element.name` is FormKit-unique within
|
|
275
|
+
// a group by construction, so collisions cannot occur via this fallback chain.
|
|
276
|
+
cleanNode.key = (element.id as string | undefined) ?? (element.name as string)
|
|
277
|
+
|
|
278
|
+
return cleanNode
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Builds the toggle name used to gate a nullable form element.
|
|
283
|
+
*/
|
|
284
|
+
export function nullableToggleName(fieldName: string): string {
|
|
285
|
+
return `__${fieldName}__enabled`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Builds a unique FormKit input id for the synthetic toggle that gates a nullable element.
|
|
290
|
+
* Uses the element's existing id (backend ref) when available so the id is unique across
|
|
291
|
+
* the whole form schema; falls back to the field name. Dots are replaced because FormKit
|
|
292
|
+
* `$get()` lookups expect slug-style ids.
|
|
293
|
+
*/
|
|
294
|
+
function nullableToggleId(element: FormElement): string {
|
|
295
|
+
const base = (element.id as string | undefined) ?? (element.name as string)
|
|
296
|
+
return `${base.replace(/\./g, '__')}__enabled_toggle`
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Combines a synthetic toggle condition with any existing condition_if.
|
|
301
|
+
*/
|
|
302
|
+
function combineConditions(toggleCondition: string, existing: string | undefined): string {
|
|
303
|
+
if (!existing) return toggleCondition
|
|
304
|
+
if (existing.startsWith('$:')) {
|
|
305
|
+
return `$: ${toggleCondition.slice(1)} && (${existing.slice(2).trim()})`
|
|
306
|
+
}
|
|
307
|
+
return `$: ${toggleCondition.slice(1)} && (${existing.slice(1)})`
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function buildNullableToggleNode(element: FormElement, label: string | undefined): Record<string, unknown> {
|
|
311
|
+
const fieldName = element.name as string
|
|
312
|
+
const toggleId = nullableToggleId(element)
|
|
313
|
+
return {
|
|
314
|
+
$formkit: 'primeCheckbox',
|
|
315
|
+
name: nullableToggleName(fieldName),
|
|
316
|
+
id: toggleId,
|
|
317
|
+
key: toggleId,
|
|
318
|
+
label: label ? `Enable ${label}` : 'Enable',
|
|
319
|
+
binary: true,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function applyNullableToggle(
|
|
324
|
+
element: FormElement,
|
|
325
|
+
baseNode: FormKitSchemaNode | FormKitSchemaNode[],
|
|
326
|
+
label: string | undefined,
|
|
327
|
+
): FormKitSchemaNode[] {
|
|
328
|
+
const nodeArray = Array.isArray(baseNode) ? baseNode : [baseNode]
|
|
329
|
+
return [buildNullableToggleNode(element, label) as FormKitSchemaNode, ...nodeArray]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function gateElement(element: FormElement, toggleCondition: string): FormElement {
|
|
333
|
+
return { ...element, if: combineConditions(toggleCondition, element.if as string | undefined) }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Transforms a form element to a FormKit schema node.
|
|
338
|
+
* Handles groups specially by wrapping them in fieldsets when they have labels.
|
|
339
|
+
* Skips repeater elements (they are handled separately).
|
|
340
|
+
*/
|
|
341
|
+
export function transformElementToSchema(
|
|
342
|
+
element: FormElement,
|
|
343
|
+
options: TransformOptions = {},
|
|
344
|
+
): FormKitSchemaNode | FormKitSchemaNode[] {
|
|
345
|
+
if (!element) return []
|
|
346
|
+
|
|
347
|
+
const formkitType = getFormkitType(element)
|
|
348
|
+
if (formkitType === 'repeater') return []
|
|
349
|
+
|
|
350
|
+
const { locale = 'en', labelTransform, optionsResolver } = options
|
|
351
|
+
|
|
352
|
+
const children = (element.children as FormElement[] || []).flatMap(
|
|
353
|
+
child => transformElementToSchema(child, options),
|
|
354
|
+
) as FormKitSchemaNode[]
|
|
355
|
+
|
|
356
|
+
let label = getLocalizedString(element.label, locale)
|
|
357
|
+
if (label && labelTransform) {
|
|
358
|
+
label = labelTransform(label)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const isNullable = element.nullable === true
|
|
362
|
+
const toggleCondition = isNullable ? `$get(${nullableToggleId(element)}).value` : undefined
|
|
363
|
+
|
|
364
|
+
if (formkitType === 'group') {
|
|
365
|
+
const gatedElement = isNullable ? gateElement(element, toggleCondition!) : element
|
|
366
|
+
const groupNode = createGroupNode(gatedElement, children, label)
|
|
367
|
+
return isNullable ? applyNullableToggle(element, groupNode, label) : groupNode
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const cleanNode = buildNodeProperties(element, formkitType, label, locale, optionsResolver)
|
|
371
|
+
if (children.length > 0) cleanNode.children = children
|
|
372
|
+
if (isNullable) {
|
|
373
|
+
cleanNode.if = combineConditions(toggleCondition!, element.if as string | undefined)
|
|
374
|
+
return applyNullableToggle(element, cleanNode as FormKitSchemaNode, label)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return cleanNode as FormKitSchemaNode
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function buildLeafNodeForRepeater(
|
|
381
|
+
element: FormElement,
|
|
382
|
+
formkitType: unknown,
|
|
383
|
+
label: string | undefined,
|
|
384
|
+
locale: string,
|
|
385
|
+
children: FormKitSchemaNode[],
|
|
386
|
+
): Record<string, unknown> {
|
|
387
|
+
if (formkitType === 'localeInput') {
|
|
388
|
+
return buildLocaleInputProperties(element, label, locale)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const cleanNode: Record<string, unknown> = { $formkit: formkitType, preserve: true }
|
|
392
|
+
for (const [key, value] of Object.entries(element)) {
|
|
393
|
+
if (EXCLUDED_FIELDS.has(key)) continue
|
|
394
|
+
if (value === undefined || value === null) continue
|
|
395
|
+
cleanNode[key] = value
|
|
396
|
+
}
|
|
397
|
+
if (label) cleanNode.label = label
|
|
398
|
+
const help = getLocalizedString(element.help, locale)
|
|
399
|
+
if (help) cleanNode.help = help
|
|
400
|
+
if (children.length > 0) cleanNode.children = children
|
|
401
|
+
cleanNode.key = (element.id as string | undefined) ?? (element.name as string)
|
|
402
|
+
return cleanNode
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Transforms a form element for use inside a repeater's children schema.
|
|
407
|
+
*/
|
|
408
|
+
export function transformElementForRepeater(
|
|
409
|
+
element: FormElement,
|
|
410
|
+
locale = 'en',
|
|
411
|
+
): FormKitSchemaNode | FormKitSchemaNode[] {
|
|
412
|
+
if (!element) return []
|
|
413
|
+
|
|
414
|
+
const formkitType = getFormkitType(element)
|
|
415
|
+
const children = (element.children as FormElement[] || []).flatMap(
|
|
416
|
+
child => transformElementForRepeater(child, locale),
|
|
417
|
+
) as FormKitSchemaNode[]
|
|
418
|
+
|
|
419
|
+
const label = getLocalizedString(element.label, locale)
|
|
420
|
+
const isNullable = element.nullable === true
|
|
421
|
+
const toggleCondition = isNullable ? `$get(${nullableToggleId(element)}).value` : undefined
|
|
422
|
+
|
|
423
|
+
if (formkitType === 'group') {
|
|
424
|
+
const gatedElement = isNullable ? gateElement(element, toggleCondition!) : element
|
|
425
|
+
const groupNode = createGroupNode(gatedElement, children, label)
|
|
426
|
+
return isNullable ? applyNullableToggle(element, groupNode, label) : groupNode
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const cleanNode = buildLeafNodeForRepeater(element, formkitType, label, locale, children)
|
|
430
|
+
if (isNullable) {
|
|
431
|
+
cleanNode.if = combineConditions(toggleCondition!, element.if as string | undefined)
|
|
432
|
+
return applyNullableToggle(element, cleanNode as FormKitSchemaNode, label)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return cleanNode as FormKitSchemaNode
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Extracts repeater configurations from a form definition.
|
|
440
|
+
*/
|
|
441
|
+
export function extractRepeaterConfigs(
|
|
442
|
+
formElements: FormElement[],
|
|
443
|
+
locale = 'en',
|
|
444
|
+
parentPath = '',
|
|
445
|
+
): RepeaterConfig[] {
|
|
446
|
+
if (!formElements || formElements.length === 0) return []
|
|
447
|
+
|
|
448
|
+
const repeaters: RepeaterConfig[] = []
|
|
449
|
+
|
|
450
|
+
for (const element of formElements) {
|
|
451
|
+
const formkitType = getFormkitType(element)
|
|
452
|
+
const elementName = element.name as string
|
|
453
|
+
|
|
454
|
+
if (formkitType === 'repeater') {
|
|
455
|
+
const childrenSchema = (element.children as FormElement[] || []).flatMap(
|
|
456
|
+
child => transformElementForRepeater(child, locale),
|
|
457
|
+
) as FormKitSchemaNode[]
|
|
458
|
+
|
|
459
|
+
// Build full path for nested data access
|
|
460
|
+
const fullPath = parentPath ? `${parentPath}.${elementName}` : elementName
|
|
461
|
+
|
|
462
|
+
repeaters.push({
|
|
463
|
+
name: elementName,
|
|
464
|
+
path: fullPath,
|
|
465
|
+
label: getLocalizedString(element.label, locale),
|
|
466
|
+
addLabel: getLocalizedString(element.addLabel || element.add_label, locale),
|
|
467
|
+
childrenSchema,
|
|
468
|
+
min: element.min as number | undefined,
|
|
469
|
+
max: element.max as number | undefined,
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
else if (formkitType === 'group' && element.children) {
|
|
473
|
+
// Recursively search for repeaters inside groups, passing the current path
|
|
474
|
+
const groupPath = parentPath ? `${parentPath}.${elementName}` : elementName
|
|
475
|
+
const nestedRepeaters = extractRepeaterConfigs(
|
|
476
|
+
element.children as FormElement[],
|
|
477
|
+
locale,
|
|
478
|
+
groupPath,
|
|
479
|
+
)
|
|
480
|
+
repeaters.push(...nestedRepeaters)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return repeaters
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Builds a FormKit schema from form elements.
|
|
489
|
+
*/
|
|
490
|
+
export function buildFormKitSchema(
|
|
491
|
+
formElements: FormElement[],
|
|
492
|
+
options: TransformOptions = {},
|
|
493
|
+
): FormKitSchemaNode[] {
|
|
494
|
+
if (!formElements || formElements.length === 0) return []
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
return formElements.flatMap(el => transformElementToSchema(el, options)) as FormKitSchemaNode[]
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
console.error('Error transforming schema:', error)
|
|
501
|
+
return []
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const LOCALE_KEYS = new Set(['de', 'en', 'fr', 'it'])
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Checks if a value is a LocaleString object (has only locale keys: de, en, fr, it).
|
|
509
|
+
*/
|
|
510
|
+
function isLocaleStringObject(value: unknown): value is Record<string, unknown> {
|
|
511
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
512
|
+
return false
|
|
513
|
+
}
|
|
514
|
+
const keys = Object.keys(value as object)
|
|
515
|
+
return keys.length > 0 && keys.every(key => LOCALE_KEYS.has(key))
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Normalizes a LocaleString object:
|
|
520
|
+
* - Always returns an object with all locale fields as strings (empty string for unfilled)
|
|
521
|
+
* - Never returns null - backend expects a dict/LocaleString object, not null
|
|
522
|
+
*
|
|
523
|
+
* Backend validation will determine if empty values are acceptable based on field constraints.
|
|
524
|
+
*/
|
|
525
|
+
function normalizeLocaleString(localeObj: Record<string, unknown>): Record<string, string> {
|
|
526
|
+
return {
|
|
527
|
+
de: (localeObj.de as string) || '',
|
|
528
|
+
en: (localeObj.en as string) || '',
|
|
529
|
+
fr: (localeObj.fr as string) || '',
|
|
530
|
+
it: (localeObj.it as string) || '',
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Recursively normalizes LocaleString fields in form data before submission.
|
|
536
|
+
* - LocaleString objects → all fields normalized to strings (empty string for unfilled)
|
|
537
|
+
* - Other values are recursively processed
|
|
538
|
+
*/
|
|
539
|
+
export function normalizeFormLocaleStrings<T>(data: T): T {
|
|
540
|
+
if (data === null || data === undefined) {
|
|
541
|
+
return data
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (Array.isArray(data)) {
|
|
545
|
+
return data.map(item => normalizeFormLocaleStrings(item)) as T
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (typeof data === 'object') {
|
|
549
|
+
if (isLocaleStringObject(data)) {
|
|
550
|
+
return normalizeLocaleString(data as Record<string, unknown>) as T
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const result: Record<string, unknown> = {}
|
|
554
|
+
for (const [key, value] of Object.entries(data)) {
|
|
555
|
+
result[key] = normalizeFormLocaleStrings(value)
|
|
556
|
+
}
|
|
557
|
+
return result as T
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return data
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Walks the form schema; for every nullable element whose synthetic toggle is off,
|
|
565
|
+
* replaces the actual field value with `null`. Always strips synthetic toggle keys.
|
|
566
|
+
*/
|
|
567
|
+
export function coerceNullableToggles(
|
|
568
|
+
data: Record<string, unknown>,
|
|
569
|
+
elements: FormElement[],
|
|
570
|
+
): Record<string, unknown> {
|
|
571
|
+
const result: Record<string, unknown> = { ...data }
|
|
572
|
+
|
|
573
|
+
for (const element of elements) {
|
|
574
|
+
const name = element.name as string
|
|
575
|
+
if (element.nullable === true) {
|
|
576
|
+
const toggleKey = nullableToggleName(name)
|
|
577
|
+
const enabled = result[toggleKey] === true
|
|
578
|
+
Reflect.deleteProperty(result, toggleKey)
|
|
579
|
+
if (!enabled) {
|
|
580
|
+
result[name] = null
|
|
581
|
+
continue
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const formkitType = getFormkitType(element)
|
|
586
|
+
const children = (element.children as FormElement[] | undefined) ?? []
|
|
587
|
+
const value = result[name]
|
|
588
|
+
|
|
589
|
+
if (formkitType === 'group' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
590
|
+
result[name] = coerceNullableToggles(value as Record<string, unknown>, children)
|
|
591
|
+
}
|
|
592
|
+
else if (formkitType === 'repeater' && Array.isArray(value)) {
|
|
593
|
+
result[name] = value.map(item =>
|
|
594
|
+
item && typeof item === 'object' && !Array.isArray(item)
|
|
595
|
+
? coerceNullableToggles(item as Record<string, unknown>, children)
|
|
596
|
+
: item,
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
for (const key of Object.keys(result)) {
|
|
602
|
+
if (key.startsWith('__') && key.endsWith('__enabled')) {
|
|
603
|
+
Reflect.deleteProperty(result, key)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return result
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Recursively fills missing leaf keys with the backend's serialised Pydantic defaults
|
|
612
|
+
* (`element.value`). FormKit no longer receives `value` in the schema (it would clobber
|
|
613
|
+
* the v-model on registration), so defaults must be merged into the form data instead.
|
|
614
|
+
* Existing values — including falsy ones like `false` or `""` — are preserved.
|
|
615
|
+
*
|
|
616
|
+
* NOTE: This helper is load-bearing for edit/clone/template flows but has no direct
|
|
617
|
+
* unit tests yet — Vitest is not configured for packages/web (see packages/web/CLAUDE.md).
|
|
618
|
+
* The Python-side `Form.to_formkit_form()` tests in packages/core lock in what
|
|
619
|
+
* `element.value` looks like; behaviour here is exercised end-to-end on agent and
|
|
620
|
+
* process edit forms.
|
|
621
|
+
*/
|
|
622
|
+
export function seedFormDefaults(
|
|
623
|
+
data: Record<string, unknown>,
|
|
624
|
+
elements: FormElement[],
|
|
625
|
+
): Record<string, unknown> {
|
|
626
|
+
const result: Record<string, unknown> = { ...data }
|
|
627
|
+
|
|
628
|
+
for (const element of elements) {
|
|
629
|
+
const name = element.name as string
|
|
630
|
+
const formkitType = getFormkitType(element)
|
|
631
|
+
const children = (element.children as FormElement[] | undefined) ?? []
|
|
632
|
+
const value = result[name]
|
|
633
|
+
|
|
634
|
+
if (formkitType === 'group') {
|
|
635
|
+
if (value === null) continue // nullable group disabled — leave as null
|
|
636
|
+
// value === undefined (or a non-object): materialise group defaults so children
|
|
637
|
+
// render with backend defaults. For nullable groups whose toggle is off,
|
|
638
|
+
// coerceNullableToggles re-nullifies the whole subtree at submit time, so seeding
|
|
639
|
+
// here is safe even when the toggle will end up disabled.
|
|
640
|
+
const groupValue = (value && typeof value === 'object' && !Array.isArray(value))
|
|
641
|
+
? value as Record<string, unknown>
|
|
642
|
+
: {}
|
|
643
|
+
result[name] = seedFormDefaults(groupValue, children)
|
|
644
|
+
}
|
|
645
|
+
else if (formkitType === 'repeater' && Array.isArray(value)) {
|
|
646
|
+
result[name] = value.map(item =>
|
|
647
|
+
item && typeof item === 'object' && !Array.isArray(item)
|
|
648
|
+
? seedFormDefaults(item as Record<string, unknown>, children)
|
|
649
|
+
: item,
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
else if (!(name in result) && element.value !== undefined) {
|
|
653
|
+
result[name] = element.value
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return result
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Recursively seeds synthetic toggle values from initial data: toggle is on iff the
|
|
662
|
+
* matching field was non-null/undefined in the source data.
|
|
663
|
+
*/
|
|
664
|
+
export function seedNullableToggles(
|
|
665
|
+
data: Record<string, unknown>,
|
|
666
|
+
elements: FormElement[],
|
|
667
|
+
): Record<string, unknown> {
|
|
668
|
+
const result: Record<string, unknown> = { ...data }
|
|
669
|
+
|
|
670
|
+
for (const element of elements) {
|
|
671
|
+
const name = element.name as string
|
|
672
|
+
if (element.nullable === true) {
|
|
673
|
+
result[nullableToggleName(name)] = result[name] !== null && result[name] !== undefined
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const formkitType = getFormkitType(element)
|
|
677
|
+
const children = (element.children as FormElement[] | undefined) ?? []
|
|
678
|
+
const value = result[name]
|
|
679
|
+
|
|
680
|
+
if (formkitType === 'group' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
681
|
+
result[name] = seedNullableToggles(value as Record<string, unknown>, children)
|
|
682
|
+
}
|
|
683
|
+
else if (formkitType === 'repeater' && Array.isArray(value)) {
|
|
684
|
+
result[name] = value.map(item =>
|
|
685
|
+
item && typeof item === 'object' && !Array.isArray(item)
|
|
686
|
+
? seedNullableToggles(item as Record<string, unknown>, children)
|
|
687
|
+
: item,
|
|
688
|
+
)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return result
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Categorizes form elements into simple inputs, groups, and repeaters.
|
|
697
|
+
* Used for organizing form elements into stepper steps.
|
|
698
|
+
*/
|
|
699
|
+
export function categorizeFormElements(formElements: FormElement[]): CategorizedElements {
|
|
700
|
+
if (!formElements || formElements.length === 0) {
|
|
701
|
+
return { simpleElements: [], groupElements: [], repeaterElements: [] }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const simpleElements: FormElement[] = []
|
|
705
|
+
const groupElements: FormElement[] = []
|
|
706
|
+
const repeaterElements: FormElement[] = []
|
|
707
|
+
|
|
708
|
+
for (const element of formElements) {
|
|
709
|
+
const formkitType = getFormkitType(element)
|
|
710
|
+
|
|
711
|
+
if (formkitType === 'group') {
|
|
712
|
+
groupElements.push(element)
|
|
713
|
+
}
|
|
714
|
+
else if (formkitType === 'repeater') {
|
|
715
|
+
repeaterElements.push(element)
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
simpleElements.push(element)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return { simpleElements, groupElements, repeaterElements }
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Extracts group configurations from form elements.
|
|
727
|
+
* Similar to extractRepeaterConfigs but for group elements.
|
|
728
|
+
*/
|
|
729
|
+
export function extractGroupConfigs(
|
|
730
|
+
formElements: FormElement[],
|
|
731
|
+
locale = 'en',
|
|
732
|
+
): GroupConfig[] {
|
|
733
|
+
if (!formElements || formElements.length === 0) return []
|
|
734
|
+
|
|
735
|
+
const groups: GroupConfig[] = []
|
|
736
|
+
|
|
737
|
+
for (const element of formElements) {
|
|
738
|
+
const formkitType = getFormkitType(element)
|
|
739
|
+
|
|
740
|
+
if (formkitType === 'group') {
|
|
741
|
+
const schema = transformElementToSchema(element, { locale })
|
|
742
|
+
const schemaArray = Array.isArray(schema) ? schema : [schema]
|
|
743
|
+
|
|
744
|
+
groups.push({
|
|
745
|
+
name: element.name as string,
|
|
746
|
+
label: getLocalizedString(element.label, locale),
|
|
747
|
+
schema: schemaArray,
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return groups
|
|
753
|
+
}
|