@finema/core 2.12.3 → 2.13.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.
Files changed (25) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +1 -1
  3. package/dist/runtime/components/FlexDeck/Base.vue +1 -1
  4. package/dist/runtime/components/Form/InputUploadDropzoneAuto/EmptyState.vue +35 -0
  5. package/dist/runtime/components/Form/InputUploadDropzoneAuto/EmptyState.vue.d.ts +12 -0
  6. package/dist/runtime/components/Form/InputUploadDropzoneAuto/FailedState.vue +54 -0
  7. package/dist/runtime/components/Form/InputUploadDropzoneAuto/FailedState.vue.d.ts +14 -0
  8. package/dist/runtime/components/Form/InputUploadDropzoneAuto/LoadingState.vue +54 -0
  9. package/dist/runtime/components/Form/InputUploadDropzoneAuto/LoadingState.vue.d.ts +8 -0
  10. package/dist/runtime/components/Form/InputUploadDropzoneAuto/PreviewModal.vue +35 -0
  11. package/dist/runtime/components/Form/InputUploadDropzoneAuto/PreviewModal.vue.d.ts +10 -0
  12. package/dist/runtime/components/Form/InputUploadDropzoneAuto/SuccessState.vue +88 -0
  13. package/dist/runtime/components/Form/InputUploadDropzoneAuto/SuccessState.vue.d.ts +17 -0
  14. package/dist/runtime/components/Form/InputUploadDropzoneAuto/index.vue +91 -76
  15. package/dist/runtime/components/Form/InputUploadDropzoneAuto/index.vue.d.ts +10 -6
  16. package/dist/runtime/components/Form/InputUploadDropzoneAuto/types.d.ts +7 -5
  17. package/dist/runtime/components/Form/InputUploadDropzoneAuto/useUploadState.d.ts +25 -0
  18. package/dist/runtime/components/Form/InputUploadDropzoneAuto/useUploadState.js +187 -0
  19. package/dist/runtime/components/Image.vue +1 -1
  20. package/dist/runtime/composables/useUpload.js +2 -4
  21. package/dist/runtime/helpers/componentHelper.d.ts +3 -2
  22. package/dist/runtime/helpers/componentHelper.js +6 -2
  23. package/dist/runtime/theme/uploadFileDropzone.d.ts +32 -0
  24. package/dist/runtime/theme/uploadFileDropzone.js +41 -5
  25. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "2.12.3",
3
+ "version": "2.13.0",
4
4
  "configKey": "core",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.1",
package/dist/module.mjs CHANGED
@@ -3,7 +3,7 @@ import defu from 'defu';
3
3
  import * as theme from '../dist/runtime/theme/index.js';
4
4
 
5
5
  const name = "@finema/core";
6
- const version = "2.12.3";
6
+ const version = "2.13.0";
7
7
 
