@ramathibodi/nuxt-commons 0.1.47 → 0.1.49

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.47",
7
+ "version": "0.1.49",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -9,10 +9,12 @@ const module = defineNuxtModule({
9
9
  }
10
10
  },
11
11
  // Default configuration options of the Nuxt module
12
- defaults: {},
12
+ defaults: {
13
+ // default empty, will use fallback if not provided
14
+ },
13
15
  async setup(_options, _nuxt) {
14
16
  const resolver = createResolver(import.meta.url);
15
- await addComponentsDir({
17
+ addComponentsDir({
16
18
  path: resolver.resolve("runtime/components"),
17
19
  pathPrefix: true,
18
20
  global: true
@@ -21,26 +23,22 @@ const module = defineNuxtModule({
21
23
  addPlugin({
22
24
  src: resolver.resolve("runtime/plugins/permission")
23
25
  });
24
- addTypeTemplate({
25
- src: resolver.resolve("runtime/types/modules.d.ts"),
26
- filename: "types/modules.d.ts"
27
- });
28
- addTypeTemplate({
29
- src: resolver.resolve("runtime/types/alert.d.ts"),
30
- filename: "types/alert.d.ts"
31
- });
32
- addTypeTemplate({
33
- src: resolver.resolve("runtime/types/menu.d.ts"),
34
- filename: "types/menu.d.ts"
35
- });
36
- addTypeTemplate({
37
- src: resolver.resolve("runtime/types/graphqlOperation.d.ts"),
38
- filename: "types/graphqlOperation.d.ts"
39
- });
40
- addTypeTemplate({
41
- src: resolver.resolve("runtime/types/formDialog.d.ts"),
42
- filename: "types/formDialog.d.ts"
26
+ addPlugin({
27
+ src: resolver.resolve("runtime/plugins/dialogManager"),
28
+ mode: "client"
43
29
  });
30
+ const typeFiles = ["modules", "alert", "menu", "graphqlOperation", "formDialog", "dialogManager"];
31
+ for (const file of typeFiles) {
32
+ addTypeTemplate({
33
+ src: resolver.resolve(`runtime/types/${file}.d.ts`),
34
+ filename: `types/${file}.d.ts`
35
+ });
36
+ }
37
+ const runtimeConfig = _nuxt.options.runtimeConfig.public["nuxt-commons"] || {};
38
+ _nuxt.options.runtimeConfig.public["nuxt-commons"] = {
39
+ ...runtimeConfig,
40
+ ..._options
41
+ };
44
42
  }
45
43
  });
46
44
 
@@ -0,0 +1,84 @@
1
+ <script setup lang="ts">
2
+ import {shallowReactive, onMounted} from 'vue'
3
+ import {useDialog} from "../../composables/dialog"
4
+ import type {DialogOpenResult} from "../../types/dialogManager";
5
+
6
+ interface DialogEntry {
7
+ id: number
8
+ component: any
9
+ props: Record<string, any>
10
+ resolve: (val: any) => void
11
+ reject: (val: any) => void
12
+ }
13
+
14
+ const dialogs = shallowReactive<DialogEntry[]>([])
15
+ let idCounter = 0
16
+
17
+ function open(component: any, inputProps: Record<string, any> = {}) : DialogOpenResult {
18
+ const id = ++idCounter
19
+
20
+ const promise = new Promise<any>((resolve,reject) => {
21
+ const dialogProps = {
22
+ ...inputProps,
23
+ modelValue: true,
24
+ 'onUpdate:modelValue': (v: boolean) => {
25
+ if (!v) close(id, null)
26
+ },
27
+ onResolve: (val: any) => close(id, val),
28
+ onReject: (val: any) => closeWithReject(id, val)
29
+ }
30
+
31
+ dialogs.push({
32
+ id,
33
+ component,
34
+ props: dialogProps,
35
+ resolve,
36
+ reject
37
+ })
38
+ })
39
+
40
+ const closeInstance = (val?: any) => {
41
+ close(id,val)
42
+ }
43
+
44
+ const rejectInstance = (val?: any) => {
45
+ closeWithReject(id,val)
46
+ }
47
+
48
+ return { id, promise, closeInstance, rejectInstance}
49
+ }
50
+
51
+ function close(id: number, val: any) {
52
+ const index = dialogs.findIndex((d) => d.id === id)
53
+ if (index !== -1) {
54
+ dialogs[index].resolve(val)
55
+ dialogs.splice(index, 1)
56
+ }
57
+ }
58
+
59
+ function closeWithReject(id: number, val: any) {
60
+ const index = dialogs.findIndex((d) => d.id === id)
61
+ if (index !== -1) {
62
+ dialogs[index].reject(val)
63
+ dialogs.splice(index, 1)
64
+ }
65
+ }
66
+
67
+ onMounted(() => {
68
+ useDialog()?.setDialogHost(open, close)
69
+ })
70
+
71
+ defineExpose({ open, close })
72
+ </script>
73
+ <template>
74
+ <Teleport to="body">
75
+ <div>
76
+ <component
77
+ v-for="dialog in dialogs"
78
+ :key="dialog.id"
79
+ :is="dialog.component"
80
+ v-bind="dialog.props"
81
+ />
82
+ </div>
83
+ </Teleport>
84
+ </template>
@@ -0,0 +1,101 @@
1
+ <script lang="ts" setup>
2
+ import { ref, watch, computed, type Ref } from 'vue'
3
+ import { isEqual, isUndefined, isEmpty } from 'lodash-es'
4
+
5
+ interface DialogProps {
6
+ title?: string
7
+ modelValue: boolean
8
+ message?: string
9
+ buttonTrueText?: string
10
+ buttonFalseText?: string
11
+ type?: 'primary' | 'success' | 'warning' | 'info' | 'error'
12
+ confirmData?: string
13
+ width?: string
14
+ }
15
+
16
+ const props = withDefaults(defineProps<DialogProps>(), {
17
+ title: 'Confirm',
18
+ message: 'Do you want to proceed?',
19
+ buttonTrueText: 'Ok',
20
+ buttonFalseText: 'Cancel',
21
+ type: 'primary',
22
+ width: 'auto',
23
+ })
24
+
25
+ const emit = defineEmits<{
26
+ (e: 'update:modelValue', value: boolean): void
27
+ (e: 'resolve', value: boolean): void
28
+ (e: 'reject', value: any): void
29
+ }>()
30
+
31
+ const dialogVisible: Ref<boolean> = ref(props.modelValue)
32
+
33
+ watch(() => props.modelValue, (val) => {
34
+ dialogVisible.value = val
35
+ })
36
+
37
+ watch(dialogVisible, (val) => {
38
+ if (!val) emit('update:modelValue', false)
39
+ })
40
+
41
+ const txtConfirm = ref<string>('')
42
+
43
+ const isDisabled = computed(() => {
44
+ if (isUndefined(props.confirmData)) return false
45
+ if (isEmpty(props.confirmData)) return true
46
+ return !isEqual(txtConfirm.value, props.confirmData)
47
+ })
48
+
49
+ const handleResult = (result: boolean) => {
50
+ dialogVisible.value = false
51
+ emit('resolve', result) // emit back to dialog-manager
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <v-row justify="center">
57
+ <v-dialog
58
+ v-model="dialogVisible"
59
+ persistent
60
+ :width="props.width"
61
+ >
62
+ <v-card>
63
+ <v-toolbar
64
+ :color="props.type"
65
+ :title="props.title"
66
+ />
67
+ <v-card-text>{{ props.message }}</v-card-text>
68
+ <v-card-text v-if="props.confirmData">
69
+ <v-text-field
70
+ v-model="txtConfirm"
71
+ variant="underlined"
72
+ >
73
+ <template #label>
74
+ <slot
75
+ name="labelTextConfirm"
76
+ :text="props.confirmData"
77
+ />
78
+ </template>
79
+ </v-text-field>
80
+ </v-card-text>
81
+ <v-card-actions>
82
+ <v-spacer />
83
+ <v-btn
84
+ variant="text"
85
+ @click="handleResult(false)"
86
+ >
87
+ {{ props.buttonFalseText }}
88
+ </v-btn>
89
+ <v-btn
90
+ :color="props.type"
91
+ variant="text"
92
+ :disabled="isDisabled"
93
+ @click="handleResult(true)"
94
+ >
95
+ {{ props.buttonTrueText }}
96
+ </v-btn>
97
+ </v-card-actions>
98
+ </v-card>
99
+ </v-dialog>
100
+ </v-row>
101
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue'
3
+
4
+ interface DialogProps {
5
+ modelValue: boolean
6
+ color?: string
7
+ }
8
+ const emit = defineEmits(['update:modelValue'])
9
+
10
+ const props = defineProps<DialogProps>()
11
+ const dialogVisible = ref<boolean>(props.modelValue)
12
+
13
+ watch(() => props.modelValue, (newValue) => {
14
+ dialogVisible.value = newValue
15
+ })
16
+
17
+ watch(() => dialogVisible.value, (newValue) => {
18
+ emit('update:modelValue', newValue)
19
+ })
20
+ </script>
21
+
22
+ <template>
23
+ <v-dialog
24
+ v-model="dialogVisible"
25
+ persistent
26
+ max-width="400"
27
+ class="rounded-xl"
28
+ >
29
+ <v-card :color="props.color || 'surface'" elevation="10" class="pa-4 rounded-xl">
30
+ <v-card-title class="text-h6 text-center">
31
+ <slot name="title">Please wait</slot>
32
+ </v-card-title>
33
+
34
+ <v-card-text class="text-center">
35
+ <v-progress-circular
36
+ indeterminate
37
+ size="40"
38
+ width="4"
39
+ color="primary"
40
+ class="mb-4"
41
+ />
42
+ <div>
43
+ <slot>Loading data, please wait...</slot>
44
+ </div>
45
+ </v-card-text>
46
+ </v-card>
47
+ </v-dialog>
48
+ </template>
@@ -0,0 +1,70 @@
1
+ <script lang="ts" setup>
2
+ import { ref, watch, type Ref } from 'vue'
3
+
4
+ interface DialogProps {
5
+ title?: string
6
+ modelValue: boolean
7
+ message?: string
8
+ buttonText?: string
9
+ type?: 'primary' | 'success' | 'warning' | 'info' | 'error'
10
+ width?: string
11
+ }
12
+
13
+ const props = withDefaults(defineProps<DialogProps>(), {
14
+ title: 'Notice',
15
+ message: 'Something happened.',
16
+ buttonText: 'OK',
17
+ type: 'primary',
18
+ width: 'auto',
19
+ })
20
+
21
+ const emit = defineEmits<{
22
+ (e: 'update:modelValue', value: boolean): void
23
+ (e: 'resolve', value: boolean): void
24
+ }>()
25
+
26
+ const dialogVisible: Ref<boolean> = ref(props.modelValue)
27
+
28
+ watch(() => props.modelValue, (val) => {
29
+ dialogVisible.value = val
30
+ })
31
+
32
+ watch(dialogVisible, (val) => {
33
+ if (!val) emit('update:modelValue', false)
34
+ })
35
+
36
+ const handleClose = () => {
37
+ dialogVisible.value = false
38
+ emit('resolve', true)
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <v-row justify="center">
44
+ <v-dialog
45
+ v-model="dialogVisible"
46
+ persistent
47
+ :width="props.width"
48
+ >
49
+ <v-card>
50
+ <v-toolbar
51
+ :color="props.type"
52
+ :title="props.title"
53
+ density="compact"
54
+ />
55
+ <v-card-text class="text-body-2 text-center py-4">
56
+ {{ props.message }}
57
+ </v-card-text>
58
+ <v-card-actions class="justify-center">
59
+ <v-btn
60
+ :color="props.type"
61
+ variant="elevated"
62
+ @click="handleClose"
63
+ >
64
+ {{ props.buttonText }}
65
+ </v-btn>
66
+ </v-card-actions>
67
+ </v-card>
68
+ </v-dialog>
69
+ </v-row>
70
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue'
3
+
4
+ interface DialogProps {
5
+ modelValue: boolean
6
+ color?: string
7
+ }
8
+ const emit = defineEmits(['update:modelValue'])
9
+ const props = defineProps<DialogProps>()
10
+ const dialogVisible = ref(props.modelValue)
11
+
12
+ watch(() => props.modelValue, (newValue) => {
13
+ dialogVisible.value = newValue
14
+ })
15
+
16
+ watch(() => dialogVisible.value, (newValue) => {
17
+ emit('update:modelValue', newValue)
18
+ })
19
+ </script>
20
+
21
+ <template>
22
+ <v-dialog
23
+ v-model="dialogVisible"
24
+ persistent
25
+ max-width="320"
26
+ class="rounded-lg"
27
+ >
28
+ <v-card :color="props.color || 'surface'" class="pa-3 rounded-lg d-flex flex-column align-center">
29
+ <v-icon size="36" color="primary" class="mb-2">mdi mdi-printer</v-icon>
30
+ <div class="text-center text-body-2">
31
+ <slot>Preparing to print...</slot>
32
+ </div>
33
+ </v-card>
34
+ </v-dialog>
35
+ </template>
@@ -0,0 +1,132 @@
1
+ <script lang="ts" setup>
2
+ import {computed, ref, type Ref, watch} from 'vue'
3
+ import {useDialog} from "../../../composables/dialog"
4
+ import {useGraphQlOperation} from "../../../composables/graphqlOperation";
5
+ import {type AuthenticationState, useState} from '#imports'
6
+
7
+ interface DialogProps {
8
+ title?: string
9
+ modelValue: boolean
10
+ message?: string
11
+ confirmButtonText?: string
12
+ cancelButtonText?: string
13
+ type?: 'primary' | 'success' | 'warning' | 'info' | 'error'
14
+ fixedUsername?: boolean | string
15
+ width?: string
16
+ }
17
+
18
+ const props = withDefaults(defineProps<DialogProps>(), {
19
+ title: 'Verify User',
20
+ message: 'Please enter your credentials to continue.',
21
+ confirmButtonText: 'Verify',
22
+ cancelButtonText: 'Cancel',
23
+ type: 'primary',
24
+ fixedUsername: true,
25
+ width: '400',
26
+ })
27
+
28
+ const emit = defineEmits<{
29
+ (e: 'update:modelValue', value: boolean): void
30
+ (e: 'resolve', value: string): void
31
+ (e: 'reject', value: any): void
32
+ }>()
33
+
34
+ const authenState = useState<AuthenticationState>("authentication")
35
+
36
+ const dialogVisible: Ref<boolean> = ref(props.modelValue)
37
+
38
+ watch(() => props.modelValue, (val) => {
39
+ dialogVisible.value = val
40
+ })
41
+
42
+ watch(dialogVisible, (val) => {
43
+ if (!val) emit('update:modelValue', false)
44
+ })
45
+
46
+ const username = ref<string>('')
47
+ const password = ref<string>('')
48
+
49
+ watch(()=>props.fixedUsername, (fixedUsername) => {
50
+ if (fixedUsername === true) {
51
+ let userProfile = authenState.value?.userProfile as {username: string}
52
+ username.value = userProfile?.username ?? ""
53
+ } else {
54
+ username.value = (fixedUsername) ? fixedUsername as string : ""
55
+ }
56
+ },{immediate: true})
57
+
58
+ const isDisabled = computed(() => {
59
+ return !username.value || !password.value
60
+ })
61
+
62
+ const verifyUser = async (username: string, password: string): Promise<boolean> => {
63
+ try {
64
+ let result: any = await useGraphQlOperation('Mutation',"verifyUser",["success"], {username: username, password: password})
65
+ return result.success
66
+ } catch (_error) {
67
+ return false
68
+ }
69
+ }
70
+
71
+ const handleConfirm = async () => {
72
+ const verified = await verifyUser(username.value, password.value)
73
+ if (verified) {
74
+ dialogVisible.value = false
75
+ emit('resolve', username.value)
76
+ } else {
77
+ useDialog().notify({message: "Invalid username or password",type: 'warning',title:"Error"})
78
+ }
79
+ }
80
+
81
+ const handleCancel = () => {
82
+ dialogVisible.value = false
83
+ emit('reject', 'User cancelled')
84
+ }
85
+ </script>
86
+
87
+ <template>
88
+ <v-row justify="center">
89
+ <v-dialog
90
+ v-model="dialogVisible"
91
+ :width="props.width"
92
+ persistent
93
+ >
94
+ <v-card>
95
+ <v-toolbar
96
+ :color="props.type"
97
+ :title="props.title"
98
+ density="compact"
99
+ />
100
+ <v-card-text>
101
+ <div class="mb-2">{{ props.message }}</div>
102
+ <v-text-field
103
+ v-model="username"
104
+ :readonly="!!props.fixedUsername"
105
+ label="Username"
106
+ variant="underlined"
107
+ />
108
+ <v-text-field
109
+ v-model="password"
110
+ label="Password"
111
+ type="password"
112
+ variant="underlined"
113
+ />
114
+ </v-card-text>
115
+ <v-card-actions>
116
+ <v-spacer />
117
+ <v-btn variant="text" @click="handleCancel">
118
+ {{ props.cancelButtonText }}
119
+ </v-btn>
120
+ <v-btn
121
+ :color="props.type"
122
+ :disabled="isDisabled"
123
+ variant="text"
124
+ @click="handleConfirm"
125
+ >
126
+ {{ props.confirmButtonText }}
127
+ </v-btn>
128
+ </v-card-actions>
129
+ </v-card>
130
+ </v-dialog>
131
+ </v-row>
132
+ </template>
@@ -143,6 +143,7 @@ const inputTypeChoice = ref([
143
143
  { label: 'File Upload', value: 'FormFile' },
144
144
  { label: 'Signature Pad', value: 'FormSignPad' },
145
145
  { label: 'Table', value: 'FormTable' },
146
+ { label: 'Table Data', value: 'FormTableData' },
146
147
  { label: '[Decoration] Header', value: 'Header' },
147
148
  { label: '[Decoration] Separator', value: 'Separator' },
148
149
  { label: '[Advanced] Hidden Field', value: 'FormHidden' },
@@ -150,7 +151,7 @@ const inputTypeChoice = ref([
150
151
  { label: '[Advanced] Custom Code', value: 'CustomCode' },
151
152
  ]);
152
153
 
153
- const requireOption = ref(['VSelect', 'VAutocomplete', 'VCombobox', 'VRadio', 'VRadioInline', 'MasterAutocomplete','FormTable','DocumentForm','FormCheckboxGroup'])
154
+ const requireOption = ref(['VSelect', 'VAutocomplete', 'VCombobox', 'VRadio', 'VRadioInline', 'MasterAutocomplete','FormTable','FormTableData','DocumentForm','FormCheckboxGroup'])
154
155
  const notRequireVariable = ref(['Header', 'Separator', 'CustomCode','DocumentForm'])
155
156
  const notRequireLabel = ref(['Separator', 'CustomCode', 'FormFile', 'FormHidden','DocumentForm'])
156
157
  const notRequireWidth = ref(['Separator', 'FormHidden','DocumentForm'])
@@ -160,7 +161,7 @@ const notRequireInputAttributes = ref(['CustomCode','Header', 'FormHidden','Docu
160
161
  const notRequireColumnAttributes = ref(['Separator', 'FormHidden','DocumentForm'])
161
162
  const notRequireAdvancedSetting = ref(['Separator','CustomCode', 'FormHidden'])
162
163
 
163
- const hasSpecificOption = ref(['FormHidden','FormTable'])
164
+ const hasSpecificOption = ref(['FormHidden','FormTable','FormTableData'])
164
165
 
165
166
  const choiceOption = ref(['VRadio', 'VRadioInline','VSelect', 'VAutocomplete', 'VCombobox','FormCheckboxGroup'])
166
167
  const inputOptionsLabel = ref<Record<string,string>>({
@@ -258,6 +259,112 @@ const ruleOptions = (inputType: string) => (value: any) => {
258
259
  </v-row>
259
260
  </template>
260
261
  </form-pad>
262
+ <form-pad v-model="data.inputOptions" v-if="data.inputType=='FormTableData'">
263
+ <template #default="{data: optionData}">
264
+ <v-row dense>
265
+ <v-col cols="12">
266
+ <form-table v-model="optionData.headers" :headers="formTableHeaders" title="Headers">
267
+ <template #form="{data: headerData,rules}">
268
+ <v-container fluid>
269
+ <v-row dense>
270
+ <v-col cols="6"><v-text-field v-model="headerData.title" label="Title"></v-text-field></v-col>
271
+ <v-col cols="3"><v-text-field v-model="headerData.key" label="Key" :rules="[rules.require()]"></v-text-field></v-col>
272
+ <v-col cols="3"><v-text-field v-model="headerData.width" label="Width"></v-text-field></v-col>
273
+ </v-row>
274
+ <v-row dense>
275
+ <v-col cols="12">
276
+ <template-builder title="Template" v-model="headerData.template"></template-builder>
277
+ </v-col>
278
+ </v-row>
279
+ <v-row dense>
280
+ <v-col cols="12">
281
+ <template-builder title="Header Template" v-model="headerData.headerTemplate"></template-builder>
282
+ </v-col>
283
+ </v-row>
284
+ <v-expansion-panels>
285
+
286
+ <v-expansion-panel >
287
+ <v-expansion-panel-title collapse-icon="mdi mdi-minus" expand-icon="mdi mdi-plus" class="font-weight-bold">Advanced settings</v-expansion-panel-title>
288
+ <v-expansion-panel-text>
289
+ <v-container fluid>
290
+ <v-row dense>
291
+ <v-col cols="12" md="6" v-if="!notRequireInputAttributes.includes(data.inputType)">
292
+ <v-text-field
293
+ v-model="headerData.inputAttributes"
294
+ label="Input Attributes"
295
+ />
296
+ </v-col>
297
+
298
+ <v-col cols="12" md="6">
299
+ <v-text-field
300
+ v-model="headerData.conditionalDisplay"
301
+ label="Conditional Display"
302
+ />
303
+ </v-col>
304
+ <v-col cols="12" md="6">
305
+ <v-text-field
306
+ v-model="headerData.computedValue"
307
+ label="Computed Value"
308
+ />
309
+ </v-col>
310
+ <v-col cols="12" md="6">
311
+ <v-text-field
312
+ v-model="headerData.retrievedValue"
313
+ label="Retrieved Value"
314
+ />
315
+ </v-col>
316
+ </v-row>
317
+ </v-container>
318
+ </v-expansion-panel-text>
319
+ </v-expansion-panel>
320
+ </v-expansion-panels>
321
+ </v-container>
322
+ </template>
323
+ </form-table>
324
+ </v-col>
325
+ <v-col cols="12">
326
+ {{optionData}}
327
+ <form-table v-model="optionData.items"
328
+ hide-default-footer
329
+ :importable="false"
330
+ :exportable="false"
331
+ :searchable="false"
332
+ :headers="optionData.headers" title="Data">
333
+ <template #form="{data: headerData,rules}">
334
+ <v-container fluid>
335
+ <v-row dense>
336
+ <v-col v-for="header of optionData.headers" cols="12">
337
+ <v-text-field v-if="header.key != 'action' && !header.template"
338
+ v-model="headerData[header.title]"
339
+ :label="header.title">
340
+
341
+ </v-text-field></v-col>
342
+ </v-row>
343
+ </v-container>
344
+ </template>
345
+
346
+ <template v-for="header of useFilter(optionData.headers,(item)=> item.template)" #[`item.${header.key}`]="{item}">
347
+ <form-pad
348
+ :template="header.template"
349
+ v-model="item[header.key]">
350
+ </form-pad>
351
+
352
+ </template>
353
+ <template v-for="header of useFilter(optionData.headers,(item)=> item.headerTemplate)" #[`header.${header.key}`]="{item}">
354
+ <form-pad
355
+ :template="header.headerTemplate"
356
+ >
357
+ </form-pad>
358
+
359
+ </template>
360
+
361
+ </form-table>
362
+
363
+ </v-col>
364
+ </v-row>
365
+ </template>
366
+
367
+ </form-pad>
261
368
  <form-pad v-model="data.inputOptions" v-if="data.inputType=='FormHidden'">
262
369
  <template #default="{data: optionData}">
263
370
  <v-row dense>
@@ -3,6 +3,7 @@ import {VDataTable} from 'vuetify/components/VDataTable'
3
3
  import {VInput} from 'vuetify/components/VInput'
4
4
  import {computed, defineOptions,defineExpose, nextTick, ref, useAttrs, watch, useTemplateRef} from 'vue'
5
5
  import {omit} from 'lodash-es'
6
+ import {useDialog} from "../../composables/dialog"
6
7
  import type {FormDialogCallback} from '../../types/formDialog'
7
8
 
8
9
  defineOptions({
@@ -25,6 +26,7 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
25
26
  inputPadOnly?: boolean
26
27
  saveAndStay?: boolean
27
28
  stringFields?: Array<string>
29
+ items?: Record<string, any>[]
28
30
  }
29
31
 
30
32
  const props = withDefaults(defineProps<Props>(), {
@@ -83,6 +85,12 @@ watch(items, (newValue) => {
83
85
  emit('update:modelValue', newValue)
84
86
  }, { deep: true })
85
87
 
88
+ onMounted(()=>{
89
+ if (props.items){
90
+ items.value = props.items
91
+ }
92
+ })
93
+
86
94
  function createItem(item: Record<string, any>, callback?: FormDialogCallback) {
87
95
  if (items.value.length > 0) item[props.modelKey] = Math.max(...items.value.map(i => i[props.modelKey] || 0)) + 1
88
96
  else item[props.modelKey] = 1
@@ -155,14 +163,16 @@ function moveToItem(currentItem: Record<string, any>, callback?: FormDialogCallb
155
163
  if (callback) callback.done();
156
164
  }
157
165
 
158
- function deleteItem(deleteItem: Record<string, any>, callback?: FormDialogCallback) {
166
+ async function deleteItem(deleteItem: Record<string, any>, callback?: FormDialogCallback) {
159
167
  const index = items.value.findIndex(item => item[props.modelKey] === deleteItem[props.modelKey])
160
168
 
161
169
  if (index !== -1) {
162
- items.value.splice(index, 1)
170
+ let confirm = await useDialog().confirm({message: "Do you want to delete record?"})
171
+ if (confirm) {
172
+ items.value.splice(index, 1)
173
+ if (callback) callback.done()
174
+ }
163
175
  }
164
-
165
- if (callback) callback.done()
166
176
  }
167
177
 
168
178
  function openDialog(item?: object) {
@@ -1,9 +1,11 @@
1
1
  <script lang="ts" setup>
2
- import { computed,watch, nextTick, ref, useAttrs } from 'vue'
3
- import { VDataTable } from 'vuetify/components/VDataTable'
4
- import { clone } from 'lodash-es'
5
- import type { GraphqlModelProps } from '../../composables/graphqlModel'
6
- import { useGraphqlModel } from '../../composables/graphqlModel'
2
+ import {computed,watch, nextTick, ref, useAttrs} from 'vue'
3
+ import {VDataTable} from 'vuetify/components/VDataTable'
4
+ import {clone} from 'lodash-es'
5
+ import {useGraphqlModel} from '../../composables/graphqlModel'
6
+ import {useDialog} from "../../composables/dialog"
7
+ import type {GraphqlModelProps} from '../../composables/graphqlModel'
8
+ import type {FormDialogCallback} from "../../types/formDialog";
7
9
 
8
10
  defineOptions({
9
11
  inheritAttrs: false,
@@ -45,6 +47,7 @@ const plainAttrs = computed(() => {
45
47
  if (props.headers) returnAttrs['headers'] = props.headers
46
48
  return returnAttrs
47
49
  })
50
+
48
51
  const currentItem = ref<Record<string, any> | undefined>(undefined)
49
52
  const isDialogOpen = ref<boolean>(false)
50
53
 
@@ -62,6 +65,11 @@ function openDialog(item?: object) {
62
65
  })
63
66
  }
64
67
 
68
+ async function confirmDeleteItem(item: Record<string, any>, callback?: FormDialogCallback) {
69
+ let confirm = await useDialog().confirm({message: "Do you want to delete record?"})
70
+ if (confirm) deleteItem(item,callback)
71
+ }
72
+
65
73
  const operation = ref({ openDialog, createItem, importItems, updateItem, deleteItem, reload, setSearch, canServerPageable, canServerSearch, canCreate, canUpdate, canDelete })
66
74
 
67
75
  const computedInitialData = computed(() => {
@@ -191,7 +199,7 @@ defineExpose({ reload,operation })
191
199
  variant="flat"
192
200
  density="compact"
193
201
  icon="mdi mdi-delete"
194
- @click="deleteItem(item)"
202
+ @click="confirmDeleteItem(item)"
195
203
  />
196
204
  </template>
197
205
  </v-data-table-server>
@@ -232,7 +240,7 @@ defineExpose({ reload,operation })
232
240
  variant="flat"
233
241
  density="compact"
234
242
  icon="mdi mdi-delete"
235
- @click="deleteItem(item)"
243
+ @click="confirmDeleteItem(item)"
236
244
  />
237
245
  </template>
238
246
  </v-data-table>
@@ -0,0 +1 @@
1
+ export declare const useDialog: () => import("../types/dialogManager").DialogPlugin;
@@ -0,0 +1,5 @@
1
+ import { useNuxtApp } from "#app";
2
+ export const useDialog = () => {
3
+ const { $dialog } = useNuxtApp();
4
+ return $dialog;
5
+ };
@@ -1,4 +1,4 @@
1
- import { processTemplateFormTable } from "./templateFormTable.js";
1
+ import { processTemplateFormTable, processTemplateFormTableData } from "./templateFormTable.js";
2
2
  import { processTemplateFormHidden } from "./templateFormHidden.js";
3
3
  import { some, includes } from "lodash-es";
4
4
  export const validationRulesRegex = /^(require(?:\([^)]*\))?|requireIf\([^)]*\)|requireTrue(?:\([^)]*\))?|requireTrueIf\([^)]*\)|numeric(?:\([^)]*\))?|range\([^)]*\)|integer(?:\([^)]*\))?|unique\([^)]*\)|length(?:\([^)]*\))?|lengthGreater\([^)]*\)|lengthLess\([^)]*\)|telephone(?:\([^)]*\))?|email(?:\([^)]*\))?|regex\([^)]*\)|idcard(?:\([^)]*\))?|DateFuture(?:\([^)]*\))?|DatetimeFuture(?:\([^)]*\))?|DateHappen(?:\([^)]*\))?|DatetimeHappen(?:\([^)]*\))?|DateAfter\([^)]*\)|DateBefore\([^)]*\)|DateEqual\([^)]*\))(,(require(?:\([^)]*\))?|requireIf\([^)]*\)|requireTrue(?:\([^)]*\))?|requireTrueIf\([^)]*\)|numeric(?:\([^)]*\))?|range\([^)]*\)|integer(?:\([^)]*\))?|unique\([^)]*\)|length(?:\([^)]*\))?|lengthGreater\([^)]*\)|lengthLess\([^)]*\)|telephone(?:\([^)]*\))?|email(?:\([^)]*\))?|regex\([^)]*\)|idcard(?:\([^)]*\))?|DateFuture(?:\([^)]*\))?|DatetimeFuture(?:\([^)]*\))?|DateHappen(?:\([^)]*\))?|DatetimeHappen(?:\([^)]*\))?|DateAfter\([^)]*\)|DateBefore\([^)]*\)|DateEqual\([^)]*\)))*$/;
