frappe-ui 0.1.215 → 0.1.218
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frappe/Billing/SignupBanner.vue +1 -1
- package/frappe/Billing/TrialBanner.vue +1 -1
- package/frappe/DataImport/DataImport.vue +160 -0
- package/frappe/DataImport/DataImportList.vue +160 -0
- package/frappe/DataImport/ImportSteps.vue +114 -0
- package/frappe/DataImport/MappingStep.vue +167 -0
- package/frappe/DataImport/PreviewStep.vue +360 -0
- package/frappe/DataImport/TemplateModal.vue +221 -0
- package/frappe/DataImport/UploadStep.vue +394 -0
- package/frappe/DataImport/dataImport.ts +53 -0
- package/frappe/DataImport/types.ts +42 -0
- package/frappe/Help/HelpModal.vue +4 -4
- package/frappe/Onboarding/GettingStartedBanner.vue +1 -1
- package/frappe/components/Link/Link.vue +162 -0
- package/frappe/index.d.ts +53 -0
- package/frappe/index.js +3 -0
- package/package.json +25 -2
- package/src/components/Breadcrumbs/Breadcrumbs.vue +28 -32
- package/src/components/Calendar/Calendar.story.vue +1 -0
- package/src/components/Calendar/Calendar.vue +65 -3
- package/src/components/Charts/index.ts +0 -0
- package/src/components/Checkbox/Checkbox.vue +0 -1
- package/src/components/Checkbox/types.ts +0 -1
- package/src/components/Combobox/Combobox.story.vue +18 -0
- package/src/components/Combobox/Combobox.vue +13 -2
- package/src/components/Combobox/types.ts +2 -1
- package/src/components/DatePicker/index.ts +6 -0
- package/src/components/Dropdown/Dropdown.vue +19 -5
- package/src/components/Dropdown/types.ts +1 -0
- package/src/components/FileUploader/FileUploader.vue +6 -1
- package/src/components/ListView/ListGroupHeader.vue +1 -1
- package/src/components/ListView/ListGroupRows.vue +5 -0
- package/src/components/Select/Select.story.vue +16 -3
- package/src/components/Select/Select.vue +109 -96
- package/src/components/Sidebar/index.ts +3 -0
- package/src/components/Toast/Toast.vue +1 -1
- package/src/data-fetching/index.ts +4 -2
- package/src/index.ts +9 -23
- package/src/resources/{index.js → index.ts} +3 -1
- package/src/resources/{local.js → local.ts} +4 -4
- package/src/resources/realtime.ts +21 -0
- package/frappe/Icons/HelpIcon.vue +0 -16
- package/frappe/Icons/LightningIcon.vue +0 -16
- package/frappe/Icons/MaximizeIcon.vue +0 -19
- package/frappe/Icons/MinimizeIcon.vue +0 -19
- package/frappe/Icons/StepsIcon.vue +0 -16
- package/src/components/GreenCheckIcon.vue +0 -16
- package/src/icons/CircleCheck.vue +0 -9
- package/src/icons/DownSolid.vue +0 -8
- package/src/resources/realtime.js +0 -15
|
@@ -0,0 +1,394 @@
|
|
|
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
|
|
7
|
+
</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
|
+
<Button
|
|
17
|
+
variant="solid"
|
|
18
|
+
@click="saveImport"
|
|
19
|
+
:disabled="disableContinueButton"
|
|
20
|
+
>
|
|
21
|
+
Continue
|
|
22
|
+
</Button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="space-y-4">
|
|
27
|
+
<div
|
|
28
|
+
v-if="showFileSelector && !importFile"
|
|
29
|
+
@dragover.prevent
|
|
30
|
+
@drop.prevent="(e) => uploadFile(e)"
|
|
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">
|
|
33
|
+
<FeatherIcon name="upload-cloud" class="size-6 stroke-1.5 text-ink-gray-6 mx-auto mb-2.5" />
|
|
34
|
+
<input
|
|
35
|
+
ref="fileInput"
|
|
36
|
+
type="file"
|
|
37
|
+
accept=".csv"
|
|
38
|
+
class="hidden"
|
|
39
|
+
@change="(e) => uploadFile(e)"
|
|
40
|
+
/>
|
|
41
|
+
<div class="leading-5">
|
|
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>
|
|
46
|
+
or
|
|
47
|
+
<span @click="openSheetSelector" class="cursor-pointer font-semibold hover:underline">
|
|
48
|
+
Google Sheet
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div v-else-if="showFileSelector && uploading" class="w-2/5 bg-surface-white border rounded-md p-2">
|
|
53
|
+
<div class="space-y-2">
|
|
54
|
+
<div class="font-medium">
|
|
55
|
+
{{ uploadingdFile.name }}
|
|
56
|
+
</div>
|
|
57
|
+
<div class="text-ink-gray-6">
|
|
58
|
+
{{ convertToKB(uploaded) }} of {{ convertToKB(total) }}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="w-full bg-surface-gray-1 h-1 rounded-full mt-3">
|
|
62
|
+
<div
|
|
63
|
+
class="bg-surface-gray-7 h-1 rounded-full transition-all duration-500 ease-in-out"
|
|
64
|
+
:style="`width: ${uploadProgress}%`"
|
|
65
|
+
></div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<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">
|
|
71
|
+
<div class="space-y-2">
|
|
72
|
+
<div class="font-medium">
|
|
73
|
+
{{ importFile.file_name || importFile.split("/").pop() }}
|
|
74
|
+
</div>
|
|
75
|
+
<div v-if="importFile.file_size" class="text-ink-gray-6">
|
|
76
|
+
{{ convertToKB(importFile.file_size) }}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<FeatherIcon
|
|
80
|
+
name="trash-2"
|
|
81
|
+
class="size-4 stroke-1.5 text-ink-red-3 cursor-pointer"
|
|
82
|
+
@click="importFile = null"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div v-else-if="showSheetSelector" class="flex flex-col h-[300px] p-4 border border-dashed border-outline-gray-3 rounded-md">
|
|
88
|
+
<div class="flex items-center space-x-2 text-ink-gray-7">
|
|
89
|
+
<FeatherIcon name="chevron-left" class="size-4 cursor-pointer" @click="backToFileSelector" />
|
|
90
|
+
<div>
|
|
91
|
+
Google Sheet
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="flex-1 flex flex-col items-center justify-center w-[400px] mx-auto space-y-3">
|
|
95
|
+
<input
|
|
96
|
+
v-model="googleSheet"
|
|
97
|
+
type="text"
|
|
98
|
+
class="w-full border border-outline-gray-2 rounded-md px-2.5 text-base"
|
|
99
|
+
placeholder="Add Google Sheets Link"
|
|
100
|
+
/>
|
|
101
|
+
<div class="text-ink-gray-5">
|
|
102
|
+
Make sure the link is publically accessible to fetch the data.
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="flex justify-end">
|
|
108
|
+
<Dropdown
|
|
109
|
+
:options="[
|
|
110
|
+
{
|
|
111
|
+
label: __('Mandatory Fields'),
|
|
112
|
+
onClick() {
|
|
113
|
+
exportTemplate('mandatory')
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: __('All Field'),
|
|
118
|
+
onClick() {
|
|
119
|
+
exportTemplate('all')
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: __('Custom Template'),
|
|
124
|
+
onClick() {
|
|
125
|
+
showTemplateModal = true
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
]"
|
|
129
|
+
>
|
|
130
|
+
<template v-slot="{ open }">
|
|
131
|
+
<Button variant="ghost">
|
|
132
|
+
<template #prefix>
|
|
133
|
+
<FeatherIcon name="download" class="size-4 stroke-1.5" />
|
|
134
|
+
</template>
|
|
135
|
+
Download CSV Template
|
|
136
|
+
<template #suffix>
|
|
137
|
+
<FeatherIcon name="chevron-down" :class="[
|
|
138
|
+
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
|
|
139
|
+
open ? 'rotate-180' : '',
|
|
140
|
+
]" />
|
|
141
|
+
</template>
|
|
142
|
+
</Button>
|
|
143
|
+
</template>
|
|
144
|
+
</Dropdown>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<TemplateModal
|
|
149
|
+
v-model="showTemplateModal"
|
|
150
|
+
:doctype="props.doctype || props.data?.reference_doctype"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
154
|
+
<script setup lang="ts">
|
|
155
|
+
import { computed, ref, watch } from 'vue'
|
|
156
|
+
import { useRouter } from 'vue-router'
|
|
157
|
+
import type { DataImports, DataImport } from './types'
|
|
158
|
+
import { toast } from "../../src/components/Toast/index"
|
|
159
|
+
import { transformFields } from './dataImport'
|
|
160
|
+
import { fieldsToIgnore, getChildTableName } from './dataImport'
|
|
161
|
+
import Badge from '../../src/components/Badge/Badge.vue'
|
|
162
|
+
import Button from '../../src/components/Button/Button.vue'
|
|
163
|
+
import call from '../../src/utils/call';
|
|
164
|
+
import Dropdown from '../../src/components/Dropdown/Dropdown.vue'
|
|
165
|
+
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
166
|
+
import FileUploadHandler from '../../src/utils/fileUploadHandler';
|
|
167
|
+
import FormControl from '../../src/components/FormControl/FormControl.vue'
|
|
168
|
+
import TemplateModal from './TemplateModal.vue'
|
|
169
|
+
|
|
170
|
+
const emit = defineEmits(['updateStep'])
|
|
171
|
+
const importFile = ref<File | null>(null)
|
|
172
|
+
const googleSheet = ref<string>('')
|
|
173
|
+
const uploading = ref(false)
|
|
174
|
+
const uploadingdFile = ref<File | null>(null)
|
|
175
|
+
const uploaded = ref(0)
|
|
176
|
+
const total = ref(0)
|
|
177
|
+
const showTemplateModal = ref(false)
|
|
178
|
+
const fileInput = ref<HTMLInputElement | null>(null)
|
|
179
|
+
const showFileSelector = ref(true)
|
|
180
|
+
const showSheetSelector = ref(false)
|
|
181
|
+
const showLibrarySelector = ref(false)
|
|
182
|
+
const router = useRouter()
|
|
183
|
+
|
|
184
|
+
const props = defineProps<{
|
|
185
|
+
dataImports: DataImports
|
|
186
|
+
doctype?: string
|
|
187
|
+
fields: any
|
|
188
|
+
data?: DataImport
|
|
189
|
+
}>()
|
|
190
|
+
|
|
191
|
+
const uploadProgress = computed(() => {
|
|
192
|
+
if (total.value === 0) return 0
|
|
193
|
+
return Math.floor((uploaded.value / total.value) * 100)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const extractFile = (e: Event): File | null => {
|
|
197
|
+
const inputFiles = (e.target as HTMLInputElement)?.files
|
|
198
|
+
const dt = (e as DragEvent).dataTransfer?.files
|
|
199
|
+
|
|
200
|
+
return inputFiles?.[0] || dt?.[0] || null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const uploadFile = (e: Event) => {
|
|
204
|
+
const file = extractFile(e)
|
|
205
|
+
if (!file) return;
|
|
206
|
+
|
|
207
|
+
if (file.type !== 'text/csv') {
|
|
208
|
+
toast.error('Please upload a valid CSV file.')
|
|
209
|
+
console.error('Please upload a valid CSV file.')
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
uploadingdFile.value = file
|
|
214
|
+
const uploader = new FileUploadHandler()
|
|
215
|
+
|
|
216
|
+
uploader.on("start", () => {
|
|
217
|
+
uploading.value = true
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
uploader.on("progress", (data: { uploaded: number, total: number }) => {
|
|
221
|
+
uploaded.value = data.uploaded
|
|
222
|
+
total.value = data.total
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
uploader.on("error", (error: any) => {
|
|
226
|
+
uploading.value = false
|
|
227
|
+
toast.error(error)
|
|
228
|
+
console.error('File upload error:', error)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
uploader.on("finish", () => {
|
|
232
|
+
uploading.value = false
|
|
233
|
+
})
|
|
234
|
+
uploader.upload(file, {}).then((data => {
|
|
235
|
+
importFile.value = data
|
|
236
|
+
})).catch(error => {
|
|
237
|
+
console.error('File upload error:', error)
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const saveImport = () => {
|
|
242
|
+
if (props.data?.name) {
|
|
243
|
+
updateImport()
|
|
244
|
+
} else {
|
|
245
|
+
createImport()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const createImport = () => {
|
|
250
|
+
props.dataImports.insert.submit({
|
|
251
|
+
reference_doctype: props.doctype,
|
|
252
|
+
import_type: "Insert New Records",
|
|
253
|
+
mute_emails: 1,
|
|
254
|
+
status: 'Pending',
|
|
255
|
+
google_sheets_url: googleSheet.value.trim(),
|
|
256
|
+
import_file: importFile.value,
|
|
257
|
+
}, {
|
|
258
|
+
onSuccess(data: DataImport) {
|
|
259
|
+
router.replace({
|
|
260
|
+
name: 'DataImport',
|
|
261
|
+
params: {
|
|
262
|
+
importName: data.name
|
|
263
|
+
},
|
|
264
|
+
query: {
|
|
265
|
+
step: 'map'
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
},
|
|
269
|
+
onError(error: any) {
|
|
270
|
+
toast.error(error)
|
|
271
|
+
console.error('Error creating data import:', error)
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const updateImport = () => {
|
|
277
|
+
props.dataImports.setValue.submit({
|
|
278
|
+
...props.data,
|
|
279
|
+
google_sheets_url: googleSheet.value.trim(),
|
|
280
|
+
import_file: importFile.value,
|
|
281
|
+
}, {
|
|
282
|
+
onSuccess(data: DataImport) {
|
|
283
|
+
emit('updateStep', 'map', data)
|
|
284
|
+
},
|
|
285
|
+
onError(error: any) {
|
|
286
|
+
toast.error(error)
|
|
287
|
+
console.error('Error updating data import:', error)
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const exportTemplate = async (type: 'mandatory' | 'all') => {
|
|
293
|
+
let url = getExportURL(type)
|
|
294
|
+
const response = await fetch(url)
|
|
295
|
+
const blob = await response.blob();
|
|
296
|
+
const link = document.createElement('a');
|
|
297
|
+
|
|
298
|
+
link.href = URL.createObjectURL(blob);
|
|
299
|
+
link.download = props.doctype + '.csv'
|
|
300
|
+
document.body.appendChild(link);
|
|
301
|
+
|
|
302
|
+
link.click();
|
|
303
|
+
document.body.removeChild(link);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const getExportURL = (type: 'mandatory' | 'all') => {
|
|
307
|
+
let exportFields = getExportFields(type)
|
|
308
|
+
|
|
309
|
+
return `/api/method/frappe.core.doctype.data_import.data_import.download_template
|
|
310
|
+
?doctype=${encodeURIComponent(props.doctype)}
|
|
311
|
+
&export_fields=${encodeURIComponent(JSON.stringify(exportFields))}
|
|
312
|
+
&export_records=blank_template
|
|
313
|
+
&file_type=CSV`
|
|
314
|
+
.replace(/\s+/g, '')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const getExportFields = (type: 'mandatory' | 'all') => {
|
|
318
|
+
/* {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']} */
|
|
319
|
+
if (type == "mandatory") {
|
|
320
|
+
return getMandatoryFields()
|
|
321
|
+
}
|
|
322
|
+
return getAllFields()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const getMandatoryFields = () => {
|
|
326
|
+
let parentDoctype = props.fields.data?.docs.find((doc: any) => doc.name == props.doctype)
|
|
327
|
+
let exportableFields = parentDoctype.fields.filter((field: DocField) => {
|
|
328
|
+
return !fieldsToIgnore.includes(field.fieldtype) && field.reqd
|
|
329
|
+
}).map((field: DocField) => field.fieldname)
|
|
330
|
+
exportableFields.unshift('name')
|
|
331
|
+
return {
|
|
332
|
+
[props.doctype]: exportableFields
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const getAllFields = () => {
|
|
337
|
+
let doctypeMap: Record<string, string[]> = {}
|
|
338
|
+
let docs = props.fields.data?.docs || []
|
|
339
|
+
docs.forEach((doc: any) => {
|
|
340
|
+
let exportableFields = doc.fields.filter((field: DocField) => {
|
|
341
|
+
return !fieldsToIgnore.includes(field.fieldtype)
|
|
342
|
+
}).map((field: DocField) => field.fieldname)
|
|
343
|
+
exportableFields.unshift('name')
|
|
344
|
+
let doctypeName = doc.name == props.doctype ? doc.name : getChildTableName(doc.name, props.doctype, docs)
|
|
345
|
+
doctypeMap[doctypeName] = exportableFields
|
|
346
|
+
})
|
|
347
|
+
return doctypeMap
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const openFileSelector = () => {
|
|
351
|
+
fileInput.value?.click()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const openSheetSelector = () => {
|
|
355
|
+
showFileSelector.value = false
|
|
356
|
+
showLibrarySelector.value = false
|
|
357
|
+
showSheetSelector.value = true
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const backToFileSelector = () => {
|
|
361
|
+
showFileSelector.value = true
|
|
362
|
+
showLibrarySelector.value = false
|
|
363
|
+
showSheetSelector.value = false
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const disableContinueButton = computed(() => {
|
|
367
|
+
return !importFile.value && !googleSheet.value.trim().length
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
watch(() => props.data, () => {
|
|
371
|
+
if (props.data) {
|
|
372
|
+
if (props.data.import_file) {
|
|
373
|
+
importFile.value = props.data.import_file
|
|
374
|
+
} else if (props.data.google_sheets_url) {
|
|
375
|
+
openSheetSelector()
|
|
376
|
+
googleSheet.value = props.data.google_sheets_url
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}, { immediate: true })
|
|
380
|
+
|
|
381
|
+
const convertToMB = (bytes: number) => {
|
|
382
|
+
return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const convertToKB = (bytes: number) => {
|
|
386
|
+
return (bytes / 1024).toFixed(2) + ' KB'
|
|
387
|
+
}
|
|
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
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { toast } from "../../src/components/Toast/index"
|
|
2
|
+
import type { DataImportStatus } from './types'
|
|
3
|
+
import call from '../../src/utils/call';
|
|
4
|
+
|
|
5
|
+
export const getBadgeColor = (status: DataImportStatus) => {
|
|
6
|
+
const colorMap: Record<DataImportStatus, string> = {
|
|
7
|
+
"Pending": "orange",
|
|
8
|
+
"Success": "green",
|
|
9
|
+
"Partial Success": "orange",
|
|
10
|
+
"Error": "red",
|
|
11
|
+
"Timed Out": "orange"
|
|
12
|
+
}
|
|
13
|
+
return colorMap[status as DataImportStatus] || "gray"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export const fieldsToIgnore = [
|
|
18
|
+
"Section Break",
|
|
19
|
+
"Column Break",
|
|
20
|
+
"Tab Break",
|
|
21
|
+
"HTML",
|
|
22
|
+
"Table",
|
|
23
|
+
"Table MultiSelect",
|
|
24
|
+
"Button",
|
|
25
|
+
"Image",
|
|
26
|
+
"Fold",
|
|
27
|
+
"Heading"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export const getChildTableName = (doctype: string, parentDocType: string, docs: any[]) => {
|
|
31
|
+
let childTableName = ''
|
|
32
|
+
let doctypeFields = docs.filter((doc: any) => {
|
|
33
|
+
return doc.name == parentDocType
|
|
34
|
+
})[0].fields
|
|
35
|
+
|
|
36
|
+
doctypeFields.forEach((field: any) => {
|
|
37
|
+
if (field.options == doctype) {
|
|
38
|
+
childTableName = field.fieldname
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
return childTableName
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const getPreviewData = (importName: string, file: string | undefined, sheet: string) => {
|
|
45
|
+
return call("frappe.core.doctype.data_import.data_import.get_preview_from_template", {
|
|
46
|
+
data_import: importName,
|
|
47
|
+
import_file: file,
|
|
48
|
+
google_sheets_url: sheet
|
|
49
|
+
}).catch((error: any) => {
|
|
50
|
+
toast.error(error)
|
|
51
|
+
console.error("Error fetching preview data:", error)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface DataImportProps {
|
|
2
|
+
label?: string
|
|
3
|
+
description?: string
|
|
4
|
+
doctype?: string | null
|
|
5
|
+
importName?: string | null
|
|
6
|
+
doctypeMap?: Record<string, { title: string; route: string }>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DataImport {
|
|
10
|
+
name?: string
|
|
11
|
+
reference_doctype: string
|
|
12
|
+
import_type: string
|
|
13
|
+
status: DataImportStatus
|
|
14
|
+
creation?: string
|
|
15
|
+
mute_emails: boolean
|
|
16
|
+
import_file?: string
|
|
17
|
+
google_sheets_url?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DataImports {
|
|
21
|
+
data: DataImport[]
|
|
22
|
+
update: (args: { filters: any[] }) => void
|
|
23
|
+
insert: { submit: (params: DataImport, options: { validate: () => boolean; onSuccess: (data: DataImport) => void; onError: (err: any) => void }) => void }
|
|
24
|
+
setValue: { submit: (params: DataImport, options: { onSuccess: (data: DataImport) => void; onError: (err: any) => void }) => void }
|
|
25
|
+
reload: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type DataImportStatus = "Pending" | "Success" | "Partial Success" | "Error" | "Timed Out"
|
|
29
|
+
|
|
30
|
+
export interface DocField {
|
|
31
|
+
label: string
|
|
32
|
+
fieldname: string
|
|
33
|
+
reqd: 0 | 1
|
|
34
|
+
fieldtype: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface File {
|
|
38
|
+
name: string
|
|
39
|
+
file_url: string
|
|
40
|
+
file_name: string
|
|
41
|
+
file_size: number
|
|
42
|
+
}
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
<script setup>
|
|
56
56
|
import Dropdown from '../../src/components/Dropdown/Dropdown.vue'
|
|
57
57
|
import Button from '../../src/components/Button/Button.vue'
|
|
58
|
-
import StepsIcon from '
|
|
59
|
-
import MinimizeIcon from '
|
|
60
|
-
import MaximizeIcon from '
|
|
61
|
-
import HelpIcon from '
|
|
58
|
+
import StepsIcon from '../../icons/StepsIcon.vue'
|
|
59
|
+
import MinimizeIcon from '../../icons/MinimizeIcon.vue'
|
|
60
|
+
import MaximizeIcon from '../../icons/MaximizeIcon.vue'
|
|
61
|
+
import HelpIcon from '../../icons/HelpIcon.vue'
|
|
62
62
|
import OnboardingSteps from '../Onboarding/OnboardingSteps.vue'
|
|
63
63
|
import HelpCenter from '../HelpCenter/HelpCenter.vue'
|
|
64
64
|
import { useOnboarding } from '../Onboarding/onboarding'
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
</Button>
|
|
57
57
|
</template>
|
|
58
58
|
<script setup>
|
|
59
|
-
import StepsIcon from '
|
|
59
|
+
import StepsIcon from '../../icons/StepsIcon.vue'
|
|
60
60
|
import Button from '../../src/components/Button/Button.vue'
|
|
61
61
|
import FeatherIcon from '../../src/components/FeatherIcon.vue'
|
|
62
62
|
import { useOnboarding } from './onboarding'
|
|
@@ -0,0 +1,162 @@
|
|
|
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>
|