@a-vision-software/vue-input-components 1.4.16 → 1.4.18
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/dist/src/types/fileupload.d.ts +1 -0
- package/dist/src/types/list.d.ts +2 -1
- package/dist/vue-input-components.cjs.js +2 -1
- package/dist/vue-input-components.css +1 -1
- package/dist/vue-input-components.es.js +2719 -2536
- package/dist/vue-input-components.umd.js +2 -1
- package/package.json +88 -88
- package/src/components/FileUpload.vue +105 -40
- package/src/components/List.vue +60 -11
- package/src/types/fileupload.ts +1 -0
- package/src/types/list.ts +2 -1
- package/src/views/FileUploadTestView.vue +55 -18
- package/src/views/ListTestView.vue +5 -3
package/package.json
CHANGED
|
@@ -1,91 +1,91 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
},
|
|
11
|
-
"homepage": "https://a-vision.github.io/vue-input-components/",
|
|
12
|
-
"keywords": [
|
|
13
|
-
"vue",
|
|
14
|
-
"vue3",
|
|
15
|
-
"components",
|
|
16
|
-
"input",
|
|
17
|
-
"form",
|
|
18
|
-
"typescript",
|
|
19
|
-
"file-upload",
|
|
20
|
-
"text-input",
|
|
21
|
-
"date-picker",
|
|
22
|
-
"dropdown",
|
|
23
|
-
"textarea"
|
|
24
|
-
],
|
|
25
|
-
"type": "module",
|
|
26
|
-
"files": [
|
|
27
|
-
"src",
|
|
28
|
-
"types",
|
|
29
|
-
"dist"
|
|
30
|
-
],
|
|
31
|
-
"main": "./dist/vue-input-components.cjs.js",
|
|
32
|
-
"module": "./dist/vue-input-components.es.js",
|
|
33
|
-
"types": "./dist/src/index.d.ts",
|
|
34
|
-
"exports": {
|
|
35
|
-
".": {
|
|
36
|
-
"types": "./dist/src/index.d.ts",
|
|
37
|
-
"import": "./dist/vue-input-components.es.js",
|
|
38
|
-
"require": "./dist/vue-input-components.cjs.js"
|
|
2
|
+
"name": "@a-vision-software/vue-input-components",
|
|
3
|
+
"version": "1.4.18",
|
|
4
|
+
"description": "A collection of reusable Vue 3 input components with TypeScript support",
|
|
5
|
+
"author": "A-Vision Software",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/a-vision/vue-input-components.git"
|
|
39
10
|
},
|
|
40
|
-
"
|
|
41
|
-
|
|
11
|
+
"homepage": "https://a-vision.github.io/vue-input-components/",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"vue",
|
|
14
|
+
"vue3",
|
|
15
|
+
"components",
|
|
16
|
+
"input",
|
|
17
|
+
"form",
|
|
18
|
+
"typescript",
|
|
19
|
+
"file-upload",
|
|
20
|
+
"text-input",
|
|
21
|
+
"date-picker",
|
|
22
|
+
"dropdown",
|
|
23
|
+
"textarea"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"types",
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"main": "./dist/vue-input-components.cjs.js",
|
|
32
|
+
"module": "./dist/vue-input-components.es.js",
|
|
33
|
+
"types": "./dist/src/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/src/index.d.ts",
|
|
37
|
+
"import": "./dist/vue-input-components.es.js",
|
|
38
|
+
"require": "./dist/vue-input-components.cjs.js"
|
|
39
|
+
},
|
|
40
|
+
"./global": {
|
|
41
|
+
"types": "./dist/src/global.d.ts"
|
|
42
|
+
},
|
|
43
|
+
"./dist/*": "./dist/*",
|
|
44
|
+
"./styles.css": "./dist/vue-input-components.css",
|
|
45
|
+
"./styles": "./dist/vue-input-components.css"
|
|
42
46
|
},
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"publishConfig": {
|
|
89
|
-
"access": "public"
|
|
90
|
-
}
|
|
91
|
-
}
|
|
47
|
+
"sideEffects": [
|
|
48
|
+
"**/*.css"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"dev": "vite",
|
|
52
|
+
"build": "vite build",
|
|
53
|
+
"preview": "vite preview",
|
|
54
|
+
"test": "vitest"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"vue": "^3.5.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@fortawesome/fontawesome-svg-core": "^6.7.0",
|
|
61
|
+
"@fortawesome/free-regular-svg-icons": "^6.7.0",
|
|
62
|
+
"@fortawesome/free-solid-svg-icons": "^6.7.0",
|
|
63
|
+
"@fortawesome/vue-fontawesome": "^3.0.0",
|
|
64
|
+
"@vuepic/vue-datepicker": "^11.0.2"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@tsconfig/node20": "^20.1.2",
|
|
68
|
+
"@types/node": "^20.11.0",
|
|
69
|
+
"@vitejs/plugin-vue": "^5.0.0",
|
|
70
|
+
"@vue/eslint-config-prettier": "^9.0.0",
|
|
71
|
+
"@vue/eslint-config-typescript": "^12.0.0",
|
|
72
|
+
"@vue/test-utils": "^2.4.0",
|
|
73
|
+
"@vue/tsconfig": "^0.5.1",
|
|
74
|
+
"eslint": "^8.56.0",
|
|
75
|
+
"eslint-plugin-vue": "^9.21.0",
|
|
76
|
+
"npm-run-all": "^4.1.5",
|
|
77
|
+
"prettier": "^3.2.0",
|
|
78
|
+
"typescript": "~5.3.0",
|
|
79
|
+
"vite": "^5.0.0",
|
|
80
|
+
"vite-plugin-dts": "^3.7.0",
|
|
81
|
+
"vitest": "^1.2.0",
|
|
82
|
+
"vue-router": "^4.2.0",
|
|
83
|
+
"vue-tsc": "^1.8.0"
|
|
84
|
+
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=18.0.0"
|
|
87
|
+
},
|
|
88
|
+
"publishConfig": {
|
|
89
|
+
"access": "public"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -1,28 +1,53 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
2
|
+
<div
|
|
3
|
+
class="file-upload"
|
|
4
|
+
:class="{
|
|
5
|
+
'label-position-top': labelPosition === 'top',
|
|
6
|
+
'label-position-left': labelPosition === 'left',
|
|
7
|
+
}"
|
|
8
|
+
:style="{
|
|
4
9
|
'--file-upload-width': width || 'auto',
|
|
5
10
|
'--file-upload-height': height || 'auto',
|
|
6
|
-
'--file-upload-color': error
|
|
11
|
+
'--file-upload-color': error
|
|
12
|
+
? 'var(--ui-error-text-color)'
|
|
13
|
+
: color || 'var(--ui-upload-text-color, #888888)',
|
|
7
14
|
'--file-upload-disabled-color': 'color-mix(in srgb, var(--file-upload-color), white 70%)',
|
|
8
15
|
'--file-upload-background-color': bgColor || 'var(--ui-upload-bg-color, #ffffff)',
|
|
9
|
-
'--file-upload-background-active-color':
|
|
16
|
+
'--file-upload-background-active-color':
|
|
17
|
+
activeColor || 'var(--ui-upload-active-bg-color, #cccccc)',
|
|
10
18
|
'--file-upload-border-color': borderColor || 'var(--ui-upload-border-color, #888888)',
|
|
11
19
|
'--file-upload-icon-color': iconColor || 'var(--file-upload-color, #888888)',
|
|
12
20
|
'--file-upload-progress-color': progressColor || 'var(--ui-progress-color, #4444aa)',
|
|
13
21
|
'--file-upload-error-color': 'var(--ui-error-text-color, #aa0000)',
|
|
14
22
|
'--file-upload-success-color': 'var(--ui-success-text-color, #00aa00)',
|
|
15
|
-
}"
|
|
23
|
+
}"
|
|
24
|
+
>
|
|
16
25
|
<label class="file-upload-label" v-if="label" for="fileInput">{{ label }}</label>
|
|
17
|
-
<div
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
<div
|
|
27
|
+
class="upload-area"
|
|
28
|
+
:class="{ 'is-dragging': isDragging, 'has-files': files.length > 0, 'is-disabled': disabled }"
|
|
29
|
+
>
|
|
30
|
+
<input
|
|
31
|
+
ref="fileInput"
|
|
32
|
+
type="file"
|
|
33
|
+
:multiple="multiple"
|
|
34
|
+
class="file-input"
|
|
35
|
+
@change="handleFileSelect"
|
|
36
|
+
:accept="accept"
|
|
37
|
+
:required="required"
|
|
38
|
+
:disabled="disabled"
|
|
39
|
+
:placeholder="placeholder"
|
|
40
|
+
@dragenter.prevent="handleDragEnter"
|
|
41
|
+
@dragleave.prevent="handleDragLeave"
|
|
42
|
+
@dragover.prevent
|
|
43
|
+
@drop.prevent="handleDrop"
|
|
44
|
+
/>
|
|
23
45
|
<div class="upload-content">
|
|
24
46
|
<font-awesome-icon v-if="icon" :icon="['fas', icon]" />
|
|
25
|
-
<p v-if="files.length === 0"
|
|
47
|
+
<p v-if="files.length === 0">
|
|
48
|
+
{{ placeholder || 'Drag & drop files here or click to select' }}
|
|
49
|
+
<div v-if="maxSize" class="max-size">(max size: {{ maxSize }}MB)</div>
|
|
50
|
+
</p>
|
|
26
51
|
<div v-else class="selected-files">
|
|
27
52
|
<p>{{ files.length }} file(s) selected</p>
|
|
28
53
|
<div v-for="(file, index) in files" :key="index" class="file-info">
|
|
@@ -39,8 +64,11 @@
|
|
|
39
64
|
<div class="progress" :style="{ width: `${uploadProgress}%` }"></div>
|
|
40
65
|
</div>
|
|
41
66
|
</div>
|
|
42
|
-
<button
|
|
43
|
-
|
|
67
|
+
<button
|
|
68
|
+
v-if="files.length > 0 && !uploadUrl && !disabled && showUploadButton"
|
|
69
|
+
class="upload-button"
|
|
70
|
+
@click="handleStartUpload"
|
|
71
|
+
>
|
|
44
72
|
Upload Files
|
|
45
73
|
</button>
|
|
46
74
|
</div>
|
|
@@ -48,7 +76,12 @@
|
|
|
48
76
|
|
|
49
77
|
<script setup lang="ts">
|
|
50
78
|
import { ref, watch, onMounted } from 'vue'
|
|
51
|
-
import type {
|
|
79
|
+
import type {
|
|
80
|
+
FileUploadProps,
|
|
81
|
+
FileUploadEmits,
|
|
82
|
+
UploadStatus,
|
|
83
|
+
UploadCallbackResponse,
|
|
84
|
+
} from '@/types/fileupload'
|
|
52
85
|
|
|
53
86
|
const props = defineProps<FileUploadProps>()
|
|
54
87
|
const emit = defineEmits<FileUploadEmits>()
|
|
@@ -65,7 +98,7 @@ const uploadStatus = ref<UploadStatus>({
|
|
|
65
98
|
})
|
|
66
99
|
|
|
67
100
|
onMounted(() => {
|
|
68
|
-
startUpload.value = props.doUpload
|
|
101
|
+
startUpload.value = props.doUpload
|
|
69
102
|
})
|
|
70
103
|
|
|
71
104
|
const formatFileSize = (bytes: number): string => {
|
|
@@ -77,7 +110,7 @@ const formatFileSize = (bytes: number): string => {
|
|
|
77
110
|
}
|
|
78
111
|
|
|
79
112
|
const validateFileSize = (file: File): boolean => {
|
|
80
|
-
if (props.maxSize && file.size > props.maxSize) {
|
|
113
|
+
if (props.maxSize && file.size > props.maxSize * 1024 * 1024) {
|
|
81
114
|
error.value = `File "${file.name}" exceeds the maximum size of ${props.maxSize}MB`
|
|
82
115
|
return false
|
|
83
116
|
}
|
|
@@ -129,12 +162,13 @@ const uploadFiles = () => {
|
|
|
129
162
|
const formData = new FormData()
|
|
130
163
|
files.value.forEach((file) => {
|
|
131
164
|
formData.append('files', file)
|
|
132
|
-
})
|
|
165
|
+
})
|
|
133
166
|
|
|
134
167
|
if (props.uploadCallback) {
|
|
135
|
-
props
|
|
168
|
+
props
|
|
169
|
+
.uploadCallback(formData)
|
|
136
170
|
.then((response: UploadCallbackResponse) => {
|
|
137
|
-
const { formData, headers } = response
|
|
171
|
+
const { formData, headers } = response
|
|
138
172
|
doUploadFiles(formData, headers)
|
|
139
173
|
})
|
|
140
174
|
.catch((err) => {
|
|
@@ -143,10 +177,11 @@ const uploadFiles = () => {
|
|
|
143
177
|
uploadStatus.value = {
|
|
144
178
|
type: 'error',
|
|
145
179
|
message: errorMessage,
|
|
180
|
+
response: err,
|
|
146
181
|
}
|
|
147
182
|
emit('upload-complete', files.value)
|
|
148
183
|
emit('upload-error', uploadStatus.value)
|
|
149
|
-
})
|
|
184
|
+
})
|
|
150
185
|
} else {
|
|
151
186
|
doUploadFiles(formData, {})
|
|
152
187
|
}
|
|
@@ -172,24 +207,43 @@ const doUploadFiles = (formData: FormData, headers: Record<string, string>) => {
|
|
|
172
207
|
|
|
173
208
|
xhr.addEventListener('load', () => {
|
|
174
209
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
210
|
+
const response = JSON.parse(xhr.response)
|
|
175
211
|
uploadStatus.value = {
|
|
176
212
|
type: 'success',
|
|
177
213
|
message: 'Upload completed successfully',
|
|
214
|
+
response: response,
|
|
178
215
|
}
|
|
179
216
|
|
|
180
|
-
|
|
181
|
-
emit('upload-complete', response)
|
|
217
|
+
emit('upload-complete', files.value)
|
|
182
218
|
emit('upload-success', uploadStatus.value)
|
|
183
219
|
files.value = []
|
|
184
220
|
uploadProgress.value = 0
|
|
185
|
-
startUpload.value = false
|
|
221
|
+
startUpload.value = false
|
|
186
222
|
} else {
|
|
187
|
-
|
|
223
|
+
const errorMessage = xhr.statusText || 'Upload failed'
|
|
224
|
+
error.value = errorMessage
|
|
225
|
+
uploadStatus.value = {
|
|
226
|
+
type: 'error',
|
|
227
|
+
message: errorMessage,
|
|
228
|
+
response: null,
|
|
229
|
+
}
|
|
230
|
+
emit('upload-complete', files.value)
|
|
231
|
+
emit('upload-error', uploadStatus.value)
|
|
232
|
+
files.value = []
|
|
233
|
+
uploadProgress.value = 0
|
|
234
|
+
startUpload.value = false
|
|
188
235
|
}
|
|
189
236
|
})
|
|
190
237
|
|
|
191
238
|
xhr.addEventListener('error', () => {
|
|
192
|
-
|
|
239
|
+
const errorMessage = xhr.statusText || 'Upload error'
|
|
240
|
+
uploadStatus.value = {
|
|
241
|
+
type: 'error',
|
|
242
|
+
message: errorMessage,
|
|
243
|
+
response: null,
|
|
244
|
+
}
|
|
245
|
+
emit('upload-complete', files.value)
|
|
246
|
+
emit('upload-error', uploadStatus.value)
|
|
193
247
|
})
|
|
194
248
|
|
|
195
249
|
xhr.open('POST', props.uploadUrl || '')
|
|
@@ -205,6 +259,7 @@ const doUploadFiles = (formData: FormData, headers: Record<string, string>) => {
|
|
|
205
259
|
uploadStatus.value = {
|
|
206
260
|
type: 'error',
|
|
207
261
|
message: errorMessage,
|
|
262
|
+
response: err,
|
|
208
263
|
}
|
|
209
264
|
emit('upload-complete', files.value)
|
|
210
265
|
emit('upload-error', uploadStatus.value)
|
|
@@ -226,20 +281,26 @@ watch(files, (newFiles) => {
|
|
|
226
281
|
}
|
|
227
282
|
})
|
|
228
283
|
|
|
229
|
-
watch(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
284
|
+
watch(
|
|
285
|
+
() => props.doUpload,
|
|
286
|
+
(newDoUpload) => {
|
|
287
|
+
if (files.value && newDoUpload) {
|
|
288
|
+
startUpload.value = newDoUpload
|
|
289
|
+
} else {
|
|
290
|
+
emit('files-selected', files.value)
|
|
291
|
+
startUpload.value = false
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
)
|
|
237
295
|
|
|
238
|
-
watch(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
296
|
+
watch(
|
|
297
|
+
() => startUpload.value,
|
|
298
|
+
(newStartUpload) => {
|
|
299
|
+
if (newStartUpload) {
|
|
300
|
+
uploadFiles()
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
)
|
|
243
304
|
</script>
|
|
244
305
|
|
|
245
306
|
<style scoped>
|
|
@@ -253,7 +314,6 @@ watch(() => startUpload.value, (newStartUpload) => {
|
|
|
253
314
|
grid-template-columns: 10em calc(100% - 10em);
|
|
254
315
|
gap: 1rem;
|
|
255
316
|
}
|
|
256
|
-
|
|
257
317
|
}
|
|
258
318
|
|
|
259
319
|
.upload-area {
|
|
@@ -299,6 +359,11 @@ watch(() => startUpload.value, (newStartUpload) => {
|
|
|
299
359
|
grid-template-rows: 1fr 1fr;
|
|
300
360
|
text-align: center;
|
|
301
361
|
gap: 1rem;
|
|
362
|
+
|
|
363
|
+
.max-size {
|
|
364
|
+
font-size: 0.75rem;
|
|
365
|
+
color: var(--file-upload-color);
|
|
366
|
+
}
|
|
302
367
|
}
|
|
303
368
|
|
|
304
369
|
.upload-content :deep(svg) {
|
package/src/components/List.vue
CHANGED
|
@@ -22,7 +22,16 @@
|
|
|
22
22
|
<!-- Actions -->
|
|
23
23
|
<div v-if="actions?.length" class="list__actions">
|
|
24
24
|
|
|
25
|
-
<Action v-for="(action, actionIndex) in
|
|
25
|
+
<Action v-for="(action, actionIndex) in [
|
|
26
|
+
...(props.CSVDownload !== undefined ? [{
|
|
27
|
+
id: 'csv-download',
|
|
28
|
+
label: 'Download',
|
|
29
|
+
icon: 'file-csv',
|
|
30
|
+
color: 'var(--primary-color)',
|
|
31
|
+
onActionClick: handleCSVDownload
|
|
32
|
+
}] : []),
|
|
33
|
+
...actions
|
|
34
|
+
]" :key="actionIndex" v-bind="action"
|
|
26
35
|
:href="presentation === 'minimal' && !action.href ? `#/${action.id ? action.id : action.label?.toLowerCase()}` : action.href"
|
|
27
36
|
@click.prevent="action.onActionClick ? action.onActionClick(undefined, action) : null"
|
|
28
37
|
:size="presentation === 'minimal' ? 'small' : 'regular'" />
|
|
@@ -85,6 +94,11 @@
|
|
|
85
94
|
<template v-if="column.type === 'text'">
|
|
86
95
|
{{ row[column.key] }}
|
|
87
96
|
</template>
|
|
97
|
+
<template v-else-if="column.type === 'email'">
|
|
98
|
+
<a v-if="row['name']" :href="`mailto:${row['name']} <${row[column.key]}>`" @click.stop>{{
|
|
99
|
+
row[column.key] }}</a>
|
|
100
|
+
<a v-else :href="`mailto:${row[column.key]}`" @click.stop>{{ row[column.key] }}</a>
|
|
101
|
+
</template>
|
|
88
102
|
<template v-else-if="column.type === 'number'">
|
|
89
103
|
{{ row[column.key] }}
|
|
90
104
|
</template>
|
|
@@ -154,6 +168,43 @@ watch(filterValue, (newValue) => {
|
|
|
154
168
|
return () => clearTimeout(timeout)
|
|
155
169
|
})
|
|
156
170
|
|
|
171
|
+
const handleCSVDownload = () => {
|
|
172
|
+
const csv = [];
|
|
173
|
+
const headerRow = [];
|
|
174
|
+
const headerKeys = []
|
|
175
|
+
for (const column of props.columns) {
|
|
176
|
+
const dataType = typeof props.data[0][column.key];
|
|
177
|
+
if (dataType !== 'object') {
|
|
178
|
+
const columnName = column.label || column.key;
|
|
179
|
+
headerRow.push(columnName);
|
|
180
|
+
headerKeys.push(column.key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
csv.push(headerRow.join(','));
|
|
184
|
+
|
|
185
|
+
for (const row of props.data) {
|
|
186
|
+
const rowData: string[] = [];
|
|
187
|
+
headerKeys.forEach(key => {
|
|
188
|
+
const value = row[key];
|
|
189
|
+
if (value == null) {
|
|
190
|
+
rowData.push('');
|
|
191
|
+
} else {
|
|
192
|
+
rowData.push(value);
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
csv.push('"' + rowData.join('","') + '"');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const blob = new Blob([csv.join('\n')], { type: 'text/csv;charset=utf-8;' })
|
|
199
|
+
const url = URL.createObjectURL(blob)
|
|
200
|
+
const a = document.createElement('a')
|
|
201
|
+
a.href = url
|
|
202
|
+
const downloadName = props.CSVDownload || 'data';
|
|
203
|
+
a.download = downloadName.toLowerCase().endsWith('.csv') ? downloadName : downloadName + '.csv'
|
|
204
|
+
a.click()
|
|
205
|
+
URL.revokeObjectURL(url)
|
|
206
|
+
}
|
|
207
|
+
|
|
157
208
|
const handleFilter = () => {
|
|
158
209
|
// No need to do anything here as the watch handles the filtering
|
|
159
210
|
}
|
|
@@ -203,16 +254,17 @@ const filteredData = computed(() => {
|
|
|
203
254
|
const value = row[column.key]
|
|
204
255
|
if (value == null) return false
|
|
205
256
|
|
|
257
|
+
const dateStr = formatDate(value)
|
|
258
|
+
const checkboxValue = value?.modelValue ? 'yes' : 'no'
|
|
259
|
+
|
|
206
260
|
switch (column.type) {
|
|
207
261
|
case 'text':
|
|
208
262
|
return String(value).toLowerCase().includes(effectiveFilterValue.value)
|
|
209
263
|
case 'number':
|
|
210
264
|
return String(value).includes(effectiveFilterValue.value)
|
|
211
265
|
case 'date':
|
|
212
|
-
const dateStr = formatDate(value)
|
|
213
266
|
return dateStr.toLowerCase().includes(effectiveFilterValue.value)
|
|
214
267
|
case 'checkbox':
|
|
215
|
-
const checkboxValue = value?.modelValue ? 'yes' : 'no'
|
|
216
268
|
return checkboxValue.includes(effectiveFilterValue.value)
|
|
217
269
|
default:
|
|
218
270
|
return false
|
|
@@ -228,32 +280,29 @@ const sortedAndFilteredData = computed(() => {
|
|
|
228
280
|
const column = sortColumn.value!
|
|
229
281
|
const aValue = a[column.key]
|
|
230
282
|
const bValue = b[column.key]
|
|
283
|
+
const sortOrder = sortDirection.value === 'asc' ? 1 : -1
|
|
231
284
|
|
|
232
285
|
// Handle different data types
|
|
233
286
|
if (column.type === 'date') {
|
|
234
287
|
const dateA = new Date(aValue).getTime()
|
|
235
288
|
const dateB = new Date(bValue).getTime()
|
|
236
|
-
return
|
|
289
|
+
return (dateA - dateB) * sortOrder
|
|
237
290
|
}
|
|
238
291
|
|
|
239
292
|
if (column.type === 'number') {
|
|
240
|
-
return
|
|
293
|
+
return (aValue - bValue) * sortOrder
|
|
241
294
|
}
|
|
242
295
|
|
|
243
296
|
if (column.type === 'checkbox') {
|
|
244
297
|
const aChecked = aValue?.modelValue || false
|
|
245
298
|
const bChecked = bValue?.modelValue || false
|
|
246
|
-
return
|
|
247
|
-
? (aChecked === bChecked ? 0 : aChecked ? 1 : -1)
|
|
248
|
-
: (aChecked === bChecked ? 0 : aChecked ? -1 : 1)
|
|
299
|
+
return (aChecked - bChecked) * sortOrder
|
|
249
300
|
}
|
|
250
301
|
|
|
251
302
|
// Default string comparison for text and other types
|
|
252
303
|
const stringA = String(aValue || '').toLowerCase()
|
|
253
304
|
const stringB = String(bValue || '').toLowerCase()
|
|
254
|
-
return
|
|
255
|
-
? stringA.localeCompare(stringB)
|
|
256
|
-
: stringB.localeCompare(stringA)
|
|
305
|
+
return stringA.localeCompare(stringB) * sortOrder
|
|
257
306
|
})
|
|
258
307
|
})
|
|
259
308
|
|
package/src/types/fileupload.ts
CHANGED
package/src/types/list.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ListActionProps } from './action'
|
|
|
2
2
|
|
|
3
3
|
type ListPresentation = 'default' | 'minimal'
|
|
4
4
|
|
|
5
|
-
type ListDataType = 'text' | 'number' | 'date' | 'action' | 'checkbox' | 'icon'
|
|
5
|
+
type ListDataType = 'text' | 'number' | 'date' | 'action' | 'checkbox' | 'icon' | 'email'
|
|
6
6
|
|
|
7
7
|
interface ListAction {
|
|
8
8
|
id: string
|
|
@@ -35,6 +35,7 @@ interface ListProps {
|
|
|
35
35
|
columns: ListColumn[]
|
|
36
36
|
data: any[]
|
|
37
37
|
actions?: ListActionProps[]
|
|
38
|
+
CSVDownload?: string
|
|
38
39
|
filter?: {
|
|
39
40
|
placeholder?: string
|
|
40
41
|
}
|