@@ -52,10 +52,10 @@ function templateItemToString(item, parentTemplates) {
52
52
  templateString = item.inputCustomCode || "";
53
53
  break;
54
54
  case "VRadio":
55
- templateString = `${item.inputLabel ? `<p class="opacity-60">${item.inputLabel}</p>` : ""} <v-radio-group v-model="data.${item.variableName || ""}"${validationRules ? " " + validationRules.trim() : ""}>${optionString ? " " + optionString.trim() : ""}</v-radio-group>`;
55
+ templateString = `${item.inputLabel ? `<p class="opacity-60">${item.inputLabel}</p>` : ""} <v-radio-group ${item.inputAttributes?.trim()} v-model="data.${item.variableName || ""}"${validationRules ? " " + validationRules.trim() : ""}>${optionString ? " " + optionString.trim() : ""}</v-radio-group>`;
56
56
  break;
57
57
  case "VRadioInline":
58
- templateString = `<v-radio-group v-model="data.${item.variableName || ""}" inline ${validationRules ? " " + validationRules.trim() : ""}>${item.inputLabel ? `<template #prepend><span class="opacity-60">${item.inputLabel}</span></template>` : ""}${optionString ? " " + optionString.trim() : ""}</v-radio-group>`;
58
+ templateString = `<v-radio-group ${item.inputAttributes?.trim()} v-model="data.${item.variableName || ""}" inline ${validationRules ? " " + validationRules.trim() : ""}>${item.inputLabel ? `<template #prepend><span class="opacity-60">${item.inputLabel}</span></template>` : ""}${optionString ? " " + optionString.trim() : ""}</v-radio-group>`;
59
59
  break;
60
60
  case "Separator":
61
61
  templateString = "</v-row><v-row dense>";
@@ -66,6 +66,9 @@ function templateItemToString(item, parentTemplates) {
66
66
  case "FormTable":
67
67
  templateString = processTemplateFormTable(item, parentTemplates);
68
68
  break;
69
+ case "FormTableData":
70
+ templateString = processTemplateFormTableData(item, parentTemplates);
71
+ break;
69
72
  case "FormHidden":
70
73
  templateString = processTemplateFormHidden(item, parentTemplates);
71
74
  break;
@@ -112,13 +115,27 @@ export function escapeObjectForInlineBinding(obj) {
112
115
  export function buildValidationRules(validationString) {
113
116
  validationString = validationString.replace(/^\[|]$/g, "").trim();
114
117
  if (!validationRulesRegex.test(validationString)) return "";
115
- const rules = validationString.split(",").map((rule) => {
118
+ const rules = [];
119
+ let current = "";
120
+ let depth = 0;
121
+ for (let char of validationString) {
122
+ if (char === "," && depth === 0) {
123
+ if (current.trim()) rules.push(current.trim());
124
+ current = "";
125
+ } else {
126
+ if (char === "(") depth++;
127
+ if (char === ")") depth--;
128
+ current += char;
129
+ }
130
+ }
131
+ if (current.trim()) rules.push(current.trim());
132
+ const formatted = rules.map((rule) => {
116
133
  rule = rule.trim();
117
134
  if (!rule.startsWith("rules.")) rule = `rules.${rule}`;
118
135
  if (!/\(.+\)$/.test(rule)) rule += "()";
119
136
  return rule.replace(/"/g, "'");
120
137
  });
121
- return `:rules="[${rules.join(",")}]"`;
138
+ return `:rules="[${formatted.join(",")}]"`;
122
139
  }
123
140
  export function processDefaultTemplate(item, insideTemplate, optionString, validationRules) {
124
141
  if (!validationRules) validationRules = item.validationRules ? buildValidationRules(item.validationRules) || "" : "";
@@ -1,2 +1,3 @@
1
1
  import { type DocumentTemplateItem } from './template.js';
2
2
  export declare function processTemplateFormTable(item: DocumentTemplateItem, parentTemplates: string | string[]): string;
3
+ export declare function processTemplateFormTableData(item: DocumentTemplateItem, parentTemplates: string | string[]): string;
@@ -5,5 +5,46 @@ import {
5
5
  } from "./template.js";
6
6
  export function processTemplateFormTable(item, parentTemplates) {
7
7
  let tableOptions = Object.assign({ title: item.inputLabel || "", formTemplate: "" }, item.inputOptions);
8
- return processDefaultTemplate(item, `<template #form="{data,rules}">${useDocumentTemplate(tableOptions.formTemplate)}</template>`, `title="${tableOptions.title}" :headers='${escapeObjectForInlineBinding(tableOptions.headers || {})}'`);
8
+ let tableHeader = tableOptions.headers || [];
9
+ if (!tableHeader.some((h) => h.key === "action")) tableHeader.push({ title: "Action", key: "action", width: "100px" });
10
+ return processDefaultTemplate(item, `<template #form="{data,rules}">${useDocumentTemplate(tableOptions.formTemplate)}</template>`, `title="${tableOptions.title}" :headers='${escapeObjectForInlineBinding(tableHeader)}'`);
11
+ }
12
+ export function processTemplateFormTableData(item, parentTemplates) {
13
+ let tableOptions = Object.assign({ title: item.inputLabel || "", formTemplate: "" }, item.inputOptions);
14
+ let tableHeader = tableOptions.headers || [];
15
+ let tableItems = tableOptions.items || [];
16
+ let tableTemplate = "";
17
+ if (item.conditionalDisplay) {
18
+ item.inputAttributes = `${item.inputAttributes?.trim() || ""} v-if="${item.conditionalDisplay}"`.trim();
19
+ }
20
+ for (const header of tableHeader) {
21
+ const inputAttributes = header.conditionalDisplay ? `v-if="${header.conditionalDisplay}"`.trim() : "";
22
+ if (header.template) {
23
+ tableTemplate += `<template #item.${header.key}="{item}">
24
+ <form-pad ${inputAttributes} template='${header.template}' v-model="item.${header.key}">
25
+ </form-pad>
26
+ </template>`;
27
+ }
28
+ if (header.headerTemplate) {
29
+ tableTemplate += `<template #header.${header.key}>
30
+ <form-pad ${inputAttributes} template='${header.headerTemplate}'
31
+ @update:model-value='(value) =>{
32
+ for (const t of data.${item.variableName}) {
33
+ t.${header.key} = {...value}
34
+ }
35
+ }'>
36
+ </form-pad>
37
+ </template>`;
38
+ }
39
+ }
40
+ item.inputType = "FormTable";
41
+ return processDefaultTemplate(item, tableTemplate, `title="${tableOptions.title}"
42
+ :headers='${escapeObjectForInlineBinding(tableHeader)}'
43
+ :items='${escapeObjectForInlineBinding(tableItems)}'
44
+ :insertable="false"
45
+ :importable="false"
46
+ :exportable="false"
47
+ :searchable="false"
48
+ hide-default-footer
49
+ `);
9
50
  }
@@ -7,7 +7,7 @@ export function useRules() {
7
7
  const requireIf = (conditionIf, customError = "This field is required") => (value) => condition(!!value || value === false || value === 0 || !conditionIf, customError);
8
8
  const requireTrue = (customError = "This field must be true") => (value) => condition(!!value, customError);
9
9
  const requireTrueIf = (conditionIf, customError = "This field must be true") => (value) => condition(!!value || !conditionIf, customError);
10
- const numeric = (customError = "This field must be a number") => (value) => condition(!value || !Number.isNaN(value), customError);
10
+ const numeric = (customError = "This field must be a number") => (value) => condition(!value || !isNaN(Number(value)), customError);
11
11
  const range = (minValue, maxValue, customError = `Value is out of range (${minValue}-${maxValue})`) => (value) => condition(!value || value >= minValue && value <= maxValue, customError);
12
12
  const integer = (customError = "This field must be an integer") => (value) => condition(!value || isInteger(value) || /^\+?-?\d+$/.test(value), customError);
13
13
  const unique = (data, fieldName, customError = "This field must be unique") => (value) => condition(!value || !data || !find(data, [fieldName, value]), customError);
@@ -0,0 +1,2 @@
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
+ export default _default;
@@ -0,0 +1,72 @@
1
+ import { defineNuxtPlugin } from "nuxt/app";
2
+ import { h, render } from "vue";
3
+ import DialogHost from "../components/dialog/Host.vue";
4
+ import DialogDefaultConfirm from "../components/dialog/default/Confirm.vue";
5
+ import DialogDefaultNotify from "../components/dialog/default/Notify.vue";
6
+ import DialogDefaultLoading from "../components/dialog/default/Loading.vue";
7
+ import DialogDefaultPrinting from "../components/dialog/default/Printing.vue";
8
+ import DialogDefaultVerifyUser from "../components/dialog/default/VerifyUser.vue";
9
+ export default defineNuxtPlugin(async (nuxtApp) => {
10
+ const dialogComponents = {
11
+ confirmDialog: DialogDefaultConfirm,
12
+ notifyDialog: DialogDefaultNotify,
13
+ loadingDialog: DialogDefaultLoading,
14
+ printingDialog: DialogDefaultPrinting,
15
+ verifyUserDialog: DialogDefaultVerifyUser
16
+ };
17
+ let hostOpenFn = null;
18
+ let hostCloseFn = null;
19
+ const setDialogHost = (openFn, closeFn) => {
20
+ hostOpenFn = openFn;
21
+ hostCloseFn = closeFn;
22
+ };
23
+ const setDialogComponents = (components) => {
24
+ Object.assign(dialogComponents, components);
25
+ };
26
+ const open = (component, props = {}) => {
27
+ return hostOpenFn?.(component, props).promise ?? Promise.reject("No dialog host registered");
28
+ };
29
+ const openPromise = async (component, promiseInput, props = {}) => {
30
+ const dialogInstance = hostOpenFn?.(component, props);
31
+ try {
32
+ if (promiseInput instanceof Promise) {
33
+ return await promiseInput;
34
+ }
35
+ if (Array.isArray(promiseInput)) {
36
+ const promises = promiseInput.map((item) => {
37
+ if (typeof item === "function") {
38
+ try {
39
+ return Promise.resolve(item());
40
+ } catch (err) {
41
+ return Promise.reject(err);
42
+ }
43
+ } else {
44
+ return Promise.resolve(item);
45
+ }
46
+ });
47
+ return await Promise.all(promises);
48
+ }
49
+ try {
50
+ return await Promise.resolve(promiseInput());
51
+ } catch (err) {
52
+ return Promise.reject(err);
53
+ }
54
+ } finally {
55
+ dialogInstance?.closeInstance();
56
+ }
57
+ };
58
+ const confirm = (props = {}) => open(dialogComponents.confirmDialog, props);
59
+ const notify = (props = {}) => open(dialogComponents.notifyDialog, props);
60
+ const verifyUser = (props = {}) => open(dialogComponents.verifyUserDialog, props);
61
+ const loading = async (promiseInput, props = {}) => openPromise(dialogComponents.loadingDialog, promiseInput, props);
62
+ const printing = async (promiseInput, props = {}) => openPromise(dialogComponents.printingDialog, promiseInput, props);
63
+ nuxtApp.hook("app:mounted", () => {
64
+ const el = document.createElement("div");
65
+ el.id = "__nuxt_dialog_host";
66
+ document.body.appendChild(el);
67
+ const vnode = h(DialogHost);
68
+ vnode.appContext = nuxtApp.vueApp._context;
69
+ render(vnode, el);
70
+ });
71
+ nuxtApp.provide("dialog", { open, confirm, notify, verifyUser, loading, printing, setDialogHost, setDialogComponents });
72
+ });
@@ -0,0 +1,36 @@
1
+ export interface DialogPlugin {
2
+ open: (component: any,props?: Record<string, any>) => Promise<any>
3
+ openPromise: (component: any, promiseInput: Promise<any> | (() => any | Promise<any>) | (Promise<any> | (() => any | Promise<any>))[], props?: Record<string, any>) => Promise<any | any[]>
4
+ confirm: (props?: Record<string, any>) => Promise<boolean>
5
+ notify: (props?: Record<string, any>) => Promise<boolean>
6
+ verifyUser: (props?: Record<string, any>) => Promise<boolean>
7
+ loading: <T>(promiseInput: Promise<any> | (() => any | Promise<any>) | (Promise<any> | (() => any | Promise<any>))[],props?: Record<string, any>) => Promise<any | any[]>
8
+ printing: <T>(promiseInput: Promise<any> | (() => any | Promise<any>) | (Promise<any> | (() => any | Promise<any>))[],props?: Record<string, any>) => Promise<any | any[]>
9
+ setDialogHost: (openFn: DialogOpenFn,closeFn: DialogCloseFn) => void
10
+ setDialogComponents: (components:object) => void
11
+ }
12
+
13
+ export interface DialogOpenResult<T = any> {
14
+ id: number
15
+ promise: Promise<T>
16
+ closeInstance: (value?: T) => void
17
+ rejectInstance: (value?: T) => void
18
+ }
19
+
20
+ export interface DialogOpenFn {
21
+ (component: any, props?: Record<string, any>): DialogOpenResult
22
+ }
23
+
24
+ export interface DialogCloseFn {
25
+ (id: number, value?: any): void
26
+ }
27
+
28
+ declare module '#app' {
29
+ interface NuxtApp {
30
+ $dialog: DialogPlugin
31
+ }
32
+
33
+ interface RuntimeNuxtApp {
34
+ $dialog: DialogPlugin
35
+ }
36
+ }
@@ -8,7 +8,7 @@ declare global {
8
8
  interface MenuMeta {
9
9
  title: string
10
10
  icon: string
11
- role: string
11
+ role?: string
12
12
  image? : string
13
13
  active? : boolean
14
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -116,6 +116,5 @@
116
116
  "typescript": "^5.8.2",
117
117
  "vitest": "^1.6.1",
118
118
  "vue-tsc": "^2.2.10"
119
- },
120
- "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"
119
+ }
121
120
  }