@ramathibodi/nuxt-commons 0.1.74 → 0.1.75
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/README.md +115 -115
- package/dist/module.json +1 -1
- package/dist/runtime/components/Alert.vue +58 -58
- package/dist/runtime/components/BarcodeReader.vue +130 -130
- package/dist/runtime/components/ExportCSV.vue +110 -110
- package/dist/runtime/components/FileBtn.vue +79 -79
- package/dist/runtime/components/ImportCSV.vue +151 -151
- package/dist/runtime/components/MrzReader.vue +168 -168
- package/dist/runtime/components/SplitterPanel.vue +67 -67
- package/dist/runtime/components/TabsGroup.vue +39 -39
- package/dist/runtime/components/TextBarcode.vue +66 -66
- package/dist/runtime/components/device/IdCardButton.vue +95 -95
- package/dist/runtime/components/device/IdCardWebSocket.vue +207 -207
- package/dist/runtime/components/device/Scanner.vue +350 -350
- package/dist/runtime/components/dialog/Confirm.vue +112 -112
- package/dist/runtime/components/dialog/Host.vue +88 -88
- package/dist/runtime/components/dialog/Index.vue +84 -84
- package/dist/runtime/components/dialog/Loading.vue +51 -51
- package/dist/runtime/components/dialog/default/Confirm.vue +112 -112
- package/dist/runtime/components/dialog/default/Loading.vue +60 -60
- package/dist/runtime/components/dialog/default/Notify.vue +82 -82
- package/dist/runtime/components/dialog/default/Printing.vue +46 -46
- package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -144
- package/dist/runtime/components/document/Form.vue +50 -50
- package/dist/runtime/components/document/TemplateBuilder.vue +536 -536
- package/dist/runtime/components/form/ActionPad.vue +156 -156
- package/dist/runtime/components/form/Birthdate.vue +116 -116
- package/dist/runtime/components/form/CheckboxGroup.vue +99 -99
- package/dist/runtime/components/form/CodeEditor.vue +45 -45
- package/dist/runtime/components/form/Date.vue +270 -270
- package/dist/runtime/components/form/DateTime.vue +220 -220
- package/dist/runtime/components/form/Dialog.vue +178 -178
- package/dist/runtime/components/form/EditPad.vue +157 -157
- package/dist/runtime/components/form/File.vue +295 -295
- package/dist/runtime/components/form/Hidden.vue +44 -44
- package/dist/runtime/components/form/Iterator.vue +538 -538
- package/dist/runtime/components/form/Login.vue +143 -143
- package/dist/runtime/components/form/Pad.vue +399 -399
- package/dist/runtime/components/form/SignPad.vue +226 -226
- package/dist/runtime/components/form/System.vue +34 -34
- package/dist/runtime/components/form/Table.vue +391 -391
- package/dist/runtime/components/form/TableData.vue +236 -236
- package/dist/runtime/components/form/Time.vue +177 -177
- package/dist/runtime/components/form/images/Capture.vue +245 -245
- package/dist/runtime/components/form/images/Edit.vue +133 -133
- package/dist/runtime/components/form/images/Field.vue +331 -331
- package/dist/runtime/components/form/images/Pad.vue +54 -54
- package/dist/runtime/components/label/Date.vue +37 -37
- package/dist/runtime/components/label/DateAgo.vue +102 -102
- package/dist/runtime/components/label/DateCount.vue +152 -152
- package/dist/runtime/components/label/Field.vue +111 -111
- package/dist/runtime/components/label/FormatMoney.vue +37 -37
- package/dist/runtime/components/label/Mask.vue +46 -46
- package/dist/runtime/components/label/Object.vue +21 -21
- package/dist/runtime/components/master/Autocomplete.vue +89 -89
- package/dist/runtime/components/master/Combobox.vue +88 -88
- package/dist/runtime/components/master/RadioGroup.vue +90 -90
- package/dist/runtime/components/master/Select.vue +70 -70
- package/dist/runtime/components/master/label.vue +55 -55
- package/dist/runtime/components/model/Autocomplete.vue +91 -91
- package/dist/runtime/components/model/Combobox.vue +90 -90
- package/dist/runtime/components/model/Pad.vue +114 -114
- package/dist/runtime/components/model/Select.vue +78 -84
- package/dist/runtime/components/model/Table.vue +370 -370
- package/dist/runtime/components/model/iterator.vue +497 -497
- package/dist/runtime/components/model/label.vue +58 -58
- package/dist/runtime/components/pdf/Print.vue +75 -75
- package/dist/runtime/components/pdf/View.vue +146 -146
- package/dist/runtime/composables/dialog.d.ts +1 -1
- package/dist/runtime/composables/graphql.d.ts +1 -1
- package/dist/runtime/composables/graphqlModel.d.ts +9 -9
- package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
- package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
- package/dist/runtime/composables/userPermission.d.ts +1 -1
- package/dist/runtime/labs/Calendar.vue +99 -99
- package/dist/runtime/labs/form/EditMobile.vue +152 -152
- package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
- package/dist/runtime/plugins/clientConfig.d.ts +1 -1
- package/dist/runtime/plugins/default.d.ts +1 -1
- package/dist/runtime/plugins/dialogManager.d.ts +1 -1
- package/dist/runtime/plugins/permission.d.ts +1 -1
- package/dist/runtime/types/alert.d.ts +11 -11
- package/dist/runtime/types/clientConfig.d.ts +13 -13
- package/dist/runtime/types/dialogManager.d.ts +35 -35
- package/dist/runtime/types/formDialog.d.ts +5 -5
- package/dist/runtime/types/graphqlOperation.d.ts +23 -23
- package/dist/runtime/types/menu.d.ts +31 -31
- package/dist/runtime/types/modules.d.ts +7 -7
- package/dist/runtime/types/permission.d.ts +13 -13
- package/package.json +131 -131
- package/scripts/enrich-vue-docs-from-ai.mjs +197 -197
- package/scripts/generate-ai-summary.mjs +321 -321
- package/scripts/generate-composables-md.mjs +129 -129
- package/scripts/postInstall.cjs +70 -70
- package/templates/.codegen/codegen.ts +32 -32
- package/templates/.codegen/plugin-schema-object.js +161 -161
|
@@ -1,370 +1,370 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
/**
|
|
3
|
-
* ModelTable connects model metadata to reusable selection, labeling, iterator, or table UI patterns.
|
|
4
|
-
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
5
|
-
*/
|
|
6
|
-
import {computed,watch, nextTick, ref, useAttrs} from 'vue'
|
|
7
|
-
import {VDataTable} from 'vuetify/components/VDataTable'
|
|
8
|
-
import {clone} from 'lodash-es'
|
|
9
|
-
import {useGraphqlModel} from '../../composables/graphqlModel'
|
|
10
|
-
import {useDialog} from "../../composables/dialog"
|
|
11
|
-
import {useUserPermission} from "../../composables/userPermission"
|
|
12
|
-
import type {GraphqlModelProps} from '../../composables/graphqlModel'
|
|
13
|
-
import type {FormDialogCallback} from "../../types/formDialog";
|
|
14
|
-
import {type AuthenticationState, useState} from "#imports";
|
|
15
|
-
|
|
16
|
-
defineOptions({
|
|
17
|
-
inheritAttrs: false,
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props']> {
|
|
21
|
-
title: string // Toolbar title and default export filename for this model table.
|
|
22
|
-
noDataText?: string // Message shown when query returns no rows.
|
|
23
|
-
dialogWidth?: string | number // Width passed to create/edit model dialog.
|
|
24
|
-
dialogMaxWidth?: string | number // Max-width cap for model dialog on large displays.
|
|
25
|
-
dialogHeight?: string | number // Height passed to create/edit model dialog.
|
|
26
|
-
dialogMaxHeight?: string | number // Max-height cap for model dialog before internal scrolling.
|
|
27
|
-
dialogFullscreen?: boolean // Opens model dialog fullscreen by default.
|
|
28
|
-
initialData?: Record<string, any> // Base values merged into create payload before dialog submission.
|
|
29
|
-
toolbarColor?: string // Color used by toolbar and built-in action buttons.
|
|
30
|
-
importable?: boolean // Enables CSV/XLS import action (requires create permission).
|
|
31
|
-
exportable?: boolean // Enables export action for currently loaded model items.
|
|
32
|
-
insertable?: boolean // Shows Add button and allows create flow.
|
|
33
|
-
searchable?: boolean // Shows search input and forwards keyword into GraphQL model search state.
|
|
34
|
-
search?: string // External search keyword; watcher keeps internal model search in sync.
|
|
35
|
-
saveAndStay?: boolean // Forwards to dialog form to keep modal open after successful save.
|
|
36
|
-
stringFields?: Array<string> // Field paths that must remain plain strings during import/export transforms.
|
|
37
|
-
onlyOwnerEdit?: boolean // When true, edit/delete are allowed only for rows created by current user.
|
|
38
|
-
onlyOwnerOverridePermission?: string | string[] // Permission key(s) that bypass `onlyOwnerEdit` restriction.
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Public props accepted by ModelTable.
|
|
43
|
-
* Document each prop field with intent, defaults, and side effects for clear generated docs.
|
|
44
|
-
*/
|
|
45
|
-
const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
|
|
46
|
-
noDataText: 'ไม่พบข้อมูล',
|
|
47
|
-
dialogFullscreen: false,
|
|
48
|
-
toolbarColor: 'primary',
|
|
49
|
-
importable: true,
|
|
50
|
-
exportable: true,
|
|
51
|
-
insertable: true,
|
|
52
|
-
searchable: true,
|
|
53
|
-
saveAndStay: false,
|
|
54
|
-
modelKey: 'id',
|
|
55
|
-
modelBy: undefined,
|
|
56
|
-
fields: () => [],
|
|
57
|
-
stringFields: ()=>[],
|
|
58
|
-
onlyOwnerEdit: false,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Custom events emitted by ModelTable.
|
|
63
|
-
* Parents can listen to these events to react to user actions and internal state changes.
|
|
64
|
-
*/
|
|
65
|
-
const emit = defineEmits(['open:dialog','close:dialog','create','update','delete'])
|
|
66
|
-
const attrs = useAttrs()
|
|
67
|
-
const plainAttrs = computed(() => {
|
|
68
|
-
const returnAttrs = clone(attrs)
|
|
69
|
-
if (props.headers) returnAttrs['headers'] = props.headers
|
|
70
|
-
return returnAttrs
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const authenState = useState<AuthenticationState>("authentication")
|
|
74
|
-
const currentUsername = computed(()=>{
|
|
75
|
-
let userProfile = authenState.value?.userProfile as {username: string}
|
|
76
|
-
return userProfile?.username ?? ""
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const currentItem = ref<Record<string, any> | undefined>(undefined)
|
|
80
|
-
const isDialogOpen = ref<boolean>(false)
|
|
81
|
-
const isDialogReadonly = ref<boolean>(false)
|
|
82
|
-
|
|
83
|
-
const { items, itemsLength,
|
|
84
|
-
search,setSearch,
|
|
85
|
-
canServerPageable, canServerSearch, canCreate, canUpdate, canDelete,
|
|
86
|
-
createItem, importItems, updateItem, deleteItem,
|
|
87
|
-
loadItems, reload,
|
|
88
|
-
isLoading } = useGraphqlModel(props)
|
|
89
|
-
|
|
90
|
-
function openDialog(item?: object) {
|
|
91
|
-
isDialogReadonly.value = false
|
|
92
|
-
currentItem.value = item
|
|
93
|
-
nextTick(() => {
|
|
94
|
-
isDialogOpen.value = true
|
|
95
|
-
emit('open:dialog' , item)
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function openDialogReadonly(item?: object) {
|
|
100
|
-
isDialogReadonly.value = true
|
|
101
|
-
currentItem.value = item
|
|
102
|
-
nextTick(() => {
|
|
103
|
-
isDialogOpen.value = true
|
|
104
|
-
emit('open:dialog' , item)
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function confirmDeleteItem(item: Record<string, any>, callback?: FormDialogCallback) {
|
|
109
|
-
let confirm = await useDialog().confirm({message: "Do you want to delete record?"})
|
|
110
|
-
if (confirm) onDeleteItem(item,callback)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const onCreateItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
|
|
114
|
-
createItem(item,callback).then(()=>{
|
|
115
|
-
emit("create",item,callback)
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const onUpdateItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
|
|
120
|
-
updateItem(item,callback).then(()=>{
|
|
121
|
-
emit("update",item,callback)
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const onDeleteItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
|
|
126
|
-
deleteItem(item,callback).then(()=>{
|
|
127
|
-
emit("delete",item,callback)
|
|
128
|
-
})
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const canEditRow = function (item: Record<string, any>) {
|
|
132
|
-
if (props.onlyOwnerEdit) {
|
|
133
|
-
if (item?.userstampField?.createdBy && item?.userstampField?.createdBy==currentUsername.value) return true
|
|
134
|
-
if (!!props.onlyOwnerOverridePermission && useUserPermission().check(props.onlyOwnerOverridePermission)) return true
|
|
135
|
-
return !item?.userstampField?.createdBy
|
|
136
|
-
}
|
|
137
|
-
return true
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const operation = ref({ openDialog, openDialogReadonly, createItem, importItems, updateItem, deleteItem, reload, setSearch, canServerPageable, canServerSearch, canCreate, canUpdate, canDelete, canEditRow, onlyOwnerEdit: props.onlyOwnerEdit, onlyOwnerOverridePermission: props.onlyOwnerOverridePermission })
|
|
141
|
-
|
|
142
|
-
const computedInitialData = computed(() => {
|
|
143
|
-
return Object.assign({}, props.initialData, props.modelBy)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
watch(()=>props.search,()=>{
|
|
147
|
-
search.value = props.search
|
|
148
|
-
},{immediate:true})
|
|
149
|
-
|
|
150
|
-
defineExpose({ reload,operation,items })
|
|
151
|
-
</script>
|
|
152
|
-
|
|
153
|
-
<template>
|
|
154
|
-
<v-card>
|
|
155
|
-
<slot
|
|
156
|
-
name="header"
|
|
157
|
-
:items="items"
|
|
158
|
-
:operation="operation"
|
|
159
|
-
>
|
|
160
|
-
<VToolbar :color="toolbarColor">
|
|
161
|
-
<v-row
|
|
162
|
-
justify="end"
|
|
163
|
-
class="ma-1"
|
|
164
|
-
dense
|
|
165
|
-
no-gutters
|
|
166
|
-
align="center"
|
|
167
|
-
>
|
|
168
|
-
<v-col cols="7">
|
|
169
|
-
<VToolbarTitle class="pl-3">
|
|
170
|
-
<slot
|
|
171
|
-
name="title"
|
|
172
|
-
:reload="reload"
|
|
173
|
-
>
|
|
174
|
-
{{ title }}
|
|
175
|
-
<v-icon
|
|
176
|
-
size="small"
|
|
177
|
-
@click="reload"
|
|
178
|
-
>
|
|
179
|
-
mdi mdi-refresh
|
|
180
|
-
</v-icon>
|
|
181
|
-
</slot>
|
|
182
|
-
</VToolbarTitle>
|
|
183
|
-
</v-col>
|
|
184
|
-
<v-col cols="5">
|
|
185
|
-
<slot name="search" :items="items" :operation="operation" v-if="props.searchable">
|
|
186
|
-
<VTextField
|
|
187
|
-
v-model="search"
|
|
188
|
-
class="justify-end w-100"
|
|
189
|
-
density="compact"
|
|
190
|
-
hide-details
|
|
191
|
-
placeholder="ค้นหา"
|
|
192
|
-
clearable
|
|
193
|
-
variant="solo"
|
|
194
|
-
/>
|
|
195
|
-
</slot>
|
|
196
|
-
</v-col>
|
|
197
|
-
</v-row>
|
|
198
|
-
|
|
199
|
-
<VToolbarItems>
|
|
200
|
-
<slot name="toolbarItems" :items="items" :operation="operation"/>
|
|
201
|
-
<ImportCSV
|
|
202
|
-
v-if="props.importable && canCreate && props.insertable"
|
|
203
|
-
icon="mdi mdi-file-upload"
|
|
204
|
-
variant="flat"
|
|
205
|
-
:color="toolbarColor"
|
|
206
|
-
:stringFields="props.stringFields"
|
|
207
|
-
@import="importItems"
|
|
208
|
-
/>
|
|
209
|
-
<ExportCSV
|
|
210
|
-
v-if="props.exportable && items.length"
|
|
211
|
-
icon="mdi mdi-file-download"
|
|
212
|
-
variant="flat"
|
|
213
|
-
:file-name="title"
|
|
214
|
-
:model-value="items"
|
|
215
|
-
:color="toolbarColor"
|
|
216
|
-
:stringFields="props.stringFields"
|
|
217
|
-
/>
|
|
218
|
-
<VBtn
|
|
219
|
-
v-if="canCreate && props.insertable"
|
|
220
|
-
:color="toolbarColor"
|
|
221
|
-
prepend-icon="mdi mdi-plus"
|
|
222
|
-
variant="flat"
|
|
223
|
-
@click="openDialog()"
|
|
224
|
-
>
|
|
225
|
-
add
|
|
226
|
-
</VBtn>
|
|
227
|
-
</VToolbarItems>
|
|
228
|
-
</VToolbar>
|
|
229
|
-
</slot>
|
|
230
|
-
<v-data-table-server
|
|
231
|
-
v-if="canServerPageable"
|
|
232
|
-
v-bind="plainAttrs"
|
|
233
|
-
color="primary"
|
|
234
|
-
:items="items"
|
|
235
|
-
:items-length="itemsLength"
|
|
236
|
-
:item-value="props.modelKey"
|
|
237
|
-
:search="search"
|
|
238
|
-
:loading="isLoading"
|
|
239
|
-
@update:options="loadItems"
|
|
240
|
-
>
|
|
241
|
-
<!-- @ts-ignore -->
|
|
242
|
-
<template
|
|
243
|
-
v-for="(_, name, index) in ($slots as {})"
|
|
244
|
-
:key="index"
|
|
245
|
-
#[name]="slotData"
|
|
246
|
-
>
|
|
247
|
-
<slot
|
|
248
|
-
:name="name"
|
|
249
|
-
v-bind="((slotData || {}) as object)"
|
|
250
|
-
:operation="operation"
|
|
251
|
-
/>
|
|
252
|
-
</template>
|
|
253
|
-
<template
|
|
254
|
-
v-if="!$slots['item.action']"
|
|
255
|
-
#item.action="{ item }"
|
|
256
|
-
>
|
|
257
|
-
<v-btn
|
|
258
|
-
v-if="!canUpdate || !canEditRow(item)"
|
|
259
|
-
variant="flat"
|
|
260
|
-
density="compact"
|
|
261
|
-
icon="mdi mdi-note-search"
|
|
262
|
-
@click="openDialogReadonly(item)"
|
|
263
|
-
/>
|
|
264
|
-
<v-btn
|
|
265
|
-
v-if="canUpdate && canEditRow(item)"
|
|
266
|
-
variant="flat"
|
|
267
|
-
density="compact"
|
|
268
|
-
icon="mdi mdi-note-edit"
|
|
269
|
-
@click="openDialog(item)"
|
|
270
|
-
/>
|
|
271
|
-
<v-btn
|
|
272
|
-
v-if="canDelete && canEditRow(item)"
|
|
273
|
-
variant="flat"
|
|
274
|
-
density="compact"
|
|
275
|
-
icon="mdi mdi-delete"
|
|
276
|
-
@click="confirmDeleteItem(item)"
|
|
277
|
-
/>
|
|
278
|
-
</template>
|
|
279
|
-
</v-data-table-server>
|
|
280
|
-
<v-data-table
|
|
281
|
-
v-else
|
|
282
|
-
v-bind="plainAttrs"
|
|
283
|
-
color="primary"
|
|
284
|
-
:items="items"
|
|
285
|
-
:item-value="props.modelKey"
|
|
286
|
-
:search="search"
|
|
287
|
-
:loading="isLoading"
|
|
288
|
-
>
|
|
289
|
-
<!-- @ts-ignore -->
|
|
290
|
-
<template
|
|
291
|
-
v-for="(_, name, index) in ($slots as {})"
|
|
292
|
-
:key="index"
|
|
293
|
-
#[name]="slotData"
|
|
294
|
-
>
|
|
295
|
-
<slot
|
|
296
|
-
:name="name"
|
|
297
|
-
v-bind="((slotData || {}) as object)"
|
|
298
|
-
:operation="operation"
|
|
299
|
-
/>
|
|
300
|
-
</template>
|
|
301
|
-
<template
|
|
302
|
-
v-if="!$slots['item.action']"
|
|
303
|
-
#item.action="{ item }"
|
|
304
|
-
>
|
|
305
|
-
<v-btn
|
|
306
|
-
v-if="!canUpdate || !canEditRow(item)"
|
|
307
|
-
variant="flat"
|
|
308
|
-
density="compact"
|
|
309
|
-
icon="mdi mdi-note-search"
|
|
310
|
-
@click="openDialogReadonly(item)"
|
|
311
|
-
/>
|
|
312
|
-
<v-btn
|
|
313
|
-
v-if="canUpdate && canEditRow(item)"
|
|
314
|
-
variant="flat"
|
|
315
|
-
density="compact"
|
|
316
|
-
icon="mdi mdi-note-edit"
|
|
317
|
-
@click="openDialog(item)"
|
|
318
|
-
/>
|
|
319
|
-
<v-btn
|
|
320
|
-
v-if="canDelete && canEditRow(item)"
|
|
321
|
-
variant="flat"
|
|
322
|
-
density="compact"
|
|
323
|
-
icon="mdi mdi-delete"
|
|
324
|
-
@click="confirmDeleteItem(item)"
|
|
325
|
-
/>
|
|
326
|
-
</template>
|
|
327
|
-
</v-data-table>
|
|
328
|
-
<FormDialog
|
|
329
|
-
v-model="isDialogOpen"
|
|
330
|
-
:title="title"
|
|
331
|
-
:fullscreen="dialogFullscreen"
|
|
332
|
-
:initial-data="computedInitialData"
|
|
333
|
-
:form-data="currentItem"
|
|
334
|
-
@create="onCreateItem"
|
|
335
|
-
@update="onUpdateItem"
|
|
336
|
-
@afterLeave="emit('close:dialog')"
|
|
337
|
-
:saveAndStay="saveAndStay"
|
|
338
|
-
:readonly="isDialogReadonly"
|
|
339
|
-
:width="dialogWidth"
|
|
340
|
-
:height="dialogHeight"
|
|
341
|
-
:max-width="dialogMaxWidth"
|
|
342
|
-
:max-height="dialogMaxHeight"
|
|
343
|
-
>
|
|
344
|
-
<template #default="slotData">
|
|
345
|
-
<slot
|
|
346
|
-
name="form"
|
|
347
|
-
:tableItems="items"
|
|
348
|
-
:tableOperation="operation"
|
|
349
|
-
v-bind="slotData"
|
|
350
|
-
/>
|
|
351
|
-
</template>
|
|
352
|
-
<template #title="slotData">
|
|
353
|
-
<slot
|
|
354
|
-
name="formTitle"
|
|
355
|
-
:tableItems="items"
|
|
356
|
-
:tableOperation="operation"
|
|
357
|
-
v-bind="slotData"
|
|
358
|
-
/>
|
|
359
|
-
</template>
|
|
360
|
-
<template #action="slotData">
|
|
361
|
-
<slot
|
|
362
|
-
name="formAction"
|
|
363
|
-
:tableItems="items"
|
|
364
|
-
:tableOperation="operation"
|
|
365
|
-
v-bind="slotData"
|
|
366
|
-
/>
|
|
367
|
-
</template>
|
|
368
|
-
</FormDialog>
|
|
369
|
-
</v-card>
|
|
370
|
-
</template>
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* ModelTable connects model metadata to reusable selection, labeling, iterator, or table UI patterns.
|
|
4
|
+
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
5
|
+
*/
|
|
6
|
+
import {computed,watch, nextTick, ref, useAttrs} from 'vue'
|
|
7
|
+
import {VDataTable} from 'vuetify/components/VDataTable'
|
|
8
|
+
import {clone} from 'lodash-es'
|
|
9
|
+
import {useGraphqlModel} from '../../composables/graphqlModel'
|
|
10
|
+
import {useDialog} from "../../composables/dialog"
|
|
11
|
+
import {useUserPermission} from "../../composables/userPermission"
|
|
12
|
+
import type {GraphqlModelProps} from '../../composables/graphqlModel'
|
|
13
|
+
import type {FormDialogCallback} from "../../types/formDialog";
|
|
14
|
+
import {type AuthenticationState, useState} from "#imports";
|
|
15
|
+
|
|
16
|
+
defineOptions({
|
|
17
|
+
inheritAttrs: false,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props']> {
|
|
21
|
+
title: string // Toolbar title and default export filename for this model table.
|
|
22
|
+
noDataText?: string // Message shown when query returns no rows.
|
|
23
|
+
dialogWidth?: string | number // Width passed to create/edit model dialog.
|
|
24
|
+
dialogMaxWidth?: string | number // Max-width cap for model dialog on large displays.
|
|
25
|
+
dialogHeight?: string | number // Height passed to create/edit model dialog.
|
|
26
|
+
dialogMaxHeight?: string | number // Max-height cap for model dialog before internal scrolling.
|
|
27
|
+
dialogFullscreen?: boolean // Opens model dialog fullscreen by default.
|
|
28
|
+
initialData?: Record<string, any> // Base values merged into create payload before dialog submission.
|
|
29
|
+
toolbarColor?: string // Color used by toolbar and built-in action buttons.
|
|
30
|
+
importable?: boolean // Enables CSV/XLS import action (requires create permission).
|
|
31
|
+
exportable?: boolean // Enables export action for currently loaded model items.
|
|
32
|
+
insertable?: boolean // Shows Add button and allows create flow.
|
|
33
|
+
searchable?: boolean // Shows search input and forwards keyword into GraphQL model search state.
|
|
34
|
+
search?: string // External search keyword; watcher keeps internal model search in sync.
|
|
35
|
+
saveAndStay?: boolean // Forwards to dialog form to keep modal open after successful save.
|
|
36
|
+
stringFields?: Array<string> // Field paths that must remain plain strings during import/export transforms.
|
|
37
|
+
onlyOwnerEdit?: boolean // When true, edit/delete are allowed only for rows created by current user.
|
|
38
|
+
onlyOwnerOverridePermission?: string | string[] // Permission key(s) that bypass `onlyOwnerEdit` restriction.
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Public props accepted by ModelTable.
|
|
43
|
+
* Document each prop field with intent, defaults, and side effects for clear generated docs.
|
|
44
|
+
*/
|
|
45
|
+
const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
|
|
46
|
+
noDataText: 'ไม่พบข้อมูล',
|
|
47
|
+
dialogFullscreen: false,
|
|
48
|
+
toolbarColor: 'primary',
|
|
49
|
+
importable: true,
|
|
50
|
+
exportable: true,
|
|
51
|
+
insertable: true,
|
|
52
|
+
searchable: true,
|
|
53
|
+
saveAndStay: false,
|
|
54
|
+
modelKey: 'id',
|
|
55
|
+
modelBy: undefined,
|
|
56
|
+
fields: () => [],
|
|
57
|
+
stringFields: ()=>[],
|
|
58
|
+
onlyOwnerEdit: false,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Custom events emitted by ModelTable.
|
|
63
|
+
* Parents can listen to these events to react to user actions and internal state changes.
|
|
64
|
+
*/
|
|
65
|
+
const emit = defineEmits(['open:dialog','close:dialog','create','update','delete'])
|
|
66
|
+
const attrs = useAttrs()
|
|
67
|
+
const plainAttrs = computed(() => {
|
|
68
|
+
const returnAttrs = clone(attrs)
|
|
69
|
+
if (props.headers) returnAttrs['headers'] = props.headers
|
|
70
|
+
return returnAttrs
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const authenState = useState<AuthenticationState>("authentication")
|
|
74
|
+
const currentUsername = computed(()=>{
|
|
75
|
+
let userProfile = authenState.value?.userProfile as {username: string}
|
|
76
|
+
return userProfile?.username ?? ""
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const currentItem = ref<Record<string, any> | undefined>(undefined)
|
|
80
|
+
const isDialogOpen = ref<boolean>(false)
|
|
81
|
+
const isDialogReadonly = ref<boolean>(false)
|
|
82
|
+
|
|
83
|
+
const { items, itemsLength,
|
|
84
|
+
search,setSearch,
|
|
85
|
+
canServerPageable, canServerSearch, canCreate, canUpdate, canDelete,
|
|
86
|
+
createItem, importItems, updateItem, deleteItem,
|
|
87
|
+
loadItems, reload,
|
|
88
|
+
isLoading } = useGraphqlModel(props)
|
|
89
|
+
|
|
90
|
+
function openDialog(item?: object) {
|
|
91
|
+
isDialogReadonly.value = false
|
|
92
|
+
currentItem.value = item
|
|
93
|
+
nextTick(() => {
|
|
94
|
+
isDialogOpen.value = true
|
|
95
|
+
emit('open:dialog' , item)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function openDialogReadonly(item?: object) {
|
|
100
|
+
isDialogReadonly.value = true
|
|
101
|
+
currentItem.value = item
|
|
102
|
+
nextTick(() => {
|
|
103
|
+
isDialogOpen.value = true
|
|
104
|
+
emit('open:dialog' , item)
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function confirmDeleteItem(item: Record<string, any>, callback?: FormDialogCallback) {
|
|
109
|
+
let confirm = await useDialog().confirm({message: "Do you want to delete record?"})
|
|
110
|
+
if (confirm) onDeleteItem(item,callback)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const onCreateItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
|
|
114
|
+
createItem(item,callback).then(()=>{
|
|
115
|
+
emit("create",item,callback)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const onUpdateItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
|
|
120
|
+
updateItem(item,callback).then(()=>{
|
|
121
|
+
emit("update",item,callback)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const onDeleteItem = (item: Record<string, any>,callback?: FormDialogCallback)=>{
|
|
126
|
+
deleteItem(item,callback).then(()=>{
|
|
127
|
+
emit("delete",item,callback)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const canEditRow = function (item: Record<string, any>) {
|
|
132
|
+
if (props.onlyOwnerEdit) {
|
|
133
|
+
if (item?.userstampField?.createdBy && item?.userstampField?.createdBy==currentUsername.value) return true
|
|
134
|
+
if (!!props.onlyOwnerOverridePermission && useUserPermission().check(props.onlyOwnerOverridePermission)) return true
|
|
135
|
+
return !item?.userstampField?.createdBy
|
|
136
|
+
}
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const operation = ref({ openDialog, openDialogReadonly, createItem, importItems, updateItem, deleteItem, reload, setSearch, canServerPageable, canServerSearch, canCreate, canUpdate, canDelete, canEditRow, onlyOwnerEdit: props.onlyOwnerEdit, onlyOwnerOverridePermission: props.onlyOwnerOverridePermission })
|
|
141
|
+
|
|
142
|
+
const computedInitialData = computed(() => {
|
|
143
|
+
return Object.assign({}, props.initialData, props.modelBy)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
watch(()=>props.search,()=>{
|
|
147
|
+
search.value = props.search
|
|
148
|
+
},{immediate:true})
|
|
149
|
+
|
|
150
|
+
defineExpose({ reload,operation,items })
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<v-card>
|
|
155
|
+
<slot
|
|
156
|
+
name="header"
|
|
157
|
+
:items="items"
|
|
158
|
+
:operation="operation"
|
|
159
|
+
>
|
|
160
|
+
<VToolbar :color="toolbarColor">
|
|
161
|
+
<v-row
|
|
162
|
+
justify="end"
|
|
163
|
+
class="ma-1"
|
|
164
|
+
dense
|
|
165
|
+
no-gutters
|
|
166
|
+
align="center"
|
|
167
|
+
>
|
|
168
|
+
<v-col cols="7">
|
|
169
|
+
<VToolbarTitle class="pl-3">
|
|
170
|
+
<slot
|
|
171
|
+
name="title"
|
|
172
|
+
:reload="reload"
|
|
173
|
+
>
|
|
174
|
+
{{ title }}
|
|
175
|
+
<v-icon
|
|
176
|
+
size="small"
|
|
177
|
+
@click="reload"
|
|
178
|
+
>
|
|
179
|
+
mdi mdi-refresh
|
|
180
|
+
</v-icon>
|
|
181
|
+
</slot>
|
|
182
|
+
</VToolbarTitle>
|
|
183
|
+
</v-col>
|
|
184
|
+
<v-col cols="5">
|
|
185
|
+
<slot name="search" :items="items" :operation="operation" v-if="props.searchable">
|
|
186
|
+
<VTextField
|
|
187
|
+
v-model="search"
|
|
188
|
+
class="justify-end w-100"
|
|
189
|
+
density="compact"
|
|
190
|
+
hide-details
|
|
191
|
+
placeholder="ค้นหา"
|
|
192
|
+
clearable
|
|
193
|
+
variant="solo"
|
|
194
|
+
/>
|
|
195
|
+
</slot>
|
|
196
|
+
</v-col>
|
|
197
|
+
</v-row>
|
|
198
|
+
|
|
199
|
+
<VToolbarItems>
|
|
200
|
+
<slot name="toolbarItems" :items="items" :operation="operation"/>
|
|
201
|
+
<ImportCSV
|
|
202
|
+
v-if="props.importable && canCreate && props.insertable"
|
|
203
|
+
icon="mdi mdi-file-upload"
|
|
204
|
+
variant="flat"
|
|
205
|
+
:color="toolbarColor"
|
|
206
|
+
:stringFields="props.stringFields"
|
|
207
|
+
@import="importItems"
|
|
208
|
+
/>
|
|
209
|
+
<ExportCSV
|
|
210
|
+
v-if="props.exportable && items.length"
|
|
211
|
+
icon="mdi mdi-file-download"
|
|
212
|
+
variant="flat"
|
|
213
|
+
:file-name="title"
|
|
214
|
+
:model-value="items"
|
|
215
|
+
:color="toolbarColor"
|
|
216
|
+
:stringFields="props.stringFields"
|
|
217
|
+
/>
|
|
218
|
+
<VBtn
|
|
219
|
+
v-if="canCreate && props.insertable"
|
|
220
|
+
:color="toolbarColor"
|
|
221
|
+
prepend-icon="mdi mdi-plus"
|
|
222
|
+
variant="flat"
|
|
223
|
+
@click="openDialog()"
|
|
224
|
+
>
|
|
225
|
+
add
|
|
226
|
+
</VBtn>
|
|
227
|
+
</VToolbarItems>
|
|
228
|
+
</VToolbar>
|
|
229
|
+
</slot>
|
|
230
|
+
<v-data-table-server
|
|
231
|
+
v-if="canServerPageable"
|
|
232
|
+
v-bind="plainAttrs"
|
|
233
|
+
color="primary"
|
|
234
|
+
:items="items"
|
|
235
|
+
:items-length="itemsLength"
|
|
236
|
+
:item-value="props.modelKey"
|
|
237
|
+
:search="search"
|
|
238
|
+
:loading="isLoading"
|
|
239
|
+
@update:options="loadItems"
|
|
240
|
+
>
|
|
241
|
+
<!-- @ts-ignore -->
|
|
242
|
+
<template
|
|
243
|
+
v-for="(_, name, index) in ($slots as {})"
|
|
244
|
+
:key="index"
|
|
245
|
+
#[name]="slotData"
|
|
246
|
+
>
|
|
247
|
+
<slot
|
|
248
|
+
:name="name"
|
|
249
|
+
v-bind="((slotData || {}) as object)"
|
|
250
|
+
:operation="operation"
|
|
251
|
+
/>
|
|
252
|
+
</template>
|
|
253
|
+
<template
|
|
254
|
+
v-if="!$slots['item.action']"
|
|
255
|
+
#item.action="{ item }"
|
|
256
|
+
>
|
|
257
|
+
<v-btn
|
|
258
|
+
v-if="!canUpdate || !canEditRow(item)"
|
|
259
|
+
variant="flat"
|
|
260
|
+
density="compact"
|
|
261
|
+
icon="mdi mdi-note-search"
|
|
262
|
+
@click="openDialogReadonly(item)"
|
|
263
|
+
/>
|
|
264
|
+
<v-btn
|
|
265
|
+
v-if="canUpdate && canEditRow(item)"
|
|
266
|
+
variant="flat"
|
|
267
|
+
density="compact"
|
|
268
|
+
icon="mdi mdi-note-edit"
|
|
269
|
+
@click="openDialog(item)"
|
|
270
|
+
/>
|
|
271
|
+
<v-btn
|
|
272
|
+
v-if="canDelete && canEditRow(item)"
|
|
273
|
+
variant="flat"
|
|
274
|
+
density="compact"
|
|
275
|
+
icon="mdi mdi-delete"
|
|
276
|
+
@click="confirmDeleteItem(item)"
|
|
277
|
+
/>
|
|
278
|
+
</template>
|
|
279
|
+
</v-data-table-server>
|
|
280
|
+
<v-data-table
|
|
281
|
+
v-else
|
|
282
|
+
v-bind="plainAttrs"
|
|
283
|
+
color="primary"
|
|
284
|
+
:items="items"
|
|
285
|
+
:item-value="props.modelKey"
|
|
286
|
+
:search="search"
|
|
287
|
+
:loading="isLoading"
|
|
288
|
+
>
|
|
289
|
+
<!-- @ts-ignore -->
|
|
290
|
+
<template
|
|
291
|
+
v-for="(_, name, index) in ($slots as {})"
|
|
292
|
+
:key="index"
|
|
293
|
+
#[name]="slotData"
|
|
294
|
+
>
|
|
295
|
+
<slot
|
|
296
|
+
:name="name"
|
|
297
|
+
v-bind="((slotData || {}) as object)"
|
|
298
|
+
:operation="operation"
|
|
299
|
+
/>
|
|
300
|
+
</template>
|
|
301
|
+
<template
|
|
302
|
+
v-if="!$slots['item.action']"
|
|
303
|
+
#item.action="{ item }"
|
|
304
|
+
>
|
|
305
|
+
<v-btn
|
|
306
|
+
v-if="!canUpdate || !canEditRow(item)"
|
|
307
|
+
variant="flat"
|
|
308
|
+
density="compact"
|
|
309
|
+
icon="mdi mdi-note-search"
|
|
310
|
+
@click="openDialogReadonly(item)"
|
|
311
|
+
/>
|
|
312
|
+
<v-btn
|
|
313
|
+
v-if="canUpdate && canEditRow(item)"
|
|
314
|
+
variant="flat"
|
|
315
|
+
density="compact"
|
|
316
|
+
icon="mdi mdi-note-edit"
|
|
317
|
+
@click="openDialog(item)"
|
|
318
|
+
/>
|
|
319
|
+
<v-btn
|
|
320
|
+
v-if="canDelete && canEditRow(item)"
|
|
321
|
+
variant="flat"
|
|
322
|
+
density="compact"
|
|
323
|
+
icon="mdi mdi-delete"
|
|
324
|
+
@click="confirmDeleteItem(item)"
|
|
325
|
+
/>
|
|
326
|
+
</template>
|
|
327
|
+
</v-data-table>
|
|
328
|
+
<FormDialog
|
|
329
|
+
v-model="isDialogOpen"
|
|
330
|
+
:title="title"
|
|
331
|
+
:fullscreen="dialogFullscreen"
|
|
332
|
+
:initial-data="computedInitialData"
|
|
333
|
+
:form-data="currentItem"
|
|
334
|
+
@create="onCreateItem"
|
|
335
|
+
@update="onUpdateItem"
|
|
336
|
+
@afterLeave="emit('close:dialog')"
|
|
337
|
+
:saveAndStay="saveAndStay"
|
|
338
|
+
:readonly="isDialogReadonly"
|
|
339
|
+
:width="dialogWidth"
|
|
340
|
+
:height="dialogHeight"
|
|
341
|
+
:max-width="dialogMaxWidth"
|
|
342
|
+
:max-height="dialogMaxHeight"
|
|
343
|
+
>
|
|
344
|
+
<template #default="slotData">
|
|
345
|
+
<slot
|
|
346
|
+
name="form"
|
|
347
|
+
:tableItems="items"
|
|
348
|
+
:tableOperation="operation"
|
|
349
|
+
v-bind="slotData"
|
|
350
|
+
/>
|
|
351
|
+
</template>
|
|
352
|
+
<template #title="slotData">
|
|
353
|
+
<slot
|
|
354
|
+
name="formTitle"
|
|
355
|
+
:tableItems="items"
|
|
356
|
+
:tableOperation="operation"
|
|
357
|
+
v-bind="slotData"
|
|
358
|
+
/>
|
|
359
|
+
</template>
|
|
360
|
+
<template #action="slotData">
|
|
361
|
+
<slot
|
|
362
|
+
name="formAction"
|
|
363
|
+
:tableItems="items"
|
|
364
|
+
:tableOperation="operation"
|
|
365
|
+
v-bind="slotData"
|
|
366
|
+
/>
|
|
367
|
+
</template>
|
|
368
|
+
</FormDialog>
|
|
369
|
+
</v-card>
|
|
370
|
+
</template>
|