8
8
  const nuxtAppOptions = {
9
9
  head: {
@@ -40,7 +40,7 @@
40
40
  <div class="flex h-60 items-center justify-center">
41
41
  <Icon
42
42
  name="i-svg-spinners:180-ring-with-bg"
43
- class="size-8 text-primary"
43
+ class="text-primary size-8"
44
44
  />
45
45
  </div>
46
46
  </slot>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <div :class="theme.placeholderWrapper()">
3
+ <Icon
4
+ :name="themeStatic.uploadIcon"
5
+ :class="theme.labelIcon()"
6
+ />
7
+ <div :class="theme.labelWrapper()">
8
+ <p
9
+ class="text-primary cursor-pointer font-bold"
10
+ @click="$emit('openFile')"
11
+ >
12
+ {{ selectFileLabel }}
13
+ </p>
14
+ <p>{{ selectFileSubLabel }}</p>
15
+ </div>
16
+ <p
17
+ v-if="placeholder"
18
+ :class="theme.placeholder()"
19
+ >
20
+ {{ placeholder }}
21
+ </p>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup>
26
+ import { useUiConfigStatic } from "#imports";
27
+ defineProps({
28
+ theme: { type: null, required: true },
29
+ selectFileLabel: { type: String, required: true },
30
+ selectFileSubLabel: { type: String, required: true },
31
+ placeholder: { type: String, required: false }
32
+ });
33
+ defineEmits(["openFile"]);
34
+ const themeStatic = useUiConfigStatic("uploadFileDropzone");
35
+ </script>
@@ -0,0 +1,12 @@
1
+ interface Props {
2
+ theme: any;
3
+ selectFileLabel: string;
4
+ selectFileSubLabel: string;
5
+ placeholder?: string;
6
+ }
7
+ declare const _default: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ openFile: () => any;
9
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
10
+ onOpenFile?: (() => any) | undefined;
11
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
+ export default _default;
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <div :class="theme.onFailedWrapper()">
3
+ <div :class="theme.onFailedFailedImgWrapper()">
4
+ <Icon
5
+ :name="getFileIcon(selectedFile)"
6
+ :class="theme.onFailedFailedIconClass()"
7
+ />
8
+ </div>
9
+ <div :class="theme.onFailedTextWrapper()">
10
+ <div class="truncate">
11
+ <h1 class="truncate font-bold">
12
+ {{ selectedFile.name }}
13
+ </h1>
14
+ <p class="text-error truncate font-light">
15
+ {{ uploadFailedLabel }}
16
+ </p>
17
+ <Button
18
+ variant="link"
19
+ :icon="themeStatic.actionRetryIcon"
20
+ :class="theme.actionRetryBtnClass()"
21
+ color="primary"
22
+ @click="$emit('retry')"
23
+ >
24
+ {{ retryLabel }}
25
+ </Button>
26
+ </div>
27
+ <Icon
28
+ :name="themeStatic.actionDeleteIcon"
29
+ :class="theme.actionDeleteIconClass()"
30
+ title="ลบไฟล์"
31
+ @click="$emit('delete')"
32
+ />
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script setup>
38
+ import { isImage } from "#core/helpers/componentHelper";
39
+ import { useUiConfigStatic } from "#imports";
40
+ const props = defineProps({
41
+ theme: { type: null, required: true },
42
+ selectedFile: { type: null, required: true },
43
+ uploadFailedLabel: { type: String, required: true },
44
+ retryLabel: { type: String, required: true }
45
+ });
46
+ defineEmits(["retry", "delete"]);
47
+ const themeStatic = useUiConfigStatic("uploadFileDropzone");
48
+ const getFileIcon = (file) => {
49
+ if (isImage(file)) {
50
+ return themeStatic.placeholderImgIcon;
51
+ }
52
+ return themeStatic.filePreviewIcon;
53
+ };
54
+ </script>
@@ -0,0 +1,14 @@
1
+ interface Props {
2
+ theme: any;
3
+ selectedFile: File;
4
+ uploadFailedLabel: string;
5
+ retryLabel: string;
6
+ }
7
+ declare const _default: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ delete: () => any;
9
+ retry: () => any;
10
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
11
+ onDelete?: (() => any) | undefined;
12
+ onRetry?: (() => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ export default _default;
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <div :class="theme.onLoadingWrapper()">
3
+ <div :class="theme.onLoadingPlaceholderWrapper()">
4
+ <Icon
5
+ :name="getFileIcon(selectedFile)"
6
+ :class="theme.onLoadingPlaceholderIconClass()"
7
+ />
8
+ </div>
9
+ <div :class="theme.onLoadingTextWrapper()">
10
+ <div class="truncate">
11
+ <h1 class="truncate font-bold">
12
+ {{ selectedFile.name }}
13
+ </h1>
14
+ <p class="truncate font-light text-gray-400">
15
+ {{ getFileSize(selectedFile) }} - {{ percent }}% {{ uploadingLabel }}
16
+ </p>
17
+ </div>
18
+ <div>
19
+ <Icon
20
+ :name="themeStatic.loadingIcon"
21
+ :class="theme.onLoadingLoadingIconClass()"
22
+ />
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup>
29
+ import { isImage } from "#core/helpers/componentHelper";
30
+ import { useUiConfigStatic } from "#imports";
31
+ const props = defineProps({
32
+ theme: { type: null, required: true },
33
+ selectedFile: { type: null, required: true },
34
+ percent: { type: Number, required: true },
35
+ uploadingLabel: { type: String, required: true }
36
+ });
37
+ const themeStatic = useUiConfigStatic("uploadFileDropzone");
38
+ const getFileIcon = (file) => {
39
+ if (isImage(file)) {
40
+ return themeStatic.placeholderImgIcon;
41
+ }
42
+ return themeStatic.filePreviewIcon;
43
+ };
44
+ const getFileSize = (file) => {
45
+ const size = file.size;
46
+ const useMb = size > 1024 * 1024;
47
+ if (useMb) {
48
+ const sizeMb = (size / (1024 * 1024)).toFixed(2);
49
+ return `${sizeMb} MB`;
50
+ }
51
+ const sizeKb = (size / 1024).toFixed(2);
52
+ return `${sizeKb} KB`;
53
+ };
54
+ </script>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ theme: any;
3
+ selectedFile: File;
4
+ percent: number;
5
+ uploadingLabel: string;
6
+ }
7
+ declare const _default: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ export default _default;
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <Modal
3
+ :close="{ onClick: () => emits('close', false) }"
4
+ :dismissible="false"
5
+ :title="value?.name"
6
+ :ui="{
7
+ content: 'max-w-3xl'
8
+ }"
9
+ >
10
+ <template #body>
11
+ <div class="flex justify-center">
12
+ <img
13
+ v-if="value && isImageFromPath(value.path)"
14
+ :src="value.url"
15
+ alt="img-preview"
16
+ class="max-h-96 max-w-full rounded-lg"
17
+ />
18
+ <video
19
+ v-else-if="value && isVideoFromPath(value.path)"
20
+ :src="value.url"
21
+ controls
22
+ class="max-h-96 max-w-full"
23
+ />
24
+ </div>
25
+ </template>
26
+ </Modal>
27
+ </template>
28
+
29
+ <script setup>
30
+ import { isImageFromPath, isVideoFromPath } from "#core/helpers/componentHelper";
31
+ defineProps({
32
+ value: { type: Object, required: false }
33
+ });
34
+ const emits = defineEmits(["close"]);
35
+ </script>
@@ -0,0 +1,10 @@
1
+ import type { IFileValue } from '#core/components/Form/types';
2
+ interface Props {
3
+ value?: IFileValue;
4
+ }
5
+ declare const _default: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ close: (args_0: boolean) => any;
7
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
8
+ onClose?: ((args_0: boolean) => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ export default _default;
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div :class="theme.onPreviewWrapper()">
3
+ <div :class="theme.onPreviewPreviewImgWrapper()">
4
+ <div
5
+ v-if="isImageFromPath(value.path)"
6
+ class="size-full overflow-hidden"
7
+ >
8
+ <img
9
+ :src="value.url"
10
+ :class="theme.onPreviewPreviewImgClass()"
11
+ alt="img-preview"
12
+ />
13
+ </div>
14
+ <div v-else>
15
+ <Icon
16
+ :name="themeStatic.filePreviewIcon"
17
+ :class="theme.onPreviewPreviewFileClass()"
18
+ />
19
+ </div>
20
+ </div>
21
+ <div :class="theme.onPreviewTextWrapper()">
22
+ <div class="truncate">
23
+ <h1 class="truncate font-bold">
24
+ {{ value.name }}
25
+ </h1>
26
+ <p class="truncate text-sm font-light text-gray-400">
27
+ {{ getFileSizeFromValue(value) }}
28
+ </p>
29
+ </div>
30
+ <div :class="theme.actionWrapper()">
31
+ <a
32
+ v-if="isPDFFromPath(value.path)"
33
+ :href="value.url"
34
+ target="_blank"
35
+ class="flex"
36
+ >
37
+ <Icon
38
+ :name="themeStatic.actionPreviewIcon"
39
+ :class="theme.actionIconClass()"
40
+ title="ดูตัวอย่าง"
41
+ />
42
+ </a>
43
+ <Icon
44
+ v-if="isImageFromPath(value.path) || isVideoFromPath(value.path)"
45
+ :name="themeStatic.actionPreviewIcon"
46
+ :class="theme.actionIconClass()"
47
+ title="ดูตัวอย่าง"
48
+ @click="$emit('preview')"
49
+ />
50
+ <Icon
51
+ :name="themeStatic.actionDownloadIcon"
52
+ :class="theme.actionIconClass()"
53
+ title="ดาวน์โหลดไฟล์"
54
+ @click="$emit('download')"
55
+ />
56
+ <Icon
57
+ v-if="!disabled && !readonly"
58
+ :name="themeStatic.actionDeleteIcon"
59
+ :class="theme.actionIconClass()"
60
+ title="ลบไฟล์"
61
+ @click="$emit('delete')"
62
+ />
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <script setup>
69
+ import {
70
+ isImageFromPath,
71
+ isPDFFromPath,
72
+ isVideoFromPath,
73
+ useFileSize
74
+ } from "#core/helpers/componentHelper";
75
+ import { useUiConfigStatic } from "#imports";
76
+ defineProps({
77
+ theme: { type: null, required: true },
78
+ value: { type: Object, required: true },
79
+ disabled: { type: Boolean, required: false },
80
+ readonly: { type: Boolean, required: false }
81
+ });
82
+ defineEmits(["preview", "download", "delete"]);
83
+ const themeStatic = useUiConfigStatic("uploadFileDropzone");
84
+ const getFileSizeFromValue = (fileValue) => {
85
+ const allocate = useFileSize(fileValue.size || 0);
86
+ return allocate.isSelectedFileUseMb.value ? `${allocate.selectedFileSizeMb.value} MB` : `${allocate.selectedFileSizeKb.value} KB`;
87
+ };
88
+ </script>
@@ -0,0 +1,17 @@
1
+ import type { IFileValue } from '#core/components/Form/types';
2
+ interface Props {
3
+ theme: any;
4
+ value: IFileValue;
5
+ disabled?: boolean;
6
+ readonly?: boolean;
7
+ }
8
+ declare const _default: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ delete: () => any;
10
+ preview: () => any;
11
+ download: () => any;
12
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
13
+ onDelete?: (() => any) | undefined;
14
+ onPreview?: (() => any) | undefined;
15
+ onDownload?: (() => any) | undefined;
16
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
+ export default _default;
@@ -1,60 +1,83 @@
1
1
  <template>
2
- <FieldWrapper v-bind="wrapperProps">
3
- <div
4
- ref="dropZoneRef"
5
- :class="theme.base()"
6
- >
7
- <div :class="theme.wrapper()">
8
- <div
9
- v-if="!selectedFile && !value"
10
- :class="[theme.placeholderWrapper()]"
11
- >
12
- <Icon
13
- :name="useUiConfigStatic('uploadFileDropzone').uploadIcon"
14
- :class="[theme.labelIcon()]"
15
- />
16
- <div :class="[theme.labelWrapper()]">
17
- <p
18
- class="text-primary bg-primary-50 cursor-pointer"
19
- @click="fileDialog.open"
20
- >
21
- {{ selectFileLabel }}
22
- </p>
23
- <p>{{ selectFileSubLabel }}</p>
24
- </div>
25
- <p
26
- v-if="placeholder"
27
- :class="theme.placeholder()"
28
- >
29
- {{ placeholder }}
30
- </p>
31
- </div>
32
- </div>
33
- </div>
34
- </FieldWrapper>
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
+ v-if="uploadState.isEmpty.value"
11
+ :theme="theme"
12
+ :select-file-label="selectFileLabel"
13
+ :select-file-sub-label="selectFileSubLabel"
14
+ :placeholder="placeholder"
15
+ @open-file="uploadState.handleOpenFile"
16
+ />
17
+
18
+ <!-- Loading State -->
19
+ <LoadingState
20
+ v-if="uploadState.isUploading.value"
21
+ :theme="theme"
22
+ :selected-file="uploadState.selectedFile.value"
23
+ :percent="uploadState.percent.value"
24
+ :uploading-label="uploadingLabel"
25
+ />
26
+
27
+ <!-- Success State -->
28
+ <SuccessState
29
+ v-if="uploadState.isSuccess.value"
30
+ :theme="theme"
31
+ :value="value"
32
+ :disabled="wrapperProps.disabled"
33
+ :readonly="wrapperProps.readonly"
34
+ @preview="uploadState.handlePreview"
35
+ @download="handleDownloadFile"
36
+ @delete="uploadState.handleDeleteFile"
37
+ />
38
+
39
+ <!-- Failed State -->
40
+ <FailedState
41
+ v-if="uploadState.isError.value"
42
+ :theme="theme"
43
+ :selected-file="uploadState.selectedFile.value"
44
+ :upload-failed-label="uploadFailedLabel"
45
+ :retry-label="retryLabel"
46
+ @retry="uploadState.handleRetryUpload"
47
+ @delete="uploadState.handleDeleteFile"
48
+ />
49
+ </div>
50
+ </div>
51
+ </FieldWrapper>
35
52
  </template>
36
53
 
37
54
  <script setup>
38
- import { useDropZone, useFileDialog } from "@vueuse/core";
39
- import { computed, ref, useTemplateRef } from "vue";
55
+ import EmptyState from "./EmptyState.vue";
56
+ import LoadingState from "./LoadingState.vue";
57
+ import SuccessState from "./SuccessState.vue";
58
+ import FailedState from "./FailedState.vue";
59
+ import { useUploadState } from "./useUploadState";
60
+ import { computed, useTemplateRef } from "#imports";
40
61
  import FieldWrapper from "#core/components/Form/FieldWrapper.vue";
41
62
  import { useFieldHOC } from "#core/composables/useForm";
42
63
  import { uploadFileDropzoneTheme } from "#core/theme/uploadFileDropzone";
43
- import { useUiConfig, useUiConfigStatic } from "#core/composables/useConfig";
44
- import { useFileAllocate, useFileProgress, useFileSize } from "#core/helpers/componentHelper";
64
+ import { useUiConfig } from "#core/composables/useConfig";
65
+ import { downloadFileFromURL } from "#core/helpers/componentHelper";
45
66
  const props = defineProps({
46
67
  requestOptions: { type: Object, required: true },
47
68
  uploadPathURL: { type: String, required: false },
48
- selectFileLabel: { type: String, required: false, default: "\u0E04\u0E25\u0E34\u0E01\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E44\u0E1F\u0E25\u0E4C" },
49
- 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" },
50
- uploadingLabel: { type: String, required: false, default: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14..." },
51
- 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" },
52
- retryLabel: { type: String, required: false },
53
- accept: { type: [Array, String], required: false },
54
69
  bodyKey: { type: String, required: false, default: "file" },
55
70
  responseURL: { type: String, required: false, default: "url" },
56
71
  responsePath: { type: String, required: false, default: "path" },
72
+ responseName: { type: String, required: false, default: "name" },
73
+ responseSize: { type: String, required: false, default: "size" },
74
+ accept: { type: [Array, String], required: false },
57
75
  maxSize: { type: Number, required: false },
76
+ selectFileLabel: { type: String, required: false, default: "\u0E04\u0E25\u0E34\u0E01\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E44\u0E1F\u0E25\u0E4C" },
77
+ 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" },
78
+ uploadingLabel: { type: String, required: false, default: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14..." },
79
+ 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" },
80
+ retryLabel: { type: String, required: false, default: "\u0E25\u0E2D\u0E07\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07" },
58
81
  form: { type: Object, required: false },
59
82
  name: { type: String, required: true },
60
83
  errorMessage: { type: String, required: false },
@@ -77,38 +100,30 @@ const {
77
100
  setErrors,
78
101
  value
79
102
  } = useFieldHOC(props);
80
- const dropZoneRef = useTemplateRef("dropZoneRef");
81
- const theme = computed(() => useUiConfig(uploadFileDropzoneTheme, "uploadFileDropzone")({
82
- dragover: dropzone.isOverDropZone.value
83
- }));
84
- const fileDialog = useFileDialog({
85
- accept: typeof props.accept === "string" ? props.accept : props.accept?.join(","),
86
- // Set to accept only image files
87
- directory: false
88
- // Select directories instead of files if set true
89
- });
90
- const selectedFile = ref();
91
- const isPreviewOpen = ref(false);
92
- const {
93
- onUploadProgress,
94
- onDownloadProgress,
95
- percent
96
- } = useFileProgress();
97
- const fileAllocate = useFileAllocate(selectedFile, props);
98
- const fileAllocateFromPath = computed(() => useFileSize(value.value?.size));
99
- fileDialog.onChange((files) => {
100
- console.log("Selected files:", files);
101
- });
102
- const onDrop = (files) => {
103
- console.log("Selected files drop:", files);
103
+ const acceptedFileTypes = computed(
104
+ () => typeof props.accept === "string" ? props.accept : props.accept?.join(",")
105
+ );
106
+ const dropzoneRef = useTemplateRef("dropzoneRef");
107
+ const uploadState = useUploadState(
108
+ props,
109
+ emits,
110
+ onChange,
111
+ setErrors,
112
+ value,
113
+ acceptedFileTypes,
114
+ wrapperProps,
115
+ dropzoneRef
116
+ );
117
+ const theme = computed(
118
+ () => useUiConfig(uploadFileDropzoneTheme, "uploadFileDropzone")({
119
+ dragover: uploadState.dropzone.isOverDropZone.value && uploadState.isEmpty.value,
120
+ disabled: wrapperProps.value.disabled,
121
+ failed: uploadState.upload.status.value.isError
122
+ })
123
+ );
124
+ const handleDownloadFile = () => {
125
+ if (value.value?.url && value.value?.name) {
126
+ downloadFileFromURL(value.value.url, value.value.name);
127
+ }
104
128
  };
105
- const dropzone = useDropZone(dropZoneRef, {
106
- onDrop,
107
- // specify the types of data to be received.
108
- dataTypes: typeof props.accept === "string" ? [props.accept] : props.accept,
109
- // control multi-file drop
110
- multiple: false,
111
- // whether to prevent default behavior for unhandled events
112
- preventDefaultForUnhandled: false
113
- });
114
129
  </script>
@@ -1,19 +1,23 @@
1
1
  import type { IUploadDropzoneAutoProps } from './types.js';
2
+ import type { IFileValue } from '#core/components/Form/types';
2
3
  declare const _default: import("vue").DefineComponent<IUploadDropzoneAutoProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
3
- success: (...args: any[]) => void;
4
- delete: (...args: any[]) => void;
5
- change: (...args: any[]) => void;
4
+ success: (res: IFileValue) => any;
5
+ delete: () => any;
6
+ change: (value: File | undefined) => any;
6
7
  }, string, import("vue").PublicProps, Readonly<IUploadDropzoneAutoProps> & Readonly<{
7
- onSuccess?: ((...args: any[]) => any) | undefined;
8
- onDelete?: ((...args: any[]) => any) | undefined;
9
- onChange?: ((...args: any[]) => any) | undefined;
8
+ onSuccess?: ((res: IFileValue) => any) | undefined;
9
+ onDelete?: (() => any) | undefined;
10
+ onChange?: ((value: File | undefined) => any) | undefined;
10
11
  }>, {
11
12
  selectFileLabel: string;
12
13
  selectFileSubLabel: string;
13
14
  uploadingLabel: string;
14
15
  uploadFailedLabel: string;
16
+ retryLabel: string;
15
17
  bodyKey: string;
16
18
  responseURL: string;
17
19
  responsePath: string;
20
+ responseName: string;
21
+ responseSize: string;
18
22
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
23
  export default _default;
@@ -5,16 +5,18 @@ export interface IUploadDropzoneAutoProps extends IFieldProps {
5
5
  baseURL: string;
6
6
  };
7
7
  uploadPathURL?: string;
8
+ bodyKey?: string;
9
+ responseURL?: string;
10
+ responsePath?: string;
11
+ responseName?: string;
12
+ responseSize?: string;
13
+ accept?: string[] | string;
14
+ maxSize?: number;
8
15
  selectFileLabel?: string;
9
16
  selectFileSubLabel?: string;
10
17
  uploadingLabel?: string;
11
18
  uploadFailedLabel?: string;
12
19
  retryLabel?: string;
13
- accept?: string[] | string;
14
- bodyKey?: string;
15
- responseURL?: string;
16
- responsePath?: string;
17
- maxSize?: number;
18
20
  }
19
21
  export type IUploadDropzoneAutoField = IFormFieldBase<INPUT_TYPES.UPLOAD_DROPZONE_AUTO, IUploadDropzoneAutoProps, {
20
22
  change: (value: File | undefined) => void;
@@ -0,0 +1,25 @@
1
+ import type { TemplateRef } from 'vue';
2
+ import type { IUploadDropzoneAutoProps } from './types.js';
3
+ import type { IFileValue } from '#core/components/Form/types';
4
+ export declare enum UploadState {
5
+ EMPTY = "empty",
6
+ UPLOADING = "uploading",
7
+ SUCCESS = "success",
8
+ ERROR = "error"
9
+ }
10
+ export declare const useUploadState: (props: IUploadDropzoneAutoProps, emits: any, onChange: (value: IFileValue | undefined) => void, setErrors: (error: string) => void, value: any, acceptedFileTypes: any, wrapperProps: any, dropzoneRef: TemplateRef<HTMLDivElement | undefined>) => {
11
+ currentState: import("vue").ComputedRef<UploadState>;
12
+ isEmpty: import("vue").ComputedRef<boolean>;
13
+ isUploading: import("vue").ComputedRef<boolean>;
14
+ isSuccess: import("vue").ComputedRef<boolean>;
15
+ isError: import("vue").ComputedRef<boolean>;
16
+ selectedFile: import("vue").Ref<File | undefined, File | undefined>;
17
+ upload: import("../../../helpers/apiObjectHelper.js").IUseObjectLoader<any, any, Record<string, any>>;
18
+ dropzone: import("@vueuse/core").UseDropZoneReturn;
19
+ percent: import("vue").Ref<number, number>;
20
+ handleInputChange: (event: Event) => void;
21
+ handleOpenFile: () => void;
22
+ handleDeleteFile: () => void;
23
+ handleRetryUpload: () => void;
24
+ handlePreview: () => void;
25
+ };
@@ -0,0 +1,187 @@
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
+ const overlay = useOverlay();
9
+ const previewModal = overlay.create(PreviewModal);
10
+ export var UploadState = /* @__PURE__ */ ((UploadState2) => {
11
+ UploadState2["EMPTY"] = "empty";
12
+ UploadState2["UPLOADING"] = "uploading";
13
+ UploadState2["SUCCESS"] = "success";
14
+ UploadState2["ERROR"] = "error";
15
+ return UploadState2;
16
+ })(UploadState || {});
17
+ export const useUploadState = (props, emits, onChange, setErrors, value, acceptedFileTypes, wrapperProps, dropzoneRef) => {
18
+ const selectedFile = ref();
19
+ const fileAllocate = useFileAllocate(selectedFile, props);
20
+ const {
21
+ percent,
22
+ onDownloadProgress,
23
+ onUploadProgress
24
+ } = useFileProgress();
25
+ const request = {
26
+ requestOptions: {
27
+ ...props.requestOptions,
28
+ onDownloadProgress,
29
+ onUploadProgress
30
+ },
31
+ pathURL: props.uploadPathURL
32
+ };
33
+ const upload = useUploadLoader(request);
34
+ const validateFile = (file) => {
35
+ const acceptedTypes = fileAllocate.acceptFile.value;
36
+ if (acceptedTypes) {
37
+ const acceptedTypesList = acceptedTypes.split(",").map((type) => type.trim());
38
+ const fileExtension = file.name.toLowerCase().split(".").pop();
39
+ const isValidFileType = acceptedTypesList.some((acceptedType) => {
40
+ if (acceptedType.startsWith(".")) {
41
+ const extension = acceptedType.slice(1).toLowerCase();
42
+ return fileExtension === extension;
43
+ } else if (!acceptedType.includes("/") && !acceptedType.includes("*")) {
44
+ return fileExtension === acceptedType.toLowerCase();
45
+ }
46
+ if (acceptedType.endsWith("/*")) {
47
+ const baseType = acceptedType.slice(0, -2);
48
+ return file.type.startsWith(baseType + "/");
49
+ }
50
+ return file.type === acceptedType;
51
+ });
52
+ if (!isValidFileType) {
53
+ setErrors("\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(", ") + ")");
54
+ return false;
55
+ }
56
+ }
57
+ const maxSizeBytes = (fileAllocate.acceptFileSizeKb.value || 0) * 1024;
58
+ if (file.size > maxSizeBytes) {
59
+ if (fileAllocate.isAcceptFileUseMb.value) {
60
+ setErrors(`\u0E02\u0E19\u0E32\u0E14\u0E44\u0E1F\u0E25\u0E4C\u0E15\u0E49\u0E2D\u0E07\u0E44\u0E21\u0E48\u0E40\u0E01\u0E34\u0E19 ${fileAllocate.acceptFileSizeMb.value} MB`);
61
+ } else {
62
+ setErrors(`\u0E02\u0E19\u0E32\u0E14\u0E44\u0E1F\u0E25\u0E4C\u0E15\u0E49\u0E2D\u0E07\u0E44\u0E21\u0E48\u0E40\u0E01\u0E34\u0E19 ${fileAllocate.acceptFileSizeKb.value} KB`);
63
+ }
64
+ return false;
65
+ }
66
+ setErrors("");
67
+ return true;
68
+ };
69
+ const processFile = (file) => {
70
+ if (!validateFile(file)) return;
71
+ selectedFile.value = file;
72
+ emits("change", file);
73
+ const formData = new FormData();
74
+ formData.append(props.bodyKey || "file", file);
75
+ upload.run({
76
+ data: formData
77
+ });
78
+ };
79
+ const handleFileDrop = (files) => {
80
+ if (wrapperProps.value.disabled || wrapperProps.value.readonly || !files?.length || !isEmpty.value) return;
81
+ const file = files[0];
82
+ if (file) {
83
+ processFile(file);
84
+ }
85
+ };
86
+ const fileDialog = useFileDialog({
87
+ accept: acceptedFileTypes.value || "",
88
+ directory: false,
89
+ multiple: false
90
+ });
91
+ const dropzone = useDropZone(dropzoneRef, {
92
+ onDrop: handleFileDrop,
93
+ // dataTypes: typeof props.accept === 'string' ? [props.accept] : props.accept,
94
+ multiple: false,
95
+ preventDefaultForUnhandled: false
96
+ });
97
+ const currentState = computed(() => {
98
+ if (value.value) return "success" /* SUCCESS */;
99
+ if (selectedFile.value && upload.status.value.isLoading) return "uploading" /* UPLOADING */;
100
+ if (selectedFile.value && upload.status.value.isError) return "error" /* ERROR */;
101
+ return "empty" /* EMPTY */;
102
+ });
103
+ const isEmpty = computed(() => currentState.value === "empty" /* EMPTY */);
104
+ const isUploading = computed(() => currentState.value === "uploading" /* UPLOADING */);
105
+ const isSuccess = computed(() => currentState.value === "success" /* SUCCESS */);
106
+ const isError = computed(() => currentState.value === "error" /* ERROR */);
107
+ const handleInputChange = (event) => {
108
+ if (wrapperProps.value.disabled || wrapperProps.value.readonly) return;
109
+ const file = event.target.files?.[0];
110
+ if (file) {
111
+ processFile(file);
112
+ }
113
+ };
114
+ const handleOpenFile = () => {
115
+ if (wrapperProps.value.disabled || wrapperProps.value.readonly) return;
116
+ fileDialog.open();
117
+ };
118
+ const handleDeleteFile = () => {
119
+ fileDialog.reset();
120
+ upload.clear();
121
+ selectedFile.value = void 0;
122
+ onChange(void 0);
123
+ emits("delete");
124
+ };
125
+ const handleRetryUpload = () => {
126
+ if (selectedFile.value) {
127
+ const formData = new FormData();
128
+ formData.append(props.bodyKey || "file", selectedFile.value);
129
+ upload.run(formData);
130
+ }
131
+ };
132
+ const handlePreview = () => {
133
+ previewModal.open({
134
+ value: value.value
135
+ });
136
+ };
137
+ watch(
138
+ () => fileDialog.files.value,
139
+ (files) => {
140
+ if (files?.length) {
141
+ processFile(files[0]);
142
+ }
143
+ }
144
+ );
145
+ watch(
146
+ () => upload.status.value.isSuccess,
147
+ (isSuccess2) => {
148
+ if (isSuccess2 && upload.data.value) {
149
+ const fileValue = {
150
+ url: _get(upload.data.value, props.responseURL),
151
+ path: _get(upload.data.value, props.responsePath),
152
+ name: _get(upload.data.value, props.responseName) || selectedFile.value?.name,
153
+ size: Number(_get(upload.data.value, props.responseSize) || selectedFile.value?.size)
154
+ };
155
+ value.value = fileValue;
156
+ emits("success", fileValue);
157
+ }
158
+ }
159
+ );
160
+ watch(
161
+ () => upload.status.value.isError,
162
+ (isError2) => {
163
+ if (isError2) {
164
+ setErrors(StringHelper.getError(upload.status.value.errorData));
165
+ }
166
+ }
167
+ );
168
+ return {
169
+ // State
170
+ currentState,
171
+ isEmpty,
172
+ isUploading,
173
+ isSuccess,
174
+ isError,
175
+ selectedFile,
176
+ // Upload utilities
177
+ upload,
178
+ dropzone,
179
+ percent,
180
+ // Handlers
181
+ handleInputChange,
182
+ handleOpenFile,
183
+ handleDeleteFile,
184
+ handleRetryUpload,
185
+ handlePreview
186
+ };
187
+ };
@@ -20,7 +20,7 @@
20
20
  <p class="text-error-400">
21
21
  <Icon
22
22
  name="i-heroicons:exclamation-circle-solid"
23
- class="size-8 text-error-400"
23
+ class="text-error-400 size-8"
24
24
  />
25
25
  </p>
26
26
  </div>
@@ -3,14 +3,12 @@ export const useUploadLoader = (request) => {
3
3
  return useObjectLoader({
4
4
  method: "post",
5
5
  url: request.pathURL || "",
6
- getRequestOptions: (_data, opts) => ({
6
+ getRequestOptions: () => ({
7
7
  ...request.requestOptions,
8
8
  headers: {
9
9
  ...request.requestOptions.headers,
10
10
  "Content-Type": "multipart/form-data"
11
- },
12
- onDownloadProgress: opts.data?.onDownloadProgress,
13
- onUploadProgress: opts.data?.onUploadProgress
11
+ }
14
12
  })
15
13
  });
16
14
  };
@@ -1,4 +1,5 @@
1
1
  import type { Ref } from 'vue';
2
+ import type { AxiosProgressEvent } from 'axios';
2
3
  export declare const checkMaxSize: (file: File, acceptFileSize?: number) => boolean;
3
4
  export declare const checkFileType: (file: File, acceptFileType: string | string[]) => boolean;
4
5
  export declare const generateURL: (file: File) => string;
@@ -25,7 +26,7 @@ export declare const useFileSize: (size?: number) => {
25
26
  };
26
27
  export declare const useFileProgress: () => {
27
28
  percent: Ref<number, number>;
28
- onUploadProgress: (progressEvent: ProgressEvent) => void;
29
- onDownloadProgress: (progressEvent: ProgressEvent) => void;
29
+ onUploadProgress: (progressEvent: AxiosProgressEvent) => void;
30
+ onDownloadProgress: (progressEvent: AxiosProgressEvent) => void;
30
31
  };
31
32
  export declare const downloadFileFromURL: (url: string, filename?: string) => Promise<void>;
@@ -86,6 +86,10 @@ export const useFileSize = (size = 0) => {
86
86
  export const useFileProgress = () => {
87
87
  const percent = ref(0);
88
88
  const onUploadProgress = (progressEvent) => {
89
+ if (!progressEvent.total || progressEvent.total === 0) {
90
+ percent.value = 100;
91
+ return;
92
+ }
89
93
  percent.value = Number.parseFloat(
90
94
  StringHelper.withFixed(
91
95
  (Math.floor(progressEvent.loaded * 100 / progressEvent.total) || 0) * 0.8
@@ -93,13 +97,13 @@ export const useFileProgress = () => {
93
97
  );
94
98
  };
95
99
  const onDownloadProgress = (progressEvent) => {
96
- if (progressEvent.total === 0) {
100
+ if (!progressEvent.total || progressEvent.total === 0) {
97
101
  percent.value = 100;
98
102
  return;
99
103
  }
100
104
  percent.value = Number.parseFloat(
101
105
  StringHelper.withFixed(
102
- (Math.floor(progressEvent.loaded * 100 / progressEvent.total) || 0) * 0.2 + 80
106
+ Math.floor(progressEvent.loaded * 100 / progressEvent.total) * 0.2 + 80
103
107
  )
104
108
  );
105
109
  };
@@ -13,6 +13,28 @@ export declare const uploadFileDropzoneTheme: {
13
13
  failedImgIcon: string;
14
14
  loadingIcon: string;
15
15
  labelIcon: string;
16
+ onLoadingWrapper: string;
17
+ onLoadingPlaceholderWrapper: string;
18
+ onLoadingPlaceholderIconClass: string;
19
+ onLoadingTextWrapper: string;
20
+ onLoadingLoadingIconClass: string;
21
+ onPreviewWrapper: string;
22
+ onPreviewPreviewImgWrapper: string;
23
+ onPreviewPreviewImgClass: string;
24
+ onPreviewPreviewFileClass: string;
25
+ onPreviewTextWrapper: string;
26
+ onFailedWrapper: string;
27
+ onFailedFailedImgWrapper: string;
28
+ onFailedFailedIconClass: string;
29
+ onFailedTextWrapper: string;
30
+ actionWrapper: string;
31
+ actionIconClass: string;
32
+ actionDeleteIconClass: string;
33
+ actionPreviewIcon: string;
34
+ actionDownloadIcon: string;
35
+ actionDeleteIcon: string;
36
+ actionRetryIcon: string;
37
+ actionRetryBtnClass: string;
16
38
  };
17
39
  variants: {
18
40
  dragover: {
@@ -20,5 +42,15 @@ export declare const uploadFileDropzoneTheme: {
20
42
  base: string;
21
43
  };
22
44
  };
45
+ disabled: {
46
+ true: {
47
+ base: string;
48
+ };
49
+ };
50
+ failed: {
51
+ true: {
52
+ base: string;
53
+ };
54
+ };
23
55
  };
24
56
  };
@@ -1,23 +1,59 @@
1
1
  export const uploadFileDropzoneTheme = {
2
2
  slots: {
3
- base: "relative w-full p-4 transition rounded-lg flex items-center justify-center ring-1 bg-white ring-gray-300",
3
+ base: "relative w-full text-base p-4 transition rounded-lg flex items-center justify-center ring-1 bg-white ring-gray-300",
4
4
  wrapper: "flex flex-col items-center w-full",
5
5
  disabled: "bg-gray-100 border-none grayscale cursor-not-allowed",
6
- failed: "border-danger",
6
+ failed: "border-error",
7
7
  placeholderWrapper: "py-4 flex flex-col items-center justify-center",
8
8
  placeholder: "text-gray-400 text-center font-light text-sm truncate",
9
9
  labelWrapper: "flex items-center space-x-2 text-gray-400 text-center",
10
10
  filePreviewIcon: "i-heroicons:document-text-solid",
11
- uploadIcon: "i-ph:cloud-arrow-up",
11
+ uploadIcon: "ri:upload-cloud-2-line",
12
12
  placeholderImgIcon: "i-material-symbols:imagesmode-outline",
13
13
  failedImgIcon: "i-material-symbols:imagesmode-outline",
14
14
  loadingIcon: "i-svg-spinners:180-ring-with-bg",
15
- labelIcon: "w-6 h-6 text-gray-400 text-center mb-4"
15
+ labelIcon: "size-6 text-gray-400 text-center mb-3",
16
+ // Loading state
17
+ onLoadingWrapper: "flex items-center space-x-4 w-full",
18
+ onLoadingPlaceholderWrapper: "flex-shrink-0",
19
+ onLoadingPlaceholderIconClass: "size-12 text-gray-400",
20
+ onLoadingTextWrapper: "flex-1 min-w-0 flex items-center justify-between",
21
+ onLoadingLoadingIconClass: "size-10 text-primary animate-spin",
22
+ // Preview state
23
+ onPreviewWrapper: "flex items-center space-x-4 rounded-lg w-full",
24
+ onPreviewPreviewImgWrapper: "flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden bg-gray-100",
25
+ onPreviewPreviewImgClass: "w-full h-full object-cover",
26
+ onPreviewPreviewFileClass: "size-8 text-gray-400 m-auto mt-4",
27
+ onPreviewTextWrapper: "flex-1 min-w-0 flex items-center justify-between",
28
+ // Failed state
29
+ onFailedWrapper: "flex items-start space-x-4 w-full rounded-lg",
30
+ onFailedFailedImgWrapper: "flex-shrink-0",
31
+ onFailedFailedIconClass: "size-12",
32
+ onFailedTextWrapper: "flex-1 min-w-0 flex items-start justify-between",
33
+ // Actions
34
+ actionWrapper: "flex items-center space-x-2",
35
+ actionIconClass: "size-6 text-dimmed hover:text-dimmed-600 cursor-pointer transition-colors",
36
+ actionDeleteIconClass: "size-6 text-(--ui-color-error-500) hover:text-(--ui-color-error-600) cursor-pointer transition-colors",
37
+ actionPreviewIcon: "ic:outline-remove-red-eye",
38
+ actionDownloadIcon: "material-symbols:download",
39
+ actionDeleteIcon: "material-symbols:delete",
40
+ actionRetryIcon: "stash:arrow-retry",
41
+ actionRetryBtnClass: "px-0"
16
42
  },
17
43
  variants: {
18
44
  dragover: {
19
45
  true: {
20
- base: "ring-primary bg-primary-50"
46
+ base: "ring-primary bg-(--ui-color-primary-50)"
47
+ }
48
+ },
49
+ disabled: {
50
+ true: {
51
+ base: "bg-gray-100 border-none grayscale cursor-not-allowed"
52
+ }
53
+ },
54
+ failed: {
55
+ true: {
56
+ base: "ring-(--ui-color-error-500) bg-(--ui-color-error-50)"
21
57
  }
22
58
  }
23
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "2.12.3",
3
+ "version": "2.13.0",
4
4
  "repository": "https://gitlab.finema.co/finema/ui-kit",
5
5
  "license": "MIT",
6
6
  "author": "Finema Dev Core Team",