@ramathibodi/nuxt-commons 0.1.33 → 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.33",
7
+ "version": "0.1.35",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.3",
10
10
  "unbuild": "2.0.0"
@@ -1,5 +1,4 @@
1
1
  <script lang="ts" setup>
2
- import {ref} from 'vue'
3
2
  import {VTabs} from 'vuetify/components'
4
3
 
5
4
  interface Props extends /* @vue-ignore */ InstanceType<typeof VTabs['$props']> {
@@ -11,7 +10,7 @@ defineOptions({
11
10
  })
12
11
 
13
12
  const props = defineProps<Props>()
14
- const currentTab = ref<string | number>()
13
+ const currentTab = defineModel<string|number>()
15
14
  </script>
16
15
 
17
16
  <template>
@@ -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>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import {computed, defineOptions, nextTick, ref, useAttrs, useSlots, watch} from 'vue'
2
+ import {computed, defineExpose, defineOptions, nextTick, ref, useAttrs, useSlots, watch} from 'vue'
3
3
  import {omit} from 'lodash-es'
4
4
  import type {FormDialogCallback} from '../../types/formDialog'
5
5
  import {VDataIterator} from "vuetify/components/VDataIterator";
@@ -20,6 +20,9 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
20
20
  importable?: boolean
21
21
  exportable?: boolean
22
22
  insertable?: boolean
23
+ searchable?: boolean
24
+
25
+ loading?: boolean
23
26
 
24
27
  viewSwitch?: boolean
25
28
  viewSwitchMultiple?: boolean
@@ -41,6 +44,9 @@ const props = withDefaults(defineProps<Props>(), {
41
44
  importable: true,
42
45
  exportable: true,
43
46
  insertable: true,
47
+ searchable: true,
48
+
49
+ loading: false,
44
50
 
45
51
  viewSwitch: false,
46
52
  viewSwitchMultiple:false,
@@ -72,6 +78,10 @@ const items = ref<Record<string, any>[]>([])
72
78
  const search = ref<string>()
73
79
  const currentItem = ref<Record<string, any> | undefined>(undefined)
74
80
 
81
+ function setSearch(keyword: string) {
82
+ search.value = keyword
83
+ }
84
+
75
85
  const isDialogOpen = ref<boolean>(false)
76
86
 
77
87
  watch(() => props.modelValue, (newValue) => {
@@ -189,7 +199,9 @@ function openDialog(item?: object) {
189
199
  })
190
200
  }
191
201
 
192
- const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem })
202
+ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem,setSearch })
203
+
204
+ defineExpose({operation})
193
205
  </script>
194
206
 
195
207
  <template>
@@ -200,6 +212,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
200
212
  :items="items"
201
213
  :item-value="modelKey"
202
214
  :search="search"
215
+ :loading="loading"
203
216
  >
204
217
  <template #default="defaultProps" v-if="viewType.includes('iterator')">
205
218
  <slot
@@ -236,7 +249,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
236
249
  <v-container fluid>
237
250
  <v-row>
238
251
  <v-col
239
- v-for="key in computedSkeletonPerPage"
252
+ v-for="key in itemsPerPage"
240
253
  :key="key"
241
254
  :cols="cols"
242
255
  :sm="sm"
@@ -278,7 +291,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
278
291
  </VToolbarTitle>
279
292
  </v-col>
280
293
  <v-col cols="5">
281
- <slot name="search">
294
+ <slot name="search" :items="items" :operation="operation" v-if="props.searchable">
282
295
  <VTextField
283
296
  v-model="search"
284
297
  class="justify-end w-100"
@@ -293,7 +306,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
293
306
  </v-row>
294
307
 
295
308
  <VToolbarItems>
296
- <slot name="toolbarItems" />
309
+ <slot name="toolbarItems" :items="items" :operation="operation"/>
297
310
  <ImportCSV
298
311
  v-if="props.importable"
299
312
  icon="mdi:mdi-file-upload"
@@ -332,6 +345,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
332
345
  color="primary"
333
346
  :items="items"
334
347
  :search="search"
348
+ :loading="loading"
335
349
  v-if="viewType.includes('table')"
336
350
  >
