@ramathibodi/nuxt-commons 0.1.49 → 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 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.50",
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`),
@@ -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>
@@ -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"
@@ -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>
@@ -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.50",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",