@ramathibodi/nuxt-commons 0.1.49 → 0.1.51
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 +1 -1
- package/dist/runtime/components/form/ActionPad.vue +64 -58
- package/dist/runtime/components/form/Dialog.vue +4 -0
- package/dist/runtime/components/form/EditPad.vue +140 -0
- package/dist/runtime/components/form/Table.vue +0 -6
- package/dist/runtime/components/form/TableData.vue +82 -0
- package/dist/runtime/components/master/Autocomplete.vue +1 -1
- package/dist/runtime/components/model/Autocomplete.vue +12 -13
- package/dist/runtime/components/model/Pad.vue +26 -72
- package/dist/runtime/components/model/Table.vue +47 -3
- package/dist/runtime/composables/document/templateFormTable.js +1 -1
- package/dist/runtime/composables/userPermission.d.ts +1 -0
- package/dist/runtime/composables/userPermission.js +5 -0
- package/dist/runtime/plugins/permission.js +35 -14
- package/dist/runtime/types/permission.d.ts +14 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -27,7 +27,7 @@ const module = defineNuxtModule({
|
|
|
27
27
|
src: resolver.resolve("runtime/plugins/dialogManager"),
|
|
28
28
|
mode: "client"
|
|
29
29
|
});
|
|
30
|
-
const typeFiles = ["modules", "alert", "menu", "graphqlOperation", "formDialog", "dialogManager"];
|
|
30
|
+
const typeFiles = ["modules", "alert", "menu", "graphqlOperation", "formDialog", "dialogManager", "permission"];
|
|
31
31
|
for (const file of typeFiles) {
|
|
32
32
|
addTypeTemplate({
|
|
33
33
|
src: resolver.resolve(`runtime/types/${file}.d.ts`),
|
|
@@ -6,23 +6,23 @@ import type {FormDialogCallback} from '../../types/formDialog'
|
|
|
6
6
|
interface Props {
|
|
7
7
|
title?: string
|
|
8
8
|
initialData?: object
|
|
9
|
-
|
|
10
|
-
updateCaption?: string
|
|
9
|
+
saveCaption?: string
|
|
11
10
|
cancelCaption?: string
|
|
11
|
+
readonly?: boolean
|
|
12
12
|
showTitle?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const props = withDefaults(defineProps<Props>(), {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
saveCaption: 'บันทึก',
|
|
17
|
+
cancelCaption: 'ยกเลิก',
|
|
18
|
+
readonly: false,
|
|
19
19
|
showTitle: false,
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
const isSaving = ref<boolean>(false)
|
|
23
23
|
const formPadRef = ref()
|
|
24
|
-
const formOriginalData = ref<object>()
|
|
25
24
|
const formData = ref<object>({})
|
|
25
|
+
const formDataOriginalValue = ref<object>()
|
|
26
26
|
|
|
27
27
|
const emit = defineEmits(['create', 'update'])
|
|
28
28
|
|
|
@@ -38,7 +38,8 @@ function cancel() {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function reset() {
|
|
41
|
-
|
|
41
|
+
formDataOriginalValue.value = undefined
|
|
42
|
+
formPadRef.value.reset()
|
|
42
43
|
loadFormData()
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -53,11 +54,11 @@ const callback: FormDialogCallback = {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
const isDataChange = computed(() => {
|
|
56
|
-
return !((isCreating.value) ? isEqual(formData.value, createOriginalValue.value) : isEqual(formData.value,
|
|
57
|
+
return !((isCreating.value) ? isEqual(formData.value, createOriginalValue.value) : isEqual(formData.value, formDataOriginalValue.value))
|
|
57
58
|
})
|
|
58
59
|
|
|
59
60
|
const isCreating = computed(() => {
|
|
60
|
-
return !
|
|
61
|
+
return !formDataOriginalValue.value
|
|
61
62
|
})
|
|
62
63
|
|
|
63
64
|
const createOriginalValue = computed(() => {
|
|
@@ -65,69 +66,74 @@ const createOriginalValue = computed(() => {
|
|
|
65
66
|
})
|
|
66
67
|
|
|
67
68
|
const loadFormData = () => {
|
|
68
|
-
if (
|
|
69
|
-
formData.value = cloneDeep(
|
|
69
|
+
if (formDataOriginalValue.value) {
|
|
70
|
+
formData.value = cloneDeep(formDataOriginalValue.value)
|
|
70
71
|
}
|
|
71
72
|
else {
|
|
72
73
|
formData.value = Object.assign({}, props.initialData)
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
const operation = ref({ isDataChange, isCreating, isSaving, save, cancel })
|
|
78
|
+
|
|
76
79
|
watchEffect(loadFormData)
|
|
77
80
|
|
|
78
81
|
function setOriginalData(originalData?: object) {
|
|
79
|
-
|
|
82
|
+
formDataOriginalValue.value = originalData
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
defineExpose({setOriginalData,
|
|
85
|
+
defineExpose({setOriginalData,operation})
|
|
83
86
|
</script>
|
|
84
87
|
|
|
85
88
|
<template>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
<VCard flat>
|
|
90
|
+
<VToolbar v-if="showTitle">
|
|
91
|
+
<slot name="titleToolbar" :operation="operation">
|
|
92
|
+
<VToolbarTitle>
|
|
93
|
+
<slot name="title" :operation="operation">
|
|
94
|
+
{{ (isCreating) ? "New" : "Edit" }} {{ title }}
|
|
95
|
+
</slot>
|
|
96
|
+
</VToolbarTitle>
|
|
91
97
|
</slot>
|
|
92
|
-
</
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
isolated
|
|
100
|
-
>
|
|
101
|
-
<template #default="slotData">
|
|
102
|
-
<slot
|
|
103
|
-
v-bind="slotData"
|
|
104
|
-
:is-creating="isCreating"
|
|
105
|
-
:is-data-change="isDataChange"
|
|
106
|
-
/>
|
|
107
|
-
</template>
|
|
108
|
-
</form-pad>
|
|
109
|
-
</VCardText>
|
|
110
|
-
<VCardActions>
|
|
111
|
-
<slot name="action" :save="save" :cancel="cancel">
|
|
112
|
-
<VSpacer />
|
|
113
|
-
<VBtn
|
|
114
|
-
color="primary"
|
|
115
|
-
variant="flat"
|
|
116
|
-
:loading="isSaving"
|
|
117
|
-
:disabled="!isDataChange"
|
|
118
|
-
@click="save"
|
|
119
|
-
>
|
|
120
|
-
{{ (isCreating) ? createCaption : updateCaption }}
|
|
121
|
-
</VBtn>
|
|
122
|
-
<VBtn
|
|
123
|
-
color="error"
|
|
124
|
-
variant="flat"
|
|
125
|
-
:disabled="isSaving"
|
|
126
|
-
@click="cancel"
|
|
98
|
+
</VToolbar>
|
|
99
|
+
<VCardText>
|
|
100
|
+
<form-pad
|
|
101
|
+
ref="formPadRef"
|
|
102
|
+
v-model="formData"
|
|
103
|
+
:readonly="readonly"
|
|
104
|
+
isolated
|
|
127
105
|
>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
106
|
+
<template #default="slotData">
|
|
107
|
+
<slot
|
|
108
|
+
v-bind="slotData"
|
|
109
|
+
:is-creating="isCreating"
|
|
110
|
+
:is-data-change="isDataChange"
|
|
111
|
+
/>
|
|
112
|
+
</template>
|
|
113
|
+
</form-pad>
|
|
114
|
+
</VCardText>
|
|
115
|
+
<VCardActions>
|
|
116
|
+
<slot name="action" :operation="operation">
|
|
117
|
+
<VSpacer />
|
|
118
|
+
<VBtn
|
|
119
|
+
color="primary"
|
|
120
|
+
variant="flat"
|
|
121
|
+
:loading="isSaving"
|
|
122
|
+
:disabled="!isDataChange"
|
|
123
|
+
@click="save"
|
|
124
|
+
v-if="!readonly"
|
|
125
|
+
>
|
|
126
|
+
{{ saveCaption }}
|
|
127
|
+
</VBtn>
|
|
128
|
+
<VBtn
|
|
129
|
+
color="error"
|
|
130
|
+
variant="flat"
|
|
131
|
+
:disabled="isSaving"
|
|
132
|
+
@click="cancel"
|
|
133
|
+
>
|
|
134
|
+
{{ cancelCaption }}
|
|
135
|
+
</VBtn>
|
|
136
|
+
</slot>
|
|
137
|
+
</VCardActions>
|
|
138
|
+
</VCard>
|
|
133
139
|
</template>
|
|
@@ -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>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import {computed, ref, watchEffect} from 'vue'
|
|
3
|
+
import {cloneDeep, isEqual} from 'lodash-es'
|
|
4
|
+
import type {FormDialogCallback} from '../../types/formDialog'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
title?: string
|
|
8
|
+
initialData?: object
|
|
9
|
+
formData?: object
|
|
10
|
+
saveCaption?: string
|
|
11
|
+
cancelCaption?: string
|
|
12
|
+
readonly?: boolean
|
|
13
|
+
showTitle?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
17
|
+
saveCaption: 'บันทึก',
|
|
18
|
+
cancelCaption: 'ยกเลิก',
|
|
19
|
+
readonly: false,
|
|
20
|
+
showTitle: false,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const isSaving = ref<boolean>(false)
|
|
24
|
+
const formPadRef = ref()
|
|
25
|
+
const formData = ref<object>({})
|
|
26
|
+
const formDataOriginalValue = ref<object>()
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits(['create', 'update'])
|
|
29
|
+
|
|
30
|
+
function save() {
|
|
31
|
+
if (formPadRef.value.isValid) {
|
|
32
|
+
isSaving.value = true
|
|
33
|
+
emit((isCreating.value) ? 'create' : 'update', cloneDeep(formData.value), callback)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cancel() {
|
|
38
|
+
reset()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function reset() {
|
|
42
|
+
formDataOriginalValue.value = undefined
|
|
43
|
+
formPadRef.value.reset()
|
|
44
|
+
loadFormData()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const callback: FormDialogCallback = {
|
|
48
|
+
done: function () {
|
|
49
|
+
isSaving.value = false
|
|
50
|
+
},
|
|
51
|
+
error: function () {
|
|
52
|
+
isSaving.value = false
|
|
53
|
+
},
|
|
54
|
+
setData: function (item: object) {
|
|
55
|
+
formData.value = cloneDeep(item)
|
|
56
|
+
formDataOriginalValue.value = cloneDeep(item)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isDataChange = computed(() => {
|
|
61
|
+
return !((isCreating.value) ? isEqual(formData.value, createOriginalValue.value) : isEqual(formData.value, formDataOriginalValue.value))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const isCreating = computed(() => {
|
|
65
|
+
return !props.formData
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const createOriginalValue = computed(() => {
|
|
69
|
+
return Object.assign({}, props.initialData)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const loadFormData = () => {
|
|
73
|
+
if (props.formData) {
|
|
74
|
+
formData.value = cloneDeep(props.formData)
|
|
75
|
+
formDataOriginalValue.value = cloneDeep(props.formData)
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
formData.value = Object.assign({}, props.initialData)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const operation = ref({ isDataChange, isCreating, isSaving, save, cancel })
|
|
83
|
+
|
|
84
|
+
watchEffect(loadFormData)
|
|
85
|
+
|
|
86
|
+
defineExpose({operation})
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<VCard flat>
|
|
91
|
+
<VToolbar v-if="showTitle">
|
|
92
|
+
<slot name="titleToolbar" :operation="operation">
|
|
93
|
+
<VToolbarTitle>
|
|
94
|
+
<slot name="title" :operation="operation">
|
|
95
|
+
{{ (isCreating) ? "New" : "Edit" }} {{ title }}
|
|
96
|
+
</slot>
|
|
97
|
+
</VToolbarTitle>
|
|
98
|
+
</slot>
|
|
99
|
+
</VToolbar>
|
|
100
|
+
<VCardText>
|
|
101
|
+
<form-pad
|
|
102
|
+
ref="formPadRef"
|
|
103
|
+
v-model="formData"
|
|
104
|
+
:readonly="readonly"
|
|
105
|
+
isolated
|
|
106
|
+
>
|
|
107
|
+
<template #default="slotData">
|
|
108
|
+
<slot
|
|
109
|
+
v-bind="slotData"
|
|
110
|
+
:is-creating="isCreating"
|
|
111
|
+
:is-data-change="isDataChange"
|
|
112
|
+
/>
|
|
113
|
+
</template>
|
|
114
|
+
</form-pad>
|
|
115
|
+
</VCardText>
|
|
116
|
+
<VCardActions>
|
|
117
|
+
<slot name="action" :operation="operation">
|
|
118
|
+
<VSpacer />
|
|
119
|
+
<VBtn
|
|
120
|
+
color="primary"
|
|
121
|
+
variant="flat"
|
|
122
|
+
:loading="isSaving"
|
|
123
|
+
:disabled="!isDataChange"
|
|
124
|
+
@click="save"
|
|
125
|
+
v-if="!readonly"
|
|
126
|
+
>
|
|
127
|
+
{{ saveCaption }}
|
|
128
|
+
</VBtn>
|
|
129
|
+
<VBtn
|
|
130
|
+
color="error"
|
|
131
|
+
variant="flat"
|
|
132
|
+
:disabled="isSaving"
|
|
133
|
+
@click="cancel"
|
|
134
|
+
>
|
|
135
|
+
{{ cancelCaption }}
|
|
136
|
+
</VBtn>
|
|
137
|
+
</slot>
|
|
138
|
+
</VCardActions>
|
|
139
|
+
</VCard>
|
|
140
|
+
</template>
|
|
@@ -26,7 +26,6 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
|
|
|
26
26
|
inputPadOnly?: boolean
|
|
27
27
|
saveAndStay?: boolean
|
|
28
28
|
stringFields?: Array<string>
|
|
29
|
-
items?: Record<string, any>[]
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -85,11 +84,6 @@ watch(items, (newValue) => {
|
|
|
85
84
|
emit('update:modelValue', newValue)
|
|
86
85
|
}, { deep: true })
|
|
87
86
|
|
|
88
|
-
onMounted(()=>{
|
|
89
|
-
if (props.items){
|
|
90
|
-
items.value = props.items
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
87
|
|
|
94
88
|
function createItem(item: Record<string, any>, callback?: FormDialogCallback) {
|
|
95
89
|
if (items.value.length > 0) item[props.modelKey] = Math.max(...items.value.map(i => i[props.modelKey] || 0)) + 1
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {VDataTable} from 'vuetify/components/VDataTable'
|
|
3
|
+
import {computed, onMounted, ref, useAttrs, watch} from 'vue'
|
|
4
|
+
import {omit} from "lodash-es";
|
|
5
|
+
interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props']> {
|
|
6
|
+
title: string
|
|
7
|
+
modelValue?: Record<string, any>[]
|
|
8
|
+
modelKey?: string
|
|
9
|
+
toolbarColor?: string
|
|
10
|
+
items : Record<string, any>
|
|
11
|
+
}
|
|
12
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
+
modelKey: 'id',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits(['update:modelValue'])
|
|
17
|
+
const attrs = useAttrs()
|
|
18
|
+
const plainAttrs = computed(() => {
|
|
19
|
+
return omit(attrs, ['modelValue', 'onUpdate:modelValue'])
|
|
20
|
+
})
|
|
21
|
+
const modelValue = ref()
|
|
22
|
+
const setDataItems = ()=>{
|
|
23
|
+
modelValue.value = props.items
|
|
24
|
+
}
|
|
25
|
+
watch(() => props.modelValue, (newValue) => {
|
|
26
|
+
if (!Array.isArray(newValue) || !newValue.every(item => typeof item === 'object')) {
|
|
27
|
+
modelValue.value = []
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
let maxKey = 0
|
|
31
|
+
|
|
32
|
+
newValue.forEach((item) => {
|
|
33
|
+
if (!item.hasOwnProperty(props.modelKey)) {
|
|
34
|
+
maxKey = Math.max(maxKey, ...newValue.map(i => i[props.modelKey] || 0))
|
|
35
|
+
item[props.modelKey] = maxKey + 1
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
modelValue.value = newValue
|
|
40
|
+
}
|
|
41
|
+
}, { immediate: true })
|
|
42
|
+
|
|
43
|
+
watch(modelValue, (newValue) => {
|
|
44
|
+
emit('update:modelValue', newValue)
|
|
45
|
+
}, { deep: true })
|
|
46
|
+
|
|
47
|
+
onMounted(()=>{
|
|
48
|
+
setDataItems()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<form-table
|
|
56
|
+
v-bind="plainAttrs"
|
|
57
|
+
v-model="modelValue"
|
|
58
|
+
:title="props.title"
|
|
59
|
+
:toolbarColor="props.toolbarColor"
|
|
60
|
+
:model-key="props.modelKey"
|
|
61
|
+
:insertable="false"
|
|
62
|
+
:importable="false"
|
|
63
|
+
:exportable="false"
|
|
64
|
+
:searchable="false"
|
|
65
|
+
hide-default-footer>
|
|
66
|
+
<!-- @ts-ignore -->
|
|
67
|
+
<template
|
|
68
|
+
v-for="(_, name, index) in ($slots as {})"
|
|
69
|
+
:key="index"
|
|
70
|
+
#[name]="slotData"
|
|
71
|
+
>
|
|
72
|
+
<slot
|
|
73
|
+
:name="name"
|
|
74
|
+
v-bind="((slotData || {}) as object)"
|
|
75
|
+
/>
|
|
76
|
+
</template>
|
|
77
|
+
</form-table>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<style scoped>
|
|
81
|
+
|
|
82
|
+
</style>
|
|
@@ -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,17 +1,13 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { computed,
|
|
3
|
-
import { cloneDeep, isEqual } from 'lodash-es'
|
|
2
|
+
import { computed, useTemplateRef } from 'vue'
|
|
4
3
|
import { type GraphqlModelItemProps, useGraphqlModelItem } from '../../composables/graphqlModelItem'
|
|
4
|
+
import EditPad from '../form/EditPad.vue'
|
|
5
5
|
|
|
6
6
|
defineOptions({
|
|
7
7
|
inheritAttrs: false,
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
interface Props {
|
|
11
|
-
title?: string
|
|
12
|
-
initialData?: object
|
|
13
|
-
saveCaption?: string
|
|
14
|
-
cancelCaption?: string
|
|
10
|
+
interface Props extends /* @vue-ignore */ InstanceType<typeof EditPad['$props']> {
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
const props = withDefaults(defineProps<Props & GraphqlModelItemProps>(), {
|
|
@@ -25,92 +21,43 @@ const { item,
|
|
|
25
21
|
createItem, updateItem,
|
|
26
22
|
reload, isLoading } = useGraphqlModelItem(props)
|
|
27
23
|
|
|
28
|
-
const
|
|
29
|
-
const formData = ref<object>({})
|
|
24
|
+
const editPad = useTemplateRef<typeof EditPad>("editPadRef")
|
|
30
25
|
|
|
31
|
-
const canSave = computed(() => { return (canCreate.value &&
|
|
26
|
+
const canSave = computed<boolean>(() => { return ((canCreate.value && editPad.value?.operation?.isCreating?.value) || canUpdate.value) as boolean})
|
|
32
27
|
|
|
33
|
-
const
|
|
34
|
-
return
|
|
28
|
+
const operation : any = computed(()=>{
|
|
29
|
+
return Object.assign({},editPad.value?.operation,{canSave,reload,item})
|
|
35
30
|
})
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
return !item.value
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const createOriginalValue = computed(() => {
|
|
42
|
-
return Object.assign({}, props.initialData)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
const loadFormData = () => {
|
|
46
|
-
if (item.value) {
|
|
47
|
-
formData.value = cloneDeep(item.value)
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
formData.value = Object.assign({}, props.initialData)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
watchEffect(loadFormData)
|
|
55
|
-
|
|
56
|
-
function save() {
|
|
57
|
-
if (formPadRef.value.isValid) {
|
|
58
|
-
if (isCreating.value) createItem(formData.value)
|
|
59
|
-
else updateItem(formData.value)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function cancel() {
|
|
64
|
-
formPadRef.value.reset()
|
|
65
|
-
loadFormData()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const operation = ref({save, cancel, reload, item, canSave, isLoading,isDataChange,isCreating})
|
|
69
|
-
|
|
70
|
-
defineExpose({ save, cancel, reload, item, isLoading })
|
|
32
|
+
defineExpose({ operation })
|
|
71
33
|
</script>
|
|
72
34
|
|
|
73
35
|
<template>
|
|
74
|
-
<
|
|
75
|
-
<
|
|
36
|
+
<FormEditPad :form-data="item" ref="editPadRef" @create="createItem" @update="updateItem">
|
|
37
|
+
<template #titleToolbar="slotData">
|
|
76
38
|
<slot name="titleToolbar" :operation="operation">
|
|
77
39
|
<VToolbarTitle>
|
|
78
40
|
<slot name="title" :operation="operation">
|
|
79
41
|
{{ title }}
|
|
80
42
|
<v-icon
|
|
81
|
-
|
|
82
|
-
|
|
43
|
+
size="small"
|
|
44
|
+
@click="reload"
|
|
83
45
|
>
|
|
84
46
|
mdi mdi-refresh
|
|
85
47
|
</v-icon>
|
|
86
48
|
</slot>
|
|
87
49
|
</VToolbarTitle>
|
|
88
50
|
</slot>
|
|
89
|
-
</
|
|
90
|
-
<
|
|
91
|
-
<form-pad
|
|
92
|
-
ref="formPadRef"
|
|
93
|
-
v-model="formData"
|
|
94
|
-
isolated
|
|
95
|
-
>
|
|
96
|
-
<template #default="slotData">
|
|
97
|
-
<slot
|
|
98
|
-
v-bind="slotData"
|
|
99
|
-
:is-creating="isCreating"
|
|
100
|
-
:is-data-change="isDataChange"
|
|
101
|
-
/>
|
|
102
|
-
</template>
|
|
103
|
-
</form-pad>
|
|
104
|
-
</VCardText>
|
|
105
|
-
<VCardActions>
|
|
51
|
+
</template>
|
|
52
|
+
<template #action="slotData">
|
|
106
53
|
<slot name="action" :operation="operation">
|
|
107
54
|
<VSpacer />
|
|
108
55
|
<VBtn
|
|
109
56
|
color="primary"
|
|
110
57
|
variant="flat"
|
|
111
58
|
:loading="isLoading"
|
|
112
|
-
:disabled="!isDataChange || !canSave"
|
|
113
|
-
@click="save"
|
|
59
|
+
:disabled="!slotData.operation?.isDataChange || !canSave"
|
|
60
|
+
@click="slotData.operation?.save"
|
|
114
61
|
>
|
|
115
62
|
{{ saveCaption }}
|
|
116
63
|
</VBtn>
|
|
@@ -118,11 +65,18 @@ defineExpose({ save, cancel, reload, item, isLoading })
|
|
|
118
65
|
color="error"
|
|
119
66
|
variant="flat"
|
|
120
67
|
:disabled="isLoading"
|
|
121
|
-
@click="cancel"
|
|
68
|
+
@click="slotData.operation?.cancel"
|
|
122
69
|
>
|
|
123
70
|
{{ cancelCaption }}
|
|
124
71
|
</VBtn>
|
|
125
72
|
</slot>
|
|
126
|
-
</
|
|
127
|
-
|
|
73
|
+
</template>
|
|
74
|
+
<template #default="slotData">
|
|
75
|
+
<slot
|
|
76
|
+
v-bind="slotData"
|
|
77
|
+
:is-creating="isCreating"
|
|
78
|
+
:is-data-change="isDataChange"
|
|
79
|
+
/>
|
|
80
|
+
</template>
|
|
81
|
+
</FormEditPad>
|
|
128
82
|
</template>
|
|
@@ -4,8 +4,10 @@ import {VDataTable} from 'vuetify/components/VDataTable'
|
|
|
4
4
|
import {clone} from 'lodash-es'
|
|
5
5
|
import {useGraphqlModel} from '../../composables/graphqlModel'
|
|
6
6
|
import {useDialog} from "../../composables/dialog"
|
|
7
|
+
import {useUserPermission} from "../../composables/userPermission"
|
|
7
8
|
import type {GraphqlModelProps} from '../../composables/graphqlModel'
|
|
8
9
|
import type {FormDialogCallback} from "../../types/formDialog";
|
|
10
|
+
import {type AuthenticationState, useState} from "#imports";
|
|
9
11
|
|
|
10
12
|
defineOptions({
|
|
11
13
|
inheritAttrs: false,
|
|
@@ -24,6 +26,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
|
|
|
24
26
|
search?: string
|
|
25
27
|
saveAndStay?: boolean
|
|
26
28
|
stringFields?: Array<string>
|
|
29
|
+
onlyOwnerEdit?: boolean
|
|
30
|
+
onlyOwnerOverridePermission?: string | string[]
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
|
|
@@ -39,6 +43,7 @@ const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
|
|
|
39
43
|
modelBy: undefined,
|
|
40
44
|
fields: () => [],
|
|
41
45
|
stringFields: ()=>[],
|
|
46
|
+
onlyOwnerEdit: false,
|
|
42
47
|
})
|
|
43
48
|
|
|
44
49
|
const attrs = useAttrs()
|
|
@@ -48,8 +53,15 @@ const plainAttrs = computed(() => {
|
|
|
48
53
|
return returnAttrs
|
|
49
54
|
})
|
|
50
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
|
+
|
|
51
62
|
const currentItem = ref<Record<string, any> | undefined>(undefined)
|
|
52
63
|
const isDialogOpen = ref<boolean>(false)
|
|
64
|
+
const isDialogReadonly = ref<boolean>(false)
|
|
53
65
|
|
|
54
66
|
const { items, itemsLength,
|
|
55
67
|
search,setSearch,
|
|
@@ -59,6 +71,15 @@ const { items, itemsLength,
|
|
|
59
71
|
isLoading } = useGraphqlModel(props)
|
|
60
72
|
|
|
61
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
|
|
62
83
|
currentItem.value = item
|
|
63
84
|
nextTick(() => {
|
|
64
85
|
isDialogOpen.value = true
|
|
@@ -70,7 +91,16 @@ async function confirmDeleteItem(item: Record<string, any>, callback?: FormDialo
|
|
|
70
91
|
if (confirm) deleteItem(item,callback)
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
const
|
|
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 })
|
|
74
104
|
|
|
75
105
|
const computedInitialData = computed(() => {
|
|
76
106
|
return Object.assign({}, props.initialData, props.modelBy)
|
|
@@ -188,14 +218,21 @@ defineExpose({ reload,operation })
|
|
|
188
218
|
#item.action="{ item }"
|
|
189
219
|
>
|
|
190
220
|
<v-btn
|
|
191
|
-
|
|
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)"
|
|
192
229
|
variant="flat"
|
|
193
230
|
density="compact"
|
|
194
231
|
icon="mdi mdi-note-edit"
|
|
195
232
|
@click="openDialog(item)"
|
|
196
233
|
/>
|
|
197
234
|
<v-btn
|
|
198
|
-
v-if="canDelete"
|
|
235
|
+
v-if="canDelete && canEditRow(item)"
|
|
199
236
|
variant="flat"
|
|
200
237
|
density="compact"
|
|
201
238
|
icon="mdi mdi-delete"
|
|
@@ -253,22 +290,29 @@ defineExpose({ reload,operation })
|
|
|
253
290
|
@create="createItem"
|
|
254
291
|
@update="updateItem"
|
|
255
292
|
:saveAndStay="saveAndStay"
|
|
293
|
+
:readonly="isDialogReadonly"
|
|
256
294
|
>
|
|
257
295
|
<template #default="slotData">
|
|
258
296
|
<slot
|
|
259
297
|
name="form"
|
|
298
|
+
:tableItems="items"
|
|
299
|
+
:tableOperation="operation"
|
|
260
300
|
v-bind="slotData"
|
|
261
301
|
/>
|
|
262
302
|
</template>
|
|
263
303
|
<template #title="slotData">
|
|
264
304
|
<slot
|
|
265
305
|
name="formTitle"
|
|
306
|
+
:tableItems="items"
|
|
307
|
+
:tableOperation="operation"
|
|
266
308
|
v-bind="slotData"
|
|
267
309
|
/>
|
|
268
310
|
</template>
|
|
269
311
|
<template #action="slotData">
|
|
270
312
|
<slot
|
|
271
313
|
name="formAction"
|
|
314
|
+
:tableItems="items"
|
|
315
|
+
:tableOperation="operation"
|
|
272
316
|
v-bind="slotData"
|
|
273
317
|
/>
|
|
274
318
|
</template>
|
|
@@ -37,7 +37,7 @@ export function processTemplateFormTableData(item, parentTemplates) {
|
|
|
37
37
|
</template>`;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
item.inputType = "
|
|
40
|
+
item.inputType = "FormTableData";
|
|
41
41
|
return processDefaultTemplate(item, tableTemplate, `title="${tableOptions.title}"
|
|
42
42
|
:headers='${escapeObjectForInlineBinding(tableHeader)}'
|
|
43
43
|
:items='${escapeObjectForInlineBinding(tableItems)}'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useUserPermission: () => unknown;
|
|
@@ -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,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
|
+
}
|