337
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>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import {VDataTable} from 'vuetify/components/VDataTable'
3
- import {computed, defineOptions, nextTick, ref, useAttrs, watch} from 'vue'
3
+ import {computed, defineOptions,defineExpose, nextTick, ref, useAttrs, watch} from 'vue'
4
4
  import {omit} from 'lodash-es'
5
5
  import type {FormDialogCallback} from '../../types/formDialog'
6
6
 
@@ -19,6 +19,7 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
19
19
  importable?: boolean
20
20
  exportable?: boolean
21
21
  insertable?: boolean
22
+ searchable?: boolean
22
23
  }
23
24
 
24
25
  const props = withDefaults(defineProps<Props>(), {
@@ -29,6 +30,7 @@ const props = withDefaults(defineProps<Props>(), {
29
30
  importable: true,
30
31
  exportable: true,
31
32
  insertable: true,
33
+ searchable: true,
32
34
  })
33
35
 
34
36
  const emit = defineEmits(['update:modelValue'])
@@ -42,6 +44,10 @@ const items = ref<Record<string, any>[]>([])
42
44
  const search = ref<string>()
43
45
  const currentItem = ref<Record<string, any> | undefined>(undefined)
44
46
 
47
+ function setSearch(keyword: string) {
48
+ search.value = keyword
49
+ }
50
+
45
51
  const isDialogOpen = ref<boolean>(false)
46
52
 
47
53
  watch(() => props.modelValue, (newValue) => {
@@ -153,69 +159,77 @@ function openDialog(item?: object) {
153
159
  })
154
160
  }
155
161
 
156
- const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem })
162
+ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem,setSearch })
163
+
164
+ defineExpose({operation})
157
165
  </script>
158
166
 
159
167
  <template>
160
168
  <v-card>
161
- <VToolbar :color="toolbarColor">
162
- <v-row
163
- justify="end"
164
- class="ma-1"
165
- dense
166
- no-gutters
167
- align="center"
168
- >
169
- <v-col cols="7">
170
- <VToolbarTitle class="pl-3">
171
- <slot name="title">
172
- {{ title }}
173
- </slot>
174
- </VToolbarTitle>
175
- </v-col>
176
- <v-col cols="5">
177
- <slot name="search">
178
- <VTextField
179
- v-model="search"
180
- class="justify-end w-100"
181
- density="compact"
182
- hide-details
183
- placeholder="ค้นหา"
184
- clearable
185
- variant="solo"
186
- />
187
- </slot>
188
- </v-col>
189
- </v-row>
190
-
191
- <VToolbarItems>
192
- <slot name="toolbarItems" />
193
- <ImportCSV
194
- v-if="props.importable"
195
- icon="mdi:mdi-file-upload"
196
- variant="flat"
197
- @import="importItems"
198
- :color="toolbarColor"
199
- />
200
- <ExportCSV
201
- v-if="props.exportable && items.length"
202
- icon="mdi:mdi-file-download"
203
- variant="flat"
204
- :file-name="title"
205
- :model-value="items"
206
- :color="toolbarColor"
207
- />
208
- <VBtn
209
- v-if="props.insertable"
210
- :color="toolbarColor"
211
- prepend-icon="mdi:mdi-plus"
212
- variant="flat"
213
- @click="openDialog()"
169
+ <slot
170
+ name="header"
171
+ :items="items"
172
+ :operation="operation"
173
+ >
174
+ <VToolbar :color="toolbarColor">
175
+ <v-row
176
+ justify="end"
177
+ class="ma-1"
178
+ dense
179
+ no-gutters
180
+ align="center"
214
181
  >
