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.
Files changed (50) hide show
  1. package/frappe/Billing/SignupBanner.vue +1 -1
  2. package/frappe/Billing/TrialBanner.vue +1 -1
  3. package/frappe/DataImport/DataImport.vue +160 -0
  4. package/frappe/DataImport/DataImportList.vue +160 -0
  5. package/frappe/DataImport/ImportSteps.vue +114 -0
  6. package/frappe/DataImport/MappingStep.vue +167 -0
  7. package/frappe/DataImport/PreviewStep.vue +360 -0
  8. package/frappe/DataImport/TemplateModal.vue +221 -0
  9. package/frappe/DataImport/UploadStep.vue +394 -0
  10. package/frappe/DataImport/dataImport.ts +53 -0
  11. package/frappe/DataImport/types.ts +42 -0
  12. package/frappe/Help/HelpModal.vue +4 -4
  13. package/frappe/Onboarding/GettingStartedBanner.vue +1 -1
  14. package/frappe/components/Link/Link.vue +162 -0
  15. package/frappe/index.d.ts +53 -0
  16. package/frappe/index.js +3 -0
  17. package/package.json +25 -2
  18. package/src/components/Breadcrumbs/Breadcrumbs.vue +28 -32
  19. package/src/components/Calendar/Calendar.story.vue +1 -0
  20. package/src/components/Calendar/Calendar.vue +65 -3
  21. package/src/components/Charts/index.ts +0 -0
  22. package/src/components/Checkbox/Checkbox.vue +0 -1
  23. package/src/components/Checkbox/types.ts +0 -1
  24. package/src/components/Combobox/Combobox.story.vue +18 -0
  25. package/src/components/Combobox/Combobox.vue +13 -2
  26. package/src/components/Combobox/types.ts +2 -1
  27. package/src/components/DatePicker/index.ts +6 -0
  28. package/src/components/Dropdown/Dropdown.vue +19 -5
  29. package/src/components/Dropdown/types.ts +1 -0
  30. package/src/components/FileUploader/FileUploader.vue +6 -1
  31. package/src/components/ListView/ListGroupHeader.vue +1 -1
  32. package/src/components/ListView/ListGroupRows.vue +5 -0
  33. package/src/components/Select/Select.story.vue +16 -3
  34. package/src/components/Select/Select.vue +109 -96
  35. package/src/components/Sidebar/index.ts +3 -0
  36. package/src/components/Toast/Toast.vue +1 -1
  37. package/src/data-fetching/index.ts +4 -2
  38. package/src/index.ts +9 -23
  39. package/src/resources/{index.js → index.ts} +3 -1
  40. package/src/resources/{local.js → local.ts} +4 -4
  41. package/src/resources/realtime.ts +21 -0
  42. package/frappe/Icons/HelpIcon.vue +0 -16
  43. package/frappe/Icons/LightningIcon.vue +0 -16
  44. package/frappe/Icons/MaximizeIcon.vue +0 -19
  45. package/frappe/Icons/MinimizeIcon.vue +0 -19
  46. package/frappe/Icons/StepsIcon.vue +0 -16
  47. package/src/components/GreenCheckIcon.vue +0 -16
  48. package/src/icons/CircleCheck.vue +0 -9
  49. package/src/icons/DownSolid.vue +0 -8
  50. 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 '../Icons/StepsIcon.vue'
59
- import MinimizeIcon from '../Icons/MinimizeIcon.vue'
60
- import MaximizeIcon from '../Icons/MaximizeIcon.vue'
61
- import HelpIcon from '../Icons/HelpIcon.vue'
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 '../Icons/StepsIcon.vue'
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>