frappe-ui 0.1.215 → 0.1.218
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frappe/Billing/SignupBanner.vue +1 -1
- package/frappe/Billing/TrialBanner.vue +1 -1
- package/frappe/DataImport/DataImport.vue +160 -0
- package/frappe/DataImport/DataImportList.vue +160 -0
- package/frappe/DataImport/ImportSteps.vue +114 -0
- package/frappe/DataImport/MappingStep.vue +167 -0
- package/frappe/DataImport/PreviewStep.vue +360 -0
- package/frappe/DataImport/TemplateModal.vue +221 -0
- package/frappe/DataImport/UploadStep.vue +394 -0
- package/frappe/DataImport/dataImport.ts +53 -0
- package/frappe/DataImport/types.ts +42 -0
- package/frappe/Help/HelpModal.vue +4 -4
- package/frappe/Onboarding/GettingStartedBanner.vue +1 -1
- package/frappe/components/Link/Link.vue +162 -0
- package/frappe/index.d.ts +53 -0
- package/frappe/index.js +3 -0
- package/package.json +25 -2
- package/src/components/Breadcrumbs/Breadcrumbs.vue +28 -32
- package/src/components/Calendar/Calendar.story.vue +1 -0
- package/src/components/Calendar/Calendar.vue +65 -3
- package/src/components/Charts/index.ts +0 -0
- package/src/components/Checkbox/Checkbox.vue +0 -1
- package/src/components/Checkbox/types.ts +0 -1
- package/src/components/Combobox/Combobox.story.vue +18 -0
- package/src/components/Combobox/Combobox.vue +13 -2
- package/src/components/Combobox/types.ts +2 -1
- package/src/components/DatePicker/index.ts +6 -0
- package/src/components/Dropdown/Dropdown.vue +19 -5
- package/src/components/Dropdown/types.ts +1 -0
- package/src/components/FileUploader/FileUploader.vue +6 -1
- package/src/components/ListView/ListGroupHeader.vue +1 -1
- package/src/components/ListView/ListGroupRows.vue +5 -0
- package/src/components/Select/Select.story.vue +16 -3
- package/src/components/Select/Select.vue +109 -96
- package/src/components/Sidebar/index.ts +3 -0
- package/src/components/Toast/Toast.vue +1 -1
- package/src/data-fetching/index.ts +4 -2
- package/src/index.ts +9 -23
- package/src/resources/{index.js → index.ts} +3 -1
- package/src/resources/{local.js → local.ts} +4 -4
- package/src/resources/realtime.ts +21 -0
- package/frappe/Icons/HelpIcon.vue +0 -16
- package/frappe/Icons/LightningIcon.vue +0 -16
- package/frappe/Icons/MaximizeIcon.vue +0 -19
- package/frappe/Icons/MinimizeIcon.vue +0 -19
- package/frappe/Icons/StepsIcon.vue +0 -16
- package/src/components/GreenCheckIcon.vue +0 -16
- package/src/icons/CircleCheck.vue +0 -9
- package/src/icons/DownSolid.vue +0 -8
- package/src/resources/realtime.js +0 -15
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
</Button>
|
|
24
24
|
</template>
|
|
25
25
|
<script setup>
|
|
26
|
-
import LightningIcon from '
|
|
26
|
+
import LightningIcon from '../../icons/LightningIcon.vue'
|
|
27
27
|
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
28
28
|
import { Button } from '../../src/components/Button'
|
|
29
29
|
import { createResource } from '../../src/resources'
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<header
|
|
3
|
+
class="sticky flex items-center justify-between space-x-28 top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
|
4
|
+
>
|
|
5
|
+
<Breadcrumbs :items="breadcrumbs" />
|
|
6
|
+
<ImportSteps class="flex-1" v-if="step != 'list'" :data="data" :step="step" @updateStep="updateStep" />
|
|
7
|
+
</header>
|
|
8
|
+
<div>
|
|
9
|
+
<DataImportList
|
|
10
|
+
v-if="step === 'list'"
|
|
11
|
+
:dataImports="dataImports"
|
|
12
|
+
@updateStep="updateStep"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<UploadStep
|
|
16
|
+
v-else-if="step === 'upload'"
|
|
17
|
+
:dataImports="dataImports"
|
|
18
|
+
:doctype="doctype || data?.reference_doctype"
|
|
19
|
+
:fields="fields"
|
|
20
|
+
:data="data"
|
|
21
|
+
@updateStep="updateStep"
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<MappingStep
|
|
25
|
+
v-else-if="step === 'map'"
|
|
26
|
+
:dataImports="dataImports"
|
|
27
|
+
:data="data"
|
|
28
|
+
:fields="fields"
|
|
29
|
+
@updateStep="updateStep"
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
<PreviewStep
|
|
33
|
+
v-else-if="step === 'preview'"
|
|
34
|
+
:dataImports="dataImports"
|
|
35
|
+
:data="data"
|
|
36
|
+
:fields="fields"
|
|
37
|
+
:doctypeMap="doctypeMap"
|
|
38
|
+
@updateStep="updateStep"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { computed, nextTick, ref, watch } from 'vue'
|
|
44
|
+
import type { DataImportProps, DataImport } from './types'
|
|
45
|
+
import { createListResource, createResource } from '../../src/resources'
|
|
46
|
+
import { useRoute } from 'vue-router'
|
|
47
|
+
import Breadcrumbs from '../../src/components/Breadcrumbs/Breadcrumbs.vue'
|
|
48
|
+
import DataImportList from './DataImportList.vue'
|
|
49
|
+
import ImportSteps from './ImportSteps.vue'
|
|
50
|
+
import MappingStep from './MappingStep.vue'
|
|
51
|
+
import PreviewStep from './PreviewStep.vue'
|
|
52
|
+
import UploadStep from './UploadStep.vue'
|
|
53
|
+
|
|
54
|
+
const route = useRoute()
|
|
55
|
+
const step = ref('list')
|
|
56
|
+
const data = ref<DataImport | null>(null)
|
|
57
|
+
|
|
58
|
+
const props = defineProps<Partial<DataImportProps>>()
|
|
59
|
+
|
|
60
|
+
const dataImports = createListResource({
|
|
61
|
+
doctype: 'Data Import',
|
|
62
|
+
fields: [
|
|
63
|
+
'name',
|
|
64
|
+
'reference_doctype',
|
|
65
|
+
'import_type',
|
|
66
|
+
'status',
|
|
67
|
+
'creation',
|
|
68
|
+
'mute_emails',
|
|
69
|
+
'import_file',
|
|
70
|
+
'google_sheets_url',
|
|
71
|
+
'template_options'
|
|
72
|
+
],
|
|
73
|
+
auto: true,
|
|
74
|
+
orderBy: 'modified desc',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const fields = createResource({
|
|
78
|
+
url: "frappe.desk.form.load.getdoctype",
|
|
79
|
+
makeParams: (values) => {
|
|
80
|
+
return {
|
|
81
|
+
doctype: values.doctype,
|
|
82
|
+
with_parent: 1,
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
auto: false,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
watch(
|
|
89
|
+
() => [props, dataImports.data],
|
|
90
|
+
() => {
|
|
91
|
+
if (!dataImports.data?.length) return
|
|
92
|
+
if (props.doctype) {
|
|
93
|
+
step.value = 'upload'
|
|
94
|
+
fields.reload({
|
|
95
|
+
doctype: route.params.doctype,
|
|
96
|
+
})
|
|
97
|
+
} else if (props.importName) {
|
|
98
|
+
updateData()
|
|
99
|
+
if (!data.value?.import_file && !data.value?.google_sheets_url) {
|
|
100
|
+
step.value = 'upload'
|
|
101
|
+
} else if (step.value == 'upload' && route.query.step == 'map') {
|
|
102
|
+
step.value = route.query.step
|
|
103
|
+
} else {
|
|
104
|
+
step.value = 'preview'
|
|
105
|
+
}
|
|
106
|
+
fields.reload({
|
|
107
|
+
doctype: data.value?.reference_doctype,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{ immediate: true },
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
watch(() => route.query, () => {
|
|
115
|
+
if (route.query.step == 'list') {
|
|
116
|
+
step.value = 'list'
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const updateData = () => {
|
|
121
|
+
data.value = dataImports.data?.find(
|
|
122
|
+
(di) => di.name === props.importName,
|
|
123
|
+
) || null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const doctypeTitle = computed(() => {
|
|
127
|
+
let doctype = props.doctype || data.value?.reference_doctype
|
|
128
|
+
return props.doctypeMap?.[doctype || '']?.title || doctype || ''
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const updateStep = (newStep: 'list' | 'upload' | 'map' | 'preview', newData: DataImport) => {
|
|
132
|
+
step.value = newStep
|
|
133
|
+
if (newData) {
|
|
134
|
+
data.value = newData
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const breadcrumbs = computed(() => {
|
|
139
|
+
let crumbs = [
|
|
140
|
+
{
|
|
141
|
+
label: 'Data Import',
|
|
142
|
+
route: {
|
|
143
|
+
name: 'DataImportList', query: {
|
|
144
|
+
step: 'list'
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
if (step.value !== 'list') {
|
|
151
|
+
crumbs.push({
|
|
152
|
+
label: `Importing ${doctypeTitle.value}`,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return crumbs
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
</script>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex min-h-0 flex-col text-base py-5 w-[700px] mx-auto">
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
|
6
|
+
Data Import
|
|
7
|
+
</div>
|
|
8
|
+
<div class="text-ink-gray-6 leading-5">
|
|
9
|
+
Import data into your system using CSV files.
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<Button variant="solid" @click="showModal = true">
|
|
13
|
+
<template #prefix>
|
|
14
|
+
<FeatherIcon name="plus" class="size-4 stroke-1.5" />
|
|
15
|
+
</template>
|
|
16
|
+
Import
|
|
17
|
+
</Button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="flex items-center space-x-2 my-5">
|
|
21
|
+
<FormControl
|
|
22
|
+
v-model="search"
|
|
23
|
+
placeholder="Search imported files"
|
|
24
|
+
type="text"
|
|
25
|
+
class="flex-1"
|
|
26
|
+
/>
|
|
27
|
+
<FormControl
|
|
28
|
+
v-model="importStatus"
|
|
29
|
+
type="select"
|
|
30
|
+
:options="importOptions"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div v-if="dataImports.data?.length" class="overflow-y-scroll">
|
|
35
|
+
<div class="divide-y">
|
|
36
|
+
<div class="grid grid-cols-[85%,20%] items-center text-sm text-ink-gray-5 py-1.5 mx-2 my-0.5 px-1">
|
|
37
|
+
<div>
|
|
38
|
+
Name
|
|
39
|
+
</div>
|
|
40
|
+
<div class="pl-1">
|
|
41
|
+
Status
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div
|
|
45
|
+
v-for="dataImport in dataImports.data"
|
|
46
|
+
@click="() => redirectToImport(dataImport.name)"
|
|
47
|
+
class="grid grid-cols-[85%,20%] items-center cursor-pointer py-2.5 px-1 mx-2"
|
|
48
|
+
>
|
|
49
|
+
<div class="space-y-1">
|
|
50
|
+
<div class="text-ink-gray-7">
|
|
51
|
+
{{ dataImport.reference_doctype }}
|
|
52
|
+
</div>
|
|
53
|
+
<div class="text-ink-gray-5">
|
|
54
|
+
{{ dayjs(dataImport.creation).fromNow() }}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<Badge :label="dataImport.status" :theme="getBadgeColor(dataImport.status) as BadgeProps['theme']" class="w-fit" />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="my-5 flex justify-center">
|
|
61
|
+
<Button v-if="props.dataImports.hasNextPage" @click="props.dataImports.next()">
|
|
62
|
+
<template #prefix>
|
|
63
|
+
<FeatherIcon name="refresh-cw" class="size-4 stroke-1.5" />
|
|
64
|
+
</template>
|
|
65
|
+
Load More
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div v-else class="text-sm italic text-ink-gray-5 mt-5">
|
|
70
|
+
No data imports found.
|
|
71
|
+
</div>
|
|
72
|
+
<Dialog
|
|
73
|
+
v-model="showModal"
|
|
74
|
+
:options="{
|
|
75
|
+
title: 'New Data Import',
|
|
76
|
+
actions: [{
|
|
77
|
+
label: 'Continue',
|
|
78
|
+
variant: 'solid',
|
|
79
|
+
onClick({ close }) {
|
|
80
|
+
createDataImport(close)
|
|
81
|
+
}
|
|
82
|
+
}]
|
|
83
|
+
}"
|
|
84
|
+
>
|
|
85
|
+
<template #body-content>
|
|
86
|
+
<div>
|
|
87
|
+
<Link
|
|
88
|
+
v-model="doctypeForImport"
|
|
89
|
+
doctype="DocType"
|
|
90
|
+
:filters="{
|
|
91
|
+
'allow_import': 1
|
|
92
|
+
}"
|
|
93
|
+
label="Choose a Document Type to import"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
</Dialog>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
<script setup lang="ts">
|
|
101
|
+
import { computed, ref, watch } from 'vue'
|
|
102
|
+
import { useRouter } from 'vue-router'
|
|
103
|
+
import type { DataImports } from './types'
|
|
104
|
+
import { dayjs } from "../../src/utils/dayjs"
|
|
105
|
+
import { getBadgeColor } from "./dataImport"
|
|
106
|
+
import Badge from '../../src/components/Badge/Badge.vue'
|
|
107
|
+
import type { BadgeProps } from '../../src/components/Badge/types'
|
|
108
|
+
import Button from '../../src/components/Button/Button.vue'
|
|
109
|
+
import Dialog from '../../src/components/Dialog/Dialog.vue'
|
|
110
|
+
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
111
|
+
import FormControl from '../../src/components/FormControl/FormControl.vue'
|
|
112
|
+
import Link from "../Link/Link.vue"
|
|
113
|
+
|
|
114
|
+
const search = ref('')
|
|
115
|
+
const importStatus = ref('All')
|
|
116
|
+
const showModal = ref(false)
|
|
117
|
+
const doctypeForImport = ref<string | null>(null)
|
|
118
|
+
const emit = defineEmits(['updateStep'])
|
|
119
|
+
const router = useRouter()
|
|
120
|
+
|
|
121
|
+
const props = defineProps<{
|
|
122
|
+
dataImports: DataImports
|
|
123
|
+
}>()
|
|
124
|
+
|
|
125
|
+
const importOptions = computed(() => {
|
|
126
|
+
const options = ["All", "Pending", "Success", "Partial Success", "Error", "Timed Out"]
|
|
127
|
+
return options.map(option => ({ label: option, value: option }))
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
watch([search, importStatus], ([newSearch, newStatus]) => {
|
|
131
|
+
props.dataImports.update({
|
|
132
|
+
filters: [
|
|
133
|
+
newSearch ? [['name', 'like', `%${newSearch}%`]] : [],
|
|
134
|
+
newStatus !== 'All' ? [['status', '=', newStatus]] : [],
|
|
135
|
+
].flat(),
|
|
136
|
+
})
|
|
137
|
+
props.dataImports.reload()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const createDataImport = (close) => {
|
|
141
|
+
props.dataImports.insert.submit({
|
|
142
|
+
reference_doctype: doctypeForImport.value,
|
|
143
|
+
import_type: 'Insert New Records',
|
|
144
|
+
}, {
|
|
145
|
+
onSuccess(data: DataImport) {
|
|
146
|
+
router.replace({
|
|
147
|
+
name: 'DataImport',
|
|
148
|
+
params: {
|
|
149
|
+
importName: data.name
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
close()
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const redirectToImport = (importName: string) => {
|
|
158
|
+
window.location.href = `/data-import/${importName}`;
|
|
159
|
+
}
|
|
160
|
+
</script>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center space-x-10">
|
|
3
|
+
<div class="flex items-center space-x-2 text-ink-gray-5 cursor-pointer"
|
|
4
|
+
:class="{
|
|
5
|
+
'text-ink-gray-9 font-semibold': onUploadStep
|
|
6
|
+
}"
|
|
7
|
+
@click="emit('updateStep', 'upload', { ...data })"
|
|
8
|
+
>
|
|
9
|
+
<FeatherIcon v-if="uploadStepCompleted" name="check" class="size-5 text-sm border rounded-[5px] p-0.5" :class="{
|
|
10
|
+
'text-ink-white bg-surface-gray-7': onUploadStep,
|
|
11
|
+
}"/>
|
|
12
|
+
<div v-else class="text-sm border rounded-[5px] px-1.5 py-0.5" :class="{
|
|
13
|
+
'text-ink-white bg-surface-gray-7': onUploadStep,
|
|
14
|
+
}">
|
|
15
|
+
<span>
|
|
16
|
+
1
|
|
17
|
+
</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div>
|
|
20
|
+
Upload File
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="flex items-center space-x-2 text-ink-gray-5"
|
|
24
|
+
:class="{
|
|
25
|
+
'text-ink-gray-9 font-semibold': onMapStep,
|
|
26
|
+
'cursor-pointer': uploadStepCompleted
|
|
27
|
+
}"
|
|
28
|
+
@click="moveToMapStep()"
|
|
29
|
+
>
|
|
30
|
+
<FeatherIcon v-if="mapStepCompleted" name="check" class="size-5 text-sm border rounded-[5px] p-0.5" :class="{
|
|
31
|
+
'text-ink-white bg-surface-gray-7': onMapStep,
|
|
32
|
+
}"/>
|
|
33
|
+
<div v-else class="text-sm border rounded-[5px] px-1.5 py-0.5" :class="{
|
|
34
|
+
'text-ink-white bg-surface-gray-7': onMapStep,
|
|
35
|
+
}">
|
|
36
|
+
<span>
|
|
37
|
+
2
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div>
|
|
41
|
+
Map File
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="flex items-center space-x-2 text-ink-gray-5"
|
|
45
|
+
:class="{
|
|
46
|
+
'text-ink-gray-9 font-semibold': onPreviewStep,
|
|
47
|
+
'cursor-pointer': uploadStepCompleted
|
|
48
|
+
}"
|
|
49
|
+
@click="moveToPreviewStep()"
|
|
50
|
+
>
|
|
51
|
+
<FeatherIcon v-if="previewStepCompleted" name="check" class="size-5 text-sm border rounded-[5px] p-0.5" :class="{
|
|
52
|
+
'text-ink-white bg-surface-gray-7': onPreviewStep,
|
|
53
|
+
}"/>
|
|
54
|
+
<div v-else class="text-sm border rounded-[5px] px-1.5 py-0.5" :class="{
|
|
55
|
+
'text-ink-white bg-surface-gray-7': onPreviewStep,
|
|
56
|
+
}">
|
|
57
|
+
<span>
|
|
58
|
+
3
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
Review & Import
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
<script setup lang="ts">
|
|
68
|
+
import type { DataImport } from './types'
|
|
69
|
+
import { computed } from 'vue'
|
|
70
|
+
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
71
|
+
|
|
72
|
+
const emit = defineEmits(['updateStep'])
|
|
73
|
+
|
|
74
|
+
const props = defineProps<{
|
|
75
|
+
data: DataImport | null
|
|
76
|
+
step: 'list' | 'new' | 'map' | 'preview'
|
|
77
|
+
}>()
|
|
78
|
+
|
|
79
|
+
const onUploadStep = computed(() => {
|
|
80
|
+
return props.step === 'upload'
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const uploadStepCompleted = computed(() => {
|
|
84
|
+
return props.data?.import_file || props.data?.google_sheets_url
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const onMapStep = computed(() => {
|
|
88
|
+
return props.step === 'map'
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const mapStepCompleted = computed(() => {
|
|
92
|
+
return props.data && props.data?.template_options && JSON.parse(props.data.template_options).column_to_field_map
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const onPreviewStep = computed(() => {
|
|
96
|
+
return props.step === 'preview'
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const previewStepCompleted = computed(() => {
|
|
100
|
+
return props.data?.status === 'Success'
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const moveToMapStep = () => {
|
|
104
|
+
if (uploadStepCompleted.value) {
|
|
105
|
+
emit('updateStep', 'map', { ...props.data })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const moveToPreviewStep = () => {
|
|
110
|
+
if (uploadStepCompleted.value) {
|
|
111
|
+
emit('updateStep', 'preview', { ...props.data })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-[700px] mx-auto pt-12 space-y-8">
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div class="space-y-2">
|
|
5
|
+
<div class="text-lg font-semibold text-ink-gray-9">
|
|
6
|
+
Map Data
|
|
7
|
+
</div>
|
|
8
|
+
<div>
|
|
9
|
+
Change the mapping of columns from your file to fields in the system
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="space-x-2">
|
|
14
|
+
<Button label="Start Over" @click="startOver" />
|
|
15
|
+
<Button label="Continue" variant="solid" @click="$emit('updateStep', 'preview')" />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div v-if="Object.keys(columnMappings).length" class="border rounded-md space-y-8">
|
|
20
|
+
<div class="grid grid-cols-2 text-ink-gray-5 border-b py-2 px-4">
|
|
21
|
+
<div>
|
|
22
|
+
Fields in File
|
|
23
|
+
</div>
|
|
24
|
+
<div>
|
|
25
|
+
Fields in System
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="grid grid-cols-2 py-2 px-4 gap-y-8">
|
|
29
|
+
<template v-for="i in columnsFromFile.length" :key="i">
|
|
30
|
+
<div class="text-ink-gray-7">{{ columnsFromFile[i - 1] }}</div>
|
|
31
|
+
<Autocomplete
|
|
32
|
+
:model-value="columnMappings[columnsFromFile[i - 1]]"
|
|
33
|
+
:options="columnsFromSystem"
|
|
34
|
+
placeholder="Select field"
|
|
35
|
+
@update:model-value="(val) => updateColumnMappings(i, val)"
|
|
36
|
+
/>
|
|
37
|
+
</template>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import type { DataImport, DataImports } from './types';
|
|
44
|
+
import { fieldsToIgnore, getPreviewData } from './dataImport'
|
|
45
|
+
import { computed, nextTick, onMounted, ref } from 'vue';
|
|
46
|
+
import Autocomplete from '../../src/components/Autocomplete/Autocomplete.vue';
|
|
47
|
+
import Button from '../../src/components/Button/Button.vue';
|
|
48
|
+
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
49
|
+
import Link from "../Link/Link.vue"
|
|
50
|
+
|
|
51
|
+
const previewData = ref<any>(null);
|
|
52
|
+
const emit = defineEmits(['updateStep'])
|
|
53
|
+
const columnMappings = ref<Record<string, string>>({});
|
|
54
|
+
|
|
55
|
+
const props = defineProps<{
|
|
56
|
+
dataImports: DataImports
|
|
57
|
+
data: DataImport
|
|
58
|
+
fields: any
|
|
59
|
+
}>()
|
|
60
|
+
|
|
61
|
+
onMounted(async () => {
|
|
62
|
+
previewData.value = await getPreviewData(props.data.name, props.data.import_file, props.data.google_sheets_url);
|
|
63
|
+
initializeColumnMappings();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const initializeColumnMappings = () => {
|
|
67
|
+
const mappings: Record<string, string> = {};
|
|
68
|
+
let columnToFieldMap = []
|
|
69
|
+
if (props.data?.template_options)
|
|
70
|
+
columnToFieldMap = JSON.parse(props.data?.template_options)?.["column_to_field_map"];
|
|
71
|
+
|
|
72
|
+
columnsFromFile.value.forEach((col: string, index: number) => {
|
|
73
|
+
if (columnToFieldMap && columnToFieldMap[index])
|
|
74
|
+
mappings[col] = getMappedColumnName(columnToFieldMap[index]);
|
|
75
|
+
else
|
|
76
|
+
mappings[col] = col;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
columnMappings.value = mappings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const getMappedColumnName = (fieldname: string) => {
|
|
83
|
+
const field = columnsFromSystem.value.find((f: any) => f.value == fieldname);
|
|
84
|
+
if (field)
|
|
85
|
+
return field.label;
|
|
86
|
+
return fieldname;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const updateColumnMappings = (index: number, value: any) => {
|
|
90
|
+
if (!value) return;
|
|
91
|
+
let columnToFieldMap = JSON.parse(props.data?.template_options)?.["column_to_field_map"] || {};
|
|
92
|
+
columnToFieldMap[index - 1] = value.value;
|
|
93
|
+
|
|
94
|
+
props.dataImports.setValue.submit({
|
|
95
|
+
...props.data,
|
|
96
|
+
template_options: JSON.stringify({
|
|
97
|
+
...JSON.parse(props.data?.template_options),
|
|
98
|
+
column_to_field_map: columnToFieldMap
|
|
99
|
+
})
|
|
100
|
+
}, {
|
|
101
|
+
onSuccess: (data: DataImport) => {
|
|
102
|
+
emit('updateStep', 'map', { ...data })
|
|
103
|
+
nextTick(() => {
|
|
104
|
+
initializeColumnMappings()
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const columnsFromFile = computed(() => {
|
|
111
|
+
const columns: string[] = [];
|
|
112
|
+
previewData.value?.columns.forEach((col: any) => {
|
|
113
|
+
if (col.header_title != "Sr. No")
|
|
114
|
+
columns.push(col.header_title);
|
|
115
|
+
})
|
|
116
|
+
return columns;
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const columnsFromSystem = computed(() => {
|
|
120
|
+
const parent = props.data.reference_doctype
|
|
121
|
+
const docs = props.fields.data?.docs || []
|
|
122
|
+
|
|
123
|
+
return docs
|
|
124
|
+
.map((doc: any) => {
|
|
125
|
+
const isParent = doc.name === parent
|
|
126
|
+
|
|
127
|
+
const columns = doc.fields
|
|
128
|
+
.filter((f: any) => !fieldsToIgnore.includes(f.fieldtype))
|
|
129
|
+
.map((f: any) => ({
|
|
130
|
+
value: f.fieldname,
|
|
131
|
+
label: isParent
|
|
132
|
+
? f.label
|
|
133
|
+
: `${f.label} (${getChildTableName(parent, doc.name)})`,
|
|
134
|
+
}))
|
|
135
|
+
|
|
136
|
+
return [
|
|
137
|
+
{ value: "name", label: "ID" },
|
|
138
|
+
...columns,
|
|
139
|
+
]
|
|
140
|
+
})
|
|
141
|
+
.flat()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const startOver = () => {
|
|
145
|
+
props.dataImports.setValue.submit({
|
|
146
|
+
...props.data,
|
|
147
|
+
template_options: JSON.stringify({
|
|
148
|
+
...JSON.parse(props.data?.template_options),
|
|
149
|
+
column_to_field_map: {}
|
|
150
|
+
})
|
|
151
|
+
}, {
|
|
152
|
+
onSuccess: (data: DataImport) => {
|
|
153
|
+
emit('updateStep', 'map', { ...data })
|
|
154
|
+
nextTick(() => {
|
|
155
|
+
initializeColumnMappings()
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const getChildTableName = (parent: string, child: string) => {
|
|
162
|
+
let parentFields = props.fields.data?.docs.find((doc: any) => doc.name == parent)?.fields || [];
|
|
163
|
+
|
|
164
|
+
let childField = parentFields.filter((field: any) => field.options == child)[0]
|
|
165
|
+
return childField?.label || child;
|
|
166
|
+
}
|
|
167
|
+
</script>
|