215
- add
216
- </VBtn>
217
- </VToolbarItems>
218
- </VToolbar>
182
+ <v-col cols="7">
183
+ <VToolbarTitle class="pl-3">
184
+ <slot name="title">
185
+ {{ title }}
186
+ </slot>
187
+ </VToolbarTitle>
188
+ </v-col>
189
+ <v-col cols="5">
190
+ <slot name="search" :items="items" :operation="operation" v-if="props.searchable">
191
+ <VTextField
192
+ v-model="search"
193
+ class="justify-end w-100"
194
+ density="compact"
195
+ hide-details
196
+ placeholder="ค้นหา"
197
+ clearable
198
+ variant="solo"
199
+ />
200
+ </slot>
201
+ </v-col>
202
+ </v-row>
203
+
204
+ <VToolbarItems>
205
+ <slot name="toolbarItems" :items="items" :operation="operation"/>
206
+ <ImportCSV
207
+ v-if="props.importable"
208
+ icon="mdi:mdi-file-upload"
209
+ variant="flat"
210
+ @import="importItems"
211
+ :color="toolbarColor"
212
+ />
213
+ <ExportCSV
214
+ v-if="props.exportable && items.length"
215
+ icon="mdi:mdi-file-download"
216
+ variant="flat"
217
+ :file-name="title"
218
+ :model-value="items"
219
+ :color="toolbarColor"
220
+ />
221
+ <VBtn
222
+ v-if="props.insertable"
223
+ :color="toolbarColor"
224
+ prepend-icon="mdi:mdi-plus"
225
+ variant="flat"
226
+ @click="openDialog()"
227
+ >
228
+ add
229
+ </VBtn>
230
+ </VToolbarItems>
231
+ </VToolbar>
232
+ </slot>
219
233
  <v-data-table
220
234
  v-bind="plainAttrs"
221
235
  color="primary"
@@ -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>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import { computed,watch, nextTick, ref, useAttrs } from 'vue'
3
3
  import { VDataTable } from 'vuetify/components/VDataTable'
4
- import { omit } from 'lodash-es'
4
+ import { clone } from 'lodash-es'
5
5
  import type { GraphqlModelProps } from '../../composables/graphqlModel'
6
6
  import { useGraphqlModel } from '../../composables/graphqlModel'
7
7
 
@@ -37,7 +37,7 @@ const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
37
37
 
38
38
  const attrs = useAttrs()
39
39
  const plainAttrs = computed(() => {
40
- const returnAttrs = omit(attrs, ['modelValue', 'onUpdate:modelValue'])
40
+ const returnAttrs = clone(attrs)
41
41
  if (props.headers) returnAttrs['headers'] = props.headers
42
42
  return returnAttrs
43
43
  })
@@ -68,7 +68,7 @@ watch(()=>props.search,()=>{
68
68
  search.value = props.search
69
69
  },{immediate:true})
70
70
 
71
- defineExpose({ reload })
71
+ defineExpose({ reload,operation })
72
72
  </script>
73
73
 
74
74
  <template>
@@ -102,8 +102,8 @@ defineExpose({ reload })
102
102
  </slot>
103
103
  </VToolbarTitle>
104
104
  </v-col>
105
- <v-col cols="5" v-if="props.searchable">
106
- <slot name="search">
105
+ <v-col cols="5">
106
+ <slot name="search" :items="items" :operation="operation" v-if="props.searchable">
107
107
  <VTextField
108
108
  v-model="search"
109
109
  class="justify-end w-100"
@@ -118,7 +118,7 @@ defineExpose({ reload })
118
118
  </v-row>
119
119
 
120
120
  <VToolbarItems>
121
- <slot name="toolbarItems" />
121
+ <slot name="toolbarItems" :items="items" :operation="operation"/>
122
122
  <ImportCSV
123
123
  v-if="props.importable && canCreate && props.insertable"
124
124
  icon="mdi mdi-file-upload"
@@ -128,7 +128,7 @@ watch(()=>props.search,()=>{
128
128
  search.value = props.search
129
129
  },{immediate:true})
130
130
 
131
- defineExpose({ reload })
131
+ defineExpose({ reload,operation })
132
132
  </script>
133
133
 
134
134
  <template>
@@ -227,8 +227,8 @@ defineExpose({ reload })
227
227
  </slot>
228
228
  </VToolbarTitle>
229
229
  </v-col>
230
- <v-col cols="5" v-if="props.searchable">
231
- <slot name="search">
230
+ <v-col cols="5">
231
+ <slot name="search" :items="items" :operation="operation" v-if="props.searchable">
232
232
  <VTextField
233
233
  v-model="search"
234
234
  class="justify-end w-100"
@@ -243,7 +243,7 @@ defineExpose({ reload })
243
243
  </v-row>
244
244
 
245
245
  <VToolbarItems>
246
- <slot name="toolbarItems" />
246
+ <slot name="toolbarItems" :items="items" :operation="operation"/>
247
247
  <ImportCSV
248
248
  v-if="props.importable && canCreate && props.insertable"
249
249
  icon="mdi mdi-file-upload"
@@ -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.33",
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.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee"
118
+ "packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
119
119
  }