@finema/core 1.4.91 → 1.4.92

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/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "1.4.91",
3
+ "version": "1.4.92",
4
4
  "configKey": "core",
5
5
  "compatibility": {
6
6
  "nuxt": "^3.7.4"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineNuxtModule, createResolver, installModule, addPlugin, addComponentsDir, addImportsDir } from '@nuxt/kit';
2
2
 
3
3
  const name = "@finema/core";
4
- const version = "1.4.91";
4
+ const version = "1.4.92";
5
5
 
6
6
  const colors = {
7
7
  black: "#20243E",
@@ -122,6 +122,14 @@
122
122
  v-bind="getFieldBinding(option)"
123
123
  v-on="option.on ?? {}"
124
124
  />
125
+ <FormInputUploadDropzoneAutoMultiple
126
+ v-else-if="option.type === INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE"
127
+ :class="option.class"
128
+ :form="form"
129
+ :request-options="option.props.requestOptions"
130
+ v-bind="getFieldBinding(option)"
131
+ v-on="option.on ?? {}"
132
+ />
125
133
  </template>
126
134
  </div>
127
135
  </template>
@@ -61,24 +61,12 @@
61
61
  </p>
62
62
  </div>
63
63
  <div :class="[ui.action.wrapper]">
64
- <PImage
64
+ <Icon
65
65
  v-if="isImage(selectedFile)"
66
- alt="image-popup-preview"
67
- preview
68
- :pt="{ button: 'absolute opacity-0 inset-0' }"
69
- >
70
- <template #image>
71
- <Icon :name="ui.action.previewIcon" :class="[ui.action.iconClass]" />
72
- </template>
73
- <template #preview="slotProps">
74
- <img
75
- :src="generateURL(selectedFile)"
76
- alt="preview"
77
- :style="slotProps.style"
78
- @click="slotProps.previewCallback"
79
- />
80
- </template>
81
- </PImage>
66
+ :name="ui.action.previewIcon"
67
+ :class="[ui.action.iconClass]"
68
+ @click="() => (isPreviewOpen = true)"
69
+ />
82
70
  <Icon
83
71
  :name="ui.action.downloadIcon"
84
72
  :class="[ui.action.iconClass]"
@@ -89,6 +77,9 @@
89
77
  :class="[ui.action.iconClass]"
90
78
  @click="handleDeleteFile"
91
79
  />
80
+ <Modal v-model="isPreviewOpen" :title="selectedFile?.name">
81
+ <img :src="generateURL(selectedFile)" alt="image-preview" />
82
+ </Modal>
92
83
  </div>
93
84
  </div>
94
85
  </div>
@@ -118,6 +109,7 @@ const config = useUiConfig<typeof uploadFileDropzone>(uploadFileDropzone, 'uploa
118
109
 
119
110
  const props = withDefaults(defineProps<IUploadDropzoneProps>(), {})
120
111
  const emit = defineEmits(['change', 'delete'])
112
+ const isPreviewOpen = ref<boolean>(false)
121
113
 
122
114
  const { wrapperProps, handleChange: onChange, setErrors, value } = useFieldHOC<File>(props)
123
115
 
@@ -0,0 +1,282 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ ui.base,
5
+ ui.background.default,
6
+ {
7
+ [ui.failed]: upload.status.value.isError || errMsg,
8
+ },
9
+ ]"
10
+ >
11
+ <!-- Loading State -->
12
+ <div v-if="selectedFile && upload.status.value.isLoading" :class="[ui.onLoading.wrapper]">
13
+ <div :class="[ui.onLoading.placeholderWrapper]">
14
+ <Icon
15
+ :name="
16
+ isImage(selectedFile)
17
+ ? ui.onLoading.placeholderImgIcon || ui.default.placeholderImgIcon
18
+ : ui.onLoading.placeholderFileIcon || ui.default.filePreviewIcon
19
+ "
20
+ :class="[ui.onLoading.placeholderIconClass]"
21
+ />
22
+ </div>
23
+ <div :class="[ui.onLoading.textWrapper]">
24
+ <div class="truncate">
25
+ <h1 class="truncate font-bold">{{ selectedFile?.name }}</h1>
26
+ <p class="truncate font-light text-gray-400">
27
+ {{
28
+ fileAllocate.isSelectedFileUseMb
29
+ ? `${fileAllocate.selectedFileSizeMb} MB`
30
+ : `${fileAllocate.selectedFileSizeKb} KB`
31
+ }}
32
+ - {{ percent }}% {{ uploadingLabel ?? 'กำลังอัพโหลด...' }}
33
+ </p>
34
+ </div>
35
+ <div>
36
+ <Icon
37
+ :name="ui.onLoading.loadingIcon || ui.default.loadingIcon"
38
+ :class="[ui.onLoading.loadingIconClass]"
39
+ />
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Success State -->
45
+ <div v-if="selectedFile && upload.status.value.isSuccess" :class="[ui.onPreview.wrapper]">
46
+ <div :class="[ui.onPreview.previewImgWrapper]">
47
+ <div v-if="isImage(selectedFile)" class="size-full overflow-hidden">
48
+ <img
49
+ :src="upload.data.value[responseKey || 'url']"
50
+ :class="[ui.onPreview.previewImgClass]"
51
+ alt="image-preview"
52
+ />
53
+ </div>
54
+ <div v-else>
55
+ <Icon
56
+ :name="ui.onPreview.previewFileIcon || ui.default.filePreviewIcon"
57
+ :class="[ui.onPreview.previewFileClass]"
58
+ />
59
+ </div>
60
+ </div>
61
+ <div :class="[ui.onPreview.textWrapper]">
62
+ <div class="truncate">
63
+ <h1 class="truncate font-bold" :title="selectedFile?.name">
64
+ {{ StringHelper.truncate(selectedFile?.name, 40) }}
65
+ </h1>
66
+ <p class="truncate text-sm font-light text-gray-400">
67
+ {{
68
+ fileAllocate.isSelectedFileUseMb
69
+ ? `${fileAllocate.selectedFileSizeMb} MB`
70
+ : `${fileAllocate.selectedFileSizeKb} KB`
71
+ }}
72
+ </p>
73
+ </div>
74
+ <div :class="[ui.action.wrapper]">
75
+ <Icon
76
+ v-if="isImage(selectedFile)"
77
+ :name="ui.action.previewIcon"
78
+ :class="[ui.action.iconClass]"
79
+ title="ดูตัวอย่าง"
80
+ @click="() => (isPreviewOpen = true)"
81
+ />
82
+ <Icon
83
+ :name="ui.action.downloadIcon"
84
+ :class="[ui.action.iconClass]"
85
+ title="ดาวน์โหลดไฟล์"
86
+ @click="handleDownloadFile"
87
+ />
88
+ <Icon
89
+ :name="ui.action.deleteIcon"
90
+ :class="[ui.action.iconClass]"
91
+ title="ลบไฟล์"
92
+ @click="handleDeleteFile"
93
+ />
94
+ <Modal v-model="isPreviewOpen" :title="selectedFile?.name">
95
+ <img :src="URL.createObjectURL(props.selectedFile)" alt="image-preview" />
96
+ </Modal>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Failed State -->
102
+ <div
103
+ v-if="selectedFile && (upload.status.value.isError || errMsg)"
104
+ :class="[ui.onFailed.wrapper]"
105
+ >
106
+ <div :class="[ui.onFailed.failedImgWrapper]">
107
+ <Icon
108
+ :name="
109
+ isImage(selectedFile)
110
+ ? ui.onFailed.failedImgIcon || ui.default.placeholderImgIcon
111
+ : ui.onFailed.failedFileIcon || ui.default.filePreviewIcon
112
+ "
113
+ :class="[ui.onFailed.failedIconClass]"
114
+ />
115
+ </div>
116
+ <div :class="[ui.onFailed.textWrapper]">
117
+ <div class="truncate">
118
+ <h1 class="truncate font-bold" :title="selectedFile?.name">
119
+ {{ StringHelper.truncate(selectedFile?.name, 40) }}
120
+ </h1>
121
+ <p class="text-danger truncate font-light">
122
+ {{ errMsg ? errMsg : uploadFailedLabel || 'อัพโหลดล้มเหลว, กรุณาลองอีกครั้ง' }}
123
+ </p>
124
+ <Button
125
+ v-if="upload.status.value.isError"
126
+ variant="ghost"
127
+ :label="retryLabel || 'ลองอีกครั้ง'"
128
+ :leading-icon="ui.action.retryIcon"
129
+ :class="[ui.action.retryBtnClass]"
130
+ size="sm"
131
+ @click="handleRetryUpload"
132
+ />
133
+ </div>
134
+ <div :class="[ui.action.wrapper]">
135
+ <Icon
136
+ title="ลบไฟล์"
137
+ :name="ui.action.deleteIcon"
138
+ :class="[ui.action.deleteIconClass]"
139
+ @click="handleDeleteFile"
140
+ />
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </template>
146
+ <script lang="ts" setup>
147
+ import {
148
+ checkFileType,
149
+ checkMaxSize,
150
+ downloadFileFromURL,
151
+ getFileAllocate,
152
+ isImage,
153
+ } from '#core/helpers/componentHelper'
154
+ import { type uploadFileDropzone } from '#core/ui.config'
155
+ import type { IUploadDropzoneAutoMultipleProps } from '#core/components/Form/InputUploadDropzoneAutoMultiple/types'
156
+ import {
157
+ computed,
158
+ type IUploadRequest,
159
+ onMounted,
160
+ ref,
161
+ StringHelper,
162
+ useUploadLoader,
163
+ useWatchTrue,
164
+ } from '#imports'
165
+ import i18next from 'i18next'
166
+
167
+ const emits = defineEmits(['success', 'error', 'delete'])
168
+ const props = defineProps<
169
+ {
170
+ ui: typeof uploadFileDropzone
171
+ selectedFile: File
172
+ } & IUploadDropzoneAutoMultipleProps
173
+ >()
174
+
175
+ const request: IUploadRequest = {
176
+ pathURL: props.uploadPathURL,
177
+ requestOptions: props.requestOptions,
178
+ }
179
+
180
+ const isPreviewOpen = ref<boolean>(false)
181
+ const percent = ref<number | string>(0)
182
+ const upload = useUploadLoader(request)
183
+ const errMsg = ref<string>('')
184
+
185
+ const acceptFile = computed(() =>
186
+ typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
187
+ )
188
+
189
+ const fileAllocate = computed(() =>
190
+ getFileAllocate(props.maxSize ?? 0, props.selectedFile?.size ?? 0)
191
+ )
192
+
193
+ const handleDeleteFile = () => {
194
+ emits('delete')
195
+ }
196
+
197
+ const handleRetryUpload = () => {
198
+ const formData = new FormData()
199
+
200
+ formData.append(props.bodyKey || 'file', props.selectedFile)
201
+ upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
202
+ }
203
+
204
+ const handleDownloadFile = () => {
205
+ downloadFileFromURL(URL.createObjectURL(props.selectedFile), props.selectedFile.name)
206
+ }
207
+
208
+ const onUploadProgress = (progressEvent: ProgressEvent) => {
209
+ percent.value = StringHelper.withFixed(
210
+ (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
211
+ )
212
+ }
213
+
214
+ const onDownloadProgress = (progressEvent: ProgressEvent) => {
215
+ if (progressEvent.total === 0) {
216
+ percent.value = 100
217
+
218
+ return
219
+ }
220
+
221
+ percent.value = StringHelper.withFixed(
222
+ (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
223
+ )
224
+ }
225
+
226
+ const handleCheckFileCondition = (file: File | undefined): boolean => {
227
+ if (!file) return false
228
+
229
+ const fileType = checkFileType(file, acceptFile.value ?? '')
230
+
231
+ if (!fileType) {
232
+ errMsg.value = i18next.t('custom:invalid_file_type')
233
+
234
+ return false
235
+ }
236
+
237
+ const maxSize = checkMaxSize(file, fileAllocate.value.acceptFileSizeKb ?? 0)
238
+
239
+ if (!maxSize) {
240
+ if (fileAllocate.value.isAcceptFileUseMb) {
241
+ errMsg.value = i18next.t('custom:invalid_file_size_mb', {
242
+ size: fileAllocate.value.acceptFileSizeMb,
243
+ })
244
+ } else {
245
+ errMsg.value = i18next.t('custom:invalid_file_size_kb', {
246
+ size: fileAllocate.value.acceptFileSizeKb,
247
+ })
248
+ }
249
+
250
+ return false
251
+ }
252
+
253
+ errMsg.value = ''
254
+
255
+ return true
256
+ }
257
+
258
+ onMounted(() => {
259
+ const result = handleCheckFileCondition(props.selectedFile)
260
+
261
+ if (result) {
262
+ const formData = new FormData()
263
+
264
+ formData.append(props.bodyKey || 'file', props.selectedFile)
265
+ upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
266
+ }
267
+ })
268
+
269
+ useWatchTrue(
270
+ () => upload.status.value.isSuccess,
271
+ () => {
272
+ emits('success', upload.data.value)
273
+ }
274
+ )
275
+
276
+ useWatchTrue(
277
+ () => upload.status.value.isError,
278
+ () => {
279
+ emits('error', upload.status.value.errorData)
280
+ }
281
+ )
282
+ </script>
@@ -0,0 +1,117 @@
1
+ <template>
2
+ <FieldWrapper v-bind="wrapperProps">
3
+ <div class="space-y-3">
4
+ <div
5
+ ref="dropzoneRef"
6
+ :class="[
7
+ ui.base,
8
+ {
9
+ [ui.disabled]: isDisabled,
10
+ [ui.background.default]: !isOverDropZone && !isDisabled,
11
+ [ui.background.dragover]: isOverDropZone && !isDisabled,
12
+ },
13
+ ]"
14
+ >
15
+ <input
16
+ ref="fileInputRef"
17
+ type="file"
18
+ class="hidden"
19
+ :name="name"
20
+ :accept="acceptFile"
21
+ :disabled="isDisabled"
22
+ multiple
23
+ @change="handleChange"
24
+ />
25
+ <div :class="[ui.wrapper]">
26
+ <div :class="[ui.placeholderWrapper]">
27
+ <Icon :name="ui.default.uploadIcon" :class="[ui.labelIcon]" />
28
+ <div :class="[ui.labelWrapper]">
29
+ <p class="text-primary cursor-pointer" @click="handleOpenFile">
30
+ {{ selectFileLabel ?? 'คลิกเพื่อเลือกไฟล์' }}
31
+ </p>
32
+ <p>{{ selectFileSubLabel ?? 'หรือ ลากและวางที่นี่' }}</p>
33
+ </div>
34
+ <p v-if="placeholder" :class="[ui.placeholder]">{{ placeholder }}</p>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ <Item
39
+ v-for="(file, index) in selectedFiles"
40
+ :key="file.name + index"
41
+ v-bind="$props"
42
+ :ui="ui"
43
+ :selected-file="file"
44
+ @success="handleSuccess(index, $event)"
45
+ @delete="handleDeleteFile(index)"
46
+ @error="handleError(index, $event)"
47
+ />
48
+ </div>
49
+ </FieldWrapper>
50
+ </template>
51
+
52
+ <script lang="ts" setup>
53
+ import { useDropZone } from '@vueuse/core'
54
+ import { type IUploadDropzoneAutoMultipleProps } from './types'
55
+ import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
56
+ import { useFieldHOC } from '#core/composables/useForm'
57
+ import { _get, computed, ref, toRef, useUI, useUiConfig } from '#imports'
58
+ import { uploadFileDropzone } from '#core/ui.config'
59
+ import Item from './Item.vue'
60
+
61
+ const config = useUiConfig<typeof uploadFileDropzone>(uploadFileDropzone, 'uploadFileDropzone')
62
+
63
+ const props = withDefaults(defineProps<IUploadDropzoneAutoMultipleProps>(), {})
64
+ const emits = defineEmits(['change', 'success', 'delete'])
65
+ const selectedFiles = ref<File[]>([])
66
+
67
+ const { wrapperProps, handleChange: onChange, setErrors, value } = useFieldHOC<File[]>(props)
68
+
69
+ const { ui } = useUI('uploadFileDropzone', toRef(props, 'ui'), config)
70
+
71
+ const fileInputRef = ref<HTMLInputElement>()
72
+ const dropzoneRef = ref<HTMLDivElement>()
73
+
74
+ const acceptFile = computed(() =>
75
+ typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
76
+ )
77
+
78
+ const onDrop = (files: File[] | null) => {
79
+ if (props.isDisabled || files?.length === 0 || !files) return
80
+
81
+ for (const file of files) {
82
+ selectedFiles.value = [file, ...selectedFiles.value]
83
+ emits('change', value.value)
84
+ }
85
+ }
86
+
87
+ const { isOverDropZone } = useDropZone(dropzoneRef as unknown as HTMLElement, {
88
+ onDrop,
89
+ })
90
+
91
+ const handleChange = (e: Event) => {
92
+ if (props.isDisabled) return
93
+
94
+ for (const file of (e.target as HTMLInputElement).files ?? []) {
95
+ selectedFiles.value = [file, ...selectedFiles.value]
96
+ emits('change', value.value)
97
+ }
98
+ }
99
+
100
+ const handleOpenFile = () => {
101
+ fileInputRef.value?.click()
102
+ }
103
+
104
+ const handleDeleteFile = (index: number) => {
105
+ selectedFiles.value.splice(index, 1)
106
+ onChange(undefined)
107
+ emits('delete')
108
+ }
109
+
110
+ const handleError = (index: number, error: any) => {}
111
+
112
+ const handleSuccess = (index: number, any: any) => {
113
+ value.value = [_get(any, props.responseKey || 'url'), ...(value.value || [])]
114
+ emits('change', value.value)
115
+ emits('success', value.value)
116
+ }
117
+ </script>
@@ -0,0 +1,22 @@
1
+ import { type AxiosRequestConfig } from 'axios';
2
+ import { type IFieldProps, type IFormFieldBase, type INPUT_TYPES } from '../types';
3
+ export interface IUploadDropzoneAutoMultipleProps extends IFieldProps {
4
+ requestOptions: Omit<AxiosRequestConfig, 'baseURL'> & {
5
+ baseURL: string;
6
+ };
7
+ uploadPathURL?: string;
8
+ selectFileLabel?: string;
9
+ selectFileSubLabel?: string;
10
+ uploadingLabel?: string;
11
+ uploadFailedLabel?: string;
12
+ retryLabel?: string;
13
+ accept?: string[] | string;
14
+ bodyKey?: string;
15
+ responseKey?: string;
16
+ maxSize?: number;
17
+ }
18
+ export type IUploadDropzoneAutoMultipleField = IFormFieldBase<INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE, IUploadDropzoneAutoMultipleProps, {
19
+ change: (value: File[] | undefined) => void;
20
+ success: (res: any) => void;
21
+ delete: () => void;
22
+ }>;
@@ -13,6 +13,7 @@ import { type IUploadFileField } from '#core/components/Form/InputUploadFileClas
13
13
  import { type IUploadDropzoneField } from '#core/components/Form/InputUploadDropzone/types';
14
14
  import { type IUploadDropzoneAutoField } from '#core/components/Form/InputUploadDropzoneAuto/types';
15
15
  import type { INumberField } from '#core/components/Form/InputNumber/types';
16
+ import type { IUploadDropzoneAutoMultipleField } from '#core/components/Form/InputUploadDropzoneAutoMultiple/types';
16
17
  export declare const enum INPUT_TYPES {
17
18
  TEXT = "TEXT",
18
19
  NUMBER = "NUMBER",
@@ -29,7 +30,8 @@ export declare const enum INPUT_TYPES {
29
30
  UPLOAD_FILE_CLASSIC = "UPLOAD_FILE_CLASSIC",
30
31
  UPLOAD_FILE_CLASSIC_AUTO = "UPLOAD_FILE_CLASSIC_AUTO",
31
32
  UPLOAD_DROPZONE = "UPLOAD_DROPZONE",
32
- UPLOAD_DROPZONE_AUTO = "UPLOAD_DROPZONE_AUTO"
33
+ UPLOAD_DROPZONE_AUTO = "UPLOAD_DROPZONE_AUTO",
34
+ UPLOAD_DROPZONE_AUTO_MULTIPLE = "UPLOAD_DROPZONE_AUTO_MULTIPLE"
33
35
  }
34
36
  export interface IFieldProps {
35
37
  form?: FormContext;
@@ -58,4 +60,4 @@ export interface IFormFieldBase<I extends INPUT_TYPES, P extends IFieldProps, O>
58
60
  props: P;
59
61
  on?: O;
60
62
  }
61
- export type IFormField = ITextField | INumberField | IStaticField | ICheckboxField | IRadioField | ISelectField | IToggleField | ITextareaField | IDateTimeField | IUploadFileClassicField | IUploadFileField | IUploadDropzoneField | IUploadDropzoneAutoField;
63
+ export type IFormField = ITextField | INumberField | IStaticField | ICheckboxField | IRadioField | ISelectField | IToggleField | ITextareaField | IDateTimeField | IUploadFileClassicField | IUploadFileField | IUploadDropzoneField | IUploadDropzoneAutoField | IUploadDropzoneAutoMultipleField;
@@ -15,5 +15,6 @@ export var INPUT_TYPES = /* @__PURE__ */ ((INPUT_TYPES2) => {
15
15
  INPUT_TYPES2["UPLOAD_FILE_CLASSIC_AUTO"] = "UPLOAD_FILE_CLASSIC_AUTO";
16
16
  INPUT_TYPES2["UPLOAD_DROPZONE"] = "UPLOAD_DROPZONE";
17
17
  INPUT_TYPES2["UPLOAD_DROPZONE_AUTO"] = "UPLOAD_DROPZONE_AUTO";
18
+ INPUT_TYPES2["UPLOAD_DROPZONE_AUTO_MULTIPLE"] = "UPLOAD_DROPZONE_AUTO_MULTIPLE";
18
19
  return INPUT_TYPES2;
19
20
  })(INPUT_TYPES || {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "1.4.91",
3
+ "version": "1.4.92",
4
4
  "repository": "https://gitlab.finema.co/finema/ui-kit",
5
5
  "license": "MIT",
6
6
  "author": "Finema Dev Core Team",