@ramathibodi/nuxt-commons 0.1.34 → 0.1.35

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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.1.34",
7
+ "version": "0.1.35",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.3",
10
10
  "unbuild": "2.0.0"
@@ -70,8 +70,7 @@ const loadFormData = () => {
70
70
  watchEffect(loadFormData)
71
71
 
72
72
  watch(() => isShowing.value, (newValue) => {
73
- if (!newValue) formPadRef.value.reset()
74
- else loadFormData()
73
+ if (newValue) loadFormData()
75
74
  })
76
75
  </script>
77
76
 
@@ -1,14 +1,15 @@
1
1
  <script lang="ts" setup>
2
- import { uniqWith, isEqual } from 'lodash-es'
3
- import { ref, watch } from 'vue'
4
- import { VTextField } from 'vuetify/components/VTextField'
5
- import { useAlert } from '../../composables/alert'
2
+ import {isEqual, uniqWith} from 'lodash-es'
3
+ import {ref, watch} from 'vue'
4
+ import {VTextField} from 'vuetify/components/VTextField'
5
+ import {useAlert} from '../../composables/alert'
6
6
 
7
7
  const alert = useAlert()
8
8
 
9
9
  interface Base64String {
10
10
  base64String?: string
11
11
  fileName: string
12
+ originalFileName?: string
12
13
  id?: number
13
14
  }
14
15
 
@@ -16,22 +17,24 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VTextField['$props
16
17
  accept?: string
17
18
  multiple?: boolean
18
19
  maxSize?: number
19
- modelValue?: Base64String[]
20
+ modelValue?: Base64String | Base64String[]
21
+ downloadable?: boolean
20
22
  }
21
23
 
22
24
  const props = withDefaults(defineProps<Props>(), {
23
25
  accept: '*',
24
26
  multiple: false,
25
27
  maxSize: 5,
28
+ downloadable: false
26
29
  })
27
30
 
28
31
  const emit = defineEmits<{
29
- (e: 'update:modelValue', value: Base64String[]): void
32
+ (e: 'update:modelValue', value: Base64String | Base64String[]): void
30
33
  }>()
31
34
 
32
35
  const allFiles = ref<File[]>([])
33
36
  const allAssets = ref<Base64String[]>([])
34
- const combinedBase64String = ref<Base64String[]>([])
37
+ const combinedBase64String = ref<Base64String[] | Base64String>([])
35
38
  const fileInput = ref()
36
39
 
37
40
  function openWindowUpload() {
@@ -47,141 +50,172 @@ function addFiles(files: File | File[]) {
47
50
 
48
51
  function removeFileByIndex(i: number | string) {
49
52
  const index = Number(i)
50
- if (Array.isArray(allFiles.value) && allFiles.value.length) {
51
- if (index >= 0 && index < allFiles.value.length) allFiles.value.splice(index, 1)
52
- }
53
+ if (index >= 0 && index < allFiles.value.length) allFiles.value.splice(index, 1)
53
54
  }
54
55
 
55
56
  function removeAssetByIndex(i: number | string) {
56
57
  const index = Number(i)
57
- if (Array.isArray(allAssets.value) && allAssets.value.length) {
58
- if (index >= 0 && index < allAssets.value.length) allAssets.value.splice(index, 1)
59
- }
58
+ if (index >= 0 && index < allAssets.value.length) allAssets.value.splice(index, 1)
60
59
  }
61
60
 
62
61
  function fileToBase64(file: File) {
63
62
  const maxSize = props.maxSize * 1048576
64
63
 
65
64
  return new Promise<Base64String>((resolve, reject) => {
66
- if (file.size > maxSize) reject (`File (${file.name}) size exceeds the ${props.maxSize} MB limit.`)
65
+ if (file.size > maxSize) reject(`File (${file.name}) size exceeds the ${props.maxSize} MB limit.`)
67
66
 
68
67
  const reader = new FileReader()
69
- reader.onload = function (event) {
68
+ reader.onload = (event) => {
70
69
  resolve({ fileName: file.name, base64String: event.target?.result as string })
71
70
  }
72
- reader.onerror = function (error) {
73
- reject(error)
74
- }
71
+ reader.onerror = reject
75
72
  reader.readAsDataURL(file)
76
73
  })
77
74
  }
78
75
 
79
- function base64ToFile(base64Data: string, filename: string) {
80
- // Extract content type and base64 payload from the Base64 string
81
- const matchResult = base64Data.match(/data:([^;]*);base64,(.*)/)
82
- if (matchResult === null) {
83
- return undefined
76
+ function base64ToFile(base64Data: string, filename: string, defaultContentType: string = "application/octet-stream") {
77
+ const matchResult = base64Data.match(/data:([^;]*);base64,(.*)/);
78
+ let contentType: string;
79
+ let base64Payload: string;
80
+
81
+ if (matchResult) {
82
+ [contentType, base64Payload] = matchResult.slice(1)
83
+ } else {
84
+ contentType = defaultContentType
85
+ base64Payload = base64Data
84
86
  }
85
- const [contentType, base64Payload] = matchResult.slice(1)
86
-
87
- // Convert base64 to a Uint8Array
88
- const binaryStr = atob(base64Payload)
89
- const len = binaryStr.length
90
- const bytes = new Uint8Array(len)
91
- for (let i = 0; i < len; i++) {
92
- bytes[i] = binaryStr.charCodeAt(i)
87
+
88
+ try {
89
+ const binaryStr = atob(base64Payload)
90
+ const bytes = new Uint8Array(binaryStr.length)
91
+ for (let i = 0; i < binaryStr.length; i++) {
92
+ bytes[i] = binaryStr.charCodeAt(i)
93
+ }
94
+ return new File([bytes], filename, { type: contentType });
95
+ } catch (error) {
96
+ console.error("Invalid base64 data", error);
97
+ return undefined;
93
98
  }
99
+ }
100
+
101
+ function downloadBase64File(base64Data: string, filename: string): void {
102
+ const file = base64ToFile(base64Data,filename)
94
103
 
95
- return new File([bytes], filename, { type: contentType })
104
+ if (file) {
105
+ const link = document.createElement("a");
106
+ link.href = URL.createObjectURL(file);
107
+ link.download = filename;
108
+
109
+ // Append the link to the body temporarily and trigger the download
110
+ document.body.appendChild(link);
111
+ link.click();
112
+
113
+ // Cleanup
114
+ document.body.removeChild(link);
115
+ URL.revokeObjectURL(link.href);
116
+ }
96
117
  }
97
118
 
119
+
98
120
  watch(
99
- () => props.modelValue,
100
- () => {
101
- if (props.modelValue && Array.isArray(props.modelValue)) {
102
- allAssets.value = props.modelValue.filter((item: Base64String) => item.id !== undefined)
103
- allFiles.value = props.modelValue.filter((item: Base64String) => item.id === undefined && item.base64String !== undefined).map((base64: Base64String) => base64ToFile(base64.base64String as string, base64.fileName)).filter((item: File | undefined) => item !== undefined) as File[]
104
- }
105
- else {
106
- allAssets.value = []
107
- allFiles.value = []
108
- }
109
- },
110
- { deep: true, immediate: true },
121
+ () => props.modelValue,
122
+ () => {
123
+ if (Array.isArray(props.modelValue)) {
124
+ allAssets.value = props.modelValue.filter((item) => item.id !== undefined)
125
+ allFiles.value = props.modelValue
126
+ .filter((item) => item.id === undefined && item.base64String !== undefined)
127
+ .map((base64) => base64ToFile(base64.base64String as string, base64.fileName))
128
+ .filter((item) => item !== undefined) as File[]
129
+ } else if (props.modelValue) {
130
+ allAssets.value = props.modelValue.id !== undefined ? [props.modelValue] : []
131
+ allFiles.value =
132
+ props.modelValue.id === undefined && props.modelValue.base64String !== undefined
133
+ ? [base64ToFile(props.modelValue.base64String, props.modelValue.fileName)].filter(
134
+ (item) => item !== undefined
135
+ ) as File[]
136
+ : []
137
+ } else {
138
+ allAssets.value = []
139
+ allFiles.value = []
140
+ }
141
+ },
142
+ { deep: true, immediate: true }
111
143
  )
112
144
 
113
145
  watch([allAssets, allFiles], () => {
114
- if (allFiles.value && allFiles.value?.length) {
115
- const base64Promises = allFiles.value?.map(file => fileToBase64(file))
116
-
117
- Promise.all(base64Promises).then((base64Strings) => {
118
- combinedBase64String.value = [...allAssets.value, ...base64Strings]
119
- }).catch((error) => {
120
- alert?.addAlert({ message: error, alertType: 'error' })
121
- allFiles.value = []
122
- })
123
- }
124
- else {
125
- combinedBase64String.value = [...allAssets.value]
146
+ if (allFiles.value.length) {
147
+ const base64Promises = allFiles.value.map(fileToBase64)
148
+ Promise.all(base64Promises)
149
+ .then((base64Strings) => {
150
+ combinedBase64String.value = props.multiple
151
+ ? [...allAssets.value, ...base64Strings]
152
+ : base64Strings[0] || allAssets.value[0] || null
153
+ })
154
+ .catch((error) => {
155
+ alert?.addAlert({ message: error, alertType: 'error' })
156
+ allFiles.value = []
157
+ })
158
+ } else {
159
+ combinedBase64String.value = props.multiple
160
+ ? [...allAssets.value]
161
+ : allAssets.value[0] || null
126
162
  }
127
163
  }, { deep: true, immediate: true })
128
164
 
129
165
  watch(combinedBase64String, (newValue, oldValue) => {
130
166
  if (!isEqual(newValue, oldValue)) {
131
- emit('update:modelValue', uniqWith(newValue, isEqual))
167
+ emit('update:modelValue', props.multiple ? uniqWith(newValue as Base64String[], isEqual) : newValue)
132
168
  }
133
169
  }, { deep: true })
134
170
  </script>
135
171
 
136
172
  <template>
137
173
  <v-text-field
138
- v-bind="$attrs"
139
- label="Upload files"
140
- readonly
141
- :dirty="combinedBase64String.length>0"
142
- v-on="(combinedBase64String.length>0) ? {} : { click: openWindowUpload }"
174
+ v-bind="$attrs"
175
+ label="Upload files"
176
+ readonly
177
+ :dirty="Array.isArray(combinedBase64String) ? combinedBase64String.length > 0 : !!combinedBase64String"
178
+ v-on="Array.isArray(combinedBase64String) && combinedBase64String.length > 0 ? {} : { click: openWindowUpload }"
143
179
  >
144
180
  <template #default>
145
181
  <v-chip
146
- v-for="(asset, index) in allAssets"
147
- :key="asset"
148
- color="green"
149
- variant="flat"
150
- closable
151
- @click:close="removeAssetByIndex(index)"
182
+ v-for="(asset, index) in allAssets"
183
+ :key="asset.fileName"
184
+ color="green"
185
+ variant="flat"
186
+ closable
187
+ @click:close="removeAssetByIndex(index)"
152
188
  >
153
- {{ asset.fileName }}
189
+ {{ asset.originalFileName || asset.fileName }}
190
+ <template #append v-if="downloadable">
191
+ <slot name="download" :item="asset">
192
+ <v-icon @click="downloadBase64File(asset.base64String || '',asset.originalFileName || asset.fileName)" v-if="asset.base64String">mdi mdi-download</v-icon>
193
+ </slot>
194
+ </template>
154
195
  </v-chip>
155
196
  <v-chip
156
- v-for="(file, index) in allFiles"
157
- :key="file"
158
- color="primary"
159
- variant="flat"
160
- closable
161
- @click:close="removeFileByIndex(index)"
197
+ v-for="(file, index) in allFiles"
198
+ :key="file.name"
199
+ color="primary"
200
+ variant="flat"
201
+ closable
202
+ @click:close="removeFileByIndex(index)"
162
203
  >
163
204
  {{ file.name }}
164
205
  </v-chip>
165
206
  </template>
166
207
 
167
- <template
168
- v-if="combinedBase64String.length>0 && props.multiple"
169
- #append-inner
170
- >
171
- <VBtn
172
- variant="text"
173
- :icon="true"
174
- @click="openWindowUpload"
175
- >
208
+ <template v-if="props.multiple && Array.isArray(combinedBase64String) && combinedBase64String.length > 0" #append-inner>
209
+ <VBtn variant="text" :icon="true" @click="openWindowUpload">
176
210
  <v-icon>mdi mdi-plus</v-icon>
177
211
  </VBtn>
178
212
  </template>
179
213
  </v-text-field>
180
214
  <v-file-input
181
- ref="fileInput"
182
- :accept="props.accept"
183
- :multiple="props.multiple"
184
- style="display: none"
185
- @update:model-value="addFiles"
215
+ ref="fileInput"
216
+ :accept="props.accept"
217
+ :multiple="props.multiple"
218
+ style="display: none"
219
+ @update:model-value="addFiles"
186
220
  />
187
221
  </template>
@@ -22,6 +22,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
22
22
  insertable?: boolean
23
23
  searchable?: boolean
24
24
 
25
+ loading?: boolean
26
+
25
27
  viewSwitch?: boolean
26
28
  viewSwitchMultiple?: boolean
27
29
 
@@ -44,6 +46,8 @@ const props = withDefaults(defineProps<Props>(), {
44
46
  insertable: true,
45
47
  searchable: true,
46
48
 
49
+ loading: false,
50
+
47
51
  viewSwitch: false,
48
52
  viewSwitchMultiple:false,
49
53
 
@@ -208,6 +212,7 @@ defineExpose({operation})
208
212
  :items="items"
209
213
  :item-value="modelKey"
210
214
  :search="search"
215
+ :loading="loading"
211
216
  >
212
217
  <template #default="defaultProps" v-if="viewType.includes('iterator')">
213
218
  <slot
@@ -244,7 +249,7 @@ defineExpose({operation})
244
249
  <v-container fluid>
245
250
  <v-row>
246
251
  <v-col
247
- v-for="key in computedSkeletonPerPage"
252
+ v-for="key in itemsPerPage"
248
253
  :key="key"
249
254
  :cols="cols"
250
255
  :sm="sm"
@@ -340,6 +345,7 @@ defineExpose({operation})
340
345
  color="primary"
341
346
  :items="items"
342
347
  :search="search"
348
+ :loading="loading"
343
349
  v-if="viewType.includes('table')"
344
350
  >
345
351
  <!-- @ts-ignore -->
@@ -1,6 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import { VueSignaturePad } from 'vue-signature-pad';
3
- import { type Ref, ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
3
+ import { VInput } from 'vuetify/components/VInput'
4
+ import { type Ref, ref, onMounted, onBeforeUnmount, nextTick,watch,defineExpose,useTemplateRef } from 'vue'
4
5
 
5
6
  interface SignatureOptions {
6
7
  penColor: string
@@ -8,7 +9,7 @@ interface SignatureOptions {
8
9
  maxWidth?: number
9
10
  }
10
11
 
11
- interface SignatureProps {
12
+ interface SignatureProps extends /* @vue-ignore */ InstanceType<typeof VInput['$props']> {
12
13
  title?: string
13
14
  btnName?: string
14
15
  titleConfirm?: string
@@ -21,8 +22,10 @@ const props = withDefaults(defineProps<SignatureProps>(), {
21
22
  titleConfirm: 'I Accept My Signature',
22
23
  })
23
24
 
24
- const signaturePadRef: Ref<any> = ref(null)
25
- const signatureData: Ref<string> = ref('')
25
+ const inputRef = useTemplateRef<VInput>("inputRef")
26
+ const signaturePadRef = useTemplateRef<typeof VueSignaturePad>("signaturePadRef")
27
+
28
+ const signatureData: Ref<string|null> = ref(null)
26
29
  const colorOptions: string[] = ['#303F9F', '#1A2023', '#2E7D32', '#AC04BF']
27
30
  const defaultColor: string = colorOptions[0]
28
31
  const selectedColor: Ref<string> = ref(defaultColor)
@@ -30,28 +33,27 @@ const signatureOptions: Ref<SignatureOptions> = ref({ penColor: defaultColor, mi
30
33
  const isDialogOpen: Ref<boolean> = ref(false)
31
34
 
32
35
  const undoSignature = (): void => {
33
- signaturePadRef.value.undoSignature()
36
+ signaturePadRef.value?.undoSignature()
34
37
  }
35
38
 
36
39
  const clearSignature = (): void => {
37
- signaturePadRef.value.clearSignature()
40
+ signaturePadRef.value?.clearSignature()
38
41
  }
39
42
 
40
43
  const closeDialog = (): void => {
41
44
  isDialogOpen.value = false
42
- signaturePadRef.value.clearSignature()
43
- signaturePadRef.value.clearCacheImages()
45
+ signaturePadRef.value?.clearSignature()
46
+ signaturePadRef.value?.clearCacheImages()
44
47
  }
45
48
 
46
49
  const emit = defineEmits<{
47
- (event: 'update:modelValue', value: string): void
50
+ (event: 'update:modelValue', value: string|null): void
48
51
  }>()
49
52
 
50
53
  const saveSignature = (): void => {
51
54
  isDialogOpen.value = false
52
- const { isEmpty, data } = signaturePadRef.value.saveSignature()
53
- signatureData.value = isEmpty ? '' : data
54
- emit('update:modelValue', signatureData.value)
55
+ const { isEmpty, data } = signaturePadRef.value?.saveSignature()
56
+ signatureData.value = isEmpty ? null : data
55
57
  }
56
58
 
57
59
  const changePenColor = (color: string): void => {
@@ -65,130 +67,167 @@ const openSignatureDialog = (): void => {
65
67
  isDialogOpen.value = true
66
68
  nextTick(() =>{
67
69
  if (props.modelValue) {
68
- signaturePadRef.value.fromDataURL(props.modelValue)
70
+ signaturePadRef.value?.fromDataURL(props.modelValue)
69
71
  }
70
72
  });
71
73
  }
72
74
 
73
75
  const signaturePadHeight: Ref<string> = ref('')
74
76
  const updateSignaturePadHeight = () => {
75
- const screenHeight = window.innerHeight
76
- signaturePadHeight.value = `${screenHeight * 0.4}px`
77
+ const screenHeight = window.innerHeight
78
+ signaturePadHeight.value = `${screenHeight * 0.4}px`
77
79
  }
78
80
 
81
+ watch(signatureData,()=>{
82
+ emit('update:modelValue', signatureData.value)
83
+ })
84
+
85
+ watch(()=>props.modelValue, (newValue) => {
86
+ signatureData.value = newValue || null
87
+ },{immediate: true})
88
+
79
89
  onMounted(() => {
80
- updateSignaturePadHeight()
81
- window.addEventListener('resize', updateSignaturePadHeight)
82
- if (props.modelValue) {
83
- signatureData.value = props.modelValue
84
- }
90
+ updateSignaturePadHeight()
91
+ window.addEventListener('resize', updateSignaturePadHeight)
85
92
  })
86
93
 
87
94
  onBeforeUnmount(() => {
88
- window.removeEventListener('resize', updateSignaturePadHeight)
95
+ window.removeEventListener('resize', updateSignaturePadHeight)
96
+ })
97
+
98
+ const isValid = computed(()=>{
99
+ return inputRef.value?.isValid
100
+ })
101
+
102
+ const errorMessages = computed(()=>{
103
+ return inputRef.value?.errorMessages
104
+ })
105
+
106
+ defineExpose({
107
+ errorMessages,
108
+ isValid,
109
+ reset: ()=>inputRef.value?.reset(),
110
+ resetValidation : ()=>inputRef.value?.resetValidation(),
111
+ validate : ()=>inputRef.value?.validate(),
89
112
  })
90
113
  </script>
91
114
 
92
115
  <template>
93
- <v-card
94
- v-if="signatureData"
95
- v-bind="$attrs"
96
- @click="openSignatureDialog"
97
- >
98
- <v-img
99
- :src="signatureData"
100
- cover
101
- />
102
- </v-card>
103
- <v-btn
104
- append-icon="mdi mdi-draw-pen"
105
- block
106
- class="text-none"
107
- color="primary"
108
- variant="flat"
109
- @click="openSignatureDialog"
110
- >
111
- {{ props.btnName }}
112
- </v-btn>
113
-
114
- <v-dialog
115
- v-model="isDialogOpen"
116
- height="auto"
117
- persistent
118
- width="100%"
119
- >
120
- <v-card>
121
- <v-toolbar>
122
- <v-toolbar-title class="text-no-wrap">
123
- {{ props.title }}
124
- </v-toolbar-title>
125
- <v-btn
126
- icon
127
- @click="undoSignature"
128
- >
129
- <v-icon>fa-solid fa-arrow-rotate-left</v-icon>
130
- </v-btn>
131
- <v-btn
132
- icon
133
- @click="clearSignature"
134
- >
135
- <v-icon>fa-solid fa-trash</v-icon>
136
- </v-btn>
137
- <v-menu>
138
- <template #activator="{ props: activatorProps }">
139
- <v-btn
140
- :color="selectedColor"
141
- :icon="true"
142
- v-bind="activatorProps"
143
- >
144
- <v-icon>fa-solid fa-paintbrush</v-icon>
145
- </v-btn>
146
- </template>
147
- <v-list>
148
- <v-row>
149
- <v-col class="text-center">
150
- <v-avatar
151
- v-for="(color, index) in colorOptions"
152
- :key="index"
153
- :color="color"
154
- :value="color"
155
- class="mr-1"
156
- @click="changePenColor(color)"
157
- >
158
- <v-icon color="white">
159
- {{ selectedColor === color ? 'fa-solid fa-check' : '' }}
160
- </v-icon>
161
- </v-avatar>
162
- </v-col>
163
- </v-row>
164
- </v-list>
165
- </v-menu>
166
- <v-btn
167
- icon
168
- @click="closeDialog"
169
- >
170
- <v-icon>fa-solid fa-xmark</v-icon>
171
- </v-btn>
172
- </v-toolbar>
173
- <v-card-text>
174
- <VueSignaturePad
175
- ref="signaturePadRef"
176
- :options="signatureOptions"
177
- :height="signaturePadHeight"
178
- />
179
- </v-card-text>
180
- <v-divider />
181
- <v-card-actions class="justify-center">
182
- <v-btn
183
- class="text-none"
184
- color="success"
185
- prepend-icon="fa-solid fa-check"
186
- variant="flat"
187
- @click="saveSignature"
116
+ <v-input v-model="signatureData" v-bind="$attrs" ref="inputRef">
117
+ <template #default="{isReadonly,isDisabled}">
118
+ <v-card
119
+ class="w-100"
120
+ flat
121
+ >
122
+ <v-card-text v-if="signatureData">
123
+ <v-img
124
+ :src="signatureData"
125
+ cover
126
+ />
127
+ <v-icon
128
+ class="position-absolute"
129
+ style="top: 8px; right: 8px; z-index: 10;"
130
+ @click="signatureData=null"
131
+ v-if="signatureData && !isReadonly.value"
132
+ >
133
+ mdi mdi-close-circle
134
+ </v-icon>
135
+ </v-card-text>
136
+ <v-card-actions>
137
+ <v-btn
138
+ append-icon="mdi mdi-draw-pen"
139
+ block
140
+ class="text-none"
141
+ color="primary"
142
+ variant="flat"
143
+ @click="openSignatureDialog"
144
+ :readonly="isReadonly.value"
145
+ :disabled="isDisabled.value"
146
+ >
147
+ {{ props.btnName }}
148
+ </v-btn>
149
+ </v-card-actions>
150
+ <v-dialog
151
+ v-model="isDialogOpen"
152
+ height="auto"
153
+ persistent
154
+ width="100%"
188
155
  >
189
- {{ props.titleConfirm }}
190
- </v-btn>
191
- </v-card-actions>
192
- </v-card>
193
- </v-dialog>
156
+ <v-card>
157
+ <v-toolbar>
158
+ <v-toolbar-title class="text-no-wrap">
159
+ {{ props.title }}
160
+ </v-toolbar-title>
161
+ <v-btn
162
+ icon
163
+ @click="undoSignature"
164
+ >
165
+ <v-icon>fa-solid fa-arrow-rotate-left</v-icon>
166
+ </v-btn>
167
+ <v-btn
168
+ icon
169
+ @click="clearSignature"
170
+ >
171
+ <v-icon>fa-solid fa-trash</v-icon>
172
+ </v-btn>
173
+ <v-menu>
174
+ <template #activator="{ props: activatorProps }">
175
+ <v-btn
176
+ :color="selectedColor"
177
+ :icon="true"
178
+ v-bind="activatorProps"
179
+ >
180
+ <v-icon>fa-solid fa-paintbrush</v-icon>
181
+ </v-btn>
182
+ </template>
183
+ <v-list>
184
+ <v-row>
185
+ <v-col class="text-center">
186
+ <v-avatar
187
+ v-for="(color, index) in colorOptions"
188
+ :key="index"
189
+ :color="color"
190
+ :value="color"
191
+ class="mr-1"
192
+ @click="changePenColor(color)"
193
+ >
194
+ <v-icon color="white">
195
+ {{ selectedColor === color ? 'fa-solid fa-check' : '' }}
196
+ </v-icon>
197
+ </v-avatar>
198
+ </v-col>
199
+ </v-row>
200
+ </v-list>
201
+ </v-menu>
202
+ <v-btn
203
+ icon
204
+ @click="closeDialog"
205
+ >
206
+ <v-icon>fa-solid fa-xmark</v-icon>
207
+ </v-btn>
208
+ </v-toolbar>
209
+ <v-card-text>
210
+ <VueSignaturePad
211
+ ref="signaturePadRef"
212
+ :options="signatureOptions"
213
+ :height="signaturePadHeight"
214
+ />
215
+ </v-card-text>
216
+ <v-divider />
217
+ <v-card-actions class="justify-center">
218
+ <v-btn
219
+ class="text-none"
220
+ color="success"
221
+ prepend-icon="fa-solid fa-check"
222
+ variant="flat"
223
+ @click="saveSignature"
224
+ >
225
+ {{ props.titleConfirm }}
226
+ </v-btn>
227
+ </v-card-actions>
228
+ </v-card>
229
+ </v-dialog>
230
+ </v-card>
231
+ </template>
232
+ </v-input>
194
233
  </template>
@@ -0,0 +1,37 @@
1
+ <script lang="ts" setup>
2
+ import { computedAsync } from '@vueuse/core'
3
+ import { useGraphQl } from '../../composables/graphql'
4
+
5
+ interface Props {
6
+ groupKey?: string | null
7
+ itemCode?: string | null
8
+ locale?: string
9
+ notFoundText?: string
10
+ placeholder?: string
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ locale: 'TH',
15
+ })
16
+
17
+ const masterItemValue = computedAsync<string>(async () => {
18
+ if (props.groupKey && props.itemCode) {
19
+ try {
20
+ const result = await useGraphQl().queryPromise<any>("masterItemByGroupKeyAndItemCode",['itemValue', 'itemValueAlternative'],{ groupKey: props.groupKey, itemCode: props.itemCode })
21
+
22
+ if (result) {
23
+ return props.locale === 'TH'
24
+ ? result.itemValue
25
+ : result.itemValueAlternative || result.itemValue
26
+ }
27
+ } catch (e) {
28
+ console.error(e)
29
+ }
30
+ }
31
+ return props.notFoundText || `${props.itemCode}`
32
+ }, props.placeholder || `${props.groupKey}(${props.itemCode})`)
33
+ </script>
34
+
35
+ <template>
36
+ {{ masterItemValue }}
37
+ </template>
@@ -65,23 +65,27 @@ function cancel() {
65
65
  loadFormData()
66
66
  }
67
67
 
68
+ const operation = ref({save, cancel, reload, item, canSave, isLoading,isDataChange,isCreating})
69
+
68
70
  defineExpose({ save, cancel, reload, item, isLoading })
69
71
  </script>
70
72
 
71
73
  <template>
72
74
  <VCard>
73
75
  <VToolbar>
74
- <VToolbarTitle>
75
- <slot name="title">
76
- {{ title }}
77
- <v-icon
78
- size="small"
79
- @click="reload"
80
- >
81
- mdi mdi-refresh
82
- </v-icon>
83
- </slot>
84
- </VToolbarTitle>
76
+ <slot name="titleToolbar" :operation="operation">
77
+ <VToolbarTitle>
78
+ <slot name="title" :operation="operation">
79
+ {{ title }}
80
+ <v-icon
81
+ size="small"
82
+ @click="reload"
83
+ >
84
+ mdi mdi-refresh
85
+ </v-icon>
86
+ </slot>
87
+ </VToolbarTitle>
88
+ </slot>
85
89
  </VToolbar>
86
90
  <VCardText>
87
91
  <form-pad
@@ -99,24 +103,26 @@ defineExpose({ save, cancel, reload, item, isLoading })
99
103
  </form-pad>
100
104
  </VCardText>
101
105
  <VCardActions>
102
- <VSpacer />
103
- <VBtn
104
- color="primary"
105
- variant="flat"
106
- :loading="isLoading"
107
- :disabled="!isDataChange || !canSave"
108
- @click="save"
109
- >
110
- {{ saveCaption }}
111
- </VBtn>
112
- <VBtn
113
- color="error"
114
- variant="flat"
115
- :disabled="isLoading"
116
- @click="cancel"
117
- >
118
- {{ cancelCaption }}
119
- </VBtn>
106
+ <slot name="action" :operation="operation">
107
+ <VSpacer />
108
+ <VBtn
109
+ color="primary"
110
+ variant="flat"
111
+ :loading="isLoading"
112
+ :disabled="!isDataChange || !canSave"
113
+ @click="save"
114
+ >
115
+ {{ saveCaption }}
116
+ </VBtn>
117
+ <VBtn
118
+ color="error"
119
+ variant="flat"
120
+ :disabled="isLoading"
121
+ @click="cancel"
122
+ >
123
+ {{ cancelCaption }}
124
+ </VBtn>
125
+ </slot>
120
126
  </VCardActions>
121
127
  </VCard>
122
128
  </template>
@@ -0,0 +1,41 @@
1
+ <script lang="ts" setup>
2
+ import { computedAsync } from '@vueuse/core'
3
+ import { useGraphQlOperation } from '../../composables/graphqlOperation'
4
+
5
+ interface Props {
6
+ modelName: string
7
+ modelBy?: object
8
+ itemTitle: string
9
+ fields?: Array<string | object>
10
+ cache?: boolean
11
+
12
+ notFoundText?: string
13
+ placeholder?: string
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ cache: false,
18
+ })
19
+
20
+ const modelItemValue = computedAsync<string>(async () => {
21
+ if (props.modelName && props.itemTitle) {
22
+ let fields: any[] = [props.itemTitle]
23
+ const variables: Record<string, any> = Object.assign({},props.modelBy)
24
+
25
+ try {
26
+ const result : Record<string, any> = await useGraphQlOperation('Query',props.modelName,fields,variables,props.cache)
27
+
28
+ if (result) {
29
+ return result[props.itemTitle]
30
+ }
31
+ } catch (e) {
32
+ console.error(e)
33
+ }
34
+ }
35
+ return props.notFoundText
36
+ }, props.placeholder )
37
+ </script>
38
+
39
+ <template>
40
+ {{ modelItemValue }}
41
+ </template>
@@ -5,12 +5,23 @@ import printJS from 'print-js'
5
5
  import { ref, computed } from 'vue'
6
6
  import { useAlert } from '../../composables/alert'
7
7
 
8
- const props = defineProps<{
8
+ interface Props extends /* @vue-ignore */ InstanceType<typeof PDF['$props']> {
9
9
  base64String?: string
10
10
  title?: string
11
11
  fileName?: string
12
12
  disabled?: boolean
13
- }>()
13
+ isPrint?: boolean
14
+ showBackToTopBtn?: boolean
15
+ }
16
+
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ base64String: "",
19
+ title: "",
20
+ fileName: "",
21
+ disabled: false,
22
+ isPrint: false,
23
+ showBackToTopBtn: false
24
+ })
14
25
 
15
26
  const emit = defineEmits(['closeDialog'])
16
27
  const base64 = ref(props.base64String)
@@ -61,9 +72,19 @@ const endLoadPdf = () => {
61
72
  base64.value = ''
62
73
  }
63
74
 
64
- const isMobile = computed(() => {
65
- if (/Android|Mobi|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Macintosh/i.test(navigator.userAgent)) {
66
- return true
75
+ const isMobile = () => {
76
+ return /Android|Mobi|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Macintosh/i.test(navigator.userAgent);
77
+ }
78
+
79
+ const checkMobileAndPrint = computed(() => {
80
+ return !isMobile() && props.isPrint;
81
+ });
82
+
83
+ const setWidthPdf = computed(() => {
84
+ if (isMobile()) {
85
+ return "100%"
86
+ } else {
87
+ return "100dvh"
67
88
  }
68
89
  })
69
90
  </script>
@@ -74,7 +95,7 @@ const isMobile = computed(() => {
74
95
  <v-toolbar-title>{{ props.title }}</v-toolbar-title>
75
96
  <v-spacer />
76
97
  <v-btn
77
- v-if="!isMobile"
98
+ v-if="checkMobileAndPrint"
78
99
  icon="mdi mdi-printer"
79
100
  variant="plain"
80
101
  @click="printPdf"
@@ -91,13 +112,12 @@ const isMobile = computed(() => {
91
112
  @click="endLoadPdf"
92
113
  />
93
114
  </v-toolbar>
94
- <v-card-text class="justify-center">
115
+ <v-card-text class="justify-center h-screen">
95
116
  <PDF
96
- :show-progress="true"
97
- pdf-width="100%"
98
- :src="base64"
99
- :show-page-tooltip="false"
100
- :show-back-to-top-btn="false"
117
+ v-bind="$attrs"
118
+ :pdf-width="setWidthPdf"
119
+ :src="base64"
120
+ :show-back-to-top-btn="props.showBackToTopBtn"
101
121
  />
102
122
  </v-card-text>
103
123
  </v-card>
@@ -4,7 +4,7 @@ import { graphqlInputType, graphqlOperation, graphqlType, scalarType } from "#im
4
4
  export function buildFields(operationFields, fields, depth = 0) {
5
5
  if (!operationFields) return [];
6
6
  if (isClassConstructor(fields)) fields = classAttributes(fields);
7
- if (!fields || fields.length == 0) fields = [...operationFields.fields];
7
+ if (!fields || fields.length == 0) fields = ["*"];
8
8
  if (fields.includes("*")) {
9
9
  operationFields.fields.forEach((field) => {
10
10
  if (!fields.includes(field) && !field.toLowerCase().endsWith("base64") && !field.toLowerCase().endsWith("base64string")) fields.push(field);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -115,5 +115,5 @@
115
115
  "vitest": "^1.6.0",
116
116
  "vue-tsc": "2.0.29"
117
117
  },
118
- "packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a"
118
+ "packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
119
119
  }