@finema/core 1.4.26 → 1.4.28

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.26",
3
+ "version": "1.4.28",
4
4
  "configKey": "core",
5
5
  "compatibility": {
6
6
  "nuxt": "^3.7.4"
package/dist/module.mjs CHANGED
@@ -2,7 +2,7 @@ import { defineNuxtModule, createResolver, installModule, addPlugin, addComponen
2
2
  import 'lodash-es';
3
3
 
4
4
  const name = "@finema/core";
5
- const version = "1.4.26";
5
+ const version = "1.4.28";
6
6
 
7
7
  const colors = {
8
8
  black: "#20243E",
@@ -159,7 +159,8 @@ const table = {
159
159
  class: "-m-1.5 text-gray-700 font-normal"
160
160
  },
161
161
  loadingState: {
162
- label: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..."
162
+ label: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14...",
163
+ icon: "i-svg-spinners:180-ring-with-bg"
163
164
  },
164
165
  emptyState: {
165
166
  label: "\u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25"
@@ -4,7 +4,7 @@
4
4
  :name="name"
5
5
  :description="description"
6
6
  :hint="hint"
7
- :size="size"
7
+ :size="size as FormGroupSize"
8
8
  :data-testid="name"
9
9
  :help="help"
10
10
  :error="errorMessage"
@@ -14,8 +14,10 @@
14
14
  <slot />
15
15
  </UFormGroup>
16
16
  </template>
17
+
17
18
  <script lang="ts" setup>
18
19
  import { type IFieldProps } from '#core/components/Form/types'
20
+ import { type FormGroupSize } from '#ui/types/form-group'
19
21
 
20
22
  defineProps<IFieldProps>()
21
23
  </script>
@@ -91,6 +91,13 @@
91
91
  v-bind="getFieldBinding(option)"
92
92
  v-on="option.on ?? {}"
93
93
  />
94
+ <FormInputUploadFileClassicAuto
95
+ v-else-if="option.type === INPUT_TYPES.UPLOAD_FILE_CLASSIC_AUTO"
96
+ :class="option.class"
97
+ :form="form"
98
+ v-bind="getFieldBinding(option)"
99
+ v-on="option.on ?? {}"
100
+ />
94
101
  </template>
95
102
  </div>
96
103
  </template>
@@ -0,0 +1,181 @@
1
+ <template>
2
+ <FieldWrapper v-bind="wrapperProps">
3
+ <div :class="[ui.base]">
4
+ <input
5
+ ref="fileInput"
6
+ type="file"
7
+ class="hidden"
8
+ :accept="acceptFile"
9
+ :required="isRequired"
10
+ :disabled="isDisabled"
11
+ @change="handleChange"
12
+ />
13
+ <div :class="[ui.wrapper]">
14
+ <div :class="[ui.selectFileBox]">
15
+ <Button size="2xs" @click="handleOpenFile">{{ selectFileLabel || 'Choose File' }}</Button>
16
+ <p :class="ui.placeholder">
17
+ {{ selectedFile?.name ?? placeholder ?? 'No file chosen' }}
18
+ </p>
19
+ <Badge v-if="selectedFile" size="xs" variant="outline"> {{ selectedFileSize }} MB </Badge>
20
+ </div>
21
+ <div v-if="selectedFile">
22
+ <Icon
23
+ v-if="upload.status.value.isSuccess"
24
+ name="heroicons:check-circle-20-solid"
25
+ class="text-success"
26
+ />
27
+ <Icon
28
+ v-if="upload.status.value.isError"
29
+ name="heroicons:x-circle-20-solid"
30
+ class="text-danger"
31
+ />
32
+ <Icon
33
+ v-if="upload.status.value.isLoading"
34
+ name="i-svg-spinners:180-ring-with-bg"
35
+ class="text-primary"
36
+ />
37
+ </div>
38
+ </div>
39
+ </div>
40
+ <img v-if="imagePreviewURL && value" :src="imagePreviewURL" alt="" :class="ui.previewURL" />
41
+ </FieldWrapper>
42
+ </template>
43
+
44
+ <script lang="tsx" setup>
45
+ import {
46
+ _isEmpty,
47
+ computed,
48
+ ref,
49
+ StringHelper,
50
+ toRef,
51
+ useUI,
52
+ useUiConfig,
53
+ useWatchTrue,
54
+ } from '#imports'
55
+ import { type IUploadFileProps } from './types'
56
+ import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
57
+ import { useFieldHOC } from '#core/composables/useForm'
58
+ import { type IUploadRequest, useUploadLoader } from '#core/composables/useUpload'
59
+ import { uploadFileInputClassicAuto } from '#core/ui.config'
60
+ import i18next from 'i18next'
61
+
62
+ const config = useUiConfig<typeof uploadFileInputClassicAuto>(
63
+ uploadFileInputClassicAuto,
64
+ 'uploadFileInput'
65
+ )
66
+
67
+ const props = withDefaults(defineProps<IUploadFileProps>(), {})
68
+
69
+ const { wrapperProps, handleChange: onChange, setErrors, value } = useFieldHOC<string>(props)
70
+
71
+ const request: IUploadRequest = {
72
+ pathURL: props.uploadPathURL,
73
+ requestOptions: props.requestOptions,
74
+ }
75
+
76
+ const upload = useUploadLoader(request)
77
+
78
+ const fileInput = ref<HTMLInputElement>()
79
+ const selectedFile = ref<File | undefined>()
80
+ const percent = ref<number>(0)
81
+
82
+ const selectedFileSize = computed(() => ((selectedFile.value?.size || 0) / 1000 / 1000).toFixed(2))
83
+ const acceptFileSize = computed(() => props.maxSize)
84
+ const acceptFileSizeMb = computed(() => ((acceptFileSize.value || 0) / 1024).toFixed(2))
85
+ const acceptFile = computed(() =>
86
+ typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
87
+ )
88
+
89
+ const { ui } = useUI('breadcrumb', toRef(props, 'ui'), config)
90
+
91
+ const handleOpenFile = () => {
92
+ fileInput.value?.click()
93
+ }
94
+
95
+ const handleChange = (e: Event) => {
96
+ const file = (e.target as HTMLInputElement).files?.[0]
97
+ const result = handleCheckFileCondition(file)
98
+
99
+ if (result && file) {
100
+ selectedFile.value = file
101
+ const formData = new FormData()
102
+
103
+ formData.append('file', file)
104
+ upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
105
+ }
106
+ }
107
+
108
+ const handleCheckFileCondition = (file: File | undefined): boolean => {
109
+ if (!file) return false
110
+ const accept = checkAcceptFile(file)
111
+
112
+ if (!accept) {
113
+ setErrors(i18next.t('custom:invalid_file_type'))
114
+
115
+ return false
116
+ }
117
+
118
+ const maxSize = checkMaxSize(file)
119
+
120
+ if (!maxSize) {
121
+ setErrors(i18next.t('custom:invalid_file_size', { size: acceptFileSizeMb.value }))
122
+
123
+ return false
124
+ }
125
+
126
+ setErrors('')
127
+
128
+ return true
129
+ }
130
+
131
+ const checkAcceptFile = (file: File): boolean => {
132
+ let fileType = ''
133
+
134
+ if (_isEmpty(file.type)) {
135
+ fileType = file.name.split('.').pop() || ''
136
+ } else {
137
+ fileType = file.type.split('/').pop() || ''
138
+ }
139
+
140
+ return acceptFile.value ? acceptFile.value.includes(fileType) : true
141
+ }
142
+
143
+ const checkMaxSize = (file: File): boolean => {
144
+ if (acceptFileSize.value) {
145
+ return file.size / 1000 <= acceptFileSize.value
146
+ }
147
+
148
+ return true
149
+ }
150
+
151
+ const onUploadProgress = (progressEvent: ProgressEvent) => {
152
+ percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
153
+ }
154
+
155
+ const onDownloadProgress = (progressEvent: ProgressEvent) => {
156
+ if (progressEvent.total === 0) {
157
+ percent.value = 100
158
+
159
+ return
160
+ }
161
+
162
+ percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
163
+ }
164
+
165
+ useWatchTrue(
166
+ () => upload.status.value.isSuccess,
167
+ () => {
168
+ if (upload.data.value?.url) {
169
+ value.value = upload.data.value?.url
170
+ onChange(upload.data.value?.url)
171
+ }
172
+ }
173
+ )
174
+
175
+ useWatchTrue(
176
+ () => upload.status.value.isError,
177
+ () => {
178
+ setErrors(StringHelper.getError(upload.status.value.errorData))
179
+ }
180
+ )
181
+ </script>
@@ -0,0 +1,13 @@
1
+ import { type AxiosRequestConfig } from 'axios';
2
+ import { type IFieldProps, type IFormFieldBase, type INPUT_TYPES } from '../types';
3
+ export interface IUploadFileProps extends IFieldProps {
4
+ requestOptions: Omit<AxiosRequestConfig, 'baseURL'> & {
5
+ baseURL: string;
6
+ };
7
+ uploadPathURL: string;
8
+ selectFileLabel?: string;
9
+ accept?: string[] | string;
10
+ maxSize?: number;
11
+ imagePreviewURL?: string;
12
+ }
13
+ export type IUploadFileField = IFormFieldBase<INPUT_TYPES.UPLOAD_FILE_CLASSIC_AUTO, IUploadFileProps, never>;
@@ -9,6 +9,7 @@ import { type IToggleField } from '#core/components/Form/InputToggle/types';
9
9
  import { type ITextareaField } from '#core/components/Form/InputTextarea/types';
10
10
  import { type IDateTimeField } from '#core/components/Form/InputDateTime/date_time_field.types';
11
11
  import { type IUploadFileClassicField } from '#core/components/Form/InputUploadFileClassic/types';
12
+ import { type IUploadFileField } from '#core/components/Form/InputUploadFileClassicAuto/types';
12
13
  export declare const enum INPUT_TYPES {
13
14
  TEXT = "TEXT",
14
15
  TEXTAREA = "TEXTAREA",
@@ -21,7 +22,8 @@ export declare const enum INPUT_TYPES {
21
22
  CHECKBOX = "CHECKBOX",
22
23
  DATE_TIME = "DATE_TIME",
23
24
  DATE = "DATE",
24
- UPLOAD_FILE_CLASSIC = "UPLOAD_FILE_CLASSIC"
25
+ UPLOAD_FILE_CLASSIC = "UPLOAD_FILE_CLASSIC",
26
+ UPLOAD_FILE_CLASSIC_AUTO = "UPLOAD_FILE_CLASSIC_AUTO"
25
27
  }
26
28
  export interface IFieldProps {
27
29
  form?: FormContext;
@@ -50,4 +52,4 @@ export interface IFormFieldBase<I extends INPUT_TYPES, P extends IFieldProps, O>
50
52
  props: P;
51
53
  on?: O;
52
54
  }
53
- export type IFormField = ITextField | IStaticField | ICheckboxField | IRadioField | ISelectField | IToggleField | ITextareaField | IDateTimeField | IUploadFileClassicField;
55
+ export type IFormField = ITextField | IStaticField | ICheckboxField | IRadioField | ISelectField | IToggleField | ITextareaField | IDateTimeField | IUploadFileClassicField | IUploadFileField;
@@ -11,5 +11,6 @@ export var INPUT_TYPES = /* @__PURE__ */ ((INPUT_TYPES2) => {
11
11
  INPUT_TYPES2["DATE_TIME"] = "DATE_TIME";
12
12
  INPUT_TYPES2["DATE"] = "DATE";
13
13
  INPUT_TYPES2["UPLOAD_FILE_CLASSIC"] = "UPLOAD_FILE_CLASSIC";
14
+ INPUT_TYPES2["UPLOAD_FILE_CLASSIC_AUTO"] = "UPLOAD_FILE_CLASSIC_AUTO";
14
15
  return INPUT_TYPES2;
15
16
  })(INPUT_TYPES || {});
@@ -0,0 +1,15 @@
1
+ import { type AxiosRequestConfig } from 'axios';
2
+ export interface IUploadData {
3
+ name: string;
4
+ url: string;
5
+ path: string;
6
+ size: number;
7
+ content_type: string;
8
+ }
9
+ export interface IUploadRequest {
10
+ requestOptions: Omit<AxiosRequestConfig, 'baseURL'> & {
11
+ baseURL: string;
12
+ };
13
+ pathURL: string;
14
+ }
15
+ export declare const useUploadLoader: (request: IUploadRequest) => import("../helpers/apiObjectHelper").IUseObjectLoader<IUploadData, any, Record<string, any>>;
@@ -0,0 +1,16 @@
1
+ import { useObjectLoader } from "./loaderObject.mjs";
2
+ export const useUploadLoader = (request) => {
3
+ return useObjectLoader({
4
+ method: "post",
5
+ url: request.pathURL,
6
+ getRequestOptions: (_data, opts) => ({
7
+ ...request.requestOptions,
8
+ headers: {
9
+ ...request.requestOptions.headers,
10
+ "Content-Type": "multipart/form-data"
11
+ },
12
+ onDownloadProgress: opts.data?.onDownloadProgress,
13
+ onUploadProgress: opts.data?.onUploadProgress
14
+ })
15
+ });
16
+ };
@@ -121,7 +121,20 @@ export default defineNuxtPlugin((nuxtApp) => {
121
121
  set: "set"
122
122
  }
123
123
  },
124
+ custom: {
125
+ invalid_file_type: "\u0E1B\u0E23\u0E30\u0E40\u0E20\u0E17\u0E44\u0E1F\u0E25\u0E4C\u0E44\u0E21\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07",
126
+ invalid_file_size: "\u0E02\u0E19\u0E32\u0E14\u0E44\u0E1F\u0E25\u0E4C\u0E40\u0E01\u0E34\u0E19\u0E01\u0E27\u0E48\u0E32\u0E17\u0E35\u0E48\u0E01\u0E33\u0E2B\u0E19\u0E14 {{size}} Mb"
127
+ },
124
128
  en
129
+ },
130
+ en: {
131
+ zod: {
132
+ ...en
133
+ },
134
+ custom: {
135
+ invalid_file_type: "Invalid file type",
136
+ invalid_file_size: "Invalid exceed of file size {{size}} Mb"
137
+ }
125
138
  }
126
139
  }
127
140
  });
@@ -1 +1 @@
1
- export type UIComponentList = 'modal' | 'slideover' | 'dropdown' | 'icon' | 'button' | 'buttonGroup' | 'tabs' | 'card' | 'breadcrumb' | 'badge' | 'input' | 'pagination' | 'notification';
1
+ export type UIComponentList = 'modal' | 'slideover' | 'dropdown' | 'icon' | 'button' | 'buttonGroup' | 'tabs' | 'card' | 'breadcrumb' | 'badge' | 'input' | 'pagination' | 'notification' | 'uploadFileInput';
@@ -9,3 +9,4 @@ export { icon } from './icon';
9
9
  export { modal } from './modal';
10
10
  export { slideover } from './slideover';
11
11
  export { breadcrumb } from './breadcrumb';
12
+ export { uploadFileInputClassicAuto } from './uploadFileInputClassicAuto';
@@ -9,3 +9,4 @@ export { icon } from "./icon.mjs";
9
9
  export { modal } from "./modal.mjs";
10
10
  export { slideover } from "./slideover.mjs";
11
11
  export { breadcrumb } from "./breadcrumb.mjs";
12
+ export { uploadFileInputClassicAuto } from "./uploadFileInputClassicAuto.mjs";
@@ -33,7 +33,8 @@ export const table = {
33
33
  class: "-m-1.5 text-gray-700 font-normal"
34
34
  },
35
35
  loadingState: {
36
- label: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..."
36
+ label: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14...",
37
+ icon: "i-svg-spinners:180-ring-with-bg"
37
38
  },
38
39
  emptyState: {
39
40
  label: "\u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25"
@@ -0,0 +1,8 @@
1
+ export declare const uploadFileInputClassicAuto: {
2
+ base: string;
3
+ wrapper: string;
4
+ selectFileBox: string;
5
+ placeholder: string;
6
+ previewURL: string;
7
+ default: {};
8
+ };
@@ -0,0 +1,8 @@
1
+ export const uploadFileInputClassicAuto = {
2
+ base: "rounded-lg ring-1 ring-inset ring-gray-300 shadow-sm bg-white px-3 py-2",
3
+ wrapper: "flex items-center justify-between gap-4",
4
+ selectFileBox: "flex items-center gap-2 truncate",
5
+ placeholder: "truncate text-gray-400 text-sm",
6
+ previewURL: "mt-4 max-w-[300px] rounded",
7
+ default: {}
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "1.4.26",
3
+ "version": "1.4.28",
4
4
  "repository": "https://gitlab.finema.co/finema/ui-kit",
5
5
  "license": "MIT",
6
6
  "author": "Finema Dev Core Team",