frappe-ui 0.1.215 → 0.1.216

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.
@@ -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>