@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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.1.49",
7
+ "version": "0.1.51",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
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
- createCaption?: string
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
- createCaption: 'Add',
17
- updateCaption: 'Save',
18
- cancelCaption: 'Cancel',
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
- formOriginalData.value = undefined
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, formOriginalData.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 !formOriginalData.value
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 (formOriginalData.value) {
69
- formData.value = cloneDeep(formOriginalData.value)
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
- formOriginalData.value = originalData
82
+ formDataOriginalValue.value = originalData
80
83
  }
81
84
 
82
- defineExpose({setOriginalData,reset})
85
+ defineExpose({setOriginalData,operation})
83
86
  </script>
84
87
 
85
88
  <template>
86
- <VCard flat>
87
- <VToolbar v-if="showTitle">
88
- <VToolbarTitle>
89
- <slot name="title">
90
- {{ (isCreating) ? "New" : "Edit" }} {{ title }}
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
- </VToolbarTitle>
93
- <VSpacer />
94
- </VToolbar>
95
- <VCardText>
96
- <form-pad
97
- ref="formPadRef"
98
- v-model="formData"
99
- isolated
100
- >
101
- <template #default="slotData">
102
- <slot
103
- v-bind="slotData"
104
- :is-creating="isCreating"
105
- :is-data-change="isDataChange"
106
- />
107
- </template>
108
- </form-pad>
109
- </VCardText>
110
- <VCardActions>
111
- <slot name="action" :save="save" :cancel="cancel">
112
- <VSpacer />
113
- <VBtn
114
- color="primary"
115
- variant="flat"
116
- :loading="isSaving"
117
- :disabled="!isDataChange"
118
- @click="save"
119
- >
120
- {{ (isCreating) ? createCaption : updateCaption }}
121
- </VBtn>
122
- <VBtn
123
- color="error"
124
- variant="flat"
125
- :disabled="isSaving"
126
- @click="cancel"
98
+ </VToolbar>
99
+ <VCardText>
100
+ <form-pad
101
+ ref="formPadRef"
102
+ v-model="formData"
103
+ :readonly="readonly"
104
+ isolated
127
105
  >
128
- {{ cancelCaption }}
129
- </VBtn>
130
- </slot>
131
- </VCardActions>
132
- </VCard>
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="(showCode ? item.raw.itemCode+'-' : '')+(item.title || item.raw.itemValue || item.raw.itemCode)"
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
- if (props.fuzzy && !isEmpty(searchData.value)) {
101
- return items.value
102
- } else {
103
- return sortBy(items.value, sortByField)
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
- v-if="!$slots.item"
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, ref, watchEffect } from 'vue'
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 formPadRef = ref()
29
- const formData = ref<object>({})
24
+ const editPad = useTemplateRef<typeof EditPad>("editPadRef")
30
25
 
31
- const canSave = computed(() => { return (canCreate.value && isCreating.value) || canUpdate.value })
26
+ const canSave = computed<boolean>(() => { return ((canCreate.value && editPad.value?.operation?.isCreating?.value) || canUpdate.value) as boolean})
32
27
 
33
- const isDataChange = computed(() => {
34
- return !((isCreating.value) ? isEqual(formData.value, createOriginalValue.value) : isEqual(formData.value, item.value))
28
+ const operation : any = computed(()=>{
29
+ return Object.assign({},editPad.value?.operation,{canSave,reload,item})
35
30
  })
36
31
 
37
- const isCreating = computed(() => {
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
- <VCard>
75
- <VToolbar>
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
- size="small"
82
- @click="reload"
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
- </VToolbar>
90
- <VCardText>
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
- </VCardActions>
127
- </VCard>
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 operation = ref({ openDialog, createItem, importItems, updateItem, deleteItem, reload, setSearch, canServerPageable, canServerSearch, canCreate, canUpdate, canDelete })
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
- v-if="canUpdate"
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 = "FormTable";
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;
@@ -0,0 +1,5 @@
1
+ import { useNuxtApp } from "#app";
2
+ export const useUserPermission = () => {
3
+ const { $permission } = useNuxtApp();
4
+ return $permission;
5
+ };
@@ -1,21 +1,42 @@
1
1
  import { defineNuxtPlugin } from "nuxt/app";
2
2
  import { useAuthentication } from "#imports";
3
- export default defineNuxtPlugin(async (nuxtApp) => {
4
- function permission(permissionId) {
5
- if (!permissionId || useAuthentication()?.hasPermission(permissionId) === void 0) return true;
6
- return useAuthentication()?.hasPermission(permissionId);
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 (!permission(binding.value)) {
11
- if (vnode) {
12
- if (vnode.um?.length) {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",