@bagelink/vue 1.2.101 → 1.2.105

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.
Files changed (47) hide show
  1. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  2. package/dist/components/DragOver.vue.d.ts +27 -0
  3. package/dist/components/DragOver.vue.d.ts.map +1 -0
  4. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  5. package/dist/components/ImportData.vue.d.ts +20 -0
  6. package/dist/components/ImportData.vue.d.ts.map +1 -0
  7. package/dist/components/calendar/views/CalendarPopover.vue.d.ts +2 -2
  8. package/dist/components/calendar/views/CalendarPopover.vue.d.ts.map +1 -1
  9. package/dist/components/form/FieldArray.vue.d.ts +3 -1
  10. package/dist/components/form/FieldArray.vue.d.ts.map +1 -1
  11. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  12. package/dist/components/index.d.ts +2 -0
  13. package/dist/components/index.d.ts.map +1 -1
  14. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  15. package/dist/composables/index.d.ts +2 -1
  16. package/dist/composables/index.d.ts.map +1 -1
  17. package/dist/composables/useExcel.d.ts +25 -0
  18. package/dist/composables/useExcel.d.ts.map +1 -0
  19. package/dist/index.cjs +2562 -675
  20. package/dist/index.mjs +2563 -676
  21. package/dist/plugins/modalTypes.d.ts +1 -3
  22. package/dist/plugins/modalTypes.d.ts.map +1 -1
  23. package/dist/style.css +222 -186
  24. package/dist/types/BagelForm.d.ts +5 -4
  25. package/dist/types/BagelForm.d.ts.map +1 -1
  26. package/dist/types/timeago.d.ts +23 -0
  27. package/dist/types/timeago.d.ts.map +1 -0
  28. package/dist/utils/BagelFormUtils.d.ts +9 -4
  29. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  30. package/dist/utils/index.d.ts +1 -0
  31. package/dist/utils/index.d.ts.map +1 -1
  32. package/package.json +2 -2
  33. package/src/components/DragOver.vue +112 -0
  34. package/src/components/ImportData.vue +1964 -0
  35. package/src/components/form/FieldArray.vue +4 -2
  36. package/src/components/form/inputs/RichText/utils/media.ts +2 -2
  37. package/src/components/index.ts +2 -0
  38. package/src/components/lightbox/Lightbox.vue +2 -14
  39. package/src/composables/index.ts +2 -1
  40. package/src/composables/useExcel.ts +220 -0
  41. package/src/plugins/modalTypes.ts +1 -1
  42. package/src/styles/buttons.css +79 -75
  43. package/src/types/BagelForm.ts +10 -10
  44. package/src/utils/BagelFormUtils.ts +18 -6
  45. package/src/utils/index.ts +21 -0
  46. package/dist/components/Carousel2.vue.d.ts +0 -89
  47. 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>