@ramathibodi/nuxt-commons 0.1.60 → 0.1.62

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.60",
7
+ "version": "0.1.62",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -103,6 +103,7 @@ defineExpose({setOriginalData,operation,formPad: formPadRef})
103
103
  <form-pad
104
104
  ref="formPadRef"
105
105
  v-model="formData"
106
+ :originalData="formDataOriginalValue"
106
107
  :readonly="readonly"
107
108
  isolated
108
109
  v-bind="$attrs"
@@ -125,6 +125,7 @@ watch(() => isShowing.value, (newValue) => {
125
125
  <form-pad
126
126
  ref="formPadRef"
127
127
  v-model="formData"
128
+ :originalData="formDataOriginalValue"
128
129
  :readonly="readonly"
129
130
  isolated
130
131
  >
@@ -104,6 +104,7 @@ defineExpose({operation,formPad:formPadRef})
104
104
  <form-pad
105
105
  ref="formPadRef"
106
106
  v-model="formData"
107
+ :originalData="formDataOriginalValue"
107
108
  :readonly="readonly"
108
109
  isolated
109
110
  v-bind="$attrs"
@@ -12,11 +12,10 @@ import {
12
12
  watch,
13
13
  withDefaults
14
14
  } from 'vue'
15
- import {isObject} from 'lodash-es'
16
15
  import {watchDebounced} from '@vueuse/core'
17
16
  import {useRules} from '../../composables/utils/validation'
18
17
  import {useDocumentTemplate} from '../../composables/document/template'
19
- import { isArray, isString, isPlainObject } from 'lodash-es'
18
+ import { isObject, isArray, isString, isPlainObject, isEqual } from 'lodash-es'
20
19
  import FormPad from './Pad.vue'
21
20
 
22
21
  defineOptions({
@@ -25,6 +24,7 @@ defineOptions({
25
24
 
26
25
  interface Props {
27
26
  modelValue?: object
27
+ originalData?: object
28
28
  template?: any
29
29
  templateScript?: string
30
30
  disabled?: boolean
@@ -32,6 +32,8 @@ interface Props {
32
32
  isolated?: boolean
33
33
  decoration?: object
34
34
  parentTemplates?: string|string[]
35
+ dirtyClass?: string
36
+ dirtyOnCreate?: boolean
35
37
  }
36
38
 
37
39
  const props = withDefaults(defineProps<Props>(), {
@@ -40,6 +42,8 @@ const props = withDefaults(defineProps<Props>(), {
40
42
  isolated: false,
41
43
  decoration: () => { return {} },
42
44
  parentTemplates: (): string[] => [],
45
+ dirtyClass: "form-data-dirty",
46
+ dirtyOnCreate: false
43
47
  })
44
48
 
45
49
  const emit = defineEmits(['update:modelValue'])
@@ -80,39 +84,43 @@ const formInjected = ref()
80
84
  const formData = ref<any>({})
81
85
 
82
86
  function isBlankString(v: unknown): v is string {
83
- return isString(v) && v.length === 0
87
+ return isString(v) && v.trim().length === 0
84
88
  }
85
89
 
86
- function sanitizeBlankStrings(val: any): boolean {
87
- let changed = false
90
+ function sanitizeBlankStrings(val: any, original?: any): void {
91
+ if (!original && props.originalData) {
92
+ sanitizeBlankStrings(val, props.originalData)
93
+ return
94
+ }
88
95
 
89
96
  if (isArray(val)) {
90
97
  for (let i = val.length - 1; i >= 0; i--) {
91
98
  const item = val[i]
92
99
  if (isBlankString(item)) {
93
- val.splice(i, 1) // remove array element
94
- changed = true
100
+ val.splice(i, 1)
95
101
  } else if (isPlainObject(item) || isArray(item)) {
96
- if (sanitizeBlankStrings(item)) changed = true
102
+ sanitizeBlankStrings(item,{})
103
+ } else {
104
+ if (item && typeof item.trimEnd == "function") val[i] = item.trimEnd()
97
105
  }
98
106
  }
99
- return changed
107
+ return
100
108
  }
101
109
 
102
110
  if (isPlainObject(val)) {
103
111
  for (const key of Object.keys(val)) {
104
112
  const v = val[key]
105
113
  if (isBlankString(v)) {
106
- delete val[key] // remove property directly
107
- changed = true
114
+ if (original && !Object.keys(original).includes(key)) delete val[key]
115
+ else val[key] = null
108
116
  } else if (isPlainObject(v) || isArray(v)) {
109
- if (sanitizeBlankStrings(v)) changed = true
117
+ let originalChild = (original && (isPlainObject(original[key]) || isArray(original[key]))) ? original[key] : {}
118
+ sanitizeBlankStrings(v, originalChild)
119
+ } else {
120
+ if (v && typeof v.trimEnd == "function") val[key] = v.trimEnd()
110
121
  }
111
122
  }
112
- return changed
113
123
  }
114
-
115
- return false
116
124
  }
117
125
 
118
126
  watch(formData, (newValue) => {
@@ -124,6 +132,52 @@ watch(() => props.modelValue, (newValue) => {
124
132
  formData.value = isObject(newValue) ? newValue : {}
125
133
  }, { deep: true, immediate: true })
126
134
 
135
+ function diffPaths(
136
+ a: any,
137
+ b: any,
138
+ base: string[] = [],
139
+ out: string[] = []
140
+ ): string[] {
141
+ const keys = new Set<string>([
142
+ ...Object.keys(a ?? {}),
143
+ ...Object.keys(b ?? {}),
144
+ ])
145
+
146
+ for (const k of keys) {
147
+ const av = a?.[k]
148
+ const bv = b?.[k]
149
+ if (!av && !bv) continue // ignore when both are falsy
150
+
151
+ const path = [...base, k]
152
+
153
+ if (isPlainObject(av) && isPlainObject(bv)) {
154
+ diffPaths(av, bv, path, out)
155
+ continue
156
+ }
157
+
158
+ if (!isEqual(av, bv)) {
159
+ out.push(path.join('.'))
160
+ }
161
+ }
162
+
163
+ return out
164
+ }
165
+
166
+ const injectedClass = computed<Record<string, string>>(() => {
167
+ const data = (formData.value ?? {}) as Record<string, any>
168
+ const result: Record<string, string> = {}
169
+
170
+ if (!props.dirtyOnCreate && !props.originalData) return result
171
+
172
+ const original = (props.originalData ?? {}) as Record<string, any>
173
+ const cls = props.dirtyClass || 'form-data-dirty'
174
+
175
+ const paths = diffPaths(data, original)
176
+ for (const p of paths) result[p] = cls
177
+
178
+ return result
179
+ })
180
+
127
181
  const formComponent = shallowRef()
128
182
 
129
183
  function buildFormComponent() {
@@ -131,12 +185,13 @@ function buildFormComponent() {
131
185
  const originalConsoleError = console.warn
132
186
  console.warn = (error: any) => { throw new Error(error) } // eslint-disable-line
133
187
  try {
134
- const componentTemplate = '<form-pad ref="formPadTemplate" v-model="formComponentData" :disabled="disabled" :readonly="readonly" :decoration="decoration" :isolated="isolated"><template v-slot="{ data,isDisabled,isReadonly,rules,formProvided,decoration }">' + trimmedTemplate.value + '</template></form-pad>'
188
+ const componentTemplate = '<form-pad ref="formPadTemplate" v-model="formComponentData" :originalData="originalData" :disabled="disabled" :readonly="readonly" :decoration="decoration" :isolated="isolated"><template v-slot="{ data,isDisabled,isReadonly,rules,formProvided,decoration,injectedClass }">' + trimmedTemplate.value + '</template></form-pad>'
135
189
  compile(componentTemplate)
136
190
  formComponent.value = defineComponent({
137
191
  components: { FormPad },
138
192
  props: {
139
193
  modelValue: { type: Object, default: undefined },
194
+ originalData: { type: Object, default: undefined },
140
195
  disabled: { type: Boolean, default: false },
141
196
  readonly: { type: Boolean, default: false },
142
197
  decoration: { type: Object, default: () => { return {} } },
@@ -231,6 +286,7 @@ defineExpose({
231
286
  :is-readonly="readonly"
232
287
  :rules="rules"
233
288
  :decoration="decoration"
289
+ :injectedClass="injectedClass"
234
290
  />
235
291
  </template>
236
292
  </v-form>
@@ -242,6 +298,7 @@ defineExpose({
242
298
  :is-readonly="readonly"
243
299
  :rules="rules"
244
300
  :decoration="decoration"
301
+ :injectedClass="injectedClass"
245
302
  />
246
303
  </template>
247
304
  <component
@@ -249,6 +306,7 @@ defineExpose({
249
306
  v-if="trimmedTemplate"
250
307
  ref="formPad"
251
308
  v-model="formData"
309
+ :originalData="originalData"
252
310
  :disabled="disabled"
253
311
  :readonly="readonly"
254
312
  :decoration="decoration"
@@ -256,3 +314,6 @@ defineExpose({
256
314
  :class="$attrs.class"
257
315
  />
258
316
  </template>
317
+ <style>
318
+ .form-data-dirty,.form-data-dirty *{color:color-mix(in srgb,currentColor 70%,rgb(var(--v-theme-primary)))!important;text-shadow:0 0 .02em currentColor}
319
+ </style>
@@ -1,7 +1,9 @@
1
1
  <script lang="ts" setup>
2
2
  import { computed, useTemplateRef, defineExpose } from 'vue'
3
3
  import { type GraphqlModelItemProps, useGraphqlModelItem } from '../../composables/graphqlModelItem'
4
+ import type {FormDialogCallback} from '../../types/formDialog'
4
5
  import EditPad from '../form/EditPad.vue'
6
+ import {useDialog} from "../../composables/dialog";
5
7
 
6
8
  defineOptions({
7
9
  inheritAttrs: false,
@@ -12,8 +14,13 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof EditPad['$props']>
12
14
 
13
15
  const props = withDefaults(defineProps<Props & GraphqlModelItemProps>(), {
14
16
  fields: () => ['*'],
17
+
18
+ saveCaption: 'บันทึก',
19
+ cancelCaption: 'ยกเลิก',
15
20
  })
16
21
 
22
+ const emit = defineEmits(['create', 'update'])
23
+
17
24
  const { item,
18
25
  canCreate, canUpdate,
19
26
  createItem, updateItem,
@@ -27,11 +34,25 @@ const operation : any = computed(()=>{
27
34
  return Object.assign({},editPad.value?.operation,{canSave,reload,item})
28
35
  })
29
36
 
37
+ const onCreateItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
38
+ createItem(item,callback).then(()=>{
39
+ useDialog().notify({message: "Data created successfully",type: 'success',title:"Success"})
40
+ emit("create",item,callback)
41
+ })
42
+ }
43
+
44
+ const onUpdateItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
45
+ updateItem(item,callback).then(()=>{
46
+ useDialog().notify({message: "Data updated successfully",type: 'success',title:"Success"})
47
+ emit("update",item,callback)
48
+ })
49
+ }
50
+
30
51
  defineExpose({ operation, formPad: editPad.value?.formPad })
31
52
  </script>
32
53
 
33
54
  <template>
34
- <FormEditPad v-bind="$attrs" :form-data="item" ref="editPadRef" @create="createItem" @update="updateItem">
55
+ <FormEditPad v-bind="$attrs" :form-data="item" ref="editPadRef" @create="onCreateItem" @update="onUpdateItem">
35
56
  <template #titleToolbar>
36
57
  <slot name="titleToolbar" :operation="operation">
37
58
  <VToolbarTitle>
@@ -57,7 +78,7 @@ defineExpose({ operation, formPad: editPad.value?.formPad })
57
78
  :disabled="!slotData.operation?.isDataChange || !canSave"
58
79
  @click="slotData.operation?.save"
59
80
  >
60
- {{ saveCaption }}
81
+ {{ props.saveCaption }}
61
82
  </VBtn>
62
83
  <VBtn
63
84
  color="error"
@@ -65,15 +86,13 @@ defineExpose({ operation, formPad: editPad.value?.formPad })
65
86
  :disabled="isLoading"
66
87
  @click="slotData.operation?.cancel"
67
88
  >
68
- {{ cancelCaption }}
89
+ {{ props.cancelCaption }}
69
90
  </VBtn>
70
91
  </slot>
71
92
  </template>
72
93
  <template #default="slotData">
73
94
  <slot
74
95
  v-bind="slotData"
75
- :is-creating="isCreating"
76
- :is-data-change="isDataChange"
77
96
  />
78
97
  </template>
79
98
  </FormEditPad>
@@ -50,7 +50,7 @@ const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
50
50
  onlyOwnerEdit: false,
51
51
  })
52
52
 
53
- const emit = defineEmits(['open:dialog','close:dialog'])
53
+ const emit = defineEmits(['open:dialog','close:dialog','create','update','delete'])
54
54
  const attrs = useAttrs()
55
55
  const plainAttrs = computed(() => {
56
56
  const returnAttrs = clone(attrs)
@@ -95,7 +95,25 @@ function openDialogReadonly(item?: object) {
95
95
 
96
96
  async function confirmDeleteItem(item: Record<string, any>, callback?: FormDialogCallback) {
97
97
  let confirm = await useDialog().confirm({message: "Do you want to delete record?"})
98
- if (confirm) deleteItem(item,callback)
98
+ if (confirm) onDeleteItem(item,callback)
99
+ }
100
+
101
+ const onCreateItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
102
+ createItem(item,callback).then(()=>{
103
+ emit("create",item,callback)
104
+ })
105
+ }
106
+
107
+ const onUpdateItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
108
+ updateItem(item,callback).then(()=>{
109
+ emit("update",item,callback)
110
+ })
111
+ }
112
+
113
+ const onDeleteItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
114
+ deleteItem(item,callback).then(()=>{
115
+ emit("delete",item,callback)
116
+ })
99
117
  }
100
118
 
101
119
  const canEditRow = function (item: Record<string, any>) {
@@ -301,8 +319,8 @@ defineExpose({ reload,operation })
301
319
  :fullscreen="dialogFullscreen"
302
320
  :initial-data="computedInitialData"
303
321
  :form-data="currentItem"
304
- @create="createItem"
305
- @update="updateItem"
322
+ @create="onCreateItem"
323
+ @update="onUpdateItem"
306
324
  @afterLeave="emit('close:dialog')"
307
325
  :saveAndStay="saveAndStay"
308
326
  :readonly="isDialogReadonly"
@@ -89,7 +89,7 @@ export function templateItemToString(inputItem, parentTemplates, dataVariable =
89
89
  }
90
90
  function columnWrapTemplateItemString(item, parentTemplates, dataVariable = "data") {
91
91
  let templateString = templateItemToString(item, parentTemplates, dataVariable);
92
- if (!["Separator", "FormHidden"].includes(item.inputType)) templateString = `<v-col${item.width ? ` cols="${item.width}"` : ""}${item.columnAttributes ? " " + item.columnAttributes : ""}${item.customClass ? ` class="${item.customClass}"` : ""}${item.customStyle ? ` class="${item.customStyle}"` : ""}>${templateString}</v-col>`;
92
+ if (!["Separator", "FormHidden"].includes(item.inputType)) templateString = `<v-col${item.width ? ` cols="${item.width}"` : ""}${item.columnAttributes ? " " + item.columnAttributes : ""} :class="(injectedClass) ? injectedClass['${item.variableName || ""}'] : undefined"${item.customClass ? ` class="${item.customClass}"` : ""}${item.customStyle ? ` class="${item.customStyle}"` : ""}>${templateString}</v-col>`;
93
93
  if (["Header", "DocumentForm"].includes(item.inputType)) templateString = `</v-row><v-row dense${item.customClass ? ` class="${item.customClass}"` : ""}${item.customStyle ? ` class="${item.customStyle}"` : ""}>${templateString}</v-row><v-row dense>`;
94
94
  return templateString || "";
95
95
  }
@@ -11,6 +11,11 @@ export declare function useRules(): {
11
11
  lengthGreater: (length: number, customError?: string) => (value: any) => string | true;
12
12
  lengthLess: (length: number, customError?: string) => (value: any) => string | true;
13
13
  telephone: (customError?: string) => (value: any) => string | true;
14
+ telephoneSingle: (customError?: string) => (value: any) => string | true;
15
+ internationalTelephone: (customError?: string) => (value: any) => string | true;
16
+ internationalTelephoneSingle: (customError?: string) => (value: any) => string | true;
17
+ mobilePhone: (customError?: string) => (value: any) => string | true;
18
+ mobilePhoneSingle: (customError?: string) => (value: any) => string | true;
14
19
  email: (customError?: string) => (value: any) => string | true;
15
20
  regex: (regex: RegExp | string, customError?: string) => (value: any) => string | true;
16
21
  idcard: (customError?: string) => (value: any) => string | true;
@@ -34,6 +39,11 @@ export declare function useRules(): {
34
39
  lengthGreater: (length: number, customError?: string) => (value: any) => string | true;
35
40
  lengthLess: (length: number, customError?: string) => (value: any) => string | true;
36
41
  telephone: (customError?: string) => (value: any) => string | true;
42
+ telephoneSingle: (customError?: string) => (value: any) => string | true;
43
+ internationalTelephone: (customError?: string) => (value: any) => string | true;
44
+ internationalTelephoneSingle: (customError?: string) => (value: any) => string | true;
45
+ mobilePhone: (customError?: string) => (value: any) => string | true;
46
+ mobilePhoneSingle: (customError?: string) => (value: any) => string | true;
37
47
  email: (customError?: string) => (value: any) => string | true;
38
48
  regex: (regex: RegExp | string, customError?: string) => (value: any) => string | true;
39
49
  idcard: (customError?: string) => (value: any) => string | true;
@@ -57,6 +67,11 @@ export declare function useRules(): {
57
67
  lengthGreater: (length: number, customError?: string) => (value: any) => string | true;
58
68
  lengthLess: (length: number, customError?: string) => (value: any) => string | true;
59
69
  telephone: (customError?: string) => (value: any) => string | true;
70
+ telephoneSingle: (customError?: string) => (value: any) => string | true;
71
+ internationalTelephone: (customError?: string) => (value: any) => string | true;
72
+ internationalTelephoneSingle: (customError?: string) => (value: any) => string | true;
73
+ mobilePhone: (customError?: string) => (value: any) => string | true;
74
+ mobilePhoneSingle: (customError?: string) => (value: any) => string | true;
60
75
  email: (customError?: string) => (value: any) => string | true;
61
76
  regex: (regex: RegExp | string, customError?: string) => (value: any) => string | true;
62
77
  idcard: (customError?: string) => (value: any) => string | true;
@@ -80,6 +95,11 @@ export declare function useRules(): {
80
95
  lengthGreater: (length: number, customError?: string) => (value: any) => string | true;
81
96
  lengthLess: (length: number, customError?: string) => (value: any) => string | true;
82
97
  telephone: (customError?: string) => (value: any) => string | true;
98
+ telephoneSingle: (customError?: string) => (value: any) => string | true;
99
+ internationalTelephone: (customError?: string) => (value: any) => string | true;
100
+ internationalTelephoneSingle: (customError?: string) => (value: any) => string | true;
101
+ mobilePhone: (customError?: string) => (value: any) => string | true;
102
+ mobilePhoneSingle: (customError?: string) => (value: any) => string | true;
83
103
  email: (customError?: string) => (value: any) => string | true;
84
104
  regex: (regex: RegExp | string, customError?: string) => (value: any) => string | true;
85
105
  idcard: (customError?: string) => (value: any) => string | true;
@@ -14,7 +14,12 @@ export function useRules() {
14
14
  const length = (length2, customError = `Length must be ${length2}`) => (value) => condition(!value || value.length == length2, customError);
15
15
  const lengthGreater = (length2, customError = `Length must be greater than ${length2}`) => (value) => condition(!value || value.length >= length2, customError);
16
16
  const lengthLess = (length2, customError = `Length must be less than ${length2}`) => (value) => condition(!value || value.length <= length2, customError);
17
- const telephone = (customError = "Invalid telephone number") => (value) => condition(!value || /^(?:\s*(?:0[2-57]\d{7}|0[689]\d{8})(?:#\d{1,5})?\s*(?:\([^()]+\)\s*)?(?:,(?=.+))?)*$/.test(value), customError);
17
+ const telephone = (customError = "Invalid telephone number") => (value) => condition(!value || /^(?:\s*(?:\+66|0)\s?(?:[2-57](?:[\s-]?\d){7}|[689](?:[\s-]?\d){8})(?:#\d{1,5})?\s*(?:\([^()]+\)\s*)?(?:,(?=\s*\+?6?0)|,|$))*$/.test(value), customError);
18
+ const telephoneSingle = (customError = "Invalid telephone number") => (value) => condition(!value || /^\s*(?:\+66|0)\s?(?:[2-57](?:[\s-]?\d){7}|[689](?:[\s-]?\d){8})(?:#\d{1,5})?\s*(?:\([^()]+\)\s*)?$/.test(value), customError);
19
+ const internationalTelephone = (customError = "Invalid telephone number") => (value) => condition(!value || /^(?:\s*(?:\+?\d{1,3}[-\s]?)?(?:0?\d{1,4}[-\s]?)?\d{3,4}[-\s]?\d{3,4}(?:#\d{1,5})?\s*(?:\([^()]+\)\s*)?(?:,(?=\s*\+?\d)|,|$))*$/.test(value), customError);
20
+ const internationalTelephoneSingle = (customError = "Invalid telephone number") => (value) => condition(!value || /^\s*(?:\+?\d{1,3}[-\s]?)?(?:0?\d{1,4}[-\s]?)?\d{3,4}[-\s]?\d{3,4}(?:#\d{1,5})?\s*(?:\([^()]+\)\s*)?$/.test(value), customError);
21
+ const mobilePhone = (customError = "Invalid telephone number") => (value) => condition(!value || /^(?:\s*(?:\+66|0)\s?(?:6\d{8}|8\d{8}|9\d{8})\s*(?:,(?=\s*\+?6?0)|,|$))*$/.test(value), customError);
22
+ const mobilePhoneSingle = (customError = "Invalid telephone number") => (value) => condition(!value || /^\s*(?:\+66|0)\s?(?:6\d{8}|8\d{8}|9\d{8})\s*$/.test(value), customError);
18
23
  const email = (customError = "Invalid email address") => (value) => condition(!value || /^([\w\-.]+)@([\w\-.]+)\.([a-z]{2,5})$/i.test(value), customError);
19
24
  const regex = (regex2, customError = "Invalid format") => (value) => condition(!value || new RegExp(regex2).test(value), customError);
20
25
  const idcard = (customError = "Invalid ID Card format") => (value) => condition(!value || /^\d{13}$/.test(value) && (11 - [...value].slice(0, 12).reduce((sum, n, i) => sum + +n * (13 - i), 0) % 11) % 10 === +value[12], customError);
@@ -38,6 +43,11 @@ export function useRules() {
38
43
  lengthGreater,
39
44
  lengthLess,
40
45
  telephone,
46
+ telephoneSingle,
47
+ internationalTelephone,
48
+ internationalTelephoneSingle,
49
+ mobilePhone,
50
+ mobilePhoneSingle,
41
51
  email,
42
52
  regex,
43
53
  idcard,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",