frappe-ui 0.1.226 → 0.1.228
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/DataImport/DataImport.vue +25 -20
- package/frappe/DataImport/DataImportList.vue +20 -8
- package/frappe/DataImport/ImportSteps.vue +6 -6
- package/frappe/DataImport/MappingStep.vue +36 -16
- package/frappe/DataImport/PreviewStep.vue +26 -31
- package/frappe/DataImport/TemplateModal.vue +9 -15
- package/frappe/DataImport/UploadStep.vue +58 -54
- package/frappe/DataImport/dataImport.ts +4 -5
- package/frappe/DataImport/types.ts +5 -2
- package/frappe/Link/types.ts +1 -1
- package/frappe/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/components/Tabs/Tabs.vue +8 -5
- package/frappe/components/Link/Link.vue +0 -162
|
@@ -3,9 +3,11 @@
|
|
|
3
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
4
|
>
|
|
5
5
|
<Breadcrumbs :items="breadcrumbs" />
|
|
6
|
-
<ImportSteps class="flex-1" v-if="step != 'list'" :data="data" :step="step" @updateStep="updateStep" />
|
|
6
|
+
<ImportSteps class="flex-1 hidden lg:flex" v-if="step != 'list'" :data="data" :step="step" @updateStep="updateStep" />
|
|
7
7
|
</header>
|
|
8
8
|
<div>
|
|
9
|
+
<ImportSteps class="flex-1 lg:hidden w-[90%] mx-auto mt-5" v-if="step != 'list'" :data="data" :step="step" @updateStep="updateStep" />
|
|
10
|
+
|
|
9
11
|
<DataImportList
|
|
10
12
|
v-if="step === 'list'"
|
|
11
13
|
:dataImports="dataImports"
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
<MappingStep
|
|
25
27
|
v-else-if="step === 'map'"
|
|
26
28
|
:dataImports="dataImports"
|
|
27
|
-
:data="data"
|
|
29
|
+
:data="data as DataImport"
|
|
28
30
|
:fields="fields"
|
|
29
31
|
@updateStep="updateStep"
|
|
30
32
|
/>
|
|
@@ -32,9 +34,9 @@
|
|
|
32
34
|
<PreviewStep
|
|
33
35
|
v-else-if="step === 'preview'"
|
|
34
36
|
:dataImports="dataImports"
|
|
35
|
-
:data="data"
|
|
37
|
+
:data="data as DataImport"
|
|
36
38
|
:fields="fields"
|
|
37
|
-
:doctypeMap="doctypeMap"
|
|
39
|
+
:doctypeMap="doctypeMap as Record<string, { title: string; listRoute?: string; pageRoute?: string }>"
|
|
38
40
|
@updateStep="updateStep"
|
|
39
41
|
/>
|
|
40
42
|
</div>
|
|
@@ -52,7 +54,7 @@ import PreviewStep from './PreviewStep.vue'
|
|
|
52
54
|
import UploadStep from './UploadStep.vue'
|
|
53
55
|
|
|
54
56
|
const route = useRoute()
|
|
55
|
-
const step = ref('list')
|
|
57
|
+
const step = ref<'upload' | 'map' | 'list' | 'preview'>('list')
|
|
56
58
|
const data = ref<DataImport | null>(null)
|
|
57
59
|
|
|
58
60
|
const props = defineProps<Partial<DataImportProps>>()
|
|
@@ -76,7 +78,7 @@ const props = defineProps<Partial<DataImportProps>>()
|
|
|
76
78
|
|
|
77
79
|
const fields = createResource({
|
|
78
80
|
url: "frappe.desk.form.load.getdoctype",
|
|
79
|
-
makeParams: (values) => {
|
|
81
|
+
makeParams: (values: { doctype: string }) => {
|
|
80
82
|
return {
|
|
81
83
|
doctype: values.doctype,
|
|
82
84
|
with_parent: 1,
|
|
@@ -86,7 +88,7 @@ const fields = createResource({
|
|
|
86
88
|
})
|
|
87
89
|
|
|
88
90
|
watch(
|
|
89
|
-
() => [props, dataImports.data],
|
|
91
|
+
() => [route.params, props, dataImports.data],
|
|
90
92
|
() => {
|
|
91
93
|
if (!dataImports.data?.length) return
|
|
92
94
|
if (props.doctype) {
|
|
@@ -103,9 +105,11 @@ watch(
|
|
|
103
105
|
} else {
|
|
104
106
|
step.value = 'preview'
|
|
105
107
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
if (data.value?.reference_doctype) {
|
|
109
|
+
fields.reload({
|
|
110
|
+
doctype: data.value?.reference_doctype,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
109
113
|
}
|
|
110
114
|
},
|
|
111
115
|
{ immediate: true },
|
|
@@ -119,15 +123,10 @@ watch(() => route.query, () => {
|
|
|
119
123
|
|
|
120
124
|
const updateData = () => {
|
|
121
125
|
data.value = dataImports.data?.find(
|
|
122
|
-
(di) => di.name === props.importName,
|
|
126
|
+
(di: DataImport) => di.name === props.importName,
|
|
123
127
|
) || null
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
const doctypeTitle = computed(() => {
|
|
127
|
-
let doctype = props.doctype || data.value?.reference_doctype
|
|
128
|
-
return props.doctypeMap?.[doctype || '']?.title || doctype || ''
|
|
129
|
-
})
|
|
130
|
-
|
|
131
130
|
const updateStep = (newStep: 'list' | 'upload' | 'map' | 'preview', newData: DataImport) => {
|
|
132
131
|
step.value = newStep
|
|
133
132
|
if (newData) {
|
|
@@ -135,16 +134,22 @@ const updateStep = (newStep: 'list' | 'upload' | 'map' | 'preview', newData: Dat
|
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
136
|
|
|
137
|
+
const doctypeTitle = computed(() => {
|
|
138
|
+
let doctype = props.doctype || data.value?.reference_doctype
|
|
139
|
+
return props.doctypeMap?.[doctype || '']?.title || doctype || ''
|
|
140
|
+
})
|
|
141
|
+
|
|
138
142
|
const breadcrumbs = computed(() => {
|
|
139
143
|
let crumbs = [
|
|
140
144
|
{
|
|
141
145
|
label: 'Data Import',
|
|
142
146
|
route: {
|
|
143
|
-
name: 'DataImportList',
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
name: 'DataImportList',
|
|
148
|
+
query: {
|
|
149
|
+
step: 'list'
|
|
150
|
+
}
|
|
151
|
+
},
|
|
146
152
|
},
|
|
147
|
-
}
|
|
148
153
|
]
|
|
149
154
|
|
|
150
155
|
if (step.value !== 'list') {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="flex min-h-0 flex-col text-base py-5 w-[700px] mx-auto">
|
|
2
|
+
<div class="flex min-h-0 flex-col text-base py-5 w-[90%] lg:w-[700px] mx-auto">
|
|
3
3
|
<div class="flex items-center justify-between">
|
|
4
4
|
<div>
|
|
5
5
|
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
<div v-if="dataImports.data?.length" class="overflow-y-scroll">
|
|
35
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">
|
|
36
|
+
<div class="grid grid-cols-[75%,20%] lg:grid-cols-[85%,20%] items-center text-sm text-ink-gray-5 py-1.5 mx-2 my-0.5 px-1">
|
|
37
37
|
<div>
|
|
38
38
|
Name
|
|
39
39
|
</div>
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
</div>
|
|
44
44
|
<div
|
|
45
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"
|
|
46
|
+
@click="() => redirectToImport(dataImport.name!)"
|
|
47
|
+
class="grid grid-cols-[75%,20%] lg:grid-cols-[85%,20%] items-center cursor-pointer py-2.5 px-1 mx-2"
|
|
48
48
|
>
|
|
49
49
|
<div class="space-y-1">
|
|
50
50
|
<div class="text-ink-gray-7">
|
|
@@ -100,11 +100,12 @@
|
|
|
100
100
|
<script setup lang="ts">
|
|
101
101
|
import { computed, ref, watch } from 'vue'
|
|
102
102
|
import { useRouter } from 'vue-router'
|
|
103
|
-
import type { DataImports } from './types'
|
|
103
|
+
import type { DataImports, DataImport } from './types'
|
|
104
104
|
import { dayjs } from "../../src/utils/dayjs"
|
|
105
105
|
import { getBadgeColor } from "./dataImport"
|
|
106
106
|
import Badge from '../../src/components/Badge/Badge.vue'
|
|
107
107
|
import type { BadgeProps } from '../../src/components/Badge/types'
|
|
108
|
+
import { toast } from "../../src/components/Toast/index"
|
|
108
109
|
import Button from '../../src/components/Button/Button.vue'
|
|
109
110
|
import Dialog from '../../src/components/Dialog/Dialog.vue'
|
|
110
111
|
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
@@ -137,10 +138,12 @@ watch([search, importStatus], ([newSearch, newStatus]) => {
|
|
|
137
138
|
props.dataImports.reload()
|
|
138
139
|
})
|
|
139
140
|
|
|
140
|
-
const createDataImport = (close) => {
|
|
141
|
+
const createDataImport = (close: () => void) => {
|
|
141
142
|
props.dataImports.insert.submit({
|
|
142
|
-
reference_doctype: doctypeForImport.value
|
|
143
|
+
reference_doctype: doctypeForImport.value!,
|
|
143
144
|
import_type: 'Insert New Records',
|
|
145
|
+
mute_emails: true,
|
|
146
|
+
status: 'Pending',
|
|
144
147
|
}, {
|
|
145
148
|
onSuccess(data: DataImport) {
|
|
146
149
|
router.replace({
|
|
@@ -150,11 +153,20 @@ const createDataImport = (close) => {
|
|
|
150
153
|
},
|
|
151
154
|
})
|
|
152
155
|
close()
|
|
156
|
+
},
|
|
157
|
+
onError(error: any) {
|
|
158
|
+
console.error(error)
|
|
159
|
+
toast.error(error.messages?.[0] || error)
|
|
153
160
|
}
|
|
154
161
|
})
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
const redirectToImport = (importName: string) => {
|
|
158
|
-
|
|
165
|
+
router.replace({
|
|
166
|
+
name: 'DataImport',
|
|
167
|
+
params: {
|
|
168
|
+
importName
|
|
169
|
+
},
|
|
170
|
+
})
|
|
159
171
|
}
|
|
160
172
|
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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"
|
|
2
|
+
<div class="flex items-center space-x-3 lg:space-x-10 text-xs lg:text-base">
|
|
3
|
+
<div class="flex items-center space-x-1 lg:space-x-2 text-ink-gray-5 cursor-pointer"
|
|
4
4
|
:class="{
|
|
5
5
|
'text-ink-gray-9 font-semibold': onUploadStep
|
|
6
6
|
}"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
Upload File
|
|
21
21
|
</div>
|
|
22
22
|
</div>
|
|
23
|
-
<div class="flex items-center space-x-2 text-ink-gray-5"
|
|
23
|
+
<div class="flex items-center space-x-1 lg:space-x-2 text-ink-gray-5"
|
|
24
24
|
:class="{
|
|
25
25
|
'text-ink-gray-9 font-semibold': onMapStep,
|
|
26
26
|
'cursor-pointer': uploadStepCompleted
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
</span>
|
|
39
39
|
</div>
|
|
40
40
|
<div>
|
|
41
|
-
Map
|
|
41
|
+
Map Data
|
|
42
42
|
</div>
|
|
43
43
|
</div>
|
|
44
|
-
<div class="flex items-center space-x-2 text-ink-gray-5"
|
|
44
|
+
<div class="flex items-center space-x-1 lg:space-x-2 text-ink-gray-5"
|
|
45
45
|
:class="{
|
|
46
46
|
'text-ink-gray-9 font-semibold': onPreviewStep,
|
|
47
47
|
'cursor-pointer': uploadStepCompleted
|
|
@@ -73,7 +73,7 @@ const emit = defineEmits(['updateStep'])
|
|
|
73
73
|
|
|
74
74
|
const props = defineProps<{
|
|
75
75
|
data: DataImport | null
|
|
76
|
-
step: 'list' | '
|
|
76
|
+
step: 'list' | 'upload' | 'map' | 'preview'
|
|
77
77
|
}>()
|
|
78
78
|
|
|
79
79
|
const onUploadStep = computed(() => {
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="w-[700px] mx-auto
|
|
3
|
-
<div class="flex
|
|
2
|
+
<div class="w-[85%] lg:w-[700px] mx-auto py-12 space-y-8">
|
|
3
|
+
<div class="flex justify-between">
|
|
4
4
|
<div class="space-y-2">
|
|
5
5
|
<div class="text-lg font-semibold text-ink-gray-9">
|
|
6
|
-
|
|
6
|
+
<span>
|
|
7
|
+
Map Data
|
|
8
|
+
</span>
|
|
9
|
+
<Badge v-if="data?.status" :theme="getBadgeColor(data?.status)">
|
|
10
|
+
{{ data?.status }}
|
|
11
|
+
</Badge>
|
|
7
12
|
</div>
|
|
8
|
-
<div>
|
|
13
|
+
<div class="leading-5">
|
|
9
14
|
Change the mapping of columns from your file to fields in the system
|
|
10
15
|
</div>
|
|
11
16
|
</div>
|
|
12
17
|
|
|
13
|
-
<div class="space-x-2">
|
|
14
|
-
<Button label="
|
|
18
|
+
<div class="flex flex-col lg:flex-row space-y-2 lg:space-x-2 lg:space-y-0">
|
|
19
|
+
<Button v-if="mappingUpdated" label="Reset Mapping" @click="resetMapping" />
|
|
15
20
|
<Button label="Continue" variant="solid" @click="$emit('updateStep', 'preview')" />
|
|
16
21
|
</div>
|
|
17
22
|
</div>
|
|
@@ -32,7 +37,7 @@
|
|
|
32
37
|
:model-value="columnMappings[columnsFromFile[i - 1]]"
|
|
33
38
|
:options="columnsFromSystem"
|
|
34
39
|
placeholder="Select field"
|
|
35
|
-
@update:model-value="(val) => updateColumnMappings(i, val)"
|
|
40
|
+
@update:model-value="(val: any) => updateColumnMappings(i, val)"
|
|
36
41
|
/>
|
|
37
42
|
</template>
|
|
38
43
|
</div>
|
|
@@ -41,16 +46,17 @@
|
|
|
41
46
|
</template>
|
|
42
47
|
<script setup lang="ts">
|
|
43
48
|
import type { DataImport, DataImports } from './types';
|
|
44
|
-
import { fieldsToIgnore, getPreviewData } from './dataImport'
|
|
49
|
+
import { fieldsToIgnore, getBadgeColor, getPreviewData } from './dataImport'
|
|
45
50
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
|
51
|
+
import { toast } from "../../src/components/Toast/index"
|
|
46
52
|
import Autocomplete from '../../src/components/Autocomplete/Autocomplete.vue';
|
|
53
|
+
import Badge from '../../src/components/Badge/Badge.vue';
|
|
47
54
|
import Button from '../../src/components/Button/Button.vue';
|
|
48
|
-
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
49
|
-
import Link from "../Link/Link.vue"
|
|
50
55
|
|
|
51
56
|
const previewData = ref<any>(null);
|
|
52
57
|
const emit = defineEmits(['updateStep'])
|
|
53
58
|
const columnMappings = ref<Record<string, string>>({});
|
|
59
|
+
const mappingUpdated = ref(false);
|
|
54
60
|
|
|
55
61
|
const props = defineProps<{
|
|
56
62
|
dataImports: DataImports
|
|
@@ -59,7 +65,7 @@ const props = defineProps<{
|
|
|
59
65
|
}>()
|
|
60
66
|
|
|
61
67
|
onMounted(async () => {
|
|
62
|
-
previewData.value = await getPreviewData(props.data.name
|
|
68
|
+
previewData.value = await getPreviewData(props.data.name!, props.data.import_file, props.data.google_sheets_url);
|
|
63
69
|
initializeColumnMappings();
|
|
64
70
|
});
|
|
65
71
|
|
|
@@ -69,6 +75,9 @@ const initializeColumnMappings = () => {
|
|
|
69
75
|
if (props.data?.template_options)
|
|
70
76
|
columnToFieldMap = JSON.parse(props.data?.template_options)?.["column_to_field_map"];
|
|
71
77
|
|
|
78
|
+
if (Object.keys(columnToFieldMap).length > 0)
|
|
79
|
+
mappingUpdated.value = true;
|
|
80
|
+
|
|
72
81
|
columnsFromFile.value.forEach((col: string, index: number) => {
|
|
73
82
|
if (columnToFieldMap && columnToFieldMap[index])
|
|
74
83
|
mappings[col] = getMappedColumnName(columnToFieldMap[index]);
|
|
@@ -88,13 +97,15 @@ const getMappedColumnName = (fieldname: string) => {
|
|
|
88
97
|
|
|
89
98
|
const updateColumnMappings = (index: number, value: any) => {
|
|
90
99
|
if (!value) return;
|
|
91
|
-
|
|
100
|
+
mappingUpdated.value = true;
|
|
101
|
+
let templateOptions = props.data?.template_options ? JSON.parse(props.data?.template_options) : {};
|
|
102
|
+
let columnToFieldMap = templateOptions["column_to_field_map"] || {};
|
|
92
103
|
columnToFieldMap[index - 1] = value.value;
|
|
93
104
|
|
|
94
105
|
props.dataImports.setValue.submit({
|
|
95
106
|
...props.data,
|
|
96
107
|
template_options: JSON.stringify({
|
|
97
|
-
...
|
|
108
|
+
...templateOptions,
|
|
98
109
|
column_to_field_map: columnToFieldMap
|
|
99
110
|
})
|
|
100
111
|
}, {
|
|
@@ -103,6 +114,10 @@ const updateColumnMappings = (index: number, value: any) => {
|
|
|
103
114
|
nextTick(() => {
|
|
104
115
|
initializeColumnMappings()
|
|
105
116
|
})
|
|
117
|
+
},
|
|
118
|
+
onError: (error: any) => {
|
|
119
|
+
toast.error(error.messages?.[0] || error)
|
|
120
|
+
console.error("Error updating column mappings:", error);
|
|
106
121
|
}
|
|
107
122
|
})
|
|
108
123
|
}
|
|
@@ -117,7 +132,7 @@ const columnsFromFile = computed(() => {
|
|
|
117
132
|
})
|
|
118
133
|
|
|
119
134
|
const columnsFromSystem = computed(() => {
|
|
120
|
-
const parent = props.data
|
|
135
|
+
const parent = props.data!.reference_doctype
|
|
121
136
|
const docs = props.fields.data?.docs || []
|
|
122
137
|
|
|
123
138
|
return docs
|
|
@@ -141,11 +156,12 @@ const columnsFromSystem = computed(() => {
|
|
|
141
156
|
.flat()
|
|
142
157
|
})
|
|
143
158
|
|
|
144
|
-
const
|
|
159
|
+
const resetMapping = () => {
|
|
160
|
+
let templateOptions = props.data?.template_options ? JSON.parse(props.data?.template_options) : {};
|
|
145
161
|
props.dataImports.setValue.submit({
|
|
146
162
|
...props.data,
|
|
147
163
|
template_options: JSON.stringify({
|
|
148
|
-
...
|
|
164
|
+
...templateOptions,
|
|
149
165
|
column_to_field_map: {}
|
|
150
166
|
})
|
|
151
167
|
}, {
|
|
@@ -154,6 +170,10 @@ const startOver = () => {
|
|
|
154
170
|
nextTick(() => {
|
|
155
171
|
initializeColumnMappings()
|
|
156
172
|
})
|
|
173
|
+
},
|
|
174
|
+
onError: (error: any) => {
|
|
175
|
+
toast.error(error.messages?.[0] || error)
|
|
176
|
+
console.error("Error resetting column mappings:", error);
|
|
157
177
|
}
|
|
158
178
|
})
|
|
159
179
|
}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-base h-full flex flex-col w-[700px] mx-auto py-12 space-y-10">
|
|
3
|
-
<div class="flex
|
|
4
|
-
<div class="
|
|
5
|
-
<div class="text-lg font-semibold text-ink-gray-9">
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
<div class="text-base h-full flex flex-col w-[90%] lg:w-[700px] mx-auto py-12 space-y-10">
|
|
3
|
+
<div class="flex flex-col space-y-1">
|
|
4
|
+
<div class="flex items-center justify-between text-ink-gray-7">
|
|
5
|
+
<div class="flex items-center space-x-2 text-lg font-semibold text-ink-gray-9">
|
|
6
|
+
<span>
|
|
7
|
+
Review and Import
|
|
8
|
+
</span>
|
|
9
|
+
|
|
10
|
+
<Badge :theme="getBadgeColor(data.status)">
|
|
11
|
+
{{ data.status }}
|
|
12
|
+
</Badge>
|
|
10
13
|
</div>
|
|
11
|
-
</div>
|
|
12
|
-
<div class="space-x-2">
|
|
13
|
-
<Badge :theme="statusTheme">
|
|
14
|
-
{{ data.status }}
|
|
15
|
-
</Badge>
|
|
16
14
|
<Button
|
|
17
15
|
v-if="data.status != 'Success'"
|
|
18
16
|
:label="data.status != 'Pending' ? 'Retry' : 'Import'"
|
|
19
17
|
variant="solid" @click="startImport" />
|
|
20
18
|
<Button v-else-if="listRoute" label="Done" @click="redirectToList()" />
|
|
21
19
|
</div>
|
|
20
|
+
<div class="leading-5">
|
|
21
|
+
Verify the data before starting the import process
|
|
22
|
+
</div>
|
|
22
23
|
</div>
|
|
23
24
|
|
|
24
25
|
<div v-if="mapping.length" class="space-y-2">
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
Column Mapping
|
|
27
28
|
</div>
|
|
28
29
|
<div class="border rounded-md bg-surface-gray-2 p-4 space-y-4 text-sm">
|
|
29
|
-
<div v-for="map in mapping" class="grid grid-cols-3 space-x-3">
|
|
30
|
+
<div v-for="map in mapping" class="grid grid-cols-[40%,10%,40%] lg:grid-cols-3 space-x-3 items-center">
|
|
30
31
|
<div class="">
|
|
31
32
|
{{ map[0] }}
|
|
32
33
|
</div>
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
</div>
|
|
86
87
|
|
|
87
88
|
<div class="rounded-md p-2" :class="importBannerClass">
|
|
88
|
-
{{ importSuccessCount }} rows imported successfully, {{ importErrorCount }} rows failed.
|
|
89
|
+
{{ importSuccessCount }} {{ importSuccessCount == 1 ? 'row' : 'rows' }} imported successfully, {{ importErrorCount }} {{ importErrorCount == 1 ? 'row' : 'rows' }} failed.
|
|
89
90
|
</div>
|
|
90
91
|
|
|
91
92
|
<TabButtons :buttons="tabButtons" v-model="activeTab" class="w-fit" />
|
|
@@ -152,16 +153,15 @@
|
|
|
152
153
|
</div>
|
|
153
154
|
</template>
|
|
154
155
|
<script setup lang="ts">
|
|
155
|
-
import { getPreviewData } from './dataImport'
|
|
156
|
+
import { getPreviewData, getBadgeColor } from './dataImport'
|
|
156
157
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
|
158
|
+
import type { DataImport, DataImports } from './types';
|
|
157
159
|
import Badge from '../../src/components/Badge/Badge.vue';
|
|
158
160
|
import Button from '../../src/components/Button/Button.vue';
|
|
159
161
|
import call from '../../src/utils/call';
|
|
160
162
|
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
161
163
|
import initSocket from "../../src/utils/socketio";
|
|
162
|
-
import Tooltip from "../../src/components/Tooltip/Tooltip.vue"
|
|
163
164
|
import Popover from "../../src/components/Popover/Popover.vue"
|
|
164
|
-
import Switch from '../../src/components/Switch/Switch.vue';
|
|
165
165
|
import TabButtons from '../../src/components/TabButtons/TabButtons.vue';
|
|
166
166
|
|
|
167
167
|
const preview = ref<any>(null);
|
|
@@ -174,15 +174,16 @@ const props = defineProps<{
|
|
|
174
174
|
dataImports: DataImports
|
|
175
175
|
data: DataImport
|
|
176
176
|
fields: any
|
|
177
|
-
doctypeMap: Record<string, { title: string;
|
|
177
|
+
doctypeMap: Record<string, { title: string; listRoute?: string; pageRoute?: string }>
|
|
178
178
|
}>()
|
|
179
179
|
|
|
180
180
|
onMounted(async () => {
|
|
181
181
|
let socket = initSocket();
|
|
182
|
-
socket.on("data_import_refresh", (data) => {
|
|
182
|
+
socket.on("data_import_refresh", (data: { data_import: string }) => {
|
|
183
183
|
reloadPreviewData(data.data_import);
|
|
184
184
|
})
|
|
185
185
|
|
|
186
|
+
if (!props.data?.name) return;
|
|
186
187
|
preview.value = await getPreviewData(
|
|
187
188
|
props.data.name, props.data.import_file, props.data.google_sheets_url
|
|
188
189
|
);
|
|
@@ -227,9 +228,9 @@ const previewColumns = computed(() => {
|
|
|
227
228
|
})
|
|
228
229
|
|
|
229
230
|
const previewData = computed(() => {
|
|
230
|
-
const data = []
|
|
231
|
+
const data: Record<string, any>[] = [];
|
|
231
232
|
preview.value?.data.forEach((row: any) => {
|
|
232
|
-
const dataMap = {};
|
|
233
|
+
const dataMap: Record<string, any> = {};
|
|
233
234
|
Object.keys(row).forEach((key: any, index: number) => {
|
|
234
235
|
let columnLabel = getColumnLabel(index)
|
|
235
236
|
let mappedFieldIndex = getMappedColumnName(index);
|
|
@@ -308,7 +309,7 @@ const importBannerClass = computed(() => {
|
|
|
308
309
|
})
|
|
309
310
|
|
|
310
311
|
const mapping = computed(() => {
|
|
311
|
-
let warningMap = []
|
|
312
|
+
let warningMap: string[][] = [];
|
|
312
313
|
if (!preview.value?.warnings?.length) return [];
|
|
313
314
|
preview.value.warnings.forEach((warning: any) => {
|
|
314
315
|
const regex = /<strong>(.*?)<\/strong>/g;
|
|
@@ -335,7 +336,7 @@ const pageRoute = computed(() => {
|
|
|
335
336
|
return props.doctypeMap[props.data.reference_doctype]?.pageRoute
|
|
336
337
|
})
|
|
337
338
|
|
|
338
|
-
const redirectToPage = (docname) => {
|
|
339
|
+
const redirectToPage = (docname: string) => {
|
|
339
340
|
if (!pageRoute.value) return;
|
|
340
341
|
window.location.href = pageRoute.value.replace('docname', docname);
|
|
341
342
|
}
|
|
@@ -348,13 +349,7 @@ const tabButtons = computed(() => {
|
|
|
348
349
|
]
|
|
349
350
|
})
|
|
350
351
|
|
|
351
|
-
const
|
|
352
|
-
if (props.data?.status == 'Success') return 'green'
|
|
353
|
-
else if (props.data?.status == 'Error') return 'red'
|
|
354
|
-
else return 'orange'
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
const rowMessage = (row) => {
|
|
352
|
+
const rowMessage = (row: any) => {
|
|
358
353
|
return JSON.parse(row.messages)?.[0]?.message
|
|
359
354
|
}
|
|
360
355
|
</script>
|
|
@@ -12,12 +12,6 @@
|
|
|
12
12
|
:options="['Excel', 'CSV']"
|
|
13
13
|
type="select"
|
|
14
14
|
/>
|
|
15
|
-
<FormControl
|
|
16
|
-
label="Export Type"
|
|
17
|
-
v-model="exportType"
|
|
18
|
-
:options="['All Records', '5 Records', 'Blank Template']"
|
|
19
|
-
type="select"
|
|
20
|
-
/>
|
|
21
15
|
</div>
|
|
22
16
|
<div class="border-t">
|
|
23
17
|
<p class="mt-2 text-ink-gray-5">
|
|
@@ -35,14 +29,13 @@
|
|
|
35
29
|
</div>
|
|
36
30
|
<div class="grid grid-cols-2 gap-5">
|
|
37
31
|
<div v-for="field in fields.data[doctype]" :key="field.fieldname" class="flex items-center space-x-2">
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/>
|
|
32
|
+
<Checkbox
|
|
33
|
+
:id="`checkbox-${doctype}-${field.fieldname}`"
|
|
34
|
+
:checked="fieldSelection[doctype][field.fieldname]"
|
|
35
|
+
@change="(e: Event) => fieldSelection[doctype][field.fieldname] = (e.target as HTMLInputElement).checked"
|
|
36
|
+
/>
|
|
44
37
|
<label
|
|
45
|
-
:for="`checkbox-${field.fieldname}`"
|
|
38
|
+
:for="`checkbox-${doctype}-${field.fieldname}`"
|
|
46
39
|
:class="{
|
|
47
40
|
'text-ink-red-3': field.reqd
|
|
48
41
|
}">
|
|
@@ -65,10 +58,11 @@
|
|
|
65
58
|
</template>
|
|
66
59
|
<script setup lang="ts">
|
|
67
60
|
import { ref } from 'vue'
|
|
68
|
-
import type {
|
|
61
|
+
import type { DocField } from './types'
|
|
69
62
|
import { createResource } from '../../src/resources'
|
|
70
63
|
import { fieldsToIgnore, getChildTableName } from './dataImport'
|
|
71
64
|
import Button from "../../src/components/Button/Button.vue"
|
|
65
|
+
import Checkbox from "../../src/components/Checkbox/Checkbox.vue"
|
|
72
66
|
import Dialog from "../../src/components/Dialog/Dialog.vue"
|
|
73
67
|
import FormControl from "../../src/components/FormControl/FormControl.vue"
|
|
74
68
|
|
|
@@ -114,7 +108,7 @@ const prepareDoctypeMap = (docs: any[], doctypeMap: Record<string, { fieldname:
|
|
|
114
108
|
fieldname: field.fieldname,
|
|
115
109
|
label: field.label,
|
|
116
110
|
reqd: field.reqd,
|
|
117
|
-
disabled: doc.name == props.doctype && field.reqd
|
|
111
|
+
disabled: doc.name == props.doctype && field.reqd ? true : false,
|
|
118
112
|
}
|
|
119
113
|
})
|
|
120
114
|
})
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-base h-full flex flex-col w-[700px] mx-auto pt-12 space-y-8">
|
|
3
|
-
<div class="flex
|
|
4
|
-
<div class="flex
|
|
5
|
-
<div class="text-xl font-semibold text-ink-gray-9">
|
|
6
|
-
|
|
2
|
+
<div class="text-base h-full flex flex-col w-[85%] lg:w-[700px] mx-auto pt-12 space-y-8">
|
|
3
|
+
<div class="flex flex-col space-y-1 text-ink-gray-7">
|
|
4
|
+
<div class="flex items-center justify-between">
|
|
5
|
+
<div class="flex items-center space-x-2 text-xl font-semibold text-ink-gray-9">
|
|
6
|
+
<span>
|
|
7
|
+
Choose Import
|
|
8
|
+
</span>
|
|
9
|
+
<Badge v-if="data?.status" :theme="getBadgeColor(data?.status)">
|
|
10
|
+
{{ data?.status }}
|
|
11
|
+
</Badge>
|
|
7
12
|
</div>
|
|
8
|
-
<div>
|
|
9
|
-
Import data into your system using CSV files or Google Sheets.
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
12
|
-
<div class="space-x-2">
|
|
13
|
-
<Badge v-if="data?.status" :theme="statusTheme" size="lg">
|
|
14
|
-
{{ data?.status }}
|
|
15
|
-
</Badge>
|
|
16
13
|
<Button
|
|
17
14
|
variant="solid"
|
|
18
15
|
@click="saveImport"
|
|
@@ -21,6 +18,9 @@
|
|
|
21
18
|
Continue
|
|
22
19
|
</Button>
|
|
23
20
|
</div>
|
|
21
|
+
<div class="leading-5">
|
|
22
|
+
Import data into your system using CSV files or Google Sheets.
|
|
23
|
+
</div>
|
|
24
24
|
</div>
|
|
25
25
|
|
|
26
26
|
<div class="space-y-4">
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
@dragover.prevent
|
|
30
30
|
@drop.prevent="(e) => uploadFile(e)"
|
|
31
31
|
class="h-[300px] flex items-center justify-center bg-surface-gray-1 border border-dashed border-outline-gray-3 rounded-md">
|
|
32
|
-
<div v-if="showFileSelector && !uploading" class="w-2/5 text-center">
|
|
32
|
+
<div v-if="showFileSelector && !uploading" class="w-4/5 lg:w-2/5 text-center">
|
|
33
33
|
<FeatherIcon name="upload-cloud" class="size-6 stroke-1.5 text-ink-gray-6 mx-auto mb-2.5" />
|
|
34
34
|
<input
|
|
35
35
|
ref="fileInput"
|
|
@@ -40,16 +40,14 @@
|
|
|
40
40
|
/>
|
|
41
41
|
<div class="leading-5">
|
|
42
42
|
Drag and drop a CSV file, or upload from your
|
|
43
|
-
<span @click="openFileSelector" class="cursor-pointer font-semibold hover:underline">
|
|
44
|
-
Device
|
|
45
|
-
</span>
|
|
43
|
+
<span @click="openFileSelector" class="cursor-pointer font-semibold hover:underline">Device</span>
|
|
46
44
|
or
|
|
47
45
|
<span @click="openSheetSelector" class="cursor-pointer font-semibold hover:underline">
|
|
48
46
|
Google Sheet
|
|
49
47
|
</span>
|
|
50
48
|
</div>
|
|
51
49
|
</div>
|
|
52
|
-
<div v-else-if="showFileSelector && uploading" class="w-2/5 bg-surface-white border rounded-md p-2">
|
|
50
|
+
<div v-else-if="showFileSelector && uploading" class="w-4/5 lg:w-2/5 bg-surface-white border rounded-md p-2">
|
|
53
51
|
<div class="space-y-2">
|
|
54
52
|
<div class="font-medium">
|
|
55
53
|
{{ uploadingdFile.name }}
|
|
@@ -67,9 +65,9 @@
|
|
|
67
65
|
</div>
|
|
68
66
|
</div>
|
|
69
67
|
<div v-else-if="importFile" class="h-[300px] flex items-center justify-center bg-surface-gray-1 border border-dashed border-outline-gray-3 rounded-md">
|
|
70
|
-
<div class="w-2/5 bg-surface-white border rounded-md p-2 flex items-center justify-between items-center">
|
|
68
|
+
<div class="w-4/5 lg:w-2/5 bg-surface-white border rounded-md p-2 flex items-center justify-between items-center">
|
|
71
69
|
<div class="space-y-2">
|
|
72
|
-
<div class="font-medium">
|
|
70
|
+
<div class="font-medium leading-5">
|
|
73
71
|
{{ importFile.file_name || importFile.split("/").pop() }}
|
|
74
72
|
</div>
|
|
75
73
|
<div v-if="importFile.file_size" class="text-ink-gray-6">
|
|
@@ -79,7 +77,7 @@
|
|
|
79
77
|
<FeatherIcon
|
|
80
78
|
name="trash-2"
|
|
81
79
|
class="size-4 stroke-1.5 text-ink-red-3 cursor-pointer"
|
|
82
|
-
@click="
|
|
80
|
+
@click="deleteFile"
|
|
83
81
|
/>
|
|
84
82
|
</div>
|
|
85
83
|
</div>
|
|
@@ -91,7 +89,7 @@
|
|
|
91
89
|
Google Sheet
|
|
92
90
|
</div>
|
|
93
91
|
</div>
|
|
94
|
-
<div class="flex-1 flex flex-col items-center justify-center w-[400px] mx-auto space-y-3">
|
|
92
|
+
<div class="flex-1 flex flex-col items-center justify-center w-[95%] lg:w-[400px] mx-auto space-y-3">
|
|
95
93
|
<input
|
|
96
94
|
v-model="googleSheet"
|
|
97
95
|
type="text"
|
|
@@ -108,19 +106,19 @@
|
|
|
108
106
|
<Dropdown
|
|
109
107
|
:options="[
|
|
110
108
|
{
|
|
111
|
-
label:
|
|
109
|
+
label: 'Mandatory Fields',
|
|
112
110
|
onClick() {
|
|
113
111
|
exportTemplate('mandatory')
|
|
114
112
|
},
|
|
115
113
|
},
|
|
116
114
|
{
|
|
117
|
-
label:
|
|
115
|
+
label: 'All Fields',
|
|
118
116
|
onClick() {
|
|
119
117
|
exportTemplate('all')
|
|
120
118
|
},
|
|
121
119
|
},
|
|
122
120
|
{
|
|
123
|
-
label:
|
|
121
|
+
label: 'Custom Template',
|
|
124
122
|
onClick() {
|
|
125
123
|
showTemplateModal = true
|
|
126
124
|
},
|
|
@@ -146,32 +144,30 @@
|
|
|
146
144
|
</div>
|
|
147
145
|
|
|
148
146
|
<TemplateModal
|
|
147
|
+
v-if="props.doctype || props.data?.reference_doctype"
|
|
149
148
|
v-model="showTemplateModal"
|
|
150
|
-
:doctype="props.doctype || props.data?.reference_doctype"
|
|
149
|
+
:doctype="props.doctype || props.data?.reference_doctype as string"
|
|
151
150
|
/>
|
|
152
151
|
</div>
|
|
153
152
|
</template>
|
|
154
153
|
<script setup lang="ts">
|
|
155
|
-
import { computed, ref, watch } from 'vue'
|
|
154
|
+
import { computed, nextTick, ref, watch } from 'vue'
|
|
156
155
|
import { useRouter } from 'vue-router'
|
|
157
|
-
import type { DataImports, DataImport } from './types'
|
|
156
|
+
import type { DataImports, DataImport, DocField } from './types'
|
|
158
157
|
import { toast } from "../../src/components/Toast/index"
|
|
159
|
-
import {
|
|
160
|
-
import { fieldsToIgnore, getChildTableName } from './dataImport'
|
|
158
|
+
import { fieldsToIgnore, getChildTableName, getBadgeColor } from './dataImport'
|
|
161
159
|
import Badge from '../../src/components/Badge/Badge.vue'
|
|
162
160
|
import Button from '../../src/components/Button/Button.vue'
|
|
163
|
-
import call from '../../src/utils/call';
|
|
164
161
|
import Dropdown from '../../src/components/Dropdown/Dropdown.vue'
|
|
165
162
|
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
166
163
|
import FileUploadHandler from '../../src/utils/fileUploadHandler';
|
|
167
|
-
import FormControl from '../../src/components/FormControl/FormControl.vue'
|
|
168
164
|
import TemplateModal from './TemplateModal.vue'
|
|
169
165
|
|
|
170
166
|
const emit = defineEmits(['updateStep'])
|
|
171
|
-
const importFile = ref<
|
|
167
|
+
const importFile = ref<any | null>(null)
|
|
172
168
|
const googleSheet = ref<string>('')
|
|
173
169
|
const uploading = ref(false)
|
|
174
|
-
const uploadingdFile = ref<
|
|
170
|
+
const uploadingdFile = ref<any | null>(null)
|
|
175
171
|
const uploaded = ref(0)
|
|
176
172
|
const total = ref(0)
|
|
177
173
|
const showTemplateModal = ref(false)
|
|
@@ -185,7 +181,7 @@ const props = defineProps<{
|
|
|
185
181
|
dataImports: DataImports
|
|
186
182
|
doctype?: string
|
|
187
183
|
fields: any
|
|
188
|
-
data
|
|
184
|
+
data: DataImport | null
|
|
189
185
|
}>()
|
|
190
186
|
|
|
191
187
|
const uploadProgress = computed(() => {
|
|
@@ -248,12 +244,12 @@ const saveImport = () => {
|
|
|
248
244
|
|
|
249
245
|
const createImport = () => {
|
|
250
246
|
props.dataImports.insert.submit({
|
|
251
|
-
reference_doctype: props.doctype
|
|
247
|
+
reference_doctype: props.doctype!,
|
|
252
248
|
import_type: "Insert New Records",
|
|
253
|
-
mute_emails:
|
|
249
|
+
mute_emails: true,
|
|
254
250
|
status: 'Pending',
|
|
255
251
|
google_sheets_url: googleSheet.value.trim(),
|
|
256
|
-
import_file: importFile.value,
|
|
252
|
+
import_file: importFile.value?.file_url,
|
|
257
253
|
}, {
|
|
258
254
|
onSuccess(data: DataImport) {
|
|
259
255
|
router.replace({
|
|
@@ -267,23 +263,30 @@ const createImport = () => {
|
|
|
267
263
|
})
|
|
268
264
|
},
|
|
269
265
|
onError(error: any) {
|
|
270
|
-
toast.error(error)
|
|
266
|
+
toast.error(error.messages?.[0] || error)
|
|
271
267
|
console.error('Error creating data import:', error)
|
|
272
268
|
}
|
|
273
269
|
})
|
|
274
270
|
}
|
|
275
271
|
|
|
276
272
|
const updateImport = () => {
|
|
273
|
+
if (!props.data) return;
|
|
277
274
|
props.dataImports.setValue.submit({
|
|
278
275
|
...props.data,
|
|
279
276
|
google_sheets_url: googleSheet.value.trim(),
|
|
280
|
-
import_file: importFile.value,
|
|
277
|
+
import_file: importFile.value ? importFile.value.file_url : '',
|
|
281
278
|
}, {
|
|
282
279
|
onSuccess(data: DataImport) {
|
|
283
|
-
|
|
280
|
+
nextTick(() => {
|
|
281
|
+
if (importFile.value || googleSheet.value.trim().length) {
|
|
282
|
+
emit('updateStep', 'map', data)
|
|
283
|
+
} else {
|
|
284
|
+
emit('updateStep', 'upload', data)
|
|
285
|
+
}
|
|
286
|
+
})
|
|
284
287
|
},
|
|
285
288
|
onError(error: any) {
|
|
286
|
-
toast.error(error)
|
|
289
|
+
toast.error(error.messages?.[0] || error, { duration: 1000 })
|
|
287
290
|
console.error('Error updating data import:', error)
|
|
288
291
|
}
|
|
289
292
|
})
|
|
@@ -304,10 +307,11 @@ const exportTemplate = async (type: 'mandatory' | 'all') => {
|
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
const getExportURL = (type: 'mandatory' | 'all') => {
|
|
310
|
+
if (!props.doctype && !props.data?.reference_doctype) return ''
|
|
307
311
|
let exportFields = getExportFields(type)
|
|
308
312
|
|
|
309
313
|
return `/api/method/frappe.core.doctype.data_import.data_import.download_template
|
|
310
|
-
?doctype=${encodeURIComponent(props.doctype)}
|
|
314
|
+
?doctype=${encodeURIComponent(props.doctype || props.data?.reference_doctype as string)}
|
|
311
315
|
&export_fields=${encodeURIComponent(JSON.stringify(exportFields))}
|
|
312
316
|
&export_records=blank_template
|
|
313
317
|
&file_type=CSV`
|
|
@@ -329,7 +333,7 @@ const getMandatoryFields = () => {
|
|
|
329
333
|
}).map((field: DocField) => field.fieldname)
|
|
330
334
|
exportableFields.unshift('name')
|
|
331
335
|
return {
|
|
332
|
-
[props.doctype]: exportableFields
|
|
336
|
+
[props.doctype || props.data?.reference_doctype as string]: exportableFields
|
|
333
337
|
}
|
|
334
338
|
}
|
|
335
339
|
|
|
@@ -341,7 +345,7 @@ const getAllFields = () => {
|
|
|
341
345
|
return !fieldsToIgnore.includes(field.fieldtype)
|
|
342
346
|
}).map((field: DocField) => field.fieldname)
|
|
343
347
|
exportableFields.unshift('name')
|
|
344
|
-
let doctypeName = doc.name == props.doctype ? doc.name : getChildTableName(doc.name, props.doctype, docs)
|
|
348
|
+
let doctypeName = doc.name == props.doctype ? doc.name : getChildTableName(doc.name, props.doctype || props.data?.reference_doctype as string, docs)
|
|
345
349
|
doctypeMap[doctypeName] = exportableFields
|
|
346
350
|
})
|
|
347
351
|
return doctypeMap
|
|
@@ -378,17 +382,17 @@ watch(() => props.data, () => {
|
|
|
378
382
|
}
|
|
379
383
|
}, { immediate: true })
|
|
380
384
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
385
|
+
watch([importFile, googleSheet], () => {
|
|
386
|
+
if (!importFile.value || !googleSheet.value.trim().length) {
|
|
387
|
+
updateImport()
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
const deleteFile = () => {
|
|
392
|
+
importFile.value = null
|
|
393
|
+
}
|
|
384
394
|
|
|
385
395
|
const convertToKB = (bytes: number) => {
|
|
386
396
|
return (bytes / 1024).toFixed(2) + ' KB'
|
|
387
397
|
}
|
|
388
|
-
|
|
389
|
-
const statusTheme = computed(() => {
|
|
390
|
-
if (props.data?.status == 'Success') return 'green'
|
|
391
|
-
else if (props.data?.status == 'Error') return 'red'
|
|
392
|
-
else return 'orange'
|
|
393
|
-
})
|
|
394
398
|
</script>
|
|
@@ -3,17 +3,16 @@ import type { DataImportStatus } from './types'
|
|
|
3
3
|
import call from '../../src/utils/call';
|
|
4
4
|
|
|
5
5
|
export const getBadgeColor = (status: DataImportStatus) => {
|
|
6
|
-
const colorMap
|
|
6
|
+
const colorMap = {
|
|
7
7
|
"Pending": "orange",
|
|
8
8
|
"Success": "green",
|
|
9
9
|
"Partial Success": "orange",
|
|
10
10
|
"Error": "red",
|
|
11
11
|
"Timed Out": "orange"
|
|
12
|
-
}
|
|
12
|
+
} as const;
|
|
13
13
|
return colorMap[status as DataImportStatus] || "gray"
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
17
16
|
export const fieldsToIgnore = [
|
|
18
17
|
"Section Break",
|
|
19
18
|
"Column Break",
|
|
@@ -41,13 +40,13 @@ export const getChildTableName = (doctype: string, parentDocType: string, docs:
|
|
|
41
40
|
return childTableName
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
export const getPreviewData = (importName: string, file: string | undefined, sheet: string) => {
|
|
43
|
+
export const getPreviewData = (importName: string, file: string | undefined, sheet: string | undefined) => {
|
|
45
44
|
return call("frappe.core.doctype.data_import.data_import.get_preview_from_template", {
|
|
46
45
|
data_import: importName,
|
|
47
46
|
import_file: file,
|
|
48
47
|
google_sheets_url: sheet
|
|
49
48
|
}).catch((error: any) => {
|
|
50
|
-
toast.error(error)
|
|
49
|
+
toast.error(error.messages?.[0] || error)
|
|
51
50
|
console.error("Error fetching preview data:", error)
|
|
52
51
|
})
|
|
53
52
|
}
|
|
@@ -3,7 +3,7 @@ export interface DataImportProps {
|
|
|
3
3
|
description?: string
|
|
4
4
|
doctype?: string | null
|
|
5
5
|
importName?: string | null
|
|
6
|
-
doctypeMap?: Record<string, { title: string;
|
|
6
|
+
doctypeMap?: Record<string, { title: string; listRoute?: string; pageRoute?: string }>
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface DataImport {
|
|
@@ -15,14 +15,17 @@ export interface DataImport {
|
|
|
15
15
|
mute_emails: boolean
|
|
16
16
|
import_file?: string
|
|
17
17
|
google_sheets_url?: string
|
|
18
|
+
template_options?: string
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface DataImports {
|
|
21
22
|
data: DataImport[]
|
|
22
23
|
update: (args: { filters: any[] }) => void
|
|
23
|
-
insert: { submit: (params: DataImport, options: { validate
|
|
24
|
+
insert: { submit: (params: DataImport, options: { validate?: () => boolean; onSuccess: (data: DataImport) => void; onError: (err: any) => void }) => void }
|
|
24
25
|
setValue: { submit: (params: DataImport, options: { onSuccess: (data: DataImport) => void; onError: (err: any) => void }) => void }
|
|
25
26
|
reload: () => void
|
|
27
|
+
hasNextPage: () => boolean
|
|
28
|
+
next: () => Promise<void>
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export type DataImportStatus = "Pending" | "Success" | "Partial Success" | "Error" | "Timed Out"
|
package/frappe/Link/types.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface LinkProps {
|
|
|
5
5
|
variant?: ComboboxVariant
|
|
6
6
|
label?: string
|
|
7
7
|
placeholder?: string
|
|
8
|
-
filters?: Record<string, string | [string, string]>
|
|
8
|
+
filters?: Record<string, string | [string, string] | boolean | number>
|
|
9
9
|
required?: boolean
|
|
10
10
|
allowCreate?: boolean
|
|
11
11
|
}
|
package/frappe/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ import { h } from 'vue'
|
|
|
12
12
|
|
|
13
13
|
const props = defineProps<TabProps>()
|
|
14
14
|
|
|
15
|
-
const indicatorXCss = `left-0 bottom-0 h-[
|
|
15
|
+
const indicatorXCss = `left-0 bottom-0 h-[2px] w-[--reka-tabs-indicator-size] transition-[width,transform]
|
|
16
16
|
translate-x-[--reka-tabs-indicator-position] translate-y-[1px]`
|
|
17
17
|
|
|
18
18
|
const indicatorYCss = `right-0 w-[1px] h-[--reka-tabs-indicator-size] transition-[height,transform]
|
|
@@ -26,12 +26,13 @@ const Btn = h('button')
|
|
|
26
26
|
<template>
|
|
27
27
|
<TabsRoot
|
|
28
28
|
:as="props.as"
|
|
29
|
-
class="data-[orientation=vertical]:flex"
|
|
29
|
+
class="flex flex-1 overflow-hidden flex-col data-[orientation=vertical]:flex-row [&_[role='tabpanel']:not([hidden])]:flex [&_[role='tabpanel']:not([hidden])]:grow"
|
|
30
30
|
:orientation="props.vertical ? 'vertical' : 'horizontal'"
|
|
31
31
|
:default-value="props.tabs[0].label"
|
|
32
32
|
>
|
|
33
33
|
<TabsList
|
|
34
|
-
class="relative flex data-[orientation=vertical]:flex-col p-1 border-b
|
|
34
|
+
class="relative flex data-[orientation=vertical]:flex-col p-1 border-b data-[orientation=vertical]:border-r"
|
|
35
|
+
:class="{ 'overflow-auto': !props.vertical }"
|
|
35
36
|
>
|
|
36
37
|
<TabsIndicator
|
|
37
38
|
class="absolute rounded-full duration-300"
|
|
@@ -46,7 +47,7 @@ const Btn = h('button')
|
|
|
46
47
|
:is="tab.route ? 'router-link' : Btn"
|
|
47
48
|
:to="tab.route"
|
|
48
49
|
class="flex items-center gap-1.5 text-base text-ink-gray-5 duration-300 ease-in-out hover:text-ink-gray-9 p-2.5 data-[state=active]:text-ink-gray-9"
|
|
49
|
-
:class="{ 'py-2.5': props.vertical
|
|
50
|
+
:class="{ 'py-2.5': props.vertical }"
|
|
50
51
|
>
|
|
51
52
|
<component v-if="tab.icon" :is="tab.icon" class="size-4">
|
|
52
53
|
</component>
|
|
@@ -58,7 +59,9 @@ const Btn = h('button')
|
|
|
58
59
|
</TabsList>
|
|
59
60
|
|
|
60
61
|
<TabsContent v-for="(tab, i) in props.tabs" :value="i">
|
|
61
|
-
<
|
|
62
|
+
<div class="flex flex-col flex-1 grow">
|
|
63
|
+
<slot name="tab-panel" v-bind="{ tab }" />
|
|
64
|
+
</div>
|
|
62
65
|
</TabsContent>
|
|
63
66
|
</TabsRoot>
|
|
64
67
|
</template>
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="space-y-1.5">
|
|
3
|
-
<label v-if="attrs.label" class="block text-xs text-ink-gray-5">
|
|
4
|
-
{{ attrs.label }}
|
|
5
|
-
<span class="text-ink-red-3" v-if="attrs.required">*</span>
|
|
6
|
-
</label>
|
|
7
|
-
<Autocomplete
|
|
8
|
-
ref="autocomplete"
|
|
9
|
-
:options="options.data"
|
|
10
|
-
v-model="value"
|
|
11
|
-
:size="attrs.size || 'sm'"
|
|
12
|
-
:variant="attrs.variant"
|
|
13
|
-
:placeholder="attrs.placeholder"
|
|
14
|
-
:filterable="false"
|
|
15
|
-
:readonly="attrs.readonly"
|
|
16
|
-
>
|
|
17
|
-
<template #target="{ open, togglePopover }">
|
|
18
|
-
<slot name="target" v-bind="{ open, togglePopover }" />
|
|
19
|
-
</template>
|
|
20
|
-
|
|
21
|
-
<template #prefix>
|
|
22
|
-
<slot name="prefix" />
|
|
23
|
-
</template>
|
|
24
|
-
|
|
25
|
-
<template #item-prefix="{ active, selected, option }">
|
|
26
|
-
<slot name="item-prefix" v-bind="{ active, selected, option }" />
|
|
27
|
-
</template>
|
|
28
|
-
|
|
29
|
-
<template #item-label="{ active, selected, option }">
|
|
30
|
-
<slot name="item-label" v-bind="{ active, selected, option }" />
|
|
31
|
-
</template>
|
|
32
|
-
|
|
33
|
-
<template #footer="{ value, close }">
|
|
34
|
-
<div v-if="attrs.onCreate">
|
|
35
|
-
<Button
|
|
36
|
-
variant="ghost"
|
|
37
|
-
class="w-full !justify-start"
|
|
38
|
-
label="Create New"
|
|
39
|
-
@click="(attrs as any).onCreate(value, close)"
|
|
40
|
-
>
|
|
41
|
-
<template #prefix>
|
|
42
|
-
<Plus class="h-4 w-4 stroke-1.5" />
|
|
43
|
-
</template>
|
|
44
|
-
</Button>
|
|
45
|
-
</div>
|
|
46
|
-
<div>
|
|
47
|
-
<Button
|
|
48
|
-
variant="ghost"
|
|
49
|
-
class="w-full !justify-start"
|
|
50
|
-
label="Clear"
|
|
51
|
-
@click="() => clearValue(close)"
|
|
52
|
-
>
|
|
53
|
-
<template #prefix>
|
|
54
|
-
<X class="h-4 w-4 stroke-1.5" />
|
|
55
|
-
</template>
|
|
56
|
-
</Button>
|
|
57
|
-
</div>
|
|
58
|
-
</template>
|
|
59
|
-
</Autocomplete>
|
|
60
|
-
<p v-if="description" class="text-sm text-ink-gray-5">
|
|
61
|
-
{{ description }}
|
|
62
|
-
</p>
|
|
63
|
-
</div>
|
|
64
|
-
</template>
|
|
65
|
-
|
|
66
|
-
<script setup lang="ts">
|
|
67
|
-
import { watchDebounced } from '@vueuse/core'
|
|
68
|
-
import { useAttrs, computed, ref } from 'vue'
|
|
69
|
-
import { Plus, X } from 'lucide-vue-next'
|
|
70
|
-
import { createResource } from "../../../src/resources"
|
|
71
|
-
import Autocomplete from "../../../src/components/Autocomplete/Autocomplete.vue"
|
|
72
|
-
import Button from "../../../src/components/Button/Button.vue"
|
|
73
|
-
|
|
74
|
-
const props = defineProps({
|
|
75
|
-
doctype: {
|
|
76
|
-
type: String,
|
|
77
|
-
required: true,
|
|
78
|
-
},
|
|
79
|
-
filters: {
|
|
80
|
-
type: Object,
|
|
81
|
-
default: () => ({}),
|
|
82
|
-
},
|
|
83
|
-
modelValue: {
|
|
84
|
-
type: String,
|
|
85
|
-
default: '',
|
|
86
|
-
},
|
|
87
|
-
description: {
|
|
88
|
-
type: String,
|
|
89
|
-
default: '',
|
|
90
|
-
},
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
const emit = defineEmits(['update:modelValue', 'change'])
|
|
94
|
-
const attrs = useAttrs()
|
|
95
|
-
const valuePropPassed = computed(() => 'value' in attrs)
|
|
96
|
-
const autocomplete = ref<{ query: string } | null>(null)
|
|
97
|
-
const text = ref('')
|
|
98
|
-
|
|
99
|
-
const value = computed({
|
|
100
|
-
get: () => (valuePropPassed.value ? attrs.value : props.modelValue),
|
|
101
|
-
set: (val: { value: string }) => {
|
|
102
|
-
return (
|
|
103
|
-
val?.value &&
|
|
104
|
-
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val?.value)
|
|
105
|
-
)
|
|
106
|
-
},
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
watchDebounced(
|
|
110
|
-
() => autocomplete.value?.query,
|
|
111
|
-
(val) => {
|
|
112
|
-
val = val || ''
|
|
113
|
-
if (text.value === val) return
|
|
114
|
-
text.value = val
|
|
115
|
-
reload(val)
|
|
116
|
-
},
|
|
117
|
-
{ debounce: 300, immediate: true }
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
watchDebounced(
|
|
121
|
-
() => props.doctype,
|
|
122
|
-
() => reload(''),
|
|
123
|
-
{ debounce: 300, immediate: true }
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
const options = createResource({
|
|
127
|
-
url: 'frappe.desk.search.search_link',
|
|
128
|
-
cache: [props.doctype, text.value],
|
|
129
|
-
method: 'POST',
|
|
130
|
-
auto: true,
|
|
131
|
-
params: {
|
|
132
|
-
txt: text.value,
|
|
133
|
-
doctype: props.doctype,
|
|
134
|
-
filters: props.filters,
|
|
135
|
-
},
|
|
136
|
-
transform: (data: { label: string; value: string; description: string }[]) => {
|
|
137
|
-
return data.map((option) => {
|
|
138
|
-
return {
|
|
139
|
-
label: option.label || option.value,
|
|
140
|
-
value: option.value,
|
|
141
|
-
description: option.description,
|
|
142
|
-
}
|
|
143
|
-
})
|
|
144
|
-
},
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
const reload = (val: string) => {
|
|
148
|
-
options.update({
|
|
149
|
-
params: {
|
|
150
|
-
txt: val,
|
|
151
|
-
doctype: props.doctype,
|
|
152
|
-
filters: props.filters,
|
|
153
|
-
},
|
|
154
|
-
})
|
|
155
|
-
options.reload()
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const clearValue = (close: () => void) => {
|
|
159
|
-
emit(valuePropPassed.value ? 'change' : 'update:modelValue', '')
|
|
160
|
-
close()
|
|
161
|
-
}
|
|
162
|
-
</script>
|