@bagelink/vue 1.2.99 → 1.2.103
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/dist/components/AddressSearch.vue.d.ts.map +1 -1
- package/dist/components/DragOver.vue.d.ts +27 -0
- package/dist/components/DragOver.vue.d.ts.map +1 -0
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/ImportData.vue.d.ts +20 -0
- package/dist/components/ImportData.vue.d.ts.map +1 -0
- package/dist/components/calendar/views/CalendarPopover.vue.d.ts +2 -2
- package/dist/components/calendar/views/CalendarPopover.vue.d.ts.map +1 -1
- package/dist/components/form/FieldArray.vue.d.ts +3 -1
- package/dist/components/form/FieldArray.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts +1 -1
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
- package/dist/composables/index.d.ts +2 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useExcel.d.ts +25 -0
- package/dist/composables/useExcel.d.ts.map +1 -0
- package/dist/index.cjs +2562 -675
- package/dist/index.mjs +2563 -676
- package/dist/plugins/modalTypes.d.ts +1 -3
- package/dist/plugins/modalTypes.d.ts.map +1 -1
- package/dist/style.css +222 -186
- package/dist/types/BagelForm.d.ts +5 -4
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/dist/types/timeago.d.ts +23 -0
- package/dist/types/timeago.d.ts.map +1 -0
- package/dist/utils/BagelFormUtils.d.ts +9 -4
- package/dist/utils/BagelFormUtils.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/DragOver.vue +112 -0
- package/src/components/ImportData.vue +1964 -0
- package/src/components/form/FieldArray.vue +4 -2
- package/src/components/form/inputs/RichText/utils/media.ts +2 -2
- package/src/components/form/inputs/Upload/useFileUpload.ts +1 -1
- package/src/components/index.ts +2 -0
- package/src/components/lightbox/Lightbox.vue +2 -14
- package/src/composables/index.ts +2 -1
- package/src/composables/useExcel.ts +220 -0
- package/src/plugins/modalTypes.ts +1 -1
- package/src/styles/buttons.css +79 -75
- package/src/types/BagelForm.ts +10 -10
- package/src/utils/BagelFormUtils.ts +18 -6
- package/src/utils/index.ts +21 -0
- package/dist/components/Carousel2.vue.d.ts +0 -89
- package/dist/components/Carousel2.vue.d.ts.map +0 -1
|
@@ -0,0 +1,1964 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T">
|
|
2
|
+
import type { BglFormSchemaT, Option } from '@bagelink/vue'
|
|
3
|
+
import type { Field } from '../types/BagelForm'
|
|
4
|
+
import {
|
|
5
|
+
Btn,
|
|
6
|
+
Card,
|
|
7
|
+
CheckInput,
|
|
8
|
+
Icon,
|
|
9
|
+
Modal,
|
|
10
|
+
Pill,
|
|
11
|
+
SelectInput,
|
|
12
|
+
Spreadsheet,
|
|
13
|
+
useFileUpload,
|
|
14
|
+
useExcel,
|
|
15
|
+
DragOver,
|
|
16
|
+
} from '@bagelink/vue'
|
|
17
|
+
import { computed, reactive, ref, watch, watchEffect } from 'vue'
|
|
18
|
+
|
|
19
|
+
import { useSchemaField } from '../composables/useSchemaField'
|
|
20
|
+
|
|
21
|
+
// Add interface for schema items
|
|
22
|
+
interface SchemaItem {
|
|
23
|
+
id: string
|
|
24
|
+
label: string
|
|
25
|
+
$el?: string
|
|
26
|
+
required?: boolean
|
|
27
|
+
isArrayField?: boolean
|
|
28
|
+
parentField?: string
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
disabledReason?: string
|
|
31
|
+
options?: Option[] // For enum options
|
|
32
|
+
dataType?: string // Add dataType field
|
|
33
|
+
attrs?: {
|
|
34
|
+
required?: boolean
|
|
35
|
+
schema?: SchemaItem[]
|
|
36
|
+
attrs?: {
|
|
37
|
+
required?: boolean
|
|
38
|
+
}
|
|
39
|
+
options?: Option[] // For enum options
|
|
40
|
+
dataType?: string // Add dataType in attrs
|
|
41
|
+
}
|
|
42
|
+
children?: SchemaItem[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add interface for mapped row data
|
|
46
|
+
interface MappedRow {
|
|
47
|
+
[key: string]: any
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Interface for transformations
|
|
51
|
+
interface Transformation {
|
|
52
|
+
fieldId: string
|
|
53
|
+
sourceValue: any
|
|
54
|
+
targetValue: any
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const props = defineProps<{
|
|
58
|
+
schema?: BglFormSchemaT<T>
|
|
59
|
+
title?: string
|
|
60
|
+
}>()
|
|
61
|
+
|
|
62
|
+
const emit = defineEmits<{
|
|
63
|
+
(e: 'processedData', data: T[]): void
|
|
64
|
+
}>()
|
|
65
|
+
|
|
66
|
+
// Get Excel utilities from composable
|
|
67
|
+
const {
|
|
68
|
+
readSheetData,
|
|
69
|
+
getSheetNames,
|
|
70
|
+
isExcelSerialDate,
|
|
71
|
+
excelSerialDateToJSDate,
|
|
72
|
+
formatDate
|
|
73
|
+
} = useExcel()
|
|
74
|
+
|
|
75
|
+
// Data type constants
|
|
76
|
+
const DATA_TYPES = {
|
|
77
|
+
STRING: 'string',
|
|
78
|
+
NUMBER: 'number',
|
|
79
|
+
DATE: 'date',
|
|
80
|
+
DATETIME: 'datetime',
|
|
81
|
+
BOOLEAN: 'boolean'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Data type options for selection
|
|
85
|
+
const dataTypeOptions = [
|
|
86
|
+
{ value: DATA_TYPES.STRING, label: 'Text (String)' },
|
|
87
|
+
{ value: DATA_TYPES.NUMBER, label: 'Number' },
|
|
88
|
+
{ value: DATA_TYPES.DATE, label: 'Date' },
|
|
89
|
+
{ value: DATA_TYPES.DATETIME, label: 'Date & Time' },
|
|
90
|
+
{ value: DATA_TYPES.BOOLEAN, label: 'Boolean' }
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
// Component state
|
|
94
|
+
const file = ref<File | null>(null)
|
|
95
|
+
const fileData = ref<any[]>([])
|
|
96
|
+
const sheetNames = ref<string[]>([])
|
|
97
|
+
const selectedSheet = ref<string>('')
|
|
98
|
+
const hasHeaders = ref(true)
|
|
99
|
+
const fieldMapping = reactive<Record<string, string>>({})
|
|
100
|
+
const isLoading = ref(false)
|
|
101
|
+
const showPreviewModal = ref(false)
|
|
102
|
+
const previewData = ref<any[]>([])
|
|
103
|
+
const mappingComplete = ref(false)
|
|
104
|
+
const fileHeaders = ref<string[]>([])
|
|
105
|
+
|
|
106
|
+
// New state for constant values and transformations
|
|
107
|
+
const defaultValues = reactive<Record<string, any>>({})
|
|
108
|
+
|
|
109
|
+
// State for transformations
|
|
110
|
+
const transformations = reactive<Record<string, Transformation[]>>({})
|
|
111
|
+
const showTransformDialog = ref(false)
|
|
112
|
+
const selectedTransformField = ref<SchemaItem | null>(null)
|
|
113
|
+
|
|
114
|
+
// State for data types
|
|
115
|
+
const fieldDataTypes = reactive<Record<string, string>>({})
|
|
116
|
+
|
|
117
|
+
// State for related data
|
|
118
|
+
const relatedFiles = reactive<Record<string, File | null>>({})
|
|
119
|
+
const relatedFileData = reactive<Record<string, any[]>>({})
|
|
120
|
+
const relatedFileMappings = reactive<Record<string, Record<string, string>>>({})
|
|
121
|
+
const showRelatedDialog = ref(false)
|
|
122
|
+
const selectedRelationField = ref<SchemaItem | null>(null)
|
|
123
|
+
const relatedKeyField = reactive<Record<string, string>>({})
|
|
124
|
+
const parentKeyField = reactive<Record<string, string>>({})
|
|
125
|
+
|
|
126
|
+
// Improve the transformations management
|
|
127
|
+
const selectedSourceValue = ref('')
|
|
128
|
+
const selectedTargetValue = ref('')
|
|
129
|
+
|
|
130
|
+
// State for related field data types
|
|
131
|
+
const relatedFieldDataTypes = reactive<Record<string, string>>({})
|
|
132
|
+
|
|
133
|
+
// State for related default values
|
|
134
|
+
const relatedDefaultValues = reactive<Record<string, Record<string, any>>>({})
|
|
135
|
+
|
|
136
|
+
// State for related transformations
|
|
137
|
+
const relatedTransformations = reactive<Record<string, Record<string, Transformation[]>>>({})
|
|
138
|
+
const showRelatedTransformDialog = ref(false)
|
|
139
|
+
const selectedRelatedTransformField = ref<{ parentId: string, field: SchemaItem } | null>(null)
|
|
140
|
+
const selectedRelatedSourceValue = ref('')
|
|
141
|
+
const selectedRelatedTargetValue = ref('')
|
|
142
|
+
|
|
143
|
+
// Set up schema field rendering
|
|
144
|
+
const formData = ref<any>({})
|
|
145
|
+
const { renderField } = useSchemaField<any, any>({
|
|
146
|
+
mode: 'form',
|
|
147
|
+
getFormData: () => formData.value,
|
|
148
|
+
onUpdateModelValue: (field: Field<any>, value: any) => {
|
|
149
|
+
if (!field.id) return
|
|
150
|
+
|
|
151
|
+
// Check if this is a related field (id contains a dot)
|
|
152
|
+
if (field.id.includes('.')) {
|
|
153
|
+
const [parentId, childId] = field.id.split('.')
|
|
154
|
+
if (!relatedDefaultValues[parentId]) {
|
|
155
|
+
relatedDefaultValues[parentId] = {}
|
|
156
|
+
}
|
|
157
|
+
relatedDefaultValues[parentId][childId] = value
|
|
158
|
+
} else {
|
|
159
|
+
// Regular field
|
|
160
|
+
defaultValues[field.id] = value
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Add function to get unique source values for a field
|
|
166
|
+
function getUniqueSourceValues(fieldId: string): any[] {
|
|
167
|
+
if (!fieldMapping[fieldId] || !fileData.value || fileData.value.length === 0) {
|
|
168
|
+
return []
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get all values from the mapped column
|
|
172
|
+
const allValues = fileData.value
|
|
173
|
+
.map(row => row[fieldMapping[fieldId]])
|
|
174
|
+
.filter(value => value !== undefined && value !== null && value !== '')
|
|
175
|
+
|
|
176
|
+
// Create a unique set of values
|
|
177
|
+
const uniqueValues = [...new Set(allValues)]
|
|
178
|
+
|
|
179
|
+
// Filter out values that already have transformations
|
|
180
|
+
return uniqueValues.filter((value) => {
|
|
181
|
+
if (!transformations[fieldId] || transformations[fieldId].length === 0) {
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Return false if this value already has a transformation
|
|
186
|
+
return !transformations[fieldId].some(t => t.sourceValue == value || t.sourceValue === value.toString()
|
|
187
|
+
)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Computed for available source values
|
|
192
|
+
const availableSourceValues = computed(() => {
|
|
193
|
+
if (!selectedTransformField.value || !selectedTransformField.value.id) {
|
|
194
|
+
return []
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return getUniqueSourceValues(selectedTransformField.value.id)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// Create options array from unique values
|
|
201
|
+
const sourceValueOptions = computed(() => {
|
|
202
|
+
return availableSourceValues.value.map(value => ({
|
|
203
|
+
value: String(value),
|
|
204
|
+
label: String(value)
|
|
205
|
+
}))
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Fix the findMatchingTargetValue function to handle Option types correctly
|
|
209
|
+
function findMatchingTargetValue(sourceValue: string, options: Option[]): string | null {
|
|
210
|
+
if (!sourceValue || !options || !Array.isArray(options) || options.length === 0) return null
|
|
211
|
+
|
|
212
|
+
// Convert sourceValue to string and lowercase for comparison
|
|
213
|
+
const lowerSourceValue = String(sourceValue).toLowerCase().trim()
|
|
214
|
+
|
|
215
|
+
// First try exact match
|
|
216
|
+
const exactMatch = options.find((option) => {
|
|
217
|
+
const optionObj = typeof option === 'object' && option !== null ? option : { value: String(option), label: String(option) }
|
|
218
|
+
// Make sure option and label exist and is a string
|
|
219
|
+
if (!optionObj || typeof optionObj.label !== 'string') return false
|
|
220
|
+
|
|
221
|
+
const optionLabel = `${optionObj.label}`.toLowerCase().trim()
|
|
222
|
+
return optionLabel === lowerSourceValue
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
if (exactMatch) {
|
|
226
|
+
const optionObj = typeof exactMatch === 'object' && exactMatch !== null
|
|
227
|
+
? exactMatch
|
|
228
|
+
: { value: String(exactMatch), label: String(exactMatch) }
|
|
229
|
+
return String(optionObj.value)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Try more flexible matching if exact match fails
|
|
233
|
+
const fuzzyMatch = options.find((option) => {
|
|
234
|
+
const optionObj = typeof option === 'object' && option !== null ? option : { value: String(option), label: String(option) }
|
|
235
|
+
if (!optionObj || typeof optionObj.label !== 'string') return false
|
|
236
|
+
|
|
237
|
+
const optionLabel = `${optionObj.label}`.toLowerCase().trim()
|
|
238
|
+
|
|
239
|
+
// Try contains match
|
|
240
|
+
return lowerSourceValue.includes(optionLabel) || optionLabel.includes(lowerSourceValue)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
if (fuzzyMatch) {
|
|
244
|
+
const optionObj = typeof fuzzyMatch === 'object' && fuzzyMatch !== null
|
|
245
|
+
? fuzzyMatch
|
|
246
|
+
: { value: String(fuzzyMatch), label: String(fuzzyMatch) }
|
|
247
|
+
return String(optionObj.value)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Watch for changes in selected source value and auto-match if enabled
|
|
254
|
+
watch(selectedSourceValue, (newValue) => {
|
|
255
|
+
if (selectedTransformField.value?.options) {
|
|
256
|
+
const matchedValue = findMatchingTargetValue(newValue, selectedTransformField.value.options)
|
|
257
|
+
if (matchedValue) {
|
|
258
|
+
selectedTargetValue.value = matchedValue
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Add a transformation
|
|
264
|
+
function addTransformation(fieldId: string) {
|
|
265
|
+
if (!transformations[fieldId]) {
|
|
266
|
+
transformations[fieldId] = []
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (selectedSourceValue.value && selectedTargetValue.value) {
|
|
270
|
+
// Check if this source value already has a transformation
|
|
271
|
+
const existingIndex = transformations[fieldId].findIndex(t => t.sourceValue === selectedSourceValue.value
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if (existingIndex >= 0) {
|
|
275
|
+
// Update existing transformation
|
|
276
|
+
transformations[fieldId][existingIndex].targetValue = selectedTargetValue.value
|
|
277
|
+
} else {
|
|
278
|
+
// Add new transformation
|
|
279
|
+
transformations[fieldId].push({
|
|
280
|
+
fieldId,
|
|
281
|
+
sourceValue: selectedSourceValue.value,
|
|
282
|
+
targetValue: selectedTargetValue.value
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Reset selection
|
|
287
|
+
selectedSourceValue.value = ''
|
|
288
|
+
selectedTargetValue.value = ''
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Create a revised function to extract fields from the schema structure without duplicates
|
|
293
|
+
function getAllFields(schema: any[]): any[] {
|
|
294
|
+
if (!schema || !Array.isArray(schema)) return []
|
|
295
|
+
|
|
296
|
+
const allFields: any[] = []
|
|
297
|
+
const seenIds = new Set() // Keep track of field IDs we've already added
|
|
298
|
+
|
|
299
|
+
// Helper to add a field if it hasn't been seen before
|
|
300
|
+
function addFieldIfNew(field: any) {
|
|
301
|
+
if (field && field.id && field.label && !seenIds.has(field.id)) {
|
|
302
|
+
seenIds.add(field.id)
|
|
303
|
+
|
|
304
|
+
// Always create options array if it doesn't exist
|
|
305
|
+
if (!field.options) {
|
|
306
|
+
field.options = []
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Extract options if they exist in attrs
|
|
310
|
+
if (field.attrs && field.attrs.options && field.attrs.options.length > 0) {
|
|
311
|
+
// Add options from attrs to the field's options array
|
|
312
|
+
field.options = field.attrs.options
|
|
313
|
+
console.log(`Added options for field ${field.id}:`, field.options)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
allFields.push(field)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Process each schema item
|
|
321
|
+
schema.forEach((item: any) => {
|
|
322
|
+
// Direct fields (like more_info.session_rate)
|
|
323
|
+
if (item && item.id && item.label) {
|
|
324
|
+
addFieldIfNew(item)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Containers with children (like flex divs)
|
|
328
|
+
if (item && item.children && Array.isArray(item.children)) {
|
|
329
|
+
// Process each child
|
|
330
|
+
item.children.forEach((child: any) => {
|
|
331
|
+
// Regular fields
|
|
332
|
+
if (child && child.id && child.label) {
|
|
333
|
+
// Check if it's an array field
|
|
334
|
+
if (child.$el === 'array' && child.attrs && child.attrs.schema) {
|
|
335
|
+
// Add the array field itself (phones, emails)
|
|
336
|
+
addFieldIfNew(child)
|
|
337
|
+
|
|
338
|
+
// Add the child fields of the array with qualified names
|
|
339
|
+
if (Array.isArray(child.attrs.schema)) {
|
|
340
|
+
child.attrs.schema.forEach((schemaItem: SchemaItem) => {
|
|
341
|
+
if (schemaItem && schemaItem.id && schemaItem.label) {
|
|
342
|
+
// Create a qualified field name for the array item
|
|
343
|
+
const qualifiedField = {
|
|
344
|
+
...schemaItem,
|
|
345
|
+
id: `${child.id}.${schemaItem.id}`,
|
|
346
|
+
parentField: child.id,
|
|
347
|
+
isArrayField: true
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Make sure to copy options if they exist
|
|
351
|
+
if (schemaItem.options) {
|
|
352
|
+
qualifiedField.options = schemaItem.options
|
|
353
|
+
} else if (schemaItem.attrs && schemaItem.attrs.options) {
|
|
354
|
+
qualifiedField.options = schemaItem.attrs.options
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
addFieldIfNew(qualifiedField)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// Regular non-array field
|
|
363
|
+
addFieldIfNew(child)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
return allFields
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Get extracted fields from schema
|
|
374
|
+
const schemaFields = computed(() => {
|
|
375
|
+
return getAllFields(props.schema || [])
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
// Update the isFieldRequired function to handle the special case for array fields
|
|
379
|
+
function isFieldRequired(field: any): boolean {
|
|
380
|
+
if (field.isArrayField && field.parentField) {
|
|
381
|
+
return false
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// For regular fields, check the standard required attributes
|
|
385
|
+
return (field.attrs && field.attrs.required === true)
|
|
386
|
+
|| (field.required === true)
|
|
387
|
+
|| (field.attrs && field.attrs.attrs && field.attrs.attrs.required === true)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Add a function to get field display information including conditional requirements
|
|
391
|
+
function getFieldDescription(field: any): { description: string, isConditional: boolean } {
|
|
392
|
+
// For array child fields, show they're conditionally required
|
|
393
|
+
if (field.isArrayField && field.parentField) {
|
|
394
|
+
const parentLabel = schemaFields.value.find(f => f.id === field.parentField)?.label || field.parentField
|
|
395
|
+
return {
|
|
396
|
+
description: `Required only if ${parentLabel} has items`,
|
|
397
|
+
isConditional: true
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// No special handling needed
|
|
402
|
+
return {
|
|
403
|
+
description: '',
|
|
404
|
+
isConditional: false
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Replace parseFile function with simplified version
|
|
409
|
+
async function parseFile(file: File) {
|
|
410
|
+
isLoading.value = true
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
// Get sheet names using the composable
|
|
414
|
+
sheetNames.value = await getSheetNames(file)
|
|
415
|
+
selectedSheet.value = sheetNames.value[0]
|
|
416
|
+
|
|
417
|
+
await loadSheetData()
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.error('Error parsing file:', error)
|
|
420
|
+
} finally {
|
|
421
|
+
isLoading.value = false
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Replace loadSheetData function with simplified version
|
|
426
|
+
async function loadSheetData() {
|
|
427
|
+
if (!file.value || !selectedSheet.value) return
|
|
428
|
+
|
|
429
|
+
isLoading.value = true
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
// Use the readSheetData utility from our composable
|
|
433
|
+
const { headers, data } = await readSheetData(file.value, selectedSheet.value, hasHeaders.value)
|
|
434
|
+
|
|
435
|
+
fileHeaders.value = headers
|
|
436
|
+
fileData.value = data
|
|
437
|
+
resetMapping()
|
|
438
|
+
setTimeout(() => { guessDataTypes() }, 100)
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error('Error loading sheet data:', error)
|
|
441
|
+
} finally {
|
|
442
|
+
isLoading.value = false
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Add a function to check if a field is related to an array parent that's already mapped
|
|
447
|
+
function checkArrayFieldConflicts() {
|
|
448
|
+
// Get all array parent fields that are currently mapped
|
|
449
|
+
const mappedArrayParents = new Set()
|
|
450
|
+
const mappedArrayChildren = new Map() // Map from parent ID to child IDs
|
|
451
|
+
|
|
452
|
+
// Identify which array parents and children are mapped
|
|
453
|
+
Object.keys(fieldMapping).forEach((fieldId: string) => {
|
|
454
|
+
const field = schemaFields.value.find(f => f.id === fieldId)
|
|
455
|
+
if (field) {
|
|
456
|
+
if (field.$el === 'array') {
|
|
457
|
+
// This is an array parent
|
|
458
|
+
mappedArrayParents.add(field.id)
|
|
459
|
+
} else if (field.isArrayField && field.parentField) {
|
|
460
|
+
// This is an array child
|
|
461
|
+
if (!mappedArrayChildren.has(field.parentField)) {
|
|
462
|
+
mappedArrayChildren.set(field.parentField, new Set())
|
|
463
|
+
}
|
|
464
|
+
mappedArrayChildren.get(field.parentField).add(field.id)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// For each array parent that's mapped, disable its children
|
|
470
|
+
for (const parentId of mappedArrayParents) {
|
|
471
|
+
const childFields = schemaFields.value.filter(f => f.parentField === parentId)
|
|
472
|
+
childFields.forEach((child: any) => {
|
|
473
|
+
child.disabled = true
|
|
474
|
+
child.disabledReason = `Parent field "${parentId}" is already mapped`
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// For each array child that's mapped, disable its parent
|
|
479
|
+
for (const [parentId, childIds] of mappedArrayChildren.entries()) {
|
|
480
|
+
if (childIds.size > 0) {
|
|
481
|
+
const parentField = schemaFields.value.find(f => f.id === parentId)
|
|
482
|
+
if (parentField) {
|
|
483
|
+
parentField.disabled = true
|
|
484
|
+
parentField.disabledReason = `Child field(s) already mapped`
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Modify the resetMapping function to call checkArrayFieldConflicts
|
|
491
|
+
function resetMapping() {
|
|
492
|
+
// Reset field mapping
|
|
493
|
+
Object.keys(fieldMapping).forEach((key: string) => {
|
|
494
|
+
delete fieldMapping[key]
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
// Reset disabled state on all fields
|
|
498
|
+
schemaFields.value.forEach((field: any) => {
|
|
499
|
+
field.disabled = false
|
|
500
|
+
field.disabledReason = ''
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
// Try to auto-map fields based on similar names
|
|
504
|
+
if (fileHeaders.value.length > 0) {
|
|
505
|
+
schemaFields.value.forEach((field: any) => {
|
|
506
|
+
// Look for exact match
|
|
507
|
+
const exactMatch = fileHeaders.value.find(
|
|
508
|
+
header => header.toLowerCase() === field.id.toLowerCase()
|
|
509
|
+
|| header.toLowerCase() === field.label.toLowerCase()
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
if (exactMatch && !field.disabled) {
|
|
513
|
+
fieldMapping[field.id] = exactMatch
|
|
514
|
+
} else {
|
|
515
|
+
// Look for partial match
|
|
516
|
+
const partialMatch = fileHeaders.value.find(
|
|
517
|
+
header => header.toLowerCase().includes(field.id.toLowerCase())
|
|
518
|
+
|| header.toLowerCase().includes(field.label.toLowerCase())
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if (partialMatch && !field.disabled) {
|
|
522
|
+
fieldMapping[field.id] = partialMatch
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Check for array field conflicts
|
|
529
|
+
checkArrayFieldConflicts()
|
|
530
|
+
checkMappingComplete()
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Simplify the checkMappingComplete function to match canProcessData
|
|
534
|
+
function checkMappingComplete() {
|
|
535
|
+
// Use the same logic as canProcessData for consistency
|
|
536
|
+
if (!file.value || (Object.keys(fieldMapping).length === 0 && Object.keys(defaultValues).length === 0)) {
|
|
537
|
+
mappingComplete.value = false
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Get strictly required fields (non-conditional)
|
|
542
|
+
const requiredFields = schemaFields.value.filter((field) => {
|
|
543
|
+
// Exclude array children which are conditionally required
|
|
544
|
+
if (field.isArrayField && field.parentField) return false
|
|
545
|
+
|
|
546
|
+
// Check various ways a field might be marked as required
|
|
547
|
+
return (field.attrs && field.attrs.required === true)
|
|
548
|
+
|| (field.required === true)
|
|
549
|
+
|| (field.attrs && field.attrs.attrs && field.attrs.attrs.required === true)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
// If no strictly required fields, we just need at least one mapping or default value
|
|
553
|
+
if (requiredFields.length === 0) {
|
|
554
|
+
mappingComplete.value = Object.keys(fieldMapping).some(key => !!fieldMapping[key])
|
|
555
|
+
|| Object.keys(defaultValues).length > 0
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Check that all required fields are mapped or have default values
|
|
560
|
+
mappingComplete.value = requiredFields.every(field => !!fieldMapping[field.id] || hasDefaultValue(field.id))
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function showPreview() {
|
|
564
|
+
guessDataTypes()
|
|
565
|
+
const mappedData: MappedRow[] = []
|
|
566
|
+
|
|
567
|
+
for (let i = 0; i < fileData.value.length; i++) {
|
|
568
|
+
const sourceRow = fileData.value[i]
|
|
569
|
+
const mappedRow: MappedRow = {}
|
|
570
|
+
|
|
571
|
+
schemaFields.value.forEach((field) => {
|
|
572
|
+
// Skip array fields as they're handled separately
|
|
573
|
+
if (field.isArrayField || field.$el === 'array') return
|
|
574
|
+
|
|
575
|
+
// Get value from mapping or use default
|
|
576
|
+
let value: any = null
|
|
577
|
+
let useDefault = false
|
|
578
|
+
|
|
579
|
+
if (fieldMapping[field.id] && sourceRow[fieldMapping[field.id]] !== undefined) {
|
|
580
|
+
value = sourceRow[fieldMapping[field.id]]
|
|
581
|
+
// Use default value as fallback if the mapped value is empty
|
|
582
|
+
if (value === '' && defaultValues[field.id] !== undefined) {
|
|
583
|
+
value = defaultValues[field.id]
|
|
584
|
+
useDefault = true
|
|
585
|
+
}
|
|
586
|
+
} else if (defaultValues[field.id] !== undefined) {
|
|
587
|
+
// Use default value if no direct mapping or mapping is empty
|
|
588
|
+
value = defaultValues[field.id]
|
|
589
|
+
useDefault = true
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Skip if no value
|
|
593
|
+
if (value === null) {
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Apply transformations if any exist and not using default value
|
|
598
|
+
if (!useDefault && transformations[field.id] && transformations[field.id].length > 0) {
|
|
599
|
+
// Find matching transformation
|
|
600
|
+
const transform = transformations[field.id].find(t => t.sourceValue == value || t.sourceValue === String(value)
|
|
601
|
+
)
|
|
602
|
+
if (transform) {
|
|
603
|
+
value = transform.targetValue
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Apply data type conversion
|
|
608
|
+
const dataType = fieldDataTypes[field.id] || DATA_TYPES.STRING
|
|
609
|
+
value = convertValueByType(value, dataType)
|
|
610
|
+
|
|
611
|
+
if (field.id.includes('.')) {
|
|
612
|
+
// Handle dot notation for nested objects
|
|
613
|
+
const parts = field.id.split('.')
|
|
614
|
+
const rootField = parts[0]
|
|
615
|
+
|
|
616
|
+
// Create nested structure
|
|
617
|
+
if (!mappedRow[rootField]) {
|
|
618
|
+
mappedRow[rootField] = {}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Handle multi-level nesting
|
|
622
|
+
let current = mappedRow[rootField]
|
|
623
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
624
|
+
if (!current[parts[i]]) {
|
|
625
|
+
current[parts[i]] = {}
|
|
626
|
+
}
|
|
627
|
+
current = current[parts[i]]
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Set the value at the final level
|
|
631
|
+
current[parts[parts.length - 1]] = value
|
|
632
|
+
} else {
|
|
633
|
+
mappedRow[field.id] = value
|
|
634
|
+
}
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
// Process array child fields
|
|
638
|
+
const arrayChildFields = schemaFields.value.filter(field => field.isArrayField && field.parentField)
|
|
639
|
+
|
|
640
|
+
// Process each array field
|
|
641
|
+
arrayChildFields.forEach((childField) => {
|
|
642
|
+
const [parentId, childId] = childField.id.split('.')
|
|
643
|
+
if (!parentId || !childId) return
|
|
644
|
+
|
|
645
|
+
// Get value from mapping or use default
|
|
646
|
+
let value: any = null
|
|
647
|
+
let useDefault = false
|
|
648
|
+
|
|
649
|
+
if (fieldMapping[childField.id] && sourceRow[fieldMapping[childField.id]] !== undefined) {
|
|
650
|
+
value = sourceRow[fieldMapping[childField.id]]
|
|
651
|
+
// Use default value as fallback if the mapped value is empty
|
|
652
|
+
if (value === '' && defaultValues[childField.id] !== undefined) {
|
|
653
|
+
value = defaultValues[childField.id]
|
|
654
|
+
useDefault = true
|
|
655
|
+
}
|
|
656
|
+
} else if (defaultValues[childField.id] !== undefined) {
|
|
657
|
+
// Use default value if no direct mapping
|
|
658
|
+
value = defaultValues[childField.id]
|
|
659
|
+
useDefault = true
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Skip if no value
|
|
663
|
+
if (value === null) {
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Create the array structure if it doesn't exist
|
|
668
|
+
if (!mappedRow[parentId]) {
|
|
669
|
+
mappedRow[parentId] = []
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Apply transformations if any exist and not using default
|
|
673
|
+
if (!useDefault && transformations[childField.id] && transformations[childField.id].length > 0) {
|
|
674
|
+
// Find matching transformation
|
|
675
|
+
const transform = transformations[childField.id].find(t => t.sourceValue == value || t.sourceValue === String(value)
|
|
676
|
+
)
|
|
677
|
+
if (transform) {
|
|
678
|
+
value = transform.targetValue
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Apply data type conversion for array fields
|
|
683
|
+
const dataType = fieldDataTypes[childField.id] || DATA_TYPES.STRING
|
|
684
|
+
value = convertValueByType(value, dataType)
|
|
685
|
+
|
|
686
|
+
// Add the array item with the proper structure
|
|
687
|
+
mappedRow[parentId].push({
|
|
688
|
+
[childId]: value
|
|
689
|
+
})
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
// Handle related file data
|
|
693
|
+
Object.keys(relatedFiles).forEach((fieldId) => {
|
|
694
|
+
if (!relatedFiles[fieldId] || !relatedKeyField[fieldId] || !parentKeyField[fieldId]) {
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Get parent key value from this row
|
|
699
|
+
const parentKeyValue = sourceRow[parentKeyField[fieldId]]
|
|
700
|
+
if (!parentKeyValue) return
|
|
701
|
+
|
|
702
|
+
// Create the array structure if it doesn't exist
|
|
703
|
+
if (!mappedRow[fieldId]) {
|
|
704
|
+
mappedRow[fieldId] = []
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Find matching rows in related data
|
|
708
|
+
const matchingRows = relatedFileData[fieldId].filter(relatedRow => relatedRow[relatedKeyField[fieldId]] == parentKeyValue
|
|
709
|
+
|| relatedRow[relatedKeyField[fieldId]] === parentKeyValue.toString()
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
// Add each matching row as an item in the array
|
|
713
|
+
matchingRows.forEach((matchingRow) => {
|
|
714
|
+
const mappedItem: Record<string, any> = {}
|
|
715
|
+
|
|
716
|
+
// Apply mappings from related file
|
|
717
|
+
if (selectedRelationField.value?.attrs?.schema) {
|
|
718
|
+
selectedRelationField.value.attrs.schema.forEach((schemaItem: SchemaItem) => {
|
|
719
|
+
if (!schemaItem.id) return
|
|
720
|
+
|
|
721
|
+
// Get value from related mapping or use default
|
|
722
|
+
let value: any = null
|
|
723
|
+
let useDefault = false
|
|
724
|
+
|
|
725
|
+
if (relatedFileMappings[fieldId][schemaItem.id]
|
|
726
|
+
&& matchingRow[relatedFileMappings[fieldId][schemaItem.id]] !== undefined) {
|
|
727
|
+
value = matchingRow[relatedFileMappings[fieldId][schemaItem.id]]
|
|
728
|
+
// Use default as fallback for empty values
|
|
729
|
+
if (value === '' && relatedDefaultValues[fieldId][schemaItem.id] !== undefined) {
|
|
730
|
+
value = relatedDefaultValues[fieldId][schemaItem.id]
|
|
731
|
+
useDefault = true
|
|
732
|
+
}
|
|
733
|
+
} else if (relatedDefaultValues[fieldId][schemaItem.id] !== undefined) {
|
|
734
|
+
// Use default value if no mapping
|
|
735
|
+
value = relatedDefaultValues[fieldId][schemaItem.id]
|
|
736
|
+
useDefault = true
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Skip if no value
|
|
740
|
+
if (value === null) return
|
|
741
|
+
|
|
742
|
+
// Apply transformations if any exist and not using default
|
|
743
|
+
if (!useDefault
|
|
744
|
+
&& relatedTransformations[fieldId][schemaItem.id]
|
|
745
|
+
&& relatedTransformations[fieldId][schemaItem.id].length > 0) {
|
|
746
|
+
// Find matching transformation
|
|
747
|
+
const transform = relatedTransformations[fieldId][schemaItem.id].find(t => t.sourceValue == value || t.sourceValue === String(value)
|
|
748
|
+
)
|
|
749
|
+
if (transform) {
|
|
750
|
+
value = transform.targetValue
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Apply data type conversion
|
|
755
|
+
const fullChildId = `${fieldId}.${schemaItem.id}`
|
|
756
|
+
const dataType = relatedFieldDataTypes[fullChildId] || DATA_TYPES.STRING
|
|
757
|
+
value = convertValueByType(value, dataType)
|
|
758
|
+
|
|
759
|
+
// Add to mapped item
|
|
760
|
+
mappedItem[schemaItem.id] = value
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Only add item if it has at least one value
|
|
765
|
+
if (Object.keys(mappedItem).length > 0) {
|
|
766
|
+
mappedRow[fieldId].push(mappedItem)
|
|
767
|
+
}
|
|
768
|
+
})
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
// Check if this row has any data (not empty)
|
|
772
|
+
const hasData = Object.values(mappedRow).some((value) => {
|
|
773
|
+
if (value === null || value === undefined) return false
|
|
774
|
+
if (value === '') return false
|
|
775
|
+
if (Array.isArray(value) && value.length === 0) return false
|
|
776
|
+
return true
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
// Only add rows that have data
|
|
780
|
+
if (hasData) {
|
|
781
|
+
mappedData.push(mappedRow)
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Update the preview data with all non-empty rows
|
|
786
|
+
previewData.value = mappedData
|
|
787
|
+
|
|
788
|
+
// Show the modal
|
|
789
|
+
showPreviewModal.value = true
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Create a function to convert our schema fields to Spreadsheet column config
|
|
793
|
+
function createSpreadsheetColumns() {
|
|
794
|
+
return schemaFields.value
|
|
795
|
+
.filter((field) => {
|
|
796
|
+
return field.$el !== 'array'
|
|
797
|
+
})
|
|
798
|
+
.map((field) => {
|
|
799
|
+
// Create a column config for each field
|
|
800
|
+
return {
|
|
801
|
+
key: field.id,
|
|
802
|
+
title: field.label,
|
|
803
|
+
// Special formatting for array child fields
|
|
804
|
+
formatter: field.isArrayField ? formatArrayChildValue : undefined
|
|
805
|
+
}
|
|
806
|
+
})
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Helper function to format array child values for display
|
|
810
|
+
function formatArrayChildValue(value: any, row: any, fieldId: string): string {
|
|
811
|
+
// For array child fields, we need to check if it's in the parent array
|
|
812
|
+
const field = schemaFields.value.find(f => f.id === fieldId)
|
|
813
|
+
if (!field || !field.isArrayField || !field.parentField) return value
|
|
814
|
+
|
|
815
|
+
const [parentId, childId] = fieldId.split('.')
|
|
816
|
+
const parentArray = row[parentId]
|
|
817
|
+
|
|
818
|
+
if (Array.isArray(parentArray) && parentArray.length > 0) {
|
|
819
|
+
// Extract values from the parent array
|
|
820
|
+
return parentArray
|
|
821
|
+
.map(item => item[childId])
|
|
822
|
+
.filter(val => val !== undefined)
|
|
823
|
+
.join(', ')
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return ''
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Add computed for spreadsheet columns
|
|
830
|
+
const spreadsheetColumns = computed(() => createSpreadsheetColumns())
|
|
831
|
+
|
|
832
|
+
function processData() {
|
|
833
|
+
emit('processedData', previewData.value)
|
|
834
|
+
showPreviewModal.value = false
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function updateFieldMapping(fieldId: string, value: string) {
|
|
838
|
+
const previousValue = fieldMapping[fieldId]
|
|
839
|
+
if (previousValue && previousValue !== value) {
|
|
840
|
+
fieldMapping[fieldId] = ''
|
|
841
|
+
schemaFields.value.forEach((field) => {
|
|
842
|
+
field.disabled = false
|
|
843
|
+
field.disabledReason = ''
|
|
844
|
+
})
|
|
845
|
+
}
|
|
846
|
+
if (value) {
|
|
847
|
+
fieldMapping[fieldId] = value
|
|
848
|
+
const field = schemaFields.value.find(f => f.id === fieldId)
|
|
849
|
+
if (field) {
|
|
850
|
+
if (field.$el === 'array') {
|
|
851
|
+
const childFields = schemaFields.value.filter(f => f.parentField === field.id)
|
|
852
|
+
childFields.forEach((child) => {
|
|
853
|
+
child.disabled = true
|
|
854
|
+
child.disabledReason = `Parent field "${field.id}" is already mapped`
|
|
855
|
+
})
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (field.isArrayField && field.parentField) {
|
|
859
|
+
const parentField = schemaFields.value.find(f => f.id === field.parentField)
|
|
860
|
+
if (parentField) {
|
|
861
|
+
parentField.disabled = true
|
|
862
|
+
parentField.disabledReason = `Child field already mapped`
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
checkArrayFieldConflicts()
|
|
868
|
+
checkMappingComplete()
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function handleSelectChange(event: Event, fieldId: string) {
|
|
872
|
+
const target = event.target as HTMLSelectElement
|
|
873
|
+
if (target) {
|
|
874
|
+
updateFieldMapping(fieldId, target.value)
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const { addFile, browse, fileQueue } = useFileUpload()
|
|
879
|
+
|
|
880
|
+
async function handleFilesUploaded() {
|
|
881
|
+
console.log('fileQueue', fileQueue.value)
|
|
882
|
+
file.value = fileQueue.value[0].file
|
|
883
|
+
if (!file.value) return
|
|
884
|
+
isLoading.value = true
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
await parseFile(file.value)
|
|
888
|
+
} catch (error) {
|
|
889
|
+
console.error('Error parsing file:', error)
|
|
890
|
+
} finally {
|
|
891
|
+
isLoading.value = false
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
watch(fileQueue.value, handleFilesUploaded)
|
|
896
|
+
|
|
897
|
+
// Watch for changes in the selected sheet
|
|
898
|
+
watchEffect(() => {
|
|
899
|
+
if (selectedSheet.value) {
|
|
900
|
+
loadSheetData()
|
|
901
|
+
}
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
// Function to check if field has default value
|
|
905
|
+
function hasDefaultValue(fieldId: string): boolean {
|
|
906
|
+
return defaultValues[fieldId] !== undefined
|
|
907
|
+
&& defaultValues[fieldId] !== null
|
|
908
|
+
&& defaultValues[fieldId] !== ''
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Function to open transformation dialog
|
|
912
|
+
function openTransformDialog(field: SchemaItem) {
|
|
913
|
+
try {
|
|
914
|
+
console.log('Opening transform dialog for field:', field.id, field)
|
|
915
|
+
|
|
916
|
+
// Make sure to set options property from attrs if needed
|
|
917
|
+
if (!field.options) {
|
|
918
|
+
field.options = []
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (field.attrs && field.attrs.options) {
|
|
922
|
+
console.log('Copying options from attrs for field:', field.id)
|
|
923
|
+
field.options = Array.isArray(field.attrs.options)
|
|
924
|
+
? field.attrs.options
|
|
925
|
+
: []
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
console.log('Field options after processing:', field.options)
|
|
929
|
+
|
|
930
|
+
selectedTransformField.value = field
|
|
931
|
+
if (!transformations[field.id]) {
|
|
932
|
+
transformations[field.id] = []
|
|
933
|
+
}
|
|
934
|
+
showTransformDialog.value = true
|
|
935
|
+
} catch (error) {
|
|
936
|
+
console.error('Error opening transform dialog:', error)
|
|
937
|
+
alert('An error occurred while opening the transform dialog. See console for details.')
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Function to remove a transformation
|
|
942
|
+
function removeTransformation(fieldId: string, index: number) {
|
|
943
|
+
if (transformations[fieldId] && transformations[fieldId].length > index) {
|
|
944
|
+
transformations[fieldId].splice(index, 1)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Function to open related file dialog
|
|
949
|
+
function openRelatedDialog(field: SchemaItem) {
|
|
950
|
+
selectedRelationField.value = field
|
|
951
|
+
showRelatedDialog.value = true
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Function to process related file
|
|
955
|
+
async function processRelatedFile(fieldId: string, file: File) {
|
|
956
|
+
if (!file) return
|
|
957
|
+
|
|
958
|
+
relatedFiles[fieldId] = file
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
// Use the readSheetData utility from our composable
|
|
962
|
+
const { data } = await readSheetData(file, '', true)
|
|
963
|
+
|
|
964
|
+
// Store the related file data
|
|
965
|
+
relatedFileData[fieldId] = data
|
|
966
|
+
|
|
967
|
+
// Initialize mapping if not exists
|
|
968
|
+
if (!relatedFileMappings[fieldId]) {
|
|
969
|
+
relatedFileMappings[fieldId] = {}
|
|
970
|
+
}
|
|
971
|
+
} catch (error) {
|
|
972
|
+
console.error('Error processing related file:', error)
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Add function to auto-populate transformations for a field
|
|
977
|
+
function autoPopulateTransformations(fieldId: string) {
|
|
978
|
+
try {
|
|
979
|
+
const field = schemaFields.value.find(f => f.id === fieldId)
|
|
980
|
+
if (!field) {
|
|
981
|
+
console.error('Field not found:', fieldId)
|
|
982
|
+
return
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Ensure options are available either directly or from attrs
|
|
986
|
+
let fieldOptions = field.options || (field.attrs && field.attrs.options)
|
|
987
|
+
|
|
988
|
+
if (!fieldMapping[fieldId] || !fileData.value || fileData.value.length === 0) {
|
|
989
|
+
console.warn('No data or mapping found for field:', fieldId)
|
|
990
|
+
return
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (!fieldOptions) {
|
|
994
|
+
console.warn('No options found for field:', fieldId)
|
|
995
|
+
return
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Ensure options is an array
|
|
999
|
+
if (!Array.isArray(fieldOptions)) {
|
|
1000
|
+
console.warn('Options is not an array for field:', fieldId)
|
|
1001
|
+
fieldOptions = []
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const uniqueValues = getUniqueSourceValues(fieldId)
|
|
1005
|
+
|
|
1006
|
+
// Initialize transformations array if needed
|
|
1007
|
+
if (!transformations[fieldId]) {
|
|
1008
|
+
transformations[fieldId] = []
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
let matchCount = 0
|
|
1012
|
+
const unmatchedValues: string[] = []
|
|
1013
|
+
|
|
1014
|
+
// For each unique value, try to find a matching target
|
|
1015
|
+
uniqueValues.forEach((sourceValue) => {
|
|
1016
|
+
const strSourceValue = String(sourceValue)
|
|
1017
|
+
|
|
1018
|
+
const matchedValue = findMatchingTargetValue(strSourceValue, fieldOptions)
|
|
1019
|
+
|
|
1020
|
+
if (matchedValue) {
|
|
1021
|
+
// Check if this source value already has a transformation
|
|
1022
|
+
const existingIndex = transformations[fieldId].findIndex(t => t.sourceValue === strSourceValue
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
if (existingIndex >= 0) {
|
|
1026
|
+
// Update existing transformation
|
|
1027
|
+
transformations[fieldId][existingIndex].targetValue = matchedValue
|
|
1028
|
+
} else {
|
|
1029
|
+
// Add new transformation
|
|
1030
|
+
transformations[fieldId].push({
|
|
1031
|
+
fieldId,
|
|
1032
|
+
sourceValue: strSourceValue,
|
|
1033
|
+
targetValue: matchedValue
|
|
1034
|
+
})
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
matchCount++
|
|
1038
|
+
} else {
|
|
1039
|
+
unmatchedValues.push(strSourceValue)
|
|
1040
|
+
}
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
// Provide some feedback about how many matches were found
|
|
1044
|
+
if (matchCount === 0) {
|
|
1045
|
+
alert(`No automatic matches found. Try creating transformations manually.`)
|
|
1046
|
+
} else {
|
|
1047
|
+
alert(`Automatically created ${matchCount} transformations by matching source values to target labels.\n\n${unmatchedValues.length} values could not be automatically matched.`)
|
|
1048
|
+
}
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
console.error('Error auto-populating transformations:', error)
|
|
1051
|
+
alert('An error occurred while trying to auto-populate transformations. See console for details.')
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Add a debug function to log field options
|
|
1056
|
+
function debugFieldOptions() {
|
|
1057
|
+
console.log('Checking all fields for options:')
|
|
1058
|
+
schemaFields.value.forEach((field) => {
|
|
1059
|
+
console.log(`Field ${field.id} (${field.label}):`, {
|
|
1060
|
+
directOptions: field.options,
|
|
1061
|
+
hasDirectOptions: field.options && field.options.length > 0,
|
|
1062
|
+
attrOptions: field.attrs?.options,
|
|
1063
|
+
hasAttrOptions: field.attrs && field.attrs.options && field.attrs.options.length > 0,
|
|
1064
|
+
visibleButton: (field.options && field.options.length > 0) || (field.attrs && field.attrs.options && field.attrs.options.length > 0)
|
|
1065
|
+
})
|
|
1066
|
+
})
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Run debug function on schema load
|
|
1070
|
+
watchEffect(() => {
|
|
1071
|
+
if (props.schema && props.schema.length > 0) {
|
|
1072
|
+
console.log('Schema loaded, checking options')
|
|
1073
|
+
debugFieldOptions()
|
|
1074
|
+
}
|
|
1075
|
+
})
|
|
1076
|
+
|
|
1077
|
+
// Function to detect date format from string
|
|
1078
|
+
function detectDateFormat(value: string): RegExp | null {
|
|
1079
|
+
// Common date formats
|
|
1080
|
+
const formats = [
|
|
1081
|
+
/^\d{4}-\d{2}-\d{2}$/, // YYYY-MM-DD
|
|
1082
|
+
/^\d{2}\/\d{2}\/\d{4}$/, // MM/DD/YYYY
|
|
1083
|
+
/^\d{2}\.\d{2}\.\d{4}$/, // DD.MM.YYYY
|
|
1084
|
+
/^\d{1,2}\s[a-z]{3}\s\d{4}$/i, // D MMM YYYY
|
|
1085
|
+
/^\d{1,2}\s[a-z]{3,9}\s\d{4}$/i, // D MMMM YYYY
|
|
1086
|
+
]
|
|
1087
|
+
|
|
1088
|
+
for (const format of formats) {
|
|
1089
|
+
if (format.test(value)) {
|
|
1090
|
+
return format
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return null
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Function to parse date string based on detected format
|
|
1098
|
+
function parseDate(value: string): Date | null {
|
|
1099
|
+
if (!value) return null
|
|
1100
|
+
|
|
1101
|
+
// Try parsing ISO format first
|
|
1102
|
+
const isoDate = new Date(value)
|
|
1103
|
+
if (!Number.isNaN(isoDate.getTime())) {
|
|
1104
|
+
return isoDate
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Try parsing other formats
|
|
1108
|
+
const format = detectDateFormat(value)
|
|
1109
|
+
if (format) {
|
|
1110
|
+
// Handle specific formats
|
|
1111
|
+
if (/^\d{2}\/\d{2}\/\d{4}$/.test(value)) {
|
|
1112
|
+
// MM/DD/YYYY
|
|
1113
|
+
const [month, day, year] = value.split('/').map(Number)
|
|
1114
|
+
return new Date(year, month - 1, day)
|
|
1115
|
+
} else if (/^\d{2}\.\d{2}\.\d{4}$/.test(value)) {
|
|
1116
|
+
// DD.MM.YYYY
|
|
1117
|
+
const [day, month, year] = value.split('.').map(Number)
|
|
1118
|
+
return new Date(year, month - 1, day)
|
|
1119
|
+
} else if (/^\d{1,2}\s[a-z]{3,9}\s\d{4}$/i.test(value)) {
|
|
1120
|
+
// D MMM YYYY or D MMMM YYYY
|
|
1121
|
+
return new Date(value)
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return null
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Replace convertValueByType function with simplified version using composable utilities
|
|
1129
|
+
function convertValueByType(value: any, dataType: string): any {
|
|
1130
|
+
if (value === null || value === undefined || value === '') {
|
|
1131
|
+
return null
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
try {
|
|
1135
|
+
switch (dataType) {
|
|
1136
|
+
case DATA_TYPES.STRING:
|
|
1137
|
+
return String(value)
|
|
1138
|
+
|
|
1139
|
+
case DATA_TYPES.NUMBER: {
|
|
1140
|
+
const num = Number(value)
|
|
1141
|
+
return Number.isNaN(num) ? null : num
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
case DATA_TYPES.BOOLEAN:
|
|
1145
|
+
if (typeof value === 'boolean') return value
|
|
1146
|
+
if (typeof value === 'string') {
|
|
1147
|
+
const lowercased = value.toLowerCase().trim()
|
|
1148
|
+
if (['true', 'yes', '1', 'on'].includes(lowercased)) return true
|
|
1149
|
+
if (['false', 'no', '0', 'off'].includes(lowercased)) return false
|
|
1150
|
+
}
|
|
1151
|
+
return Boolean(value)
|
|
1152
|
+
|
|
1153
|
+
case DATA_TYPES.DATE:
|
|
1154
|
+
case DATA_TYPES.DATETIME:
|
|
1155
|
+
// Handle Excel serial dates
|
|
1156
|
+
if (isExcelSerialDate(value)) {
|
|
1157
|
+
const date = excelSerialDateToJSDate(value)
|
|
1158
|
+
return formatDate(date, dataType === DATA_TYPES.DATETIME)
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Handle string dates
|
|
1162
|
+
if (typeof value === 'string') {
|
|
1163
|
+
const dateObj = parseDate(value)
|
|
1164
|
+
if (dateObj) {
|
|
1165
|
+
return formatDate(dateObj, dataType === DATA_TYPES.DATETIME)
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Handle Date objects
|
|
1170
|
+
if (value instanceof Date) {
|
|
1171
|
+
return formatDate(value, dataType === DATA_TYPES.DATETIME)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
return null
|
|
1175
|
+
|
|
1176
|
+
default:
|
|
1177
|
+
return value
|
|
1178
|
+
}
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
console.error('Error converting value:', value, 'to type:', dataType, error)
|
|
1181
|
+
return null
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Function to detect the most likely data type from a value
|
|
1186
|
+
function detectDataType(value: any): string {
|
|
1187
|
+
if (value === null || value === undefined) {
|
|
1188
|
+
return DATA_TYPES.STRING
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (typeof value === 'number' || (typeof value === 'string' && !Number.isNaN(Number(value)))) {
|
|
1192
|
+
if (isExcelSerialDate(Number(value))) {
|
|
1193
|
+
return DATA_TYPES.DATE
|
|
1194
|
+
}
|
|
1195
|
+
return DATA_TYPES.NUMBER
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (typeof value === 'boolean' || (typeof value === 'string' && ['true', 'false', 'yes', 'no'].includes(value.toLowerCase()))) {
|
|
1199
|
+
return DATA_TYPES.BOOLEAN
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Check if it's a date
|
|
1203
|
+
if (value instanceof Date) {
|
|
1204
|
+
return DATA_TYPES.DATETIME
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (typeof value === 'string') {
|
|
1208
|
+
// Check if it's a date string
|
|
1209
|
+
if (detectDateFormat(value) || !Number.isNaN(new Date(value).getTime())) {
|
|
1210
|
+
return DATA_TYPES.DATE
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Default to string
|
|
1215
|
+
return DATA_TYPES.STRING
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Function to guess data types for all mapped fields
|
|
1219
|
+
function guessDataTypes(): void {
|
|
1220
|
+
schemaFields.value.forEach((field) => {
|
|
1221
|
+
if (!fieldDataTypes[field.id] && fieldMapping[field.id]) {
|
|
1222
|
+
// Get sample values from the first few rows
|
|
1223
|
+
const sampleValues = fileData.value
|
|
1224
|
+
.slice(0, 5)
|
|
1225
|
+
.map(row => row[fieldMapping[field.id]])
|
|
1226
|
+
.filter(value => value !== undefined && value !== null && value !== '')
|
|
1227
|
+
|
|
1228
|
+
if (sampleValues.length > 0) {
|
|
1229
|
+
// Detect most common type
|
|
1230
|
+
const types = sampleValues.map(detectDataType)
|
|
1231
|
+
const typeCount: Record<string, number> = {}
|
|
1232
|
+
|
|
1233
|
+
types.forEach((type: any) => {
|
|
1234
|
+
typeCount[type] = (typeCount[type] || 0) + 1
|
|
1235
|
+
})
|
|
1236
|
+
|
|
1237
|
+
// Get the most common type
|
|
1238
|
+
let maxCount = 0
|
|
1239
|
+
let mostCommonType = DATA_TYPES.STRING
|
|
1240
|
+
|
|
1241
|
+
Object.entries(typeCount).forEach(([type, count]) => {
|
|
1242
|
+
if (count > maxCount) {
|
|
1243
|
+
maxCount = count
|
|
1244
|
+
mostCommonType = type
|
|
1245
|
+
}
|
|
1246
|
+
})
|
|
1247
|
+
|
|
1248
|
+
fieldDataTypes[field.id] = mostCommonType
|
|
1249
|
+
} else {
|
|
1250
|
+
fieldDataTypes[field.id] = DATA_TYPES.STRING
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
})
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Function to open transformation dialog for related fields
|
|
1257
|
+
function openRelatedTransformDialog(parentId: string, field: SchemaItem) {
|
|
1258
|
+
try {
|
|
1259
|
+
console.log('Opening related transform dialog for field:', field.id, 'in parent:', parentId)
|
|
1260
|
+
|
|
1261
|
+
// Make sure to set options property from attrs if needed
|
|
1262
|
+
if (!field.options) {
|
|
1263
|
+
field.options = []
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (field.attrs && field.attrs.options) {
|
|
1267
|
+
console.log('Copying options from attrs for related field:', field.id)
|
|
1268
|
+
field.options = Array.isArray(field.attrs.options)
|
|
1269
|
+
? field.attrs.options
|
|
1270
|
+
: []
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
selectedRelatedTransformField.value = { parentId, field }
|
|
1274
|
+
|
|
1275
|
+
// Initialize transformations structure if needed
|
|
1276
|
+
if (!relatedTransformations[parentId]) {
|
|
1277
|
+
relatedTransformations[parentId] = {}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
if (!relatedTransformations[parentId][field.id]) {
|
|
1281
|
+
relatedTransformations[parentId][field.id] = []
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
showRelatedTransformDialog.value = true
|
|
1285
|
+
|
|
1286
|
+
// Reset selected values
|
|
1287
|
+
selectedRelatedSourceValue.value = ''
|
|
1288
|
+
selectedRelatedTargetValue.value = ''
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
console.error('Error opening related transform dialog:', error)
|
|
1291
|
+
alert('An error occurred while opening the related transform dialog. See console for details.')
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Function to get unique source values for a related field
|
|
1296
|
+
function getRelatedUniqueSourceValues(parentId: string, fieldId: string): any[] {
|
|
1297
|
+
if (!relatedFileMappings[parentId][fieldId]
|
|
1298
|
+
|| !relatedFileData[parentId]
|
|
1299
|
+
|| relatedFileData[parentId].length === 0) {
|
|
1300
|
+
return []
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Get all values from the mapped column in the related file
|
|
1304
|
+
const allValues = relatedFileData[parentId]
|
|
1305
|
+
.map(row => row[relatedFileMappings[parentId][fieldId]])
|
|
1306
|
+
.filter(value => value !== undefined && value !== null && value !== '')
|
|
1307
|
+
|
|
1308
|
+
// Create a unique set of values
|
|
1309
|
+
const uniqueValues = [...new Set(allValues)]
|
|
1310
|
+
|
|
1311
|
+
// Filter out values that already have transformations
|
|
1312
|
+
return uniqueValues.filter((value) => {
|
|
1313
|
+
if (!relatedTransformations[parentId][fieldId]
|
|
1314
|
+
|| relatedTransformations[parentId][fieldId].length === 0) {
|
|
1315
|
+
return true
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Return false if this value already has a transformation
|
|
1319
|
+
return !relatedTransformations[parentId][fieldId].some(t => t.sourceValue == value || t.sourceValue === value.toString()
|
|
1320
|
+
)
|
|
1321
|
+
})
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Computed for available related source values
|
|
1325
|
+
const availableRelatedSourceValues = computed(() => {
|
|
1326
|
+
if (!selectedRelatedTransformField.value) {
|
|
1327
|
+
return []
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const { parentId, field } = selectedRelatedTransformField.value
|
|
1331
|
+
return getRelatedUniqueSourceValues(parentId, field.id)
|
|
1332
|
+
})
|
|
1333
|
+
|
|
1334
|
+
// Create options array from unique related values
|
|
1335
|
+
const relatedSourceValueOptions = computed(() => {
|
|
1336
|
+
return availableRelatedSourceValues.value.map(value => ({
|
|
1337
|
+
value: String(value),
|
|
1338
|
+
label: String(value)
|
|
1339
|
+
}))
|
|
1340
|
+
})
|
|
1341
|
+
|
|
1342
|
+
// Function to add a related transformation
|
|
1343
|
+
function addRelatedTransformation(parentId: string, fieldId: string) {
|
|
1344
|
+
if (!relatedTransformations[parentId]) {
|
|
1345
|
+
relatedTransformations[parentId] = {}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (!relatedTransformations[parentId][fieldId]) {
|
|
1349
|
+
relatedTransformations[parentId][fieldId] = []
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (selectedRelatedSourceValue.value && selectedRelatedTargetValue.value) {
|
|
1353
|
+
// Check if this source value already has a transformation
|
|
1354
|
+
const existingIndex = relatedTransformations[parentId][fieldId].findIndex(
|
|
1355
|
+
t => t.sourceValue === selectedRelatedSourceValue.value
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
if (existingIndex >= 0) {
|
|
1359
|
+
// Update existing transformation
|
|
1360
|
+
relatedTransformations[parentId][fieldId][existingIndex].targetValue = selectedRelatedTargetValue.value
|
|
1361
|
+
} else {
|
|
1362
|
+
// Add new transformation
|
|
1363
|
+
relatedTransformations[parentId][fieldId].push({
|
|
1364
|
+
fieldId,
|
|
1365
|
+
sourceValue: selectedRelatedSourceValue.value,
|
|
1366
|
+
targetValue: selectedRelatedTargetValue.value
|
|
1367
|
+
})
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Reset selection
|
|
1371
|
+
selectedRelatedSourceValue.value = ''
|
|
1372
|
+
selectedRelatedTargetValue.value = ''
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Function to remove a related transformation
|
|
1377
|
+
function removeRelatedTransformation(parentId: string, fieldId: string, index: number) {
|
|
1378
|
+
if (relatedTransformations[parentId][fieldId]
|
|
1379
|
+
&& relatedTransformations[parentId][fieldId].length > index) {
|
|
1380
|
+
relatedTransformations[parentId][fieldId].splice(index, 1)
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Function to auto-populate transformations for a related field
|
|
1385
|
+
function autoPopulateRelatedTransformations(parentId: string, fieldId: string) {
|
|
1386
|
+
try {
|
|
1387
|
+
const field = selectedRelatedTransformField.value?.field
|
|
1388
|
+
if (!field) {
|
|
1389
|
+
console.error('Field not found for auto-populate')
|
|
1390
|
+
return
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Ensure options are available either directly or from attrs
|
|
1394
|
+
let fieldOptions = field.options || (field.attrs && field.attrs.options)
|
|
1395
|
+
|
|
1396
|
+
if (!relatedFileMappings[parentId][fieldId]
|
|
1397
|
+
|| !relatedFileData[parentId]
|
|
1398
|
+
|| relatedFileData[parentId].length === 0) {
|
|
1399
|
+
console.warn('No data or mapping found for related field:', fieldId)
|
|
1400
|
+
return
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (!fieldOptions) {
|
|
1404
|
+
console.warn('No options found for related field:', fieldId)
|
|
1405
|
+
return
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Ensure options is an array
|
|
1409
|
+
if (!Array.isArray(fieldOptions)) {
|
|
1410
|
+
console.warn('Options is not an array for related field:', fieldId)
|
|
1411
|
+
fieldOptions = []
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const uniqueValues = getRelatedUniqueSourceValues(parentId, fieldId)
|
|
1415
|
+
|
|
1416
|
+
// Initialize transformations array if needed
|
|
1417
|
+
if (!relatedTransformations[parentId]) {
|
|
1418
|
+
relatedTransformations[parentId] = {}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (!relatedTransformations[parentId][fieldId]) {
|
|
1422
|
+
relatedTransformations[parentId][fieldId] = []
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
let matchCount = 0
|
|
1426
|
+
const unmatchedValues: string[] = []
|
|
1427
|
+
|
|
1428
|
+
// For each unique value, try to find a matching target
|
|
1429
|
+
uniqueValues.forEach((sourceValue) => {
|
|
1430
|
+
const strSourceValue = String(sourceValue)
|
|
1431
|
+
|
|
1432
|
+
const matchedValue = findMatchingTargetValue(strSourceValue, fieldOptions)
|
|
1433
|
+
|
|
1434
|
+
if (matchedValue) {
|
|
1435
|
+
// Check if this source value already has a transformation
|
|
1436
|
+
const existingIndex = relatedTransformations[parentId][fieldId].findIndex(
|
|
1437
|
+
t => t.sourceValue === strSourceValue
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
if (existingIndex >= 0) {
|
|
1441
|
+
// Update existing transformation
|
|
1442
|
+
relatedTransformations[parentId][fieldId][existingIndex].targetValue = matchedValue
|
|
1443
|
+
} else {
|
|
1444
|
+
// Add new transformation
|
|
1445
|
+
relatedTransformations[parentId][fieldId].push({
|
|
1446
|
+
fieldId,
|
|
1447
|
+
sourceValue: strSourceValue,
|
|
1448
|
+
targetValue: matchedValue
|
|
1449
|
+
})
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
matchCount++
|
|
1453
|
+
} else {
|
|
1454
|
+
unmatchedValues.push(strSourceValue)
|
|
1455
|
+
}
|
|
1456
|
+
})
|
|
1457
|
+
|
|
1458
|
+
// Provide feedback about matches
|
|
1459
|
+
if (matchCount === 0) {
|
|
1460
|
+
alert(`No automatic matches found. Try creating transformations manually.`)
|
|
1461
|
+
} else {
|
|
1462
|
+
alert(`Automatically created ${matchCount} transformations by matching source values to target labels.\n\n${unmatchedValues.length} values could not be automatically matched.`)
|
|
1463
|
+
}
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
console.error('Error auto-populating related transformations:', error)
|
|
1466
|
+
alert('An error occurred while trying to auto-populate related transformations. See console for details.')
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Initialize default value structure for a field
|
|
1471
|
+
function initDefaultValue(fieldId: string) {
|
|
1472
|
+
// Make sure the field has an entry in defaultValues
|
|
1473
|
+
if (defaultValues[fieldId] === undefined) {
|
|
1474
|
+
defaultValues[fieldId] = null
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Initialize related default value structure
|
|
1479
|
+
function initRelatedDefaultValue(parentId: string, fieldId: string) {
|
|
1480
|
+
if (!relatedDefaultValues[parentId]) {
|
|
1481
|
+
relatedDefaultValues[parentId] = {}
|
|
1482
|
+
}
|
|
1483
|
+
if (relatedDefaultValues[parentId][fieldId] === undefined) {
|
|
1484
|
+
relatedDefaultValues[parentId][fieldId] = null
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Helper function to prepare a field with default values for rendering
|
|
1489
|
+
function getFieldWithDefaults(field: any) {
|
|
1490
|
+
// Get the current field type to render proper input
|
|
1491
|
+
let fieldType = field.$el || 'text'
|
|
1492
|
+
|
|
1493
|
+
// If datatype is set, adjust the field type accordingly
|
|
1494
|
+
if (fieldDataTypes[field.id]) {
|
|
1495
|
+
switch (fieldDataTypes[field.id]) {
|
|
1496
|
+
case DATA_TYPES.NUMBER:
|
|
1497
|
+
fieldType = 'number'
|
|
1498
|
+
break
|
|
1499
|
+
case DATA_TYPES.DATE:
|
|
1500
|
+
fieldType = 'date'
|
|
1501
|
+
break
|
|
1502
|
+
case DATA_TYPES.BOOLEAN:
|
|
1503
|
+
fieldType = 'toggle'
|
|
1504
|
+
break
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Update formData with current value to support the renderField function
|
|
1509
|
+
if (field.id) {
|
|
1510
|
+
formData.value[field.id] = defaultValues[field.id]
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
return {
|
|
1514
|
+
...field,
|
|
1515
|
+
$el: fieldType,
|
|
1516
|
+
placeholder: 'Set default...'
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Add a helper function for related fields too
|
|
1521
|
+
function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
1522
|
+
// Get the current field type to render proper input
|
|
1523
|
+
let fieldType = field.$el || 'text'
|
|
1524
|
+
|
|
1525
|
+
// If datatype is set, adjust the field type accordingly
|
|
1526
|
+
const fullFieldId = `${parentId}.${field.id}`
|
|
1527
|
+
if (relatedFieldDataTypes[fullFieldId]) {
|
|
1528
|
+
switch (relatedFieldDataTypes[fullFieldId]) {
|
|
1529
|
+
case DATA_TYPES.NUMBER:
|
|
1530
|
+
fieldType = 'number'
|
|
1531
|
+
break
|
|
1532
|
+
case DATA_TYPES.DATE:
|
|
1533
|
+
fieldType = 'date'
|
|
1534
|
+
break
|
|
1535
|
+
case DATA_TYPES.BOOLEAN:
|
|
1536
|
+
fieldType = 'toggle'
|
|
1537
|
+
break
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Create a modified id to avoid collision with main fields
|
|
1542
|
+
const modifiedField = {
|
|
1543
|
+
...field,
|
|
1544
|
+
id: fullFieldId,
|
|
1545
|
+
$el: fieldType,
|
|
1546
|
+
placeholder: 'Set default...'
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Update formData with current value
|
|
1550
|
+
formData.value[fullFieldId] = relatedDefaultValues[parentId][field.id]
|
|
1551
|
+
|
|
1552
|
+
return modifiedField
|
|
1553
|
+
}
|
|
1554
|
+
</script>
|
|
1555
|
+
|
|
1556
|
+
<template>
|
|
1557
|
+
<div class="upload-data-container">
|
|
1558
|
+
<h2 v-text="props.title || 'Upload and Map Data'" />
|
|
1559
|
+
<DragOver
|
|
1560
|
+
v-if="!file"
|
|
1561
|
+
accept=".csv,.xls,.xlsx"
|
|
1562
|
+
@addFiles="addFile"
|
|
1563
|
+
@click="browse(false)"
|
|
1564
|
+
>
|
|
1565
|
+
<Card class="flex flex-column items-center justify-center outline-dashed outline-3 hover">
|
|
1566
|
+
<Icon name="upload_file" size="5" />
|
|
1567
|
+
<p>Drag and drop an Excel or CSV file here</p>
|
|
1568
|
+
<p>or click to select a file</p>
|
|
1569
|
+
<p class="txt-12 color-gray">
|
|
1570
|
+
Accepts .xlsx, .xls, and .csv files
|
|
1571
|
+
</p>
|
|
1572
|
+
</Card>
|
|
1573
|
+
</DragOver>
|
|
1574
|
+
|
|
1575
|
+
<!-- Loading indicator -->
|
|
1576
|
+
<div v-if="isLoading" class="loading-container">
|
|
1577
|
+
<div class="spinner" />
|
|
1578
|
+
<p>Processing your file...</p>
|
|
1579
|
+
</div>
|
|
1580
|
+
|
|
1581
|
+
<!-- Step 2: Sheet selection and configuration -->
|
|
1582
|
+
<div v-if="file && !isLoading && sheetNames.length > 0" class="config-section">
|
|
1583
|
+
<div class="file-info mb-1">
|
|
1584
|
+
<div class="file-chip">
|
|
1585
|
+
{{ file.name }}
|
|
1586
|
+
</div>
|
|
1587
|
+
<Btn thin round value="Change File" @click="file = null" />
|
|
1588
|
+
</div>
|
|
1589
|
+
<SelectInput v-if="sheetNames.length > 1" v-model="selectedSheet" :options="sheetNames" label="Select Sheet" />
|
|
1590
|
+
|
|
1591
|
+
<div class="header-config">
|
|
1592
|
+
<label>File has headers?</label>
|
|
1593
|
+
<CheckInput v-model="hasHeaders" label="First row contains column names" />
|
|
1594
|
+
</div>
|
|
1595
|
+
</div>
|
|
1596
|
+
|
|
1597
|
+
<!-- Step 3: Field Mapping -->
|
|
1598
|
+
<Card v-if="file && !isLoading && fileHeaders.length > 0">
|
|
1599
|
+
<h3 class="mt-0">
|
|
1600
|
+
Map Fields
|
|
1601
|
+
</h3>
|
|
1602
|
+
<p class="instructions">
|
|
1603
|
+
Match each required field to a column from your file, set default values, or configure transformations
|
|
1604
|
+
</p>
|
|
1605
|
+
|
|
1606
|
+
<div class="mapping-table">
|
|
1607
|
+
<table class="tbl">
|
|
1608
|
+
<thead>
|
|
1609
|
+
<tr>
|
|
1610
|
+
<th>Schema Field</th>
|
|
1611
|
+
<th>File Column</th>
|
|
1612
|
+
<th>Default Value</th>
|
|
1613
|
+
<th>Data Type</th>
|
|
1614
|
+
<th>Actions</th>
|
|
1615
|
+
</tr>
|
|
1616
|
+
</thead>
|
|
1617
|
+
<tbody>
|
|
1618
|
+
<tr v-for="field in schemaFields" :key="field.id" :class="{ 'array-field-row': field.isArrayField || field.$el === 'array' }">
|
|
1619
|
+
<td>
|
|
1620
|
+
<div class="field-label">
|
|
1621
|
+
{{ field.label }}
|
|
1622
|
+
<span v-if="field.isArrayField">↳</span>
|
|
1623
|
+
<Pill v-if="field.$el === 'array'" outline thin value="Array" />
|
|
1624
|
+
<!-- <span v-if="field.$el === 'array'" class="array-parent-indicator">[Array]</span> -->
|
|
1625
|
+
<span v-if="isFieldRequired(field)">*</span>
|
|
1626
|
+
<span v-if="getFieldDescription(field).isConditional">†</span>
|
|
1627
|
+
</div>
|
|
1628
|
+
<div v-if="field.disabled" class="field-disabled-reason">
|
|
1629
|
+
{{ field.disabledReason }}
|
|
1630
|
+
</div>
|
|
1631
|
+
<div v-if="getFieldDescription(field).isConditional">
|
|
1632
|
+
{{ getFieldDescription(field).description }}
|
|
1633
|
+
</div>
|
|
1634
|
+
</td>
|
|
1635
|
+
<td>
|
|
1636
|
+
<SelectInput
|
|
1637
|
+
v-model="fieldMapping[field.id]"
|
|
1638
|
+
:options="fileHeaders"
|
|
1639
|
+
:required="isFieldRequired(field)"
|
|
1640
|
+
:disabled="field.disabled"
|
|
1641
|
+
@change="handleSelectChange($event, field.id)"
|
|
1642
|
+
/>
|
|
1643
|
+
</td>
|
|
1644
|
+
<td>
|
|
1645
|
+
<!-- Default Value Input -->
|
|
1646
|
+
<div class="default-value-container">
|
|
1647
|
+
{{ initDefaultValue(field.id) }}
|
|
1648
|
+
<component
|
|
1649
|
+
:is="renderField(getFieldWithDefaults(field))"
|
|
1650
|
+
/>
|
|
1651
|
+
</div>
|
|
1652
|
+
</td>
|
|
1653
|
+
<td>
|
|
1654
|
+
<SelectInput
|
|
1655
|
+
v-model="fieldDataTypes[field.id]"
|
|
1656
|
+
:options="dataTypeOptions"
|
|
1657
|
+
:disabled="!fieldMapping[field.id] && !defaultValues[field.id]"
|
|
1658
|
+
/>
|
|
1659
|
+
</td>
|
|
1660
|
+
<td>
|
|
1661
|
+
<div class="action-buttons-cell">
|
|
1662
|
+
<Btn
|
|
1663
|
+
v-tooltip="'Transform'"
|
|
1664
|
+
thin
|
|
1665
|
+
:disabled="field.disabled"
|
|
1666
|
+
icon="transform"
|
|
1667
|
+
@click="openTransformDialog(field)"
|
|
1668
|
+
/>
|
|
1669
|
+
<Btn v-if="field.$el === 'array'" v-tooltip="'Related File'" thin icon="attach_file" :disabled="field.disabled" @click="openRelatedDialog(field)" />
|
|
1670
|
+
</div>
|
|
1671
|
+
</td>
|
|
1672
|
+
</tr>
|
|
1673
|
+
</tbody>
|
|
1674
|
+
</table>
|
|
1675
|
+
</div>
|
|
1676
|
+
|
|
1677
|
+
<div v-if="mappingComplete" class="action-buttons">
|
|
1678
|
+
<Btn @click="showPreview">
|
|
1679
|
+
Preview Data
|
|
1680
|
+
</Btn>
|
|
1681
|
+
</div>
|
|
1682
|
+
<div v-else class="action-buttons">
|
|
1683
|
+
<div class="mapping-incomplete-message">
|
|
1684
|
+
Please map the required fields to continue
|
|
1685
|
+
</div>
|
|
1686
|
+
</div>
|
|
1687
|
+
</Card>
|
|
1688
|
+
|
|
1689
|
+
<!-- Transformation Modal -->
|
|
1690
|
+
<Modal v-model:visible="showTransformDialog" title="Configure Transformations" width="800">
|
|
1691
|
+
<div v-if="selectedTransformField">
|
|
1692
|
+
<p>Create transformations for <strong>{{ selectedTransformField.label }}</strong></p>
|
|
1693
|
+
<Btn icon="auto_awesome" thin value="Autodetect" @click="autoPopulateTransformations(selectedTransformField.id)" />
|
|
1694
|
+
<table>
|
|
1695
|
+
<thead>
|
|
1696
|
+
<tr>
|
|
1697
|
+
<th>Source Value</th>
|
|
1698
|
+
<th>Target Value</th>
|
|
1699
|
+
<th>Action</th>
|
|
1700
|
+
</tr>
|
|
1701
|
+
</thead>
|
|
1702
|
+
<tbody>
|
|
1703
|
+
<tr v-for="(transform, index) in transformations[selectedTransformField.id] || []" :key="index">
|
|
1704
|
+
<td>{{ transform.sourceValue }}</td>
|
|
1705
|
+
<td>{{ transform.targetValue }}</td>
|
|
1706
|
+
<td>
|
|
1707
|
+
<Btn
|
|
1708
|
+
v-tooltip="'Remove'"
|
|
1709
|
+
thin
|
|
1710
|
+
icon="delete"
|
|
1711
|
+
color="red"
|
|
1712
|
+
@click="removeTransformation(selectedTransformField.id, index)"
|
|
1713
|
+
/>
|
|
1714
|
+
</td>
|
|
1715
|
+
</tr>
|
|
1716
|
+
<tr>
|
|
1717
|
+
<td>
|
|
1718
|
+
<SelectInput
|
|
1719
|
+
v-if="fieldMapping[selectedTransformField.id]"
|
|
1720
|
+
v-model="selectedSourceValue"
|
|
1721
|
+
searchable
|
|
1722
|
+
:options="sourceValueOptions"
|
|
1723
|
+
placeholder="Select source value"
|
|
1724
|
+
/>
|
|
1725
|
+
<input v-else v-model="selectedSourceValue" type="text" placeholder="Source value">
|
|
1726
|
+
</td>
|
|
1727
|
+
<td>
|
|
1728
|
+
<SelectInput
|
|
1729
|
+
v-if="selectedTransformField.options && selectedTransformField.options.length > 0"
|
|
1730
|
+
v-model="selectedTargetValue"
|
|
1731
|
+
searchable
|
|
1732
|
+
:options="selectedTransformField.options"
|
|
1733
|
+
placeholder="Select target value"
|
|
1734
|
+
/>
|
|
1735
|
+
<input v-else v-model="selectedTargetValue" type="text" placeholder="Target value">
|
|
1736
|
+
</td>
|
|
1737
|
+
<td>
|
|
1738
|
+
<Btn
|
|
1739
|
+
v-tooltip="'Add'"
|
|
1740
|
+
thin
|
|
1741
|
+
icon="add"
|
|
1742
|
+
color="primary"
|
|
1743
|
+
@click="addTransformation(selectedTransformField.id)"
|
|
1744
|
+
/>
|
|
1745
|
+
</td>
|
|
1746
|
+
</tr>
|
|
1747
|
+
</tbody>
|
|
1748
|
+
</table>
|
|
1749
|
+
<Btn thin value="Close" @click="showTransformDialog = false" />
|
|
1750
|
+
</div>
|
|
1751
|
+
</Modal>
|
|
1752
|
+
|
|
1753
|
+
<!-- Related File Modal -->
|
|
1754
|
+
<Modal v-model:visible="showRelatedDialog" title="Configure Related Data" width="900">
|
|
1755
|
+
<div v-if="selectedRelationField">
|
|
1756
|
+
<p>Upload a file with related data for {{ selectedRelationField.label }}</p>
|
|
1757
|
+
|
|
1758
|
+
<div v-if="!relatedFiles[selectedRelationField.id]">
|
|
1759
|
+
<DragOver
|
|
1760
|
+
accept=".csv,.xls,.xlsx"
|
|
1761
|
+
@addFiles="(files) => { if (files[0]) processRelatedFile(selectedRelationField!.id, files[0]) }"
|
|
1762
|
+
>
|
|
1763
|
+
<Card class="flex flex-column items-center justify-center outline-dashed outline-3 hover">
|
|
1764
|
+
<Icon name="upload_file" size="5" />
|
|
1765
|
+
<p>Drag and drop an Excel or CSV file here</p>
|
|
1766
|
+
<p>or click to select a file</p>
|
|
1767
|
+
<p class="txt-12 color-gray">
|
|
1768
|
+
Accepts .xlsx, .xls, and .csv files
|
|
1769
|
+
</p>
|
|
1770
|
+
</Card>
|
|
1771
|
+
</DragOver>
|
|
1772
|
+
</div>
|
|
1773
|
+
|
|
1774
|
+
<div v-else>
|
|
1775
|
+
<div class="mb-1">
|
|
1776
|
+
<Pill>
|
|
1777
|
+
{{ relatedFiles[selectedRelationField.id]!.name }}
|
|
1778
|
+
</Pill>
|
|
1779
|
+
<Btn thin round value="Change File" @click="relatedFiles[selectedRelationField.id] = null" />
|
|
1780
|
+
</div>
|
|
1781
|
+
|
|
1782
|
+
<div v-if="relatedFileData[selectedRelationField.id]">
|
|
1783
|
+
<h4>Configure Relationship</h4>
|
|
1784
|
+
|
|
1785
|
+
<div class="flex gap-1">
|
|
1786
|
+
<SelectInput v-model="parentKeyField[selectedRelationField.id]" :options="fileHeaders" label="Source Key Field (from this file)" />
|
|
1787
|
+
<SelectInput v-model="relatedKeyField[selectedRelationField.id]" :options="Object.keys(relatedFileData[selectedRelationField.id][0] || {})" label="Related Key Field (from related file)" />
|
|
1788
|
+
</div>
|
|
1789
|
+
|
|
1790
|
+
<h4>Map Related Fields</h4>
|
|
1791
|
+
|
|
1792
|
+
<table>
|
|
1793
|
+
<thead>
|
|
1794
|
+
<tr>
|
|
1795
|
+
<th>Child Field</th>
|
|
1796
|
+
<th>Related File Column</th>
|
|
1797
|
+
<th>Default Value</th>
|
|
1798
|
+
<th>Data Type</th>
|
|
1799
|
+
<th>Actions</th>
|
|
1800
|
+
</tr>
|
|
1801
|
+
</thead>
|
|
1802
|
+
<tbody>
|
|
1803
|
+
<tr v-for="schemaItem in (selectedRelationField.attrs?.schema || [])" :key="schemaItem.id">
|
|
1804
|
+
<td>{{ schemaItem.label }}</td>
|
|
1805
|
+
<td>
|
|
1806
|
+
<SelectInput
|
|
1807
|
+
v-model="relatedFileMappings[selectedRelationField.id][schemaItem.id]"
|
|
1808
|
+
:options="Object.keys(relatedFileData[selectedRelationField.id][0] || {})"
|
|
1809
|
+
placeholder="Select column..."
|
|
1810
|
+
/>
|
|
1811
|
+
</td>
|
|
1812
|
+
<td>
|
|
1813
|
+
<!-- Default Value Input for Related Fields -->
|
|
1814
|
+
<div class="default-value-container">
|
|
1815
|
+
{{ initRelatedDefaultValue(selectedRelationField.id, schemaItem.id) }}
|
|
1816
|
+
<component
|
|
1817
|
+
:is="renderField(getRelatedFieldWithDefaults(selectedRelationField.id, schemaItem))"
|
|
1818
|
+
/>
|
|
1819
|
+
</div>
|
|
1820
|
+
</td>
|
|
1821
|
+
<td>
|
|
1822
|
+
<SelectInput
|
|
1823
|
+
v-model="relatedFieldDataTypes[`${selectedRelationField.id}.${schemaItem.id}`]"
|
|
1824
|
+
:options="dataTypeOptions"
|
|
1825
|
+
:disabled="!relatedFileMappings[selectedRelationField.id]?.[schemaItem.id] && !relatedDefaultValues[selectedRelationField.id]?.[schemaItem.id]"
|
|
1826
|
+
/>
|
|
1827
|
+
</td>
|
|
1828
|
+
<td>
|
|
1829
|
+
<div class="action-buttons-cell">
|
|
1830
|
+
<Btn
|
|
1831
|
+
thin
|
|
1832
|
+
icon="transform"
|
|
1833
|
+
@click="openRelatedTransformDialog(selectedRelationField.id, schemaItem)"
|
|
1834
|
+
>
|
|
1835
|
+
Transform
|
|
1836
|
+
</Btn>
|
|
1837
|
+
</div>
|
|
1838
|
+
</td>
|
|
1839
|
+
</tr>
|
|
1840
|
+
</tbody>
|
|
1841
|
+
</table>
|
|
1842
|
+
</div>
|
|
1843
|
+
</div>
|
|
1844
|
+
<Btn value="Close" @click="showRelatedDialog = false" />
|
|
1845
|
+
</div>
|
|
1846
|
+
</Modal>
|
|
1847
|
+
|
|
1848
|
+
<!-- Preview Modal -->
|
|
1849
|
+
<Modal v-model:visible="showPreviewModal" title="Data Preview & Edit" width="1200">
|
|
1850
|
+
<div>
|
|
1851
|
+
<Spreadsheet
|
|
1852
|
+
v-model="previewData"
|
|
1853
|
+
:column-config="spreadsheetColumns"
|
|
1854
|
+
allow-add-row
|
|
1855
|
+
/>
|
|
1856
|
+
</div>
|
|
1857
|
+
<div>
|
|
1858
|
+
<div>
|
|
1859
|
+
Showing all {{ previewData.length }} records. You can edit values directly.
|
|
1860
|
+
</div>
|
|
1861
|
+
<div>
|
|
1862
|
+
<Btn value="Cancel" @click="showPreviewModal = false" />
|
|
1863
|
+
<Btn value="Import Data" @click="processData()" />
|
|
1864
|
+
</div>
|
|
1865
|
+
</div>
|
|
1866
|
+
</Modal>
|
|
1867
|
+
|
|
1868
|
+
<!-- Related Transformation Modal -->
|
|
1869
|
+
<Modal v-model:visible="showRelatedTransformDialog" title="Configure Related Transformations" width="800">
|
|
1870
|
+
<div v-if="selectedRelatedTransformField">
|
|
1871
|
+
<p>Create transformations for <strong>{{ selectedRelatedTransformField.field.label }}</strong> in {{ selectedRelationField?.label }}</p>
|
|
1872
|
+
|
|
1873
|
+
<div>
|
|
1874
|
+
<div>
|
|
1875
|
+
<Btn
|
|
1876
|
+
thin
|
|
1877
|
+
icon="auto_awesome"
|
|
1878
|
+
value="Autolink"
|
|
1879
|
+
color="primary"
|
|
1880
|
+
@click="autoPopulateRelatedTransformations(
|
|
1881
|
+
selectedRelatedTransformField.parentId,
|
|
1882
|
+
selectedRelatedTransformField.field.id,
|
|
1883
|
+
)"
|
|
1884
|
+
/>
|
|
1885
|
+
</div>
|
|
1886
|
+
</div>
|
|
1887
|
+
|
|
1888
|
+
<div>
|
|
1889
|
+
<table>
|
|
1890
|
+
<thead>
|
|
1891
|
+
<tr>
|
|
1892
|
+
<th>Source Value</th>
|
|
1893
|
+
<th>Target Value</th>
|
|
1894
|
+
<th>Action</th>
|
|
1895
|
+
</tr>
|
|
1896
|
+
</thead>
|
|
1897
|
+
<tbody>
|
|
1898
|
+
<tr
|
|
1899
|
+
v-for="(transform, index) in relatedTransformations[selectedRelatedTransformField.parentId]?.[selectedRelatedTransformField.field.id] || []"
|
|
1900
|
+
:key="index"
|
|
1901
|
+
>
|
|
1902
|
+
<td>{{ transform.sourceValue }}</td>
|
|
1903
|
+
<td>{{ transform.targetValue }}</td>
|
|
1904
|
+
<td>
|
|
1905
|
+
<Btn
|
|
1906
|
+
v-tooltip="'Remove'"
|
|
1907
|
+
thin
|
|
1908
|
+
icon="delete"
|
|
1909
|
+
color="red"
|
|
1910
|
+
@click="removeRelatedTransformation(
|
|
1911
|
+
selectedRelatedTransformField.parentId,
|
|
1912
|
+
selectedRelatedTransformField.field.id,
|
|
1913
|
+
index,
|
|
1914
|
+
)"
|
|
1915
|
+
/>
|
|
1916
|
+
</td>
|
|
1917
|
+
</tr>
|
|
1918
|
+
<tr>
|
|
1919
|
+
<td>
|
|
1920
|
+
<SelectInput
|
|
1921
|
+
v-if="relatedFileMappings[selectedRelatedTransformField.parentId]?.[selectedRelatedTransformField.field.id]"
|
|
1922
|
+
v-model="selectedRelatedSourceValue"
|
|
1923
|
+
searchable
|
|
1924
|
+
:options="relatedSourceValueOptions"
|
|
1925
|
+
placeholder="Select source value"
|
|
1926
|
+
/>
|
|
1927
|
+
<input v-else v-model="selectedRelatedSourceValue" type="text" placeholder="Source value">
|
|
1928
|
+
</td>
|
|
1929
|
+
<td>
|
|
1930
|
+
<SelectInput
|
|
1931
|
+
v-if="selectedRelatedTransformField.field.options && selectedRelatedTransformField.field.options.length > 0"
|
|
1932
|
+
v-model="selectedRelatedTargetValue"
|
|
1933
|
+
searchable
|
|
1934
|
+
:options="selectedRelatedTransformField.field.options"
|
|
1935
|
+
placeholder="Select target value"
|
|
1936
|
+
/>
|
|
1937
|
+
<input v-else v-model="selectedRelatedTargetValue" type="text" placeholder="Target value">
|
|
1938
|
+
</td>
|
|
1939
|
+
<td>
|
|
1940
|
+
<Btn
|
|
1941
|
+
v-tooltip="'Add'"
|
|
1942
|
+
thin
|
|
1943
|
+
icon="add"
|
|
1944
|
+
color="primary"
|
|
1945
|
+
@click="addRelatedTransformation(
|
|
1946
|
+
selectedRelatedTransformField.parentId,
|
|
1947
|
+
selectedRelatedTransformField.field.id,
|
|
1948
|
+
)"
|
|
1949
|
+
/>
|
|
1950
|
+
</td>
|
|
1951
|
+
</tr>
|
|
1952
|
+
</tbody>
|
|
1953
|
+
</table>
|
|
1954
|
+
</div>
|
|
1955
|
+
|
|
1956
|
+
<div>
|
|
1957
|
+
<Btn @click="showRelatedTransformDialog = false">
|
|
1958
|
+
Close
|
|
1959
|
+
</Btn>
|
|
1960
|
+
</div>
|
|
1961
|
+
</div>
|
|
1962
|
+
</Modal>
|
|
1963
|
+
</div>
|
|
1964
|
+
</template>
|