@ramathibodi/nuxt-commons 0.1.48 → 0.1.50
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 +1 -1
- package/dist/module.mjs +19 -21
- package/dist/runtime/components/dialog/Host.vue +84 -0
- package/dist/runtime/components/dialog/default/Confirm.vue +101 -0
- package/dist/runtime/components/dialog/default/Loading.vue +48 -0
- package/dist/runtime/components/dialog/default/Notify.vue +70 -0
- package/dist/runtime/components/dialog/default/Printing.vue +35 -0
- package/dist/runtime/components/dialog/default/VerifyUser.vue +132 -0
- package/dist/runtime/components/document/TemplateBuilder.vue +109 -2
- package/dist/runtime/components/form/Dialog.vue +4 -0
- package/dist/runtime/components/form/Table.vue +14 -4
- package/dist/runtime/components/master/Autocomplete.vue +1 -1
- package/dist/runtime/components/model/Autocomplete.vue +12 -13
- package/dist/runtime/components/model/Table.vue +62 -10
- package/dist/runtime/composables/dialog.d.ts +1 -0
- package/dist/runtime/composables/dialog.js +5 -0
- package/dist/runtime/composables/document/template.js +6 -3
- package/dist/runtime/composables/document/templateFormTable.d.ts +1 -0
- package/dist/runtime/composables/document/templateFormTable.js +39 -0
- package/dist/runtime/composables/userPermission.d.ts +1 -0
- package/dist/runtime/composables/userPermission.js +5 -0
- package/dist/runtime/plugins/dialogManager.d.ts +2 -0
- package/dist/runtime/plugins/dialogManager.js +72 -0
- package/dist/runtime/plugins/permission.js +35 -14
- package/dist/runtime/types/dialogManager.d.ts +36 -0
- package/dist/runtime/types/menu.d.ts +1 -1
- package/dist/runtime/types/permission.d.ts +14 -0
- package/package.json +2 -3
package/dist/module.json
CHANGED
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
|
-
|
|
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
|
-
|
|
25
|
-
src: resolver.resolve("runtime/
|
|
26
|
-
|
|
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", "permission"];
|
|
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>
|
|
@@ -13,6 +13,7 @@ interface Props {
|
|
|
13
13
|
cancelCaption?: string
|
|
14
14
|
closeCaption?: string
|
|
15
15
|
saveAndStay?: boolean
|
|
16
|
+
readonly?: boolean
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -20,6 +21,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
20
21
|
cancelCaption: 'ยกเลิก',
|
|
21
22
|
closeCaption: 'ปิด',
|
|
22
23
|
saveAndStay: false,
|
|
24
|
+
readonly: false,
|
|
23
25
|
})
|
|
24
26
|
|
|
25
27
|
const isShowing = defineModel<boolean>({ default: false })
|
|
@@ -125,6 +127,7 @@ watch(() => isShowing.value, (newValue) => {
|
|
|
125
127
|
<form-pad
|
|
126
128
|
ref="formPadRef"
|
|
127
129
|
v-model="formData"
|
|
130
|
+
:readonly="readonly"
|
|
128
131
|
isolated
|
|
129
132
|
>
|
|
130
133
|
<template #default="slotData">
|
|
@@ -145,6 +148,7 @@ watch(() => isShowing.value, (newValue) => {
|
|
|
145
148
|
:loading="isSaving"
|
|
146
149
|
:disabled="!isDataChange"
|
|
147
150
|
@click="save"
|
|
151
|
+
v-if="!readonly"
|
|
148
152
|
>
|
|
149
153
|
{{ saveCaption }}
|
|
150
154
|
</VBtn>
|
|
@@ -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
|
-
|
|
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) {
|
|
@@ -102,7 +102,7 @@ const computedSortBy = computed(()=>{
|
|
|
102
102
|
>
|
|
103
103
|
<v-list-item
|
|
104
104
|
v-bind="props"
|
|
105
|
-
:title="
|
|
105
|
+
:title="item.title"
|
|
106
106
|
/>
|
|
107
107
|
</template>
|
|
108
108
|
</model-autocomplete>
|
|
@@ -97,11 +97,16 @@ watchDebounced(searchData, fuzzySearch, { debounce: 1000, maxWait: 5000 })
|
|
|
97
97
|
const computedItems = computed(()=>{
|
|
98
98
|
let sortByField = (!props.sortBy || typeof props.sortBy === "string") ? [props.sortBy || ((props.showCode) ? props.itemValue : props.itemTitle)] : props.sortBy
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
const baseItems = props.fuzzy && !isEmpty(searchData.value)
|
|
101
|
+
? items.value
|
|
102
|
+
: sortBy(items.value, sortByField)
|
|
103
|
+
|
|
104
|
+
return baseItems.map(item => ({
|
|
105
|
+
...item,
|
|
106
|
+
[props.itemTitle]: props.showCode
|
|
107
|
+
? `${item[props.itemValue]}-${item[props.itemTitle]}`
|
|
108
|
+
: item[props.itemTitle]
|
|
109
|
+
}))
|
|
105
110
|
})
|
|
106
111
|
</script>
|
|
107
112
|
|
|
@@ -126,14 +131,8 @@ const computedItems = computed(()=>{
|
|
|
126
131
|
v-bind="((slotData || {}) as object)"
|
|
127
132
|
/>
|
|
128
133
|
</template>
|
|
129
|
-
<template
|
|
130
|
-
|
|
131
|
-
#item="{ props, item }"
|
|
132
|
-
>
|
|
133
|
-
<v-list-item
|
|
134
|
-
v-bind="props"
|
|
135
|
-
:title="(showCode ? item.value+'-' : '')+item.title"
|
|
136
|
-
/>
|
|
134
|
+
<template v-if="!$slots.item" #item="{ props, item }">
|
|
135
|
+
<v-list-item v-bind="props" :title="(item as Record<string, any>)[String(props.itemTitle)]" />
|
|
137
136
|
</template>
|
|
138
137
|
<template
|
|
139
138
|
v-if="isErrorLoading"
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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 {useUserPermission} from "../../composables/userPermission"
|
|
8
|
+
import type {GraphqlModelProps} from '../../composables/graphqlModel'
|
|
9
|
+
import type {FormDialogCallback} from "../../types/formDialog";
|
|
10
|
+
import {type AuthenticationState, useState} from "#imports";
|
|
7
11
|
|
|
8
12
|
defineOptions({
|
|
9
13
|
inheritAttrs: false,
|
|
@@ -22,6 +26,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
|
|
|
22
26
|
search?: string
|
|
23
27
|
saveAndStay?: boolean
|
|
24
28
|
stringFields?: Array<string>
|
|
29
|
+
onlyOwnerEdit?: boolean
|
|
30
|
+
onlyOwnerOverridePermission?: string | string[]
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
|
|
@@ -37,6 +43,7 @@ const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
|
|
|
37
43
|
modelBy: undefined,
|
|
38
44
|
fields: () => [],
|
|
39
45
|
stringFields: ()=>[],
|
|
46
|
+
onlyOwnerEdit: false,
|
|
40
47
|
})
|
|
41
48
|
|
|
42
49
|
const attrs = useAttrs()
|
|
@@ -45,8 +52,16 @@ const plainAttrs = computed(() => {
|
|
|
45
52
|
if (props.headers) returnAttrs['headers'] = props.headers
|
|
46
53
|
return returnAttrs
|
|
47
54
|
})
|
|
55
|
+
|
|
56
|
+
const authenState = useState<AuthenticationState>("authentication")
|
|
57
|
+
const currentUsername = computed(()=>{
|
|
58
|
+
let userProfile = authenState.value?.userProfile as {username: string}
|
|
59
|
+
return userProfile?.username ?? ""
|
|
60
|
+
})
|
|
61
|
+
|
|
48
62
|
const currentItem = ref<Record<string, any> | undefined>(undefined)
|
|
49
63
|
const isDialogOpen = ref<boolean>(false)
|
|
64
|
+
const isDialogReadonly = ref<boolean>(false)
|
|
50
65
|
|
|
51
66
|
const { items, itemsLength,
|
|
52
67
|
search,setSearch,
|
|
@@ -56,13 +71,36 @@ const { items, itemsLength,
|
|
|
56
71
|
isLoading } = useGraphqlModel(props)
|
|
57
72
|
|
|
58
73
|
function openDialog(item?: object) {
|
|
74
|
+
isDialogReadonly.value = false
|
|
75
|
+
currentItem.value = item
|
|
76
|
+
nextTick(() => {
|
|
77
|
+
isDialogOpen.value = true
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function openDialogReadonly(item?: object) {
|
|
82
|
+
isDialogReadonly.value = true
|
|
59
83
|
currentItem.value = item
|
|
60
84
|
nextTick(() => {
|
|
61
85
|
isDialogOpen.value = true
|
|
62
86
|
})
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
|
|
89
|
+
async function confirmDeleteItem(item: Record<string, any>, callback?: FormDialogCallback) {
|
|
90
|
+
let confirm = await useDialog().confirm({message: "Do you want to delete record?"})
|
|
91
|
+
if (confirm) deleteItem(item,callback)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const canEditRow = function (item: Record<string, any>) {
|
|
95
|
+
if (props.onlyOwnerEdit) {
|
|
96
|
+
if (item?.userstampField?.createdBy && item?.userstampField?.createdBy==currentUsername.value) return true
|
|
97
|
+
if (!!props.onlyOwnerOverridePermission && useUserPermission().check(props.onlyOwnerOverridePermission)) return true
|
|
98
|
+
return !item?.userstampField?.createdBy
|
|
99
|
+
}
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const operation = ref({ openDialog, openDialogReadonly, createItem, importItems, updateItem, deleteItem, reload, setSearch, canServerPageable, canServerSearch, canCreate, canUpdate, canDelete, canEditRow, onlyOwnerEdit: props.onlyOwnerEdit, onlyOwnerOverridePermission: props.onlyOwnerOverridePermission })
|
|
66
104
|
|
|
67
105
|
const computedInitialData = computed(() => {
|
|
68
106
|
return Object.assign({}, props.initialData, props.modelBy)
|
|
@@ -180,18 +218,25 @@ defineExpose({ reload,operation })
|
|
|
180
218
|
#item.action="{ item }"
|
|
181
219
|
>
|
|
182
220
|
<v-btn
|
|
183
|
-
|
|
221
|
+
v-if="!canUpdate || !canEditRow(item)"
|
|
222
|
+
variant="flat"
|
|
223
|
+
density="compact"
|
|
224
|
+
icon="mdi mdi-note-search"
|
|
225
|
+
@click="openDialogReadonly(item)"
|
|
226
|
+
/>
|
|
227
|
+
<v-btn
|
|
228
|
+
v-if="canUpdate && canEditRow(item)"
|
|
184
229
|
variant="flat"
|
|
185
230
|
density="compact"
|
|
186
231
|
icon="mdi mdi-note-edit"
|
|
187
232
|
@click="openDialog(item)"
|
|
188
233
|
/>
|
|
189
234
|
<v-btn
|
|
190
|
-
v-if="canDelete"
|
|
235
|
+
v-if="canDelete && canEditRow(item)"
|
|
191
236
|
variant="flat"
|
|
192
237
|
density="compact"
|
|
193
238
|
icon="mdi mdi-delete"
|
|
194
|
-
@click="
|
|
239
|
+
@click="confirmDeleteItem(item)"
|
|
195
240
|
/>
|
|
196
241
|
</template>
|
|
197
242
|
</v-data-table-server>
|
|
@@ -232,7 +277,7 @@ defineExpose({ reload,operation })
|
|
|
232
277
|
variant="flat"
|
|
233
278
|
density="compact"
|
|
234
279
|
icon="mdi mdi-delete"
|
|
235
|
-
@click="
|
|
280
|
+
@click="confirmDeleteItem(item)"
|
|
236
281
|
/>
|
|
237
282
|
</template>
|
|
238
283
|
</v-data-table>
|
|
@@ -245,22 +290,29 @@ defineExpose({ reload,operation })
|
|
|
245
290
|
@create="createItem"
|
|
246
291
|
@update="updateItem"
|
|
247
292
|
:saveAndStay="saveAndStay"
|
|
293
|
+
:readonly="isDialogReadonly"
|
|
248
294
|
>
|
|
249
295
|
<template #default="slotData">
|
|
250
296
|
<slot
|
|
251
297
|
name="form"
|
|
298
|
+
:tableItems="items"
|
|
299
|
+
:tableOperation="operation"
|
|
252
300
|
v-bind="slotData"
|
|
253
301
|
/>
|
|
254
302
|
</template>
|
|
255
303
|
<template #title="slotData">
|
|
256
304
|
<slot
|
|
257
305
|
name="formTitle"
|
|
306
|
+
:tableItems="items"
|
|
307
|
+
:tableOperation="operation"
|
|
258
308
|
v-bind="slotData"
|
|
259
309
|
/>
|
|
260
310
|
</template>
|
|
261
311
|
<template #action="slotData">
|
|
262
312
|
<slot
|
|
263
313
|
name="formAction"
|
|
314
|
+
:tableItems="items"
|
|
315
|
+
:tableOperation="operation"
|
|
264
316
|
v-bind="slotData"
|
|
265
317
|
/>
|
|
266
318
|
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useDialog: () => import("../types/dialogManager").DialogPlugin;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -9,3 +9,42 @@ export function processTemplateFormTable(item, parentTemplates) {
|
|
|
9
9
|
if (!tableHeader.some((h) => h.key === "action")) tableHeader.push({ title: "Action", key: "action", width: "100px" });
|
|
10
10
|
return processDefaultTemplate(item, `<template #form="{data,rules}">${useDocumentTemplate(tableOptions.formTemplate)}</template>`, `title="${tableOptions.title}" :headers='${escapeObjectForInlineBinding(tableHeader)}'`);
|
|
11
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
|
+
`);
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useUserPermission: () => unknown;
|
|
@@ -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
|
+
});
|
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
import { defineNuxtPlugin } from "nuxt/app";
|
|
2
2
|
import { useAuthentication } from "#imports";
|
|
3
|
-
export default defineNuxtPlugin(
|
|
4
|
-
function
|
|
5
|
-
if (!
|
|
6
|
-
|
|
3
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
4
|
+
function normalize(input) {
|
|
5
|
+
if (!input) return [];
|
|
6
|
+
if (Array.isArray(input)) return input;
|
|
7
|
+
return [input];
|
|
8
|
+
}
|
|
9
|
+
function isBlank(input) {
|
|
10
|
+
if (!input) return true;
|
|
11
|
+
const arr = normalize(input);
|
|
12
|
+
return arr.length === 0 || arr.every((v) => v.trim() === "");
|
|
13
|
+
}
|
|
14
|
+
function check(permissionIds) {
|
|
15
|
+
if (isBlank(permissionIds)) return true;
|
|
16
|
+
const auth = useAuthentication();
|
|
17
|
+
const perms = normalize(permissionIds);
|
|
18
|
+
return perms.some((id) => auth?.hasPermission(id));
|
|
19
|
+
}
|
|
20
|
+
function checkAll(permissionIds) {
|
|
21
|
+
if (isBlank(permissionIds)) return true;
|
|
22
|
+
const auth = useAuthentication();
|
|
23
|
+
const perms = normalize(permissionIds);
|
|
24
|
+
return perms.every((id) => auth?.hasPermission(id));
|
|
7
25
|
}
|
|
8
|
-
nuxtApp.vueApp.config.globalProperties.$permission = permission;
|
|
9
26
|
nuxtApp.vueApp.directive("permission", (el, binding, vnode) => {
|
|
10
|
-
if (!
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
vnode.um.forEach((um) => {
|
|
14
|
-
um();
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
if (el.parentElement) el.parentElement.removeChild(el);
|
|
18
|
-
}
|
|
27
|
+
if (!check(binding.value)) {
|
|
28
|
+
vnode?.um?.forEach?.((um) => um());
|
|
29
|
+
el.parentElement?.removeChild(el);
|
|
19
30
|
}
|
|
20
31
|
});
|
|
32
|
+
nuxtApp.vueApp.directive("permission-all", (el, binding, vnode) => {
|
|
33
|
+
if (!checkAll(binding.value)) {
|
|
34
|
+
vnode?.um?.forEach?.((um) => um());
|
|
35
|
+
el.parentElement?.removeChild(el);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
nuxtApp.provide("permission", {
|
|
39
|
+
check,
|
|
40
|
+
checkAll
|
|
41
|
+
});
|
|
21
42
|
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PermissionPlugin {
|
|
2
|
+
check: (permission: string | string[] | null | undefined) => boolean
|
|
3
|
+
checkAll: (permission: string | string[] | null | undefined) => boolean
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare module '#app' {
|
|
7
|
+
interface NuxtApp {
|
|
8
|
+
$permission: PermissionPlugin
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RuntimeNuxtApp {
|
|
12
|
+
$permission: PermissionPlugin
|
|
13
|
+
}
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramathibodi/nuxt-commons",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.50",
|
|
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
|
}
|