@ramathibodi/nuxt-commons 0.1.34 → 0.1.36

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.36",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.3",
10
10
  "unbuild": "2.0.0"
@@ -31,7 +31,7 @@ const resize = (event: MouseEvent) => {
31
31
  <v-card :="$attrs">
32
32
  <v-sheet
33
33
  border
34
- :height="height"
34
+ :height="props.height"
35
35
  >
36
36
  <div
37
37
  ref="containerRef"
@@ -73,10 +73,14 @@ async function convertToAdvanceMode() {
73
73
  }
74
74
  }
75
75
 
76
- const inputTypeChoice = ref(['VTextField', 'VTextarea', 'VSelect', 'VAutocomplete', 'FormDate', 'FormTime', 'FormDateTime', 'VCombobox', 'VRadio', 'VRadioInline', 'VCheckbox', 'VSwitch', 'MasterAutocomplete', 'Header', 'Separator', 'CustomCode'])
76
+ const inputTypeChoice = ref(['VTextField', 'VTextarea', 'VSelect', 'VAutocomplete', 'FormDate', 'FormTime', 'FormDateTime', 'VCombobox', 'VRadio', 'VRadioInline', 'VCheckbox', 'VSwitch', 'MasterAutocomplete', 'Header', 'Separator', 'CustomCode', 'FormTable', 'FormHidden', 'FormFile', 'FormSignPad'])
77
77
  const requireOption = ref(['VSelect', 'VAutocomplete', 'VCombobox', 'VRadio', 'VRadioInline', 'MasterAutocomplete'])
78
78
  const notRequireVariable = ref(['Header', 'Separator', 'CustomCode'])
79
- const notRequireLabel = ref(['Separator', 'CustomCode'])
79
+ const notRequireLabel = ref(['Separator', 'CustomCode', 'FormFile', 'FormSignPad', 'FormTable', 'FormHidden'])
80
+ const notRequireOptions = ref(['CustomCode', 'FormSignPad', 'FormFile', 'FormTable'])
81
+ const notRequireRules = ref(['CustomCode', 'FormSignPad', 'FormFile', 'FormTable' ,'FormHidden'])
82
+ const notRequireInputAttributes = ref(['CustomCode', 'FormHidden'])
83
+ const notRequireColumnAttributes = ref(['Separator', 'FormHidden'])
80
84
 
81
85
  const choiceOption = ref(['VSelect', 'VRadio', 'VRadioInline'])
82
86
 
