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.
@@ -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
- fields.reload({
107
- doctype: data.value?.reference_doctype,
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', query: {
144
- step: 'list'
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
- window.location.href = `/data-import/${importName}`;
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 File
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' | 'new' | 'map' | 'preview'
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 pt-12 space-y-8">
3
- <div class="flex items-center justify-between">
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
- Map Data
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="Start Over" @click="startOver" />
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, props.data.import_file, props.data.google_sheets_url);
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
- let columnToFieldMap = JSON.parse(props.data?.template_options)?.["column_to_field_map"] || {};
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
- ...JSON.parse(props.data?.template_options),
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.reference_doctype
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 startOver = () => {
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
- ...JSON.parse(props.data?.template_options),
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 items-center justify-between">
4
- <div class="space-y-2 text-ink-gray-7">
5
- <div class="text-lg font-semibold text-ink-gray-9">
6
- Review and Import
7
- </div>
8
- <div>
9
- Verify the data before starting the import process
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; route: 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 statusTheme = computed(() => {
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
- <FormControl
39
- type="checkbox"
40
- v-model="fieldSelection[doctype][field.fieldname]"
41
- :id="`checkbox-${field.fieldname}`"
42
- :disabled="field.disabled"
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 { DataImport, DocField } from './types'
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 items-center justify-between">
4
- <div class="flex flex-col space-y-2 text-ink-gray-7">
5
- <div class="text-xl font-semibold text-ink-gray-9">
6
- Choose Import
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="importFile = null"
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: __('Mandatory Fields'),
109
+ label: 'Mandatory Fields',
112
110
  onClick() {
113
111
  exportTemplate('mandatory')
114
112
  },
115
113
  },
116
114
  {
117
- label: __('All Field'),
115
+ label: 'All Fields',
118
116
  onClick() {
119
117
  exportTemplate('all')
120
118
  },
121
119
  },
122
120
  {
123
- label: __('Custom Template'),
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 { transformFields } from './dataImport'
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<File | null>(null)
167
+ const importFile = ref<any | null>(null)
172
168
  const googleSheet = ref<string>('')
173
169
  const uploading = ref(false)
174
- const uploadingdFile = ref<File | null>(null)
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?: DataImport
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: 1,
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
- emit('updateStep', 'map', data)
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
- const convertToMB = (bytes: number) => {
382
- return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
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: Record<DataImportStatus, string> = {
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; route: 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: () => boolean; onSuccess: (data: DataImport) => void; onError: (err: any) => void }) => void }
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"
@@ -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
@@ -51,3 +51,6 @@ declare module 'frappe-ui/frappe' {
51
51
  export const Link: Component
52
52
  export type { LinkProps } from './Link/types'
53
53
  }
54
+
55
+ // Data Import
56
+ export const DataImport: Component
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.226",
3
+ "version": "0.1.228",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -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-[1px] w-[--reka-tabs-indicator-size] transition-[width,transform]
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 data-[orientation=vertical]:border-r"
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
- <slot name="tab-panel" v-bind="{ tab }"> </slot>
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>