@finema/core 3.0.2 → 3.2.0

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": "3.0.2",
3
+ "version": "3.2.0",
4
4
  "configKey": "core",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
package/dist/module.mjs CHANGED
@@ -4,7 +4,7 @@ import * as lodash from 'lodash-es';
4
4
  import * as theme from '../dist/runtime/theme/index.js';
5
5
 
6
6
  const name = "@finema/core";
7
- const version = "3.0.2";
7
+ const version = "3.2.0";
8
8
 
9
9
  const nuxtAppOptions = {
10
10
  head: {
@@ -37,6 +37,7 @@ import FormInputTime from "./InputTime/index.vue";
37
37
  import FormInputMonth from "./InputMonth/index.vue";
38
38
  import FormInputDateTimeRange from "./InputDateTimeRange/index.vue";
39
39
  import FormInputUploadDropzoneAuto from "./InputUploadDropzoneAuto/index.vue";
40
+ import FormInputUploadDropzoneAutoMultiple from "./InputUploadDropzoneAutoMultiple/index.vue";
40
41
  import FormInputUploadDropzone from "./InputUploadDropzone/index.vue";
41
42
  import FormInputTag from "./InputTags/index.vue";
42
43
  import FormInputWYSIWYG from "./InputWYSIWYG/index.vue";
@@ -150,7 +151,10 @@ const componentMap = {
150
151
  component: FormInputUploadDropzoneAuto,
151
152
  props: {}
152
153
  },
153
- [INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE]: void 0,
154
+ [INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE]: {
155
+ component: FormInputUploadDropzoneAutoMultiple,
156
+ props: {}
157
+ },
154
158
  [INPUT_TYPES.UPLOAD_DROPZONE_IMAGE_AUTO_MULTIPLE]: void 0,
155
159
  [INPUT_TYPES.WYSIWYG]: {
156
160
  component: FormInputWYSIWYG,
@@ -5,6 +5,10 @@ declare const __VLS_export: import("vue").DefineComponent<ISelectFieldProps, {},
5
5
  onChange?: ((...args: any[]) => any) | undefined;
6
6
  }>, {
7
7
  clearIcon: string;
8
+ searchInput: {
9
+ placeholder?: string;
10
+ icon?: string;
11
+ } | boolean;
8
12
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
13
  declare const _default: typeof __VLS_export;
10
14
  export default _default;
@@ -6,7 +6,7 @@
6
6
  :placeholder="wrapperProps.placeholder"
7
7
  :disabled="wrapperProps.disabled"
8
8
  :loading="loading"
9
- :search-input="searchInput"
9
+ :search-input="searchInput ?? void 0"
10
10
  :selected-icon="selectedIcon"
11
11
  value-key="value"
12
12
  label-key="label"
@@ -60,7 +60,7 @@ const props = defineProps({
60
60
  trailingIcon: { type: String, required: false },
61
61
  clearIcon: { type: String, required: false, default: "ph:x-circle-fill" },
62
62
  selectedIcon: { type: String, required: false },
63
- searchInput: { type: [Object, Boolean], required: false },
63
+ searchInput: { type: [Object, Boolean], required: false, default: void 0 },
64
64
  clearable: { type: Boolean, required: false },
65
65
  loading: { type: Boolean, required: false },
66
66
  options: { type: Array, required: true },
@@ -5,6 +5,10 @@ declare const __VLS_export: import("vue").DefineComponent<ISelectFieldProps, {},
5
5
  onChange?: ((...args: any[]) => any) | undefined;
6
6
  }>, {
7
7
  clearIcon: string;
8
+ searchInput: {
9
+ placeholder?: string;
10
+ icon?: string;
11
+ } | boolean;
8
12
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
13
  declare const _default: typeof __VLS_export;
10
14
  export default _default;
@@ -0,0 +1,25 @@
1
+ import type { IUploadDropzoneAutoMultipleProps } from './types.js';
2
+ import type { IFileValue } from '#core/components/Form/types';
3
+ declare const __VLS_export: import("vue").DefineComponent<IUploadDropzoneAutoMultipleProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
4
+ success: (res: IFileValue) => any;
5
+ delete: (index: number) => any;
6
+ change: (value: File[] | undefined) => any;
7
+ }, string, import("vue").PublicProps, Readonly<IUploadDropzoneAutoMultipleProps> & Readonly<{
8
+ onSuccess?: ((res: IFileValue) => any) | undefined;
9
+ onDelete?: ((index: number) => any) | undefined;
10
+ onChange?: ((value: File[] | undefined) => any) | undefined;
11
+ }>, {
12
+ selectFileLabel: string;
13
+ selectFileSubLabel: string;
14
+ uploadingLabel: string;
15
+ uploadFailedLabel: string;
16
+ retryLabel: string;
17
+ bodyKey: string;
18
+ responseURL: string;
19
+ responsePath: string;
20
+ responseName: string;
21
+ responseSize: string;
22
+ responseID: string;
23
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
@@ -0,0 +1,110 @@
1
+ <template>
2
+ <FieldWrapper v-bind="wrapperProps">
3
+ <div
4
+ ref="dropzoneRef"
5
+ :class="theme.base()"
6
+ >
7
+ <div :class="theme.wrapper()">
8
+ <!-- Empty State -->
9
+ <EmptyState
10
+ :theme="theme"
11
+ :select-file-label="selectFileLabel"
12
+ :select-file-sub-label="selectFileSubLabel"
13
+ :placeholder="placeholder"
14
+ @open-file="uploadState.handleOpenFile"
15
+ />
16
+ </div>
17
+ </div>
18
+ <!-- Multiple Files State -->
19
+ <MultipleFilesState
20
+ v-if="!uploadState.isEmpty.value"
21
+ :theme="theme"
22
+ :file-items="uploadState.fileItems.value"
23
+ :disabled="wrapperProps.disabled"
24
+ :readonly="wrapperProps.readonly"
25
+ :upload-failed-label="uploadFailedLabel"
26
+ :retry-label="retryLabel"
27
+ @preview="uploadState.handlePreview"
28
+ @download="handleDownloadFile"
29
+ @delete="uploadState.handleDeleteFile"
30
+ @retry="uploadState.handleRetryUpload"
31
+ />
32
+ </FieldWrapper>
33
+ </template>
34
+
35
+ <script setup>
36
+ import EmptyState from "../fileState/EmptyState.vue";
37
+ import MultipleFilesState from "../fileState/MultipleFilesState.vue";
38
+ import { useUploadStateMultiple } from "../fileState/useUploadStateMultiple";
39
+ import { computed, useTemplateRef } from "#imports";
40
+ import FieldWrapper from "#core/components/Form/FieldWrapper.vue";
41
+ import { useFieldHOC } from "#core/composables/useForm";
42
+ import { uploadFileDropzoneTheme } from "#core/theme/uploadFileDropzone";
43
+ import { useUiConfig } from "#core/composables/useConfig";
44
+ import { downloadFileFromURL } from "#core/helpers/componentHelper";
45
+ const emits = defineEmits(["change", "success", "delete"]);
46
+ const props = defineProps({
47
+ requestOptions: { type: Object, required: true },
48
+ uploadPathURL: { type: String, required: false },
49
+ bodyKey: { type: String, required: false, default: "file" },
50
+ responseURL: { type: String, required: false, default: "url" },
51
+ responsePath: { type: String, required: false, default: "path" },
52
+ responseName: { type: String, required: false, default: "name" },
53
+ responseSize: { type: String, required: false, default: "size" },
54
+ responseID: { type: String, required: false, default: "id" },
55
+ accept: { type: [Array, String], required: false },
56
+ maxSize: { type: Number, required: false },
57
+ maxFiles: { type: Number, required: false },
58
+ selectFileLabel: { type: String, required: false, default: "\u0E04\u0E25\u0E34\u0E01\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E44\u0E1F\u0E25\u0E4C" },
59
+ selectFileSubLabel: { type: String, required: false, default: "\u0E2B\u0E23\u0E37\u0E2D \u0E25\u0E32\u0E01\u0E41\u0E25\u0E30\u0E27\u0E32\u0E07\u0E17\u0E35\u0E48\u0E19\u0E35\u0E48" },
60
+ uploadingLabel: { type: String, required: false, default: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14..." },
61
+ uploadFailedLabel: { type: String, required: false, default: "\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14\u0E25\u0E49\u0E21\u0E40\u0E2B\u0E25\u0E27, \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07" },
62
+ retryLabel: { type: String, required: false, default: "\u0E25\u0E2D\u0E07\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07" },
63
+ form: { type: Object, required: false },
64
+ name: { type: String, required: true },
65
+ errorMessage: { type: String, required: false },
66
+ label: { type: null, required: false },
67
+ description: { type: String, required: false },
68
+ hint: { type: String, required: false },
69
+ rules: { type: null, required: false },
70
+ autoFocus: { type: Boolean, required: false },
71
+ placeholder: { type: String, required: false },
72
+ disabled: { type: Boolean, required: false },
73
+ readonly: { type: Boolean, required: false },
74
+ required: { type: Boolean, required: false },
75
+ help: { type: String, required: false },
76
+ ui: { type: null, required: false }
77
+ });
78
+ const {
79
+ wrapperProps,
80
+ handleChange: onChange,
81
+ setErrors,
82
+ value
83
+ } = useFieldHOC(props);
84
+ const acceptedFileTypes = computed(
85
+ () => typeof props.accept === "string" ? props.accept : props.accept?.join(",")
86
+ );
87
+ const dropzoneRef = useTemplateRef("dropzoneRef");
88
+ const uploadState = useUploadStateMultiple(
89
+ props,
90
+ emits,
91
+ onChange,
92
+ setErrors,
93
+ value,
94
+ acceptedFileTypes,
95
+ wrapperProps,
96
+ dropzoneRef
97
+ );
98
+ const theme = computed(
99
+ () => useUiConfig(uploadFileDropzoneTheme, "uploadFileDropzone")({
100
+ dragover: uploadState.dropzone.isOverDropZone.value && uploadState.isEmpty.value,
101
+ disabled: wrapperProps.value.disabled,
102
+ failed: false
103
+ })
104
+ );
105
+ const handleDownloadFile = (fileValue) => {
106
+ if (fileValue?.url && fileValue?.name) {
107
+ downloadFileFromURL(fileValue.url, fileValue.name);
108
+ }
109
+ };
110
+ </script>
@@ -0,0 +1,25 @@
1
+ import type { IUploadDropzoneAutoMultipleProps } from './types.js';
2
+ import type { IFileValue } from '#core/components/Form/types';
3
+ declare const __VLS_export: import("vue").DefineComponent<IUploadDropzoneAutoMultipleProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
4
+ success: (res: IFileValue) => any;
5
+ delete: (index: number) => any;
6
+ change: (value: File[] | undefined) => any;
7
+ }, string, import("vue").PublicProps, Readonly<IUploadDropzoneAutoMultipleProps> & Readonly<{
8
+ onSuccess?: ((res: IFileValue) => any) | undefined;
9
+ onDelete?: ((index: number) => any) | undefined;
10
+ onChange?: ((value: File[] | undefined) => any) | undefined;
11
+ }>, {
12
+ selectFileLabel: string;
13
+ selectFileSubLabel: string;
14
+ uploadingLabel: string;
15
+ uploadFailedLabel: string;
16
+ retryLabel: string;
17
+ bodyKey: string;
18
+ responseURL: string;
19
+ responsePath: string;
20
+ responseName: string;
21
+ responseSize: string;
22
+ responseID: string;
23
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
@@ -0,0 +1,27 @@
1
+ import type { AxiosRequestConfig } from 'axios';
2
+ import type { IFieldProps, IFormFieldBase, INPUT_TYPES } from '../types.js';
3
+ export interface IUploadDropzoneAutoMultipleProps extends IFieldProps {
4
+ requestOptions: Omit<AxiosRequestConfig, 'baseURL'> & {
5
+ baseURL: string;
6
+ };
7
+ uploadPathURL?: string;
8
+ bodyKey?: string;
9
+ responseURL?: string;
10
+ responsePath?: string;
11
+ responseName?: string;
12
+ responseSize?: string;
13
+ responseID?: string;
14
+ accept?: string[] | string;
15
+ maxSize?: number;
16
+ maxFiles?: number;
17
+ selectFileLabel?: string;
18
+ selectFileSubLabel?: string;
19
+ uploadingLabel?: string;
20
+ uploadFailedLabel?: string;
21
+ retryLabel?: string;
22
+ }
23
+ export type IUploadDropzoneAutoMultipleField = IFormFieldBase<INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE, IUploadDropzoneAutoMultipleProps, {
24
+ change: (value: File[] | undefined) => void;
25
+ success: (res: any) => void;
26
+ delete: (index: number) => void;
27
+ }>;
@@ -0,0 +1,30 @@
1
+ import type { IFileValue } from '#core/components/Form/types';
2
+ import { UploadState } from './useUploadStateMultiple.js';
3
+ interface FileUploadItem {
4
+ file: File;
5
+ state: UploadState;
6
+ progress: number;
7
+ value?: IFileValue;
8
+ error?: string;
9
+ }
10
+ interface Props {
11
+ theme: any;
12
+ fileItems: FileUploadItem[];
13
+ disabled?: boolean;
14
+ readonly?: boolean;
15
+ uploadFailedLabel?: string;
16
+ retryLabel?: string;
17
+ }
18
+ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ delete: (index: number) => any;
20
+ preview: (value: IFileValue) => any;
21
+ download: (value: IFileValue) => any;
22
+ retry: (index: number) => any;
23
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
24
+ onDelete?: ((index: number) => any) | undefined;
25
+ onPreview?: ((value: IFileValue) => any) | undefined;
26
+ onDownload?: ((value: IFileValue) => any) | undefined;
27
+ onRetry?: ((index: number) => any) | undefined;
28
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
29
+ declare const _default: typeof __VLS_export;
30
+ export default _default;
@@ -0,0 +1,172 @@
1
+ <template>
2
+ <div :class="theme.multipleFilesWrapper()">
3
+ <div
4
+ v-for="(item, index) in fileItems"
5
+ :key="index"
6
+ :class="theme.fileItemWrapper()"
7
+ >
8
+ <!-- Uploading State -->
9
+ <div
10
+ v-if="item.state === UploadState.UPLOADING"
11
+ :class="theme.uploadingItemWrapper()"
12
+ >
13
+ <div :class="theme.uploadingIconWrapper()">
14
+ <Icon
15
+ :name="icons.filePreviewIcon"
16
+ :class="theme.uploadingIconClass()"
17
+ />
18
+ </div>
19
+ <div :class="theme.uploadingTextWrapper()">
20
+ <h1 class="truncate font-bold">
21
+ {{ item.file.name }}
22
+ </h1>
23
+ <div class="flex items-center gap-2">
24
+ <div :class="theme.progressBarWrapper()">
25
+ <div
26
+ :class="theme.progressBarFill()"
27
+ :style="{ width: `${item.progress}%` }"
28
+ />
29
+ </div>
30
+ <p class="text-sm text-gray-400">
31
+ {{ item.progress }}%
32
+ </p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Success State -->
38
+ <div
39
+ v-else-if="item.state === UploadState.SUCCESS && item.value"
40
+ :class="theme.successItemWrapper()"
41
+ >
42
+ <div
43
+ v-if="isImageFromPath(item.value.path)"
44
+ :class="theme.successImgWrapper()"
45
+ >
46
+ <img
47
+ :src="item.value.url"
48
+ :class="theme.successImgClass()"
49
+ alt="img-preview"
50
+ />
51
+ </div>
52
+ <div
53
+ v-else
54
+ :class="theme.successFileWrapper()"
55
+ >
56
+ <Icon
57
+ :name="icons.filePreviewIcon"
58
+ :class="theme.successFileClass()"
59
+ />
60
+ </div>
61
+ <div :class="theme.successTextWrapper()">
62
+ <div class="truncate">
63
+ <h1 class="truncate font-bold">
64
+ {{ item.value.name }}
65
+ </h1>
66
+ <p class="truncate text-sm font-light text-gray-400">
67
+ {{ getFileSizeFromValue(item.value) }}
68
+ </p>
69
+ </div>
70
+ <div :class="theme.actionWrapper()">
71
+ <a
72
+ v-if="isPDFFromPath(item.value.path)"
73
+ :href="item.value.url"
74
+ target="_blank"
75
+ class="flex"
76
+ >
77
+ <Icon
78
+ :name="icons.actionPreviewIcon"
79
+ :class="theme.actionIconClass()"
80
+ title="ดูตัวอย่าง"
81
+ />
82
+ </a>
83
+ <Icon
84
+ v-if="isImageFromPath(item.value.path) || isVideoFromPath(item.value.path)"
85
+ :name="icons.actionPreviewIcon"
86
+ :class="theme.actionIconClass()"
87
+ title="ดูตัวอย่าง"
88
+ @click="$emit('preview', item.value)"
89
+ />
90
+ <Icon
91
+ :name="icons.actionDownloadIcon"
92
+ :class="theme.actionIconClass()"
93
+ title="ดาวน์โหลดไฟล์"
94
+ @click="$emit('download', item.value)"
95
+ />
96
+ <Icon
97
+ v-if="!disabled && !readonly"
98
+ :name="icons.actionDeleteIcon"
99
+ :class="theme.actionIconClass()"
100
+ title="ลบไฟล์"
101
+ @click="$emit('delete', index)"
102
+ />
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- Error State -->
108
+ <div
109
+ v-else-if="item.state === UploadState.ERROR"
110
+ :class="theme.errorItemWrapper()"
111
+ >
112
+ <div :class="theme.errorIconWrapper()">
113
+ <Icon
114
+ :name="icons.errorIcon"
115
+ :class="theme.errorIconClass()"
116
+ />
117
+ </div>
118
+ <div :class="theme.errorTextWrapper()">
119
+ <h1 class="truncate font-bold">
120
+ {{ item.file.name }}
121
+ </h1>
122
+ <p class="text-error-500 text-sm">
123
+ {{ item.error || uploadFailedLabel }}
124
+ </p>
125
+ </div>
126
+ <div :class="theme.errorActionWrapper()">
127
+ <Button
128
+ variant="link"
129
+ :icon="icons.actionRetryIcon"
130
+ :class="theme.actionRetryBtnClass()"
131
+ color="primary"
132
+ @click="$emit('retry', index)"
133
+ >
134
+ {{ retryLabel }}
135
+ </Button>
136
+ <Icon
137
+ v-if="!disabled && !readonly"
138
+ :name="icons.actionDeleteIcon"
139
+ :class="theme.actionIconClass()"
140
+ title="ลบไฟล์"
141
+ @click="$emit('delete', index)"
142
+ />
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </template>
148
+
149
+ <script setup>
150
+ import {
151
+ isImageFromPath,
152
+ isPDFFromPath,
153
+ isVideoFromPath,
154
+ useFileSize
155
+ } from "#core/helpers/componentHelper";
156
+ import { useUiIconConfig } from "#core/composables/useConfig";
157
+ import { UploadState } from "./useUploadStateMultiple";
158
+ defineEmits(["preview", "download", "delete", "retry"]);
159
+ defineProps({
160
+ theme: { type: null, required: true },
161
+ fileItems: { type: Array, required: true },
162
+ disabled: { type: Boolean, required: false },
163
+ readonly: { type: Boolean, required: false },
164
+ uploadFailedLabel: { type: String, required: false },
165
+ retryLabel: { type: String, required: false }
166
+ });
167
+ const icons = useUiIconConfig("uploadFileDropzone");
168
+ const getFileSizeFromValue = (fileValue) => {
169
+ const allocate = useFileSize(fileValue.size || 0);
170
+ return allocate.isSelectedFileUseMb.value ? `${allocate.selectedFileSizeMb.value} MB` : `${allocate.selectedFileSizeKb.value} KB`;
171
+ };
172
+ </script>
@@ -0,0 +1,30 @@
1
+ import type { IFileValue } from '#core/components/Form/types';
2
+ import { UploadState } from './useUploadStateMultiple.js';
3
+ interface FileUploadItem {
4
+ file: File;
5
+ state: UploadState;
6
+ progress: number;
7
+ value?: IFileValue;
8
+ error?: string;
9
+ }
10
+ interface Props {
11
+ theme: any;
12
+ fileItems: FileUploadItem[];
13
+ disabled?: boolean;
14
+ readonly?: boolean;
15
+ uploadFailedLabel?: string;
16
+ retryLabel?: string;
17
+ }
18
+ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ delete: (index: number) => any;
20
+ preview: (value: IFileValue) => any;
21
+ download: (value: IFileValue) => any;
22
+ retry: (index: number) => any;
23
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
24
+ onDelete?: ((index: number) => any) | undefined;
25
+ onPreview?: ((value: IFileValue) => any) | undefined;
26
+ onDownload?: ((value: IFileValue) => any) | undefined;
27
+ onRetry?: ((index: number) => any) | undefined;
28
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
29
+ declare const _default: typeof __VLS_export;
30
+ export default _default;
@@ -0,0 +1,126 @@
1
+ import type { Ref, TemplateRef } from 'vue';
2
+ import type { IUploadDropzoneAutoMultipleProps } from '../InputUploadDropzoneAutoMultiple/types.js';
3
+ import type { IFileValue } from '#core/components/Form/types';
4
+ import { useUploadLoader } from '#core/composables/useUpload';
5
+ export declare enum UploadState {
6
+ EMPTY = "empty",
7
+ UPLOADING = "uploading",
8
+ SUCCESS = "success",
9
+ ERROR = "error"
10
+ }
11
+ export interface FileUploadItem {
12
+ file: File;
13
+ state: UploadState;
14
+ progress: number;
15
+ value?: IFileValue;
16
+ error?: string;
17
+ uploadLoader?: ReturnType<typeof useUploadLoader>;
18
+ percentRef?: Ref<number>;
19
+ }
20
+ export declare const useUploadStateMultiple: (props: IUploadDropzoneAutoMultipleProps, emits: any, onChange: (value: IFileValue[] | undefined) => void, setErrors: (error: string) => void, value: any, acceptedFileTypes: any, wrapperProps: any, dropzoneRef: TemplateRef<HTMLDivElement | undefined>) => {
21
+ fileItems: Ref<{
22
+ file: {
23
+ readonly lastModified: number;
24
+ readonly name: string;
25
+ readonly webkitRelativePath: string;
26
+ readonly size: number;
27
+ readonly type: string;
28
+ arrayBuffer: {
29
+ (): Promise<ArrayBuffer>;
30
+ (): Promise<ArrayBuffer>;
31
+ };
32
+ bytes: {
33
+ (): Promise<Uint8Array<ArrayBuffer>>;
34
+ (): Promise<Uint8Array<ArrayBuffer>>;
35
+ };
36
+ slice: {
37
+ (start?: number, end?: number, contentType?: string): Blob;
38
+ (start?: number, end?: number, contentType?: string): Blob;
39
+ };
40
+ stream: {
41
+ (): ReadableStream<Uint8Array<ArrayBuffer>>;
42
+ (): ReadableStream<Uint8Array<ArrayBuffer>>;
43
+ };
44
+ text: {
45
+ (): Promise<string>;
46
+ (): Promise<string>;
47
+ };
48
+ };
49
+ state: UploadState;
50
+ progress: number;
51
+ value?: {
52
+ url: string;
53
+ path?: string | undefined;
54
+ name?: string | undefined;
55
+ size?: number | undefined;
56
+ id?: string | undefined;
57
+ } | undefined;
58
+ error?: string | undefined;
59
+ uploadLoader?: {
60
+ status: import("#imports").IStatus;
61
+ data: any;
62
+ options: import("#imports").IAPIOptions;
63
+ run: (payload?: import("../../../helpers/apiObjectHelper.js").IObjectRunLoaderOptions<any, any> | undefined) => Promise<void>;
64
+ clear: () => void;
65
+ setLoading: () => void;
66
+ setData: (data: any) => void;
67
+ } | undefined;
68
+ percentRef?: number | undefined;
69
+ }[], FileUploadItem[] | {
70
+ file: {
71
+ readonly lastModified: number;
72
+ readonly name: string;
73
+ readonly webkitRelativePath: string;
74
+ readonly size: number;
75
+ readonly type: string;
76
+ arrayBuffer: {
77
+ (): Promise<ArrayBuffer>;
78
+ (): Promise<ArrayBuffer>;
79
+ };
80
+ bytes: {
81
+ (): Promise<Uint8Array<ArrayBuffer>>;
82
+ (): Promise<Uint8Array<ArrayBuffer>>;
83
+ };
84
+ slice: {
85
+ (start?: number, end?: number, contentType?: string): Blob;
86
+ (start?: number, end?: number, contentType?: string): Blob;
87
+ };
88
+ stream: {
89
+ (): ReadableStream<Uint8Array<ArrayBuffer>>;
90
+ (): ReadableStream<Uint8Array<ArrayBuffer>>;
91
+ };
92
+ text: {
93
+ (): Promise<string>;
94
+ (): Promise<string>;
95
+ };
96
+ };
97
+ state: UploadState;
98
+ progress: number;
99
+ value?: {
100
+ url: string;
101
+ path?: string | undefined;
102
+ name?: string | undefined;
103
+ size?: number | undefined;
104
+ id?: string | undefined;
105
+ } | undefined;
106
+ error?: string | undefined;
107
+ uploadLoader?: {
108
+ status: import("#imports").IStatus;
109
+ data: any;
110
+ options: import("#imports").IAPIOptions;
111
+ run: (payload?: import("../../../helpers/apiObjectHelper.js").IObjectRunLoaderOptions<any, any> | undefined) => Promise<void>;
112
+ clear: () => void;
113
+ setLoading: () => void;
114
+ setData: (data: any) => void;
115
+ } | undefined;
116
+ percentRef?: number | undefined;
117
+ }[]>;
118
+ isEmpty: import("vue").ComputedRef<boolean>;
119
+ hasUploading: import("vue").ComputedRef<boolean>;
120
+ allSuccess: import("vue").ComputedRef<boolean>;
121
+ dropzone: import("@vueuse/core").UseDropZoneReturn;
122
+ handleOpenFile: () => void;
123
+ handleDeleteFile: (index: number) => void;
124
+ handleRetryUpload: (index: number) => void;
125
+ handlePreview: (fileValue: IFileValue) => void;
126
+ };
@@ -0,0 +1,238 @@
1
+ import { useDropZone, useFileDialog } from "@vueuse/core";
2
+ import PreviewModal from "./PreviewModal.vue";
3
+ import { computed, ref, useOverlay, watch } from "#imports";
4
+ import { useUploadLoader } from "#core/composables/useUpload";
5
+ import { useFileAllocate, useFileProgress } from "#core/helpers/componentHelper";
6
+ import { StringHelper } from "#core/utils/StringHelper";
7
+ import { _get } from "#core/utils/lodash";
8
+ export var UploadState = /* @__PURE__ */ ((UploadState2) => {
9
+ UploadState2["EMPTY"] = "empty";
10
+ UploadState2["UPLOADING"] = "uploading";
11
+ UploadState2["SUCCESS"] = "success";
12
+ UploadState2["ERROR"] = "error";
13
+ return UploadState2;
14
+ })(UploadState || {});
15
+ export const useUploadStateMultiple = (props, emits, onChange, setErrors, value, acceptedFileTypes, wrapperProps, dropzoneRef) => {
16
+ const overlay = useOverlay();
17
+ const previewModal = overlay.create(PreviewModal);
18
+ const fileItems = ref([]);
19
+ const fileAllocate = useFileAllocate(computed(() => fileItems.value[0]?.file), props);
20
+ const validateFile = (file) => {
21
+ if (props.accept && fileAllocate.acceptFile.value) {
22
+ const acceptedTypes = fileAllocate.acceptFile.value;
23
+ const acceptedTypesList = acceptedTypes.split(",").map((type) => type.trim());
24
+ const fileExtension = file.name.toLowerCase().split(".").pop();
25
+ const isValidFileType = acceptedTypesList.some((acceptedType) => {
26
+ if (acceptedType.startsWith(".")) {
27
+ return fileExtension === acceptedType.slice(1).toLowerCase();
28
+ }
29
+ if (!acceptedType.includes("/") && !acceptedType.includes("*")) {
30
+ return fileExtension === acceptedType.toLowerCase();
31
+ }
32
+ if (acceptedType.endsWith("/*")) {
33
+ return file.type.startsWith(acceptedType.slice(0, -2) + "/");
34
+ }
35
+ return file.type === acceptedType;
36
+ });
37
+ if (!isValidFileType) {
38
+ return {
39
+ valid: false,
40
+ error: `\u0E1B\u0E23\u0E30\u0E40\u0E20\u0E17\u0E44\u0E1F\u0E25\u0E4C\u0E44\u0E21\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07 (\u0E23\u0E2D\u0E07\u0E23\u0E31\u0E1A\u0E40\u0E09\u0E1E\u0E32\u0E30 ${acceptedTypesList.join(", ")})`
41
+ };
42
+ }
43
+ }
44
+ if (props.maxSize) {
45
+ const maxSizeBytes = (fileAllocate.acceptFileSizeKb.value || 0) * 1024;
46
+ if (file.size > maxSizeBytes) {
47
+ const sizeLabel = fileAllocate.isAcceptFileUseMb.value ? `${fileAllocate.acceptFileSizeMb.value} MB` : `${fileAllocate.acceptFileSizeKb.value} KB`;
48
+ return {
49
+ valid: false,
50
+ error: `\u0E02\u0E19\u0E32\u0E14\u0E44\u0E1F\u0E25\u0E4C\u0E15\u0E49\u0E2D\u0E07\u0E44\u0E21\u0E48\u0E40\u0E01\u0E34\u0E19 ${sizeLabel}`
51
+ };
52
+ }
53
+ }
54
+ return {
55
+ valid: true
56
+ };
57
+ };
58
+ const updateFileItem = (index, updates) => {
59
+ fileItems.value = fileItems.value.map(
60
+ (item, i) => i === index ? {
61
+ ...item,
62
+ ...updates
63
+ } : item
64
+ );
65
+ };
66
+ const setupUpload = (fileItem, index) => {
67
+ const {
68
+ percent,
69
+ onDownloadProgress,
70
+ onUploadProgress
71
+ } = useFileProgress();
72
+ const request = {
73
+ requestOptions: {
74
+ ...props.requestOptions,
75
+ onDownloadProgress,
76
+ onUploadProgress
77
+ },
78
+ pathURL: props.uploadPathURL
79
+ };
80
+ const uploadLoader = useUploadLoader(request);
81
+ updateFileItem(index, {
82
+ uploadLoader,
83
+ percentRef: percent
84
+ });
85
+ const stopProgressWatch = watch(percent, (newProgress) => {
86
+ updateFileItem(index, {
87
+ progress: newProgress
88
+ });
89
+ });
90
+ const stopSuccessWatch = watch(
91
+ () => uploadLoader.status.value.isSuccess,
92
+ (isSuccess) => {
93
+ if (isSuccess && uploadLoader.data.value) {
94
+ const fileValue = {
95
+ url: _get(uploadLoader.data.value, props.responseURL),
96
+ path: _get(uploadLoader.data.value, props.responsePath),
97
+ name: _get(uploadLoader.data.value, props.responseName) || fileItem.file.name,
98
+ size: Number(_get(uploadLoader.data.value, props.responseSize) || fileItem.file.size),
99
+ id: _get(uploadLoader.data.value, props.responseID)
100
+ };
101
+ updateFileItem(index, {
102
+ value: fileValue,
103
+ state: "success" /* SUCCESS */
104
+ });
105
+ updateFormValue();
106
+ emits("success", fileValue);
107
+ stopProgressWatch();
108
+ stopSuccessWatch();
109
+ stopErrorWatch();
110
+ }
111
+ }
112
+ );
113
+ const stopErrorWatch = watch(
114
+ () => uploadLoader.status.value.isError,
115
+ (isError) => {
116
+ if (isError) {
117
+ updateFileItem(index, {
118
+ state: "error" /* ERROR */,
119
+ error: "\u0E1E\u0E1A\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14: " + StringHelper.getError(
120
+ uploadLoader.status.value.errorData
121
+ )
122
+ });
123
+ stopProgressWatch();
124
+ stopSuccessWatch();
125
+ stopErrorWatch();
126
+ }
127
+ }
128
+ );
129
+ return uploadLoader;
130
+ };
131
+ const processFile = (file) => {
132
+ const validation = validateFile(file);
133
+ if (!validation.valid) {
134
+ setErrors(validation.error || "Invalid file");
135
+ return;
136
+ }
137
+ if (props.maxFiles && fileItems.value.length >= props.maxFiles) {
138
+ setErrors(`\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14\u0E44\u0E14\u0E49\u0E2A\u0E39\u0E07\u0E2A\u0E38\u0E14 ${props.maxFiles} \u0E44\u0E1F\u0E25\u0E4C`);
139
+ return;
140
+ }
141
+ setErrors("");
142
+ const fileItem = {
143
+ file,
144
+ state: "uploading" /* UPLOADING */,
145
+ progress: 0
146
+ };
147
+ fileItems.value = [...fileItems.value, fileItem];
148
+ const newIndex = fileItems.value.length - 1;
149
+ const uploadLoader = setupUpload(fileItem, newIndex);
150
+ const formData = new FormData();
151
+ formData.append(props.bodyKey, file);
152
+ uploadLoader.run({
153
+ data: formData
154
+ });
155
+ emits("change", fileItems.value.map((item) => item.file));
156
+ };
157
+ const updateFormValue = () => {
158
+ const successfulFiles = fileItems.value.filter((item) => item.state === "success" /* SUCCESS */ && item.value).map((item) => item.value);
159
+ onChange(successfulFiles.length > 0 ? successfulFiles : void 0);
160
+ };
161
+ const handleFileDrop = (files) => {
162
+ if (wrapperProps.value.disabled || wrapperProps.value.readonly || !files?.length) return;
163
+ files.forEach((file) => processFile(file));
164
+ };
165
+ const fileDialog = useFileDialog({
166
+ accept: acceptedFileTypes.value || "",
167
+ directory: false,
168
+ multiple: true
169
+ });
170
+ const dropzone = useDropZone(dropzoneRef, {
171
+ onDrop: handleFileDrop,
172
+ multiple: true,
173
+ preventDefaultForUnhandled: false
174
+ });
175
+ const isEmpty = computed(() => fileItems.value.length === 0);
176
+ const hasUploading = computed(
177
+ () => fileItems.value.some((item) => item.state === "uploading" /* UPLOADING */)
178
+ );
179
+ const allSuccess = computed(
180
+ () => fileItems.value.length > 0 && fileItems.value.every((item) => item.state === "success" /* SUCCESS */)
181
+ );
182
+ fileDialog.onChange((files) => {
183
+ if (files?.length) {
184
+ Array.from(files).forEach((file) => processFile(file));
185
+ fileDialog.reset();
186
+ }
187
+ });
188
+ const handleOpenFile = () => {
189
+ if (props.maxFiles && fileItems.value.length >= props.maxFiles) {
190
+ setErrors(`\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14\u0E44\u0E14\u0E49\u0E2A\u0E39\u0E07\u0E2A\u0E38\u0E14 ${props.maxFiles} \u0E44\u0E1F\u0E25\u0E4C`);
191
+ return;
192
+ }
193
+ if (wrapperProps.value.disabled || wrapperProps.value.readonly) return;
194
+ fileDialog.open();
195
+ };
196
+ const handleDeleteFile = (index) => {
197
+ fileItems.value.splice(index, 1);
198
+ updateFormValue();
199
+ if (fileItems.value.length === 0) {
200
+ setErrors("");
201
+ }
202
+ emits("delete", index);
203
+ };
204
+ const handleRetryUpload = (index) => {
205
+ const fileItem = fileItems.value[index];
206
+ if (!fileItem) return;
207
+ updateFileItem(index, {
208
+ state: "uploading" /* UPLOADING */,
209
+ error: void 0,
210
+ progress: 0
211
+ });
212
+ const uploadLoader = setupUpload(fileItem, index);
213
+ const formData = new FormData();
214
+ formData.append(props.bodyKey, fileItem.file);
215
+ uploadLoader.run({
216
+ data: formData
217
+ });
218
+ };
219
+ const handlePreview = (fileValue) => {
220
+ previewModal.open({
221
+ value: fileValue
222
+ });
223
+ };
224
+ return {
225
+ // State
226
+ fileItems,
227
+ isEmpty,
228
+ hasUploading,
229
+ allSuccess,
230
+ // Upload utilities
231
+ dropzone,
232
+ // Handlers
233
+ handleOpenFile,
234
+ handleDeleteFile,
235
+ handleRetryUpload,
236
+ handlePreview
237
+ };
238
+ };
@@ -2,6 +2,7 @@ import type { Component } from '@nuxt/schema';
2
2
  import type { FormContext } from 'vee-validate';
3
3
  import type { IUploadDropzoneField } from './InputUploadDropzone/types.js';
4
4
  import type { IUploadDropzoneAutoField } from './InputUploadDropzoneAuto/types.js';
5
+ import type { IUploadDropzoneAutoMultipleField } from './InputUploadDropzoneAutoMultiple/types.js';
5
6
  import type { IDateTimeRangeField } from './InputDateTimeRange/date_range_time_field.types.js';
6
7
  import type { ITextField } from '#core/components/Form/InputText/types';
7
8
  import type { ISearchField } from '#core/components/Form/InputSearch/types';
@@ -75,7 +76,7 @@ export interface IFormFieldBase<I extends INPUT_TYPES, P extends IFieldProps, O>
75
76
  props: P;
76
77
  on?: O;
77
78
  }
78
- export type IFormField = ITextField | ISearchField | INumberField | ICurrencyField | ITextareaField | IToggleField | ISelectField | ICheckboxField | ISelectMultipleField | IRadioField | IDateTimeField | ITimeField | IMonthField | IDateTimeRangeField | IUploadDropzoneField | IUploadDropzoneAutoField | IWYSIWYGField | IComponentField | ITagsField | IFormFieldBase<INPUT_TYPES.COMPONENT, any, any>;
79
+ export type IFormField = ITextField | ISearchField | INumberField | ICurrencyField | ITextareaField | IToggleField | ISelectField | ICheckboxField | ISelectMultipleField | IRadioField | IDateTimeField | ITimeField | IMonthField | IDateTimeRangeField | IUploadDropzoneField | IUploadDropzoneAutoField | IUploadDropzoneAutoMultipleField | IWYSIWYGField | IComponentField | ITagsField | IFormFieldBase<INPUT_TYPES.COMPONENT, any, any>;
79
80
  export interface IFileValue {
80
81
  url: string;
81
82
  path?: string;
@@ -9,6 +9,7 @@ export declare const uploadFileDropzoneTheme: {
9
9
  actionDownloadIcon: string;
10
10
  actionDeleteIcon: string;
11
11
  actionRetryIcon: string;
12
+ errorIcon: string;
12
13
  };
13
14
  slots: {
14
15
  base: string;
@@ -38,6 +39,28 @@ export declare const uploadFileDropzoneTheme: {
38
39
  actionIconClass: string;
39
40
  actionDeleteIconClass: string;
40
41
  actionRetryBtnClass: string;
42
+ multipleFilesWrapper: string;
43
+ fileItemWrapper: string;
44
+ uploadingItemWrapper: string;
45
+ uploadingIconWrapper: string;
46
+ uploadingIconClass: string;
47
+ uploadingTextWrapper: string;
48
+ progressBarWrapper: string;
49
+ progressBarFill: string;
50
+ successItemWrapper: string;
51
+ successImgWrapper: string;
52
+ successImgClass: string;
53
+ successFileWrapper: string;
54
+ successFileClass: string;
55
+ successTextWrapper: string;
56
+ errorItemWrapper: string;
57
+ errorIconWrapper: string;
58
+ errorIconClass: string;
59
+ errorTextWrapper: string;
60
+ errorActionWrapper: string;
61
+ addMoreWrapper: string;
62
+ addMoreButton: string;
63
+ addMoreIcon: string;
41
64
  };
42
65
  variants: {
43
66
  dragover: {
@@ -8,7 +8,8 @@ export const uploadFileDropzoneTheme = {
8
8
  actionPreviewIcon: "ic:outline-remove-red-eye",
9
9
  actionDownloadIcon: "material-symbols:download",
10
10
  actionDeleteIcon: "material-symbols:delete",
11
- actionRetryIcon: "stash:arrow-retry"
11
+ actionRetryIcon: "stash:arrow-retry",
12
+ errorIcon: "i-heroicons:exclamation-circle-solid"
12
13
  },
13
14
  slots: {
14
15
  base: "relative w-full text-base p-4 transition rounded-lg flex items-center justify-center ring-1 bg-white ring-accented",
@@ -28,7 +29,7 @@ export const uploadFileDropzoneTheme = {
28
29
  // Preview state
29
30
  onPreviewWrapper: "flex items-center space-x-4 rounded-lg w-full",
30
31
  onPreviewImgWrapper: "flex-shrink-0 w-16 h-16 flex justify-center items-center rounded-lg overflow-hidden bg-gray-100",
31
- onPreviewImgClass: "w-full h-full object-cover",
32
+ onPreviewImgClass: "w-full h-full object-fit",
32
33
  onPreviewFileWrapper: "flex-shrink-0 w-16 h-16 flex justify-center items-center rounded-lg overflow-hidden",
33
34
  onPreviewFileClass: "size-8 text-gray-400 m-auto",
34
35
  onPreviewTextWrapper: "flex-1 min-w-0 flex items-center justify-between",
@@ -41,7 +42,34 @@ export const uploadFileDropzoneTheme = {
41
42
  actionWrapper: "flex items-center space-x-2",
42
43
  actionIconClass: "size-6 text-dimmed hover:text-dimmed-600 cursor-pointer transition-colors",
43
44
  actionDeleteIconClass: "size-6 text-(--ui-color-error-500) hover:text-(--ui-color-error-600) cursor-pointer transition-colors",
44
- actionRetryBtnClass: "px-0"
45
+ actionRetryBtnClass: "px-0",
46
+ // Multiple files
47
+ multipleFilesWrapper: "flex flex-col gap-3 w-full mt-4",
48
+ fileItemWrapper: "w-full ring-1 bg-white ring-accented rounded-lg p-3",
49
+ // Uploading item
50
+ uploadingItemWrapper: "flex items-center space-x-4 w-full",
51
+ uploadingIconWrapper: "flex-shrink-0",
52
+ uploadingIconClass: "size-12 text-gray-400",
53
+ uploadingTextWrapper: "flex-1 min-w-0",
54
+ progressBarWrapper: "w-full bg-gray-200 rounded-full h-2 overflow-hidden",
55
+ progressBarFill: "bg-primary h-full transition-all duration-300",
56
+ // Success item
57
+ successItemWrapper: "flex items-center space-x-4 rounded-lg w-full",
58
+ successImgWrapper: "flex-shrink-0 w-16 h-16 flex justify-center items-center rounded-lg overflow-hidden bg-gray-100",
59
+ successImgClass: "w-full h-full object-fit",
60
+ successFileWrapper: "flex-shrink-0 w-16 h-16 flex justify-center items-center rounded-lg overflow-hidden",
61
+ successFileClass: "size-8 text-gray-400 m-auto",
62
+ successTextWrapper: "flex-1 min-w-0 flex items-center justify-between",
63
+ // Error item
64
+ errorItemWrapper: "flex items-center space-x-4 w-full rounded-lg bg-(--ui-color-error-50) p-3",
65
+ errorIconWrapper: "flex-shrink-0",
66
+ errorIconClass: "size-12 text-(--ui-color-error-500)",
67
+ errorTextWrapper: "flex-1 min-w-0",
68
+ errorActionWrapper: "flex items-center space-x-2",
69
+ // Add more button
70
+ addMoreWrapper: "w-full mt-3 pt-3 border-t border-gray-200",
71
+ addMoreButton: "w-full flex items-center justify-center gap-2 px-4 py-2 text-sm text-primary border border-primary rounded-lg hover:bg-primary-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
72
+ addMoreIcon: "size-4"
45
73
  },
46
74
  variants: {
47
75
  dragover: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "3.0.2",
3
+ "version": "3.2.0",
4
4
  "repository": "https://gitlab.finema.co/finema/ui-kit",
5
5
  "license": "MIT",
6
6
  "author": "Finema Dev Core Team",