@bagelink/vue 1.14.15 → 1.15.0

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