|
| 1 | +<script setup lang="ts"> |
| 2 | +import {ref, computed, watch, type Ref, onMounted, nextTick} from 'vue'; |
| 3 | +import Dialog from 'primevue/dialog'; |
| 4 | +import InputText from 'primevue/inputtext'; |
| 5 | +import Button from 'primevue/button'; |
| 6 | +import Select from 'primevue/select'; |
| 7 | +import Divider from 'primevue/divider'; |
| 8 | +import Message from 'primevue/message'; |
| 9 | +import ApiKey from '@/components/panels/ai-prompts/ApiKey.vue'; |
| 10 | +import {SessionMode} from '@/store/sessionMode'; |
| 11 | +import {getDataForMode} from '@/data/useDataLink'; |
| 12 | +import {DataMappingServiceStml} from '@/data-mapping/stml/dataMappingServiceStml'; |
| 13 | +import {DataMappingServiceJsonata} from '@/data-mapping/jsonata/dataMappingServiceJsonata'; |
| 14 | +import type {DataMappingService} from '@/data-mapping/dataMappingService'; |
| 15 | +import type {Editor} from 'brace'; |
| 16 | +import * as ace from 'brace'; |
| 17 | +import {setupAceProperties} from '@/components/panels/shared-components/aceUtils'; |
| 18 | +import {useSettings} from '@/settings/useSettings'; |
| 19 | +import {useDebounceFn} from '@vueuse/core'; |
| 20 | +import ProgressSpinner from 'primevue/progressspinner'; |
| 21 | +
|
| 22 | +const showDialog = ref(false); |
| 23 | +const editor_id = 'data-mapping-' + Math.random(); |
| 24 | +const editorInitialized: Ref<boolean> = ref(false); |
| 25 | +const editor: Ref<Editor | null> = ref(null); |
| 26 | +const input = ref({}); |
| 27 | +const result = ref(''); |
| 28 | +const resultIsValid = ref(false); |
| 29 | +const statusMessage = ref(''); |
| 30 | +const errorMessage = ref(''); |
| 31 | +const userComments = ref(''); |
| 32 | +const isLoadingMapping = ref(false); |
| 33 | +
|
| 34 | +const settings = useSettings(); |
| 35 | +
|
| 36 | +const mappingServiceTypes = ['Advanced (JSONata)', 'SimpleTransformationMappingLanguage (STML)']; |
| 37 | +
|
| 38 | +const mappingServiceWarnings = [ |
| 39 | + 'The JSONata mapping service is very expressive flexible, but may generate invalid mappings for complex inputs, which have to manually be corrected.', |
| 40 | + 'The STML mapping service usually generates valid mappings, but it can perform only simple source to target path mappings and value transformations. WARNING: It supports executing arbitrary JavaScript functions as transformations, which may lead to security issues if the input is not properly sanitized.', |
| 41 | +]; |
| 42 | +
|
| 43 | +const selectedMappingServiceType: Ref<string> = ref(mappingServiceTypes[0]); |
| 44 | +
|
| 45 | +const mappingService: Ref<DataMappingService> = computed(() => { |
| 46 | + if (selectedMappingServiceType.value === 'SimpleTransformationMappingLanguage (STML)') { |
| 47 | + return new DataMappingServiceStml(); |
| 48 | + } |
| 49 | + if (selectedMappingServiceType.value === 'Advanced (JSONata)') { |
| 50 | + return new DataMappingServiceJsonata(); |
| 51 | + } |
| 52 | + // Add other mapping service types here |
| 53 | + throw new Error('Invalid mapping service type'); |
| 54 | +}); |
| 55 | +
|
| 56 | +const mappingServiceWarning: Ref<string> = computed(() => { |
| 57 | + const index = mappingServiceTypes.indexOf(selectedMappingServiceType.value); |
| 58 | + return mappingServiceWarnings[index] || ''; |
| 59 | +}); |
| 60 | +
|
| 61 | +onMounted(() => { |
| 62 | + // when a new result is generated: replace the editor content with it |
| 63 | + watch( |
| 64 | + () => result.value, |
| 65 | + newValue => { |
| 66 | + if (newValue.length > 0) { |
| 67 | + editor.value = ace.edit(editor_id); |
| 68 | + editor.value?.setValue(newValue, -1); |
| 69 | + } |
| 70 | + } |
| 71 | + ); |
| 72 | +}); |
| 73 | +
|
| 74 | +watch(showDialog, async visible => { |
| 75 | + // when the dialog turns visible, initialize the editor |
| 76 | + if (visible) { |
| 77 | + await nextTick(); // Wait until dialog content is rendered |
| 78 | + initializeEditor(); |
| 79 | +
|
| 80 | + if (result.value.length > 0) { |
| 81 | + editor.value?.setValue(result.value, -1); |
| 82 | + } |
| 83 | + } |
| 84 | +}); |
| 85 | +
|
| 86 | +function openDialog() { |
| 87 | + // when the dialog is opened, reset old values and load the current input data into the component, sanitize it |
| 88 | + resetDialog(); |
| 89 | + input.value = getDataForMode(SessionMode.DataEditor).data.value; |
| 90 | + input.value = mappingService.value.sanitizeInputDocument(input.value); |
| 91 | + showDialog.value = true; |
| 92 | +} |
| 93 | +
|
| 94 | +function hideDialog() { |
| 95 | + showDialog.value = false; |
| 96 | +} |
| 97 | +
|
| 98 | +function resetDialog() { |
| 99 | + statusMessage.value = ''; |
| 100 | + errorMessage.value = ''; |
| 101 | + userComments.value = ''; |
| 102 | + input.value = {}; |
| 103 | + result.value = ''; |
| 104 | + resultIsValid.value = false; |
| 105 | +} |
| 106 | +
|
| 107 | +function initializeEditor() { |
| 108 | + const container = document.getElementById(editor_id); |
| 109 | +
|
| 110 | + if (!container) { |
| 111 | + console.log('Unable to initialize editor because element is not found.'); |
| 112 | + return; |
| 113 | + } |
| 114 | +
|
| 115 | + // Destroy any existing editor if present |
| 116 | + if (editor.value) { |
| 117 | + editor.value.destroy(); |
| 118 | + editor.value.container.innerHTML = ''; // Clean up old editor DOM |
| 119 | + editor.value = null; |
| 120 | + editorInitialized.value = false; |
| 121 | + } |
| 122 | +
|
| 123 | + editor.value = ace.edit(editor_id); |
| 124 | + setupAceProperties(editor.value, settings.value); |
| 125 | +
|
| 126 | + editorInitialized.value = true; |
| 127 | +
|
| 128 | + editor.value.on( |
| 129 | + 'change', |
| 130 | + useDebounceFn(() => { |
| 131 | + const editorContent = editor.value?.getValue(); |
| 132 | + if (editorContent) { |
| 133 | + validateConfig(editorContent, input.value); |
| 134 | + } |
| 135 | + }, 100) |
| 136 | + ); |
| 137 | +} |
| 138 | +
|
| 139 | +function validateConfig(config: string, input: any) { |
| 140 | + const validationResult = mappingService.value.validateMappingConfig(config, input); |
| 141 | + if (!validationResult.success) { |
| 142 | + errorMessage.value = validationResult.message; |
| 143 | + statusMessage.value = ''; |
| 144 | + resultIsValid.value = false; |
| 145 | + } else { |
| 146 | + errorMessage.value = ''; |
| 147 | + resultIsValid.value = true; |
| 148 | + } |
| 149 | +} |
| 150 | +
|
| 151 | +function generateMappingSuggestion() { |
| 152 | + isLoadingMapping.value = true; |
| 153 | + const targetSchema = getDataForMode(SessionMode.SchemaEditor).data.value; |
| 154 | + mappingService.value |
| 155 | + .generateMappingSuggestion(input.value, targetSchema, userComments.value) |
| 156 | + .then(res => { |
| 157 | + result.value = res.config; |
| 158 | + if (res.success) { |
| 159 | + statusMessage.value = res.message; |
| 160 | + errorMessage.value = ''; |
| 161 | + } else { |
| 162 | + statusMessage.value = ''; |
| 163 | + errorMessage.value = res.message; |
| 164 | + } |
| 165 | + isLoadingMapping.value = false; |
| 166 | + validateConfig(res.config, input.value); |
| 167 | + }); |
| 168 | +} |
| 169 | +
|
| 170 | +function performMapping() { |
| 171 | + const config = editor.value?.getValue(); |
| 172 | + if (!config) { |
| 173 | + errorMessage.value = 'No mapping configuration available.'; |
| 174 | + statusMessage.value = ''; |
| 175 | + return; |
| 176 | + } |
| 177 | +
|
| 178 | + mappingService.value.performDataMapping(input.value, config).then(res => { |
| 179 | + if (res.success) { |
| 180 | + statusMessage.value = res.message; |
| 181 | + errorMessage.value = ''; |
| 182 | + // write the result data to the data editor |
| 183 | + getDataForMode(SessionMode.DataEditor).setData(res.resultData); |
| 184 | + hideDialog(); |
| 185 | + } else { |
| 186 | + statusMessage.value = ''; |
| 187 | + errorMessage.value = res.message; |
| 188 | + } |
| 189 | + }); |
| 190 | +} |
| 191 | +
|
| 192 | +defineExpose({show: openDialog, close: hideDialog}); |
| 193 | +</script> |
| 194 | + |
| 195 | +<template> |
| 196 | + <Dialog |
| 197 | + v-model:visible="showDialog" |
| 198 | + header="Convert Data to Target Schema" |
| 199 | + :modal="true" |
| 200 | + :style="{width: '50vw'}"> |
| 201 | + <div class="space-y-4"> |
| 202 | + <ApiKey /> |
| 203 | + |
| 204 | + <Message severity="warn" v-if="mappingServiceWarning.length"> |
| 205 | + <span v-html="mappingServiceWarning"></span> |
| 206 | + </Message> |
| 207 | + |
| 208 | + <p class="text-sm text-gray-700"> |
| 209 | + This tool converts the data from the <strong>Data Editor</strong> to match the schema |
| 210 | + defined in the <strong>Schema Editor</strong>. You can optionally provide extra instructions |
| 211 | + below to guide the mapping. |
| 212 | + </p> |
| 213 | + |
| 214 | + <div> |
| 215 | + <label for="userComments" class="block font-semibold mb-1">Additional Mapping Hints</label> |
| 216 | + <InputText |
| 217 | + id="userComments" |
| 218 | + v-model="userComments" |
| 219 | + class="w-full" |
| 220 | + placeholder="e.g., rename fields, format dates..." /> |
| 221 | + </div> |
| 222 | + |
| 223 | + <div class="flex items-center gap-2"> |
| 224 | + <label class="font-semibold">Mapping Method</label> |
| 225 | + <Select |
| 226 | + v-model="selectedMappingServiceType" |
| 227 | + :options="mappingServiceTypes" |
| 228 | + class="flex-1" /> |
| 229 | + </div> |
| 230 | + |
| 231 | + <Button |
| 232 | + label="Generate Suggestion" |
| 233 | + icon="pi pi-wand" |
| 234 | + @click="generateMappingSuggestion" |
| 235 | + class="w-full" /> |
| 236 | + <ProgressSpinner v-if="isLoadingMapping" /> |
| 237 | + |
| 238 | + <div class="mt-6"> |
| 239 | + <Divider /> |
| 240 | + <label :for="editor_id" class="block font-semibold mb-2">Mapping Configuration</label> |
| 241 | + <div class="border rounded h-72 overflow-hidden"> |
| 242 | + <div :id="editor_id" class="h-full w-full" /> |
| 243 | + </div> |
| 244 | + <Button |
| 245 | + v-if="resultIsValid" |
| 246 | + label="Perform Mapping" |
| 247 | + icon="pi pi-play" |
| 248 | + class="mt-4 w-full" |
| 249 | + @click="performMapping" /> |
| 250 | + </div> |
| 251 | + |
| 252 | + <Message severity="info" v-if="statusMessage.length">{{ statusMessage }}</Message> |
| 253 | + <Message severity="error" v-if="errorMessage.length"> |
| 254 | + <span v-html="errorMessage"></span> |
| 255 | + </Message> |
| 256 | + </div> |
| 257 | + </Dialog> |
| 258 | +</template> |
| 259 | + |
| 260 | +<style scoped> |
| 261 | +label { |
| 262 | + font-size: 0.9rem; |
| 263 | +} |
| 264 | +</style> |
0 commit comments