@@ -128,25 +132,23 @@ const ruleOptions = (inputType: string) => (value: any) => {
128
132
  type="number"
129
133
  />
130
134
  </v-col>
131
- <v-col cols="4">
135
+ <v-col cols="4" v-if="!notRequireLabel.includes(data.inputType)">
132
136
  <v-text-field
133
- v-if="!notRequireLabel.includes(data.inputType)"
134
- v-model="data.inputLabel"
135
- label="Input Label"
136
- :rules="[rules.require()]"
137
+ v-model="data.inputLabel"
138
+ label="Input Label"
139
+ :rules="[rules.require()]"
137
140
  />
138
141
  </v-col>
139
- <v-col cols="4">
142
+ <v-col cols="4" v-if="!notRequireVariable.includes(data.inputType)">
140
143
  <v-text-field
141
- v-if="!notRequireVariable.includes(data.inputType)"
142
- v-model="data.variableName"
143
- label="Variable Name"
144
- :rules="[rules.require()]"
144
+ v-model="data.variableName"
145
+ label="Variable Name"
146
+ :rules="[rules.require()]"
145
147
  />
146
148
  </v-col>
147
149
  <v-col
148
- v-if="data.inputType!='CustomCode'"
149
- cols="12"
150
+ v-if="!notRequireOptions.includes(data.inputType)"
151
+ cols="12"
150
152
  >
151
153
  <v-textarea
152
154
  v-model="data.inputOptions"
@@ -179,9 +181,29 @@ const ruleOptions = (inputType: string) => (value: any) => {
179
181
  cols="12"
180
182
  >
181
183
  <FormCodeEditor
182
- v-model="data.inputCustomCode"
183
- label="Custom Code"
184
- :rules="[rules.require()]"
184
+ v-model="data.inputCustomCode"
185
+ label="Custom Code"
186
+ :rules="[rules.require()]"
187
+ />
188
+ </v-col>
189
+ <v-col
190
+ v-if="data.inputType=='FormTable'"
191
+ cols="12"
192
+ >
193
+ <FormCodeEditor
194
+ v-model="data.inputFormTable"
195
+ label="Form Table"
196
+ :rules="[rules.require()]"
197
+ />
198
+ </v-col>
199
+ <v-col
200
+ v-if="data.inputType=='FormHidden'"
201
+ cols="12"
202
+ >
203
+ <FormCodeEditor
204
+ v-model="data.inputFormHidden"
205
+ label="Form Table"
206
+ :rules="[rules.require()]"
185
207
  />
186
208
  </v-col>
187
209
  </v-row>
@@ -201,10 +223,18 @@ const ruleOptions = (inputType: string) => (value: any) => {
201
223
  <template v-if="props.item.inputAttributes">
202
224
  <b>Input Attributes :</b> {{ props.item.inputAttributes }}<br>
203
225
  </template>
226
+ <template v-if="props.item.inputType=='FormTable'">
227
+ <b>Form Table :</b><br>
228
+ <span style="white-space: pre-line">{{ props.item.inputFormTable }}</span>
229
+ </template>
230
+ <template v-if="props.item.inputType=='FormHidden'">
231
+ <b>Form Hidden :</b><br>
232
+ <span style="white-space: pre-line">{{ props.item.inputFormHidden }}</span>
233
+ </template>
204
234
  </template>
205
235
  </FormTable>
206
236
  <FormCodeEditor
207
- v-else
208
- v-model="modelValue"
237
+ v-else
238
+ v-model="modelValue"
209
239
  />
210
240
  </template>
@@ -0,0 +1,133 @@
1
+ <script lang="ts" setup>
2
+ import {computed, defineExpose, ref, watchEffect} from 'vue'
3
+ import {cloneDeep, isEqual} from 'lodash-es'
4
+ import type {FormDialogCallback} from '../../types/formDialog'
5
+
6
+ interface Props {
7
+ title?: string
8
+ initialData?: object
9
+ createCaption?: string
10
+ updateCaption?: string
11
+ cancelCaption?: string
12
+ showTitle?: boolean
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ createCaption: 'Add',
17
+ updateCaption: 'Save',
18
+ cancelCaption: 'Cancel',
19
+ showTitle: false,
20
+ })
21
+
22
+ const isSaving = ref<boolean>(false)
23
+ const formPadRef = ref()
24
+ const formOriginalData = ref<object>()
25
+ const formData = ref<object>({})
26
+
27
+ const emit = defineEmits(['create', 'update'])
28
+
29
+ function save() {
30
+ if (formPadRef.value.isValid) {
31
+ isSaving.value = true
32
+ emit((isCreating.value) ? 'create' : 'update', cloneDeep(formData.value), callback)
33
+ }
34
+ }
35
+
36
+ function cancel() {
37
+ reset()
38
+ }
39
+
40
+ function reset() {
41
+ formOriginalData.value = undefined
42
+ loadFormData()
43
+ }
44
+
45
+ const callback: FormDialogCallback = {
46
+ done: function () {
47
+ isSaving.value = false
48
+ reset()
49
+ },
50
+ error: function () {
51
+ isSaving.value = false
52
+ },
53
+ }
54
+
55
+ const isDataChange = computed(() => {
56
+ return !((isCreating.value) ? isEqual(formData.value, createOriginalValue.value) : isEqual(formData.value, formOriginalData.value))
57
+ })
58
+
59
+ const isCreating = computed(() => {
60
+ return !formOriginalData.value
61
+ })
62
+
63
+ const createOriginalValue = computed(() => {
64
+ return Object.assign({}, props.initialData)
65
+ })
66
+
67
+ const loadFormData = () => {
68
+ if (formOriginalData.value) {
69
+ formData.value = cloneDeep(formOriginalData.value)
70
+ }
71
+ else {
72
+ formData.value = Object.assign({}, props.initialData)
73
+ }
74
+ }
75
+
76
+ watchEffect(loadFormData)
77
+
78
+ function setOriginalData(originalData?: object) {
79
+ formOriginalData.value = originalData
80
+ }
81
+
82
+ defineExpose({setOriginalData,reset})
83
+ </script>
84
+
85
+ <template>
86
+ <VCard flat>
87
+ <VToolbar v-if="showTitle">
88
+ <VToolbarTitle>
89
+ <slot name="title">
90
+ {{ (isCreating) ? "New" : "Edit" }} {{ title }}
91
+ </slot>
92
+ </VToolbarTitle>
93
+ <VSpacer />
94
+ </VToolbar>
95
+ <VCardText>
96
+ <form-pad
97
+ ref="formPadRef"
98
+ v-model="formData"
99
+ isolated
100
+ >
101
+ <template #default="slotData">
102
+ <slot
103
+ v-bind="slotData"
104
+ :is-creating="isCreating"
105
+ :is-data-change="isDataChange"
106
+ />
107
+ </template>
108
+ </form-pad>
109
+ </VCardText>
110
+ <VCardActions>
111
+ <slot name="action" :save="save" :cancel="cancel">
112
+ <VSpacer />
113
+ <VBtn
114
+ color="primary"
115
+ variant="flat"
116
+ :loading="isSaving"
117
+ :disabled="!isDataChange"
118
+ @click="save"
119
+ >
120
+ {{ (isCreating) ? createCaption : updateCaption }}
121
+ </VBtn>
122
+ <VBtn
123
+ color="error"
124
+ variant="flat"
125
+ :disabled="isSaving"
126
+ @click="cancel"
127
+ >
128
+ {{ cancelCaption }}
129
+ </VBtn>
130
+ </slot>
131
+ </VCardActions>
132
+ </VCard>
133
+ </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>
@@ -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 -->