@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,497 +1,497 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
/**
|
|
3
|
-
* ModelIterator 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, nextTick, ref, useAttrs, useSlots, watch} from 'vue'
|
|
7
|
-
import {VDataIterator} from 'vuetify/components/VDataIterator'
|
|
8
|
-
import {VDataTable} from "vuetify/components/VDataTable";
|
|
9
|
-
import {omit} from 'lodash-es'
|
|
10
|
-
import type {GraphqlModelProps} from '../../composables/graphqlModel'
|
|
11
|
-
import {useGraphqlModel} from '../../composables/graphqlModel'
|
|
12
|
-
import {useDisplay} from 'vuetify'
|
|
13
|
-
|
|
14
|
-
defineOptions({
|
|
15
|
-
inheritAttrs: false,
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$props']> {
|
|
19
|
-
title: string // Toolbar title for this model-backed iterator/table block.
|
|
20
|
-
noDataText?: string // Empty-state message shown when model query has no results.
|
|
21
|
-
dialogFullscreen?: boolean // Opens create/edit dialog fullscreen by default.
|
|
22
|
-
initialData?: Record<string, any> // Default values merged with `modelBy` before opening create dialog.
|
|
23
|
-
toolbarColor?: string // Toolbar/action color theme token.
|
|
24
|
-
importable?: boolean // Enables import action for creating many model rows at once.
|
|
25
|
-
exportable?: boolean // Enables export action for current model result set.
|
|
26
|
-
insertable?: boolean // Enables create action for new model rows.
|
|
27
|
-
searchable?: boolean // Shows search input and updates GraphQL model search state.
|
|
28
|
-
search?: string // External search value synced into internal search ref via watcher.
|
|
29
|
-
|
|
30
|
-
viewSwitch?: boolean // Displays control for switching between card iterator and table presentations.
|
|
31
|
-
viewSwitchMultiple?: boolean // Allows multiple selected view chips when switch UI supports it.
|
|
32
|
-
|
|
33
|
-
cols?: string | number | boolean // Base columns used by iterator card grid.
|
|
34
|
-
xxl?: string | number | boolean // Card grid columns at `xxl` breakpoint.
|
|
35
|
-
xl?: string | number | boolean // Card grid columns at `xl` breakpoint.
|
|
36
|
-
lg?: string | number | boolean // Card grid columns at `lg` breakpoint.
|
|
37
|
-
md?: string | number | boolean // Card grid columns at `md` breakpoint.
|
|
38
|
-
sm?: string | number | boolean // Card grid columns at `sm` breakpoint.
|
|
39
|
-
itemsPerPage?: string | number // Items per page for iterator/table pagination (`all` supported).
|
|
40
|
-
|
|
41
|
-
preferTable?: string | number | boolean // Auto view strategy: always table (`true`) or table after N items (number).
|
|
42
|
-
preferTableXxl?: string | number | boolean // `xxl` override for `preferTable`.
|
|
43
|
-
preferTableXl?: string | number | boolean // `xl` override for `preferTable`.
|
|
44
|
-
preferTableLg?: string | number | boolean // `lg` override for `preferTable`.
|
|
45
|
-
preferTableMd?: string | number | boolean // `md` override for `preferTable`.
|
|
46
|
-
preferTableSm?: string | number | boolean // `sm` override for `preferTable`.
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Public props accepted by ModelIterator.
|
|
51
|
-
* Document each prop field with intent, defaults, and side effects for clear generated docs.
|
|
52
|
-
*/
|
|
53
|
-
const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
|
|
54
|
-
noDataText: 'ไม่พบข้อมูล',
|
|
55
|
-
dialogFullscreen: false,
|
|
56
|
-
toolbarColor: 'primary',
|
|
57
|
-
importable: false,
|
|
58
|
-
exportable: false,
|
|
59
|
-
insertable: true,
|
|
60
|
-
searchable: true,
|
|
61
|
-
modelKey: 'id',
|
|
62
|
-
modelBy: undefined,
|
|
63
|
-
fields: () => [],
|
|
64
|
-
|
|
65
|
-
viewSwitch: false,
|
|
66
|
-
viewSwitchMultiple:false,
|
|
67
|
-
|
|
68
|
-
cols: 12,
|
|
69
|
-
xxl: false,
|
|
70
|
-
xl: false,
|
|
71
|
-
lg: 2,
|
|
72
|
-
md: 4,
|
|
73
|
-
sm: 6,
|
|
74
|
-
itemsPerPage: 12,
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
const attrs = useAttrs()
|
|
78
|
-
const plainAttrs = computed(() => {
|
|
79
|
-
const returnAttrs = omit(attrs, ['modelValue', 'onUpdate:modelValue'])
|
|
80
|
-
if (props.headers) returnAttrs['headers'] = props.headers
|
|
81
|
-
return returnAttrs
|
|
82
|
-
})
|
|
83
|
-
const slots = useSlots()
|
|
84
|
-
const tableSlots = computed(() => {
|
|
85
|
-
return omit(slots, ['item'])
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const display = useDisplay()
|
|
89
|
-
const isProgrammaticSet = ref(false)
|
|
90
|
-
const userOverrodeView = ref(false)
|
|
91
|
-
|
|
92
|
-
const viewType = ref<string[] | string>('iterator')
|
|
93
|
-
|
|
94
|
-
function setViewTypeSafely(val: 'iterator' | 'table') {
|
|
95
|
-
isProgrammaticSet.value = true
|
|
96
|
-
if (Array.isArray(viewType.value)) {
|
|
97
|
-
viewType.value = [val]
|
|
98
|
-
} else {
|
|
99
|
-
viewType.value = val
|
|
100
|
-
}
|
|
101
|
-
nextTick(() => { isProgrammaticSet.value = false })
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
watch(viewType, () => {
|
|
105
|
-
if (!isProgrammaticSet.value) userOverrodeView.value = true
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
function parsePrefer(val: unknown): boolean | number {
|
|
109
|
-
if (val === true) return true
|
|
110
|
-
const n = Number(val)
|
|
111
|
-
return Number.isFinite(n) && n > 0 ? n : false
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const computedPreferTable = computed<boolean | number>(() => {
|
|
115
|
-
const bp = display.name?.value
|
|
116
|
-
if (bp === 'xxl' && props.preferTableXxl !== undefined) return parsePrefer(props.preferTableXxl)
|
|
117
|
-
if (bp === 'xl' && props.preferTableXl !== undefined) return parsePrefer(props.preferTableXl)
|
|
118
|
-
if (bp === 'lg' && props.preferTableLg !== undefined) return parsePrefer(props.preferTableLg)
|
|
119
|
-
if (bp === 'md' && props.preferTableMd !== undefined) return parsePrefer(props.preferTableMd)
|
|
120
|
-
if (bp === 'sm' && props.preferTableSm !== undefined) return parsePrefer(props.preferTableSm)
|
|
121
|
-
return parsePrefer(props.preferTable)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const currentItem = ref<Record<string, any> | undefined>(undefined)
|
|
125
|
-
const isDialogOpen = ref<boolean>(false)
|
|
126
|
-
|
|
127
|
-
const { items, itemsLength,
|
|
128
|
-
search, setSearch, currentOptions,
|
|
129
|
-
canServerPageable, canServerSearch, canCreate, canUpdate, canDelete,
|
|
130
|
-
createItem, importItems, updateItem, deleteItem,
|
|
131
|
-
loadItems, reload,
|
|
132
|
-
isLoading } = useGraphqlModel(props)
|
|
133
|
-
|
|
134
|
-
function openDialog(item?: object) {
|
|
135
|
-
currentItem.value = item
|
|
136
|
-
nextTick(() => {
|
|
137
|
-
isDialogOpen.value = true
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const itemsPerPageInternal = ref<string | number>()
|
|
142
|
-
watch(() => props.itemsPerPage, (newValue) => {
|
|
143
|
-
if (newValue.toString().toLowerCase() == 'all') itemsPerPageInternal.value = '-1'
|
|
144
|
-
else if (newValue) itemsPerPageInternal.value = newValue
|
|
145
|
-
}, { immediate: true })
|
|
146
|
-
|
|
147
|
-
type SortItem = { key: string, order?: boolean | 'asc' | 'desc' }
|
|
148
|
-
const sortBy = ref<SortItem[]>()
|
|
149
|
-
|
|
150
|
-
const pageCount = computed(() => {
|
|
151
|
-
if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value == '-1' || Number(itemsPerPageInternal.value) <= 0) return 1
|
|
152
|
-
else return Math.ceil(itemsLength.value / Number(itemsPerPageInternal.value))
|
|
153
|
-
})
|
|
154
|
-
const currentPage = ref<number>(1)
|
|
155
|
-
|
|
156
|
-
watch([currentPage, itemsPerPageInternal, sortBy], () => {
|
|
157
|
-
if (canServerPageable.value) {
|
|
158
|
-
loadItems({
|
|
159
|
-
page: currentPage.value,
|
|
160
|
-
itemsPerPage: (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) ? '-1' : itemsPerPageInternal.value,
|
|
161
|
-
sortBy: sortBy.value,
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
}, { immediate: true })
|
|
165
|
-
|
|
166
|
-
watch(
|
|
167
|
-
[itemsLength, computedPreferTable, () => display.name?.value],
|
|
168
|
-
([len, prefer]) => {
|
|
169
|
-
if (userOverrodeView.value) return // respect explicit user choice forever (until remount)
|
|
170
|
-
|
|
171
|
-
let target: 'iterator' | 'table' = 'iterator'
|
|
172
|
-
if (prefer === true) {
|
|
173
|
-
target = 'table'
|
|
174
|
-
} else if (typeof prefer === 'number') {
|
|
175
|
-
if (Number(len) >= prefer) target = 'table'
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!viewType.value?.includes(target)) setViewTypeSafely(target)
|
|
179
|
-
},
|
|
180
|
-
{ immediate: true }
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
const operation = ref({ openDialog, createItem, importItems, updateItem, deleteItem, reload, setSearch, canServerPageable, canServerSearch, canCreate, canUpdate, canDelete })
|
|
184
|
-
|
|
185
|
-
const computedInitialData = computed(() => {
|
|
186
|
-
return Object.assign({}, props.initialData, props.modelBy)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const computedSkeletonPerPage = computed(() => {
|
|
190
|
-
if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) return 1
|
|
191
|
-
else return Number(itemsPerPageInternal.value)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
watch(()=>props.search,()=>{
|
|
195
|
-
search.value = props.search
|
|
196
|
-
},{immediate:true})
|
|
197
|
-
|
|
198
|
-
defineExpose({ reload,operation })
|
|
199
|
-
</script>
|
|
200
|
-
|
|
201
|
-
<template>
|
|
202
|
-
<v-card>
|
|
203
|
-
<v-data-iterator
|
|
204
|
-
v-bind="plainAttrs"
|
|
205
|
-
v-model:items-per-page="itemsPerPageInternal"
|
|
206
|
-
v-model:sort-by="sortBy"
|
|
207
|
-
:items="items"
|
|
208
|
-
:item-value="modelKey"
|
|
209
|
-
:search="search"
|
|
210
|
-
:loading="isLoading"
|
|
211
|
-
>
|
|
212
|
-
<template #default="defaultProps" v-if="viewType.includes('iterator')">
|
|
213
|
-
<slot
|
|
214
|
-
v-bind="defaultProps"
|
|
215
|
-
:operation="operation"
|
|
216
|
-
>
|
|
217
|
-
<v-container fluid>
|
|
218
|
-
<v-row>
|
|
219
|
-
<v-col
|
|
220
|
-
v-for="(item, index) in defaultProps.items"
|
|
221
|
-
:key="index"
|
|
222
|
-
:cols="cols"
|
|
223
|
-
:sm="sm"
|
|
224
|
-
:md="md"
|
|
225
|
-
:lg="lg"
|
|
226
|
-
:xl="xl"
|
|
227
|
-
:xxl="xxl"
|
|
228
|
-
>
|
|
229
|
-
<slot
|
|
230
|
-
name="item"
|
|
231
|
-
:item="item"
|
|
232
|
-
:operation="operation"
|
|
233
|
-
/>
|
|
234
|
-
</v-col>
|
|
235
|
-
</v-row>
|
|
236
|
-
</v-container>
|
|
237
|
-
</slot>
|
|
238
|
-
</template>
|
|
239
|
-
<template #loader="loaderProps" v-if="viewType.includes('iterator')">
|
|
240
|
-
<slot
|
|
241
|
-
name="loader"
|
|
242
|
-
v-bind="loaderProps"
|
|
243
|
-
>
|
|
244
|
-
<v-container fluid>
|
|
245
|
-
<v-row>
|
|
246
|
-
<v-col
|
|
247
|
-
v-for="key in computedSkeletonPerPage"
|
|
248
|
-
:key="key"
|
|
249
|
-
:cols="cols"
|
|
250
|
-
:sm="sm"
|
|
251
|
-
:md="md"
|
|
252
|
-
:lg="lg"
|
|
253
|
-
:xl="xl"
|
|
254
|
-
:xxl="xxl"
|
|
255
|
-
>
|
|
256
|
-
<slot name="loaderItem">
|
|
257
|
-
<v-skeleton-loader
|
|
258
|
-
type="paragraph"
|
|
259
|
-
/>
|
|
260
|
-
</slot>
|
|
261
|
-
</v-col>
|
|
262
|
-
</v-row>
|
|
263
|
-
</v-container>
|
|
264
|
-
</slot>
|
|
265
|
-
</template>
|
|
266
|
-
<template #header="headerProps">
|
|
267
|
-
<slot
|
|
268
|
-
name="header"
|
|
269
|
-
v-bind="headerProps"
|
|
270
|
-
:items="items"
|
|
271
|
-
:operation="operation"
|
|
272
|
-
>
|
|
273
|
-
<VToolbar :color="toolbarColor">
|
|
274
|
-
<v-row
|
|
275
|
-
justify="end"
|
|
276
|
-
class="ma-1"
|
|
277
|
-
dense
|
|
278
|
-
no-gutters
|
|
279
|
-
align="center"
|
|
280
|
-
>
|
|
281
|
-
<v-col cols="7">
|
|
282
|
-
<VToolbarTitle class="pl-3">
|
|
283
|
-
<slot
|
|
284
|
-
name="title"
|
|
285
|
-
:reload="reload"
|
|
286
|
-
>
|
|
287
|
-
{{ title }}
|
|
288
|
-
<v-icon
|
|
289
|
-
size="small"
|
|
290
|
-
@click="reload"
|
|
291
|
-
>
|
|
292
|
-
mdi mdi-refresh
|
|
293
|
-
</v-icon>
|
|
294
|
-
</slot>
|
|
295
|
-
</VToolbarTitle>
|
|
296
|
-
</v-col>
|
|
297
|
-
<v-col cols="5">
|
|
298
|
-
<slot name="search" :items="items" :operation="operation" v-if="props.searchable">
|
|
299
|
-
<VTextField
|
|
300
|
-
v-model="search"
|
|
301
|
-
class="justify-end w-100"
|
|
302
|
-
density="compact"
|
|
303
|
-
hide-details
|
|
304
|
-
placeholder="ค้นหา"
|
|
305
|
-
clearable
|
|
306
|
-
variant="solo"
|
|
307
|
-
/>
|
|
308
|
-
</slot>
|
|
309
|
-
</v-col>
|
|
310
|
-
</v-row>
|
|
311
|
-
|
|
312
|
-
<VToolbarItems>
|
|
313
|
-
<slot name="toolbarItems" :items="items" :operation="operation"/>
|
|
314
|
-
<ImportCSV
|
|
315
|
-
v-if="props.importable && canCreate && props.insertable"
|
|
316
|
-
icon="mdi mdi-file-upload"
|
|
317
|
-
variant="flat"
|
|
318
|
-
:color="toolbarColor"
|
|
319
|
-
@import="importItems"
|
|
320
|
-
/>
|
|
321
|
-
<ExportCSV
|
|
322
|
-
v-if="props.exportable && items.length"
|
|
323
|
-
icon="mdi mdi-file-download"
|
|
324
|
-
variant="flat"
|
|
325
|
-
:file-name="title"
|
|
326
|
-
:model-value="items"
|
|
327
|
-
:color="toolbarColor"
|
|
328
|
-
/>
|
|
329
|
-
<VBtn
|
|
330
|
-
v-if="canCreate && props.insertable"
|
|
331
|
-
:color="toolbarColor"
|
|
332
|
-
prepend-icon="mdi mdi-plus"
|
|
333
|
-
variant="flat"
|
|
334
|
-
@click="openDialog()"
|
|
335
|
-
>
|
|
336
|
-
add
|
|
337
|
-
</VBtn>
|
|
338
|
-
<v-row align="center" class="mr-2 ml-2" v-if="viewSwitch">
|
|
339
|
-
<v-btn-toggle v-model="viewType" :multiple="viewSwitchMultiple">
|
|
340
|
-
<v-btn icon="mdi mdi-view-grid-outline" value="iterator"></v-btn>
|
|
341
|
-
<v-btn icon="mdi mdi-table-large" value="table"></v-btn>
|
|
342
|
-
</v-btn-toggle>
|
|
343
|
-
</v-row>
|
|
344
|
-
</VToolbarItems>
|
|
345
|
-
</VToolbar>
|
|
346
|
-
</slot>
|
|
347
|
-
<template v-if="viewType.includes('table')">
|
|
348
|
-
<v-data-table-server
|
|
349
|
-
v-if="canServerPageable"
|
|
350
|
-
v-bind="plainAttrs"
|
|
351
|
-
color="primary"
|
|
352
|
-
:items="items"
|
|
353
|
-
:items-length="itemsLength"
|
|
354
|
-
:item-value="props.modelKey"
|
|
355
|
-
:search="search"
|
|
356
|
-
:loading="isLoading"
|
|
357
|
-
@update:options="loadItems"
|
|
358
|
-
>
|
|
359
|
-
<!-- @ts-ignore -->
|
|
360
|
-
<template
|
|
361
|
-
v-for="(_, name, index) in (tableSlots as {})"
|
|
362
|
-
:key="index"
|
|
363
|
-
#[name]="slotData"
|
|
364
|
-
>
|
|
365
|
-
<slot
|
|
366
|
-
:name="name"
|
|
367
|
-
v-bind="((slotData || {}) as object)"
|
|
368
|
-
:operation="operation"
|
|
369
|
-
/>
|
|
370
|
-
</template>
|
|
371
|
-
<template
|
|
372
|
-
v-if="!$slots['item.action']"
|
|
373
|
-
#item.action="{ item }"
|
|
374
|
-
>
|
|
375
|
-
<v-btn
|
|
376
|
-
v-if="canUpdate"
|
|
377
|
-
variant="flat"
|
|
378
|
-
density="compact"
|
|
379
|
-
icon="mdi mdi-note-edit"
|
|
380
|
-
@click="openDialog(item)"
|
|
381
|
-
/>
|
|
382
|
-
<v-btn
|
|
383
|
-
v-if="canDelete"
|
|
384
|
-
variant="flat"
|
|
385
|
-
density="compact"
|
|
386
|
-
icon="mdi mdi-delete"
|
|
387
|
-
@click="deleteItem(item)"
|
|
388
|
-
/>
|
|
389
|
-
</template>
|
|
390
|
-
</v-data-table-server>
|
|
391
|
-
<v-data-table
|
|
392
|
-
v-else
|
|
393
|
-
v-bind="plainAttrs"
|
|
394
|
-
color="primary"
|
|
395
|
-
:items="items"
|
|
396
|
-
:item-value="props.modelKey"
|
|
397
|
-
:search="search"
|
|
398
|
-
:loading="isLoading"
|
|
399
|
-
>
|
|
400
|
-
<!-- @ts-ignore -->
|
|
401
|
-
<template
|
|
402
|
-
v-for="(_, name, index) in (tableSlots as {})"
|
|
403
|
-
:key="index"
|
|
404
|
-
#[name]="slotData"
|
|
405
|
-
>
|
|
406
|
-
<slot
|
|
407
|
-
:name="name"
|
|
408
|
-
v-bind="((slotData || {}) as object)"
|
|
409
|
-
:operation="operation"
|
|
410
|
-
/>
|
|
411
|
-
</template>
|
|
412
|
-
<template
|
|
413
|
-
v-if="!$slots['item.action']"
|
|
414
|
-
#item.action="{ item }"
|
|
415
|
-
>
|
|
416
|
-
<v-btn
|
|
417
|
-
v-if="canUpdate"
|
|
418
|
-
variant="flat"
|
|
419
|
-
density="compact"
|
|
420
|
-
icon="mdi mdi-note-edit"
|
|
421
|
-
@click="openDialog(item)"
|
|
422
|
-
/>
|
|
423
|
-
<v-btn
|
|
424
|
-
v-if="canDelete"
|
|
425
|
-
variant="flat"
|
|
426
|
-
density="compact"
|
|
427
|
-
icon="mdi mdi-delete"
|
|
428
|
-
@click="deleteItem(item)"
|
|
429
|
-
/>
|
|
430
|
-
</template>
|
|
431
|
-
</v-data-table>
|
|
432
|
-
</template>
|
|
433
|
-
</template>
|
|
434
|
-
<template #footer="footerProps" v-if="viewType.includes('iterator')">
|
|
435
|
-
<v-container fluid>
|
|
436
|
-
<v-row
|
|
437
|
-
align-content="center"
|
|
438
|
-
justify="end"
|
|
439
|
-
dense
|
|
440
|
-
>
|
|
441
|
-
<v-spacer />
|
|
442
|
-
<v-select
|
|
443
|
-
v-model="itemsPerPageInternal"
|
|
444
|
-
density="compact"
|
|
445
|
-
variant="outlined"
|
|
446
|
-
:items="[6, 12, 18, 24, 30, { title: 'All', value: '-1' }]"
|
|
447
|
-
min-width="220"
|
|
448
|
-
max-width="220"
|
|
449
|
-
hide-details
|
|
450
|
-
>
|
|
451
|
-
<template #prepend>
|
|
452
|
-
Items per page:
|
|
453
|
-
</template>
|
|
454
|
-
</v-select>
|
|
455
|
-
<v-pagination
|
|
456
|
-
v-if="!canServerPageable"
|
|
457
|
-
v-model="currentPage"
|
|
458
|
-
density="compact"
|
|
459
|
-
:length="footerProps.pageCount"
|
|
460
|
-
total-visible="6"
|
|
461
|
-
show-first-last-page
|
|
462
|
-
@first="footerProps.setPage(1)"
|
|
463
|
-
@last="footerProps.setPage(footerProps.pageCount)"
|
|
464
|
-
@next="footerProps.nextPage"
|
|
465
|
-
@prev="footerProps.prevPage"
|
|
466
|
-
@update:model-value="footerProps.setPage"
|
|
467
|
-
/>
|
|
468
|
-
<v-pagination
|
|
469
|
-
v-else
|
|
470
|
-
v-model="currentPage"
|
|
471
|
-
density="compact"
|
|
472
|
-
:length="pageCount"
|
|
473
|
-
total-visible="6"
|
|
474
|
-
show-first-last-page
|
|
475
|
-
/>
|
|
476
|
-
</v-row>
|
|
477
|
-
</v-container>
|
|
478
|
-
</template>
|
|
479
|
-
</v-data-iterator>
|
|
480
|
-
<FormDialog
|
|
481
|
-
v-model="isDialogOpen"
|
|
482
|
-
:title="title"
|
|
483
|
-
:fullscreen="dialogFullscreen"
|
|
484
|
-
:initial-data="computedInitialData"
|
|
485
|
-
:form-data="currentItem"
|
|
486
|
-
@create="createItem"
|
|
487
|
-
@update="updateItem"
|
|
488
|
-
>
|
|
489
|
-
<template #default="slotData">
|
|
490
|
-
<slot
|
|
491
|
-
name="form"
|
|
492
|
-
v-bind="slotData"
|
|
493
|
-
/>
|
|
494
|
-
</template>
|
|
495
|
-
</FormDialog>
|
|
496
|
-
</v-card>
|
|
497
|
-
</template>
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* ModelIterator 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, nextTick, ref, useAttrs, useSlots, watch} from 'vue'
|
|
7
|
+
import {VDataIterator} from 'vuetify/components/VDataIterator'
|
|
8
|
+
import {VDataTable} from "vuetify/components/VDataTable";
|
|
9
|
+
import {omit} from 'lodash-es'
|
|
10
|
+
import type {GraphqlModelProps} from '../../composables/graphqlModel'
|
|
11
|
+
import {useGraphqlModel} from '../../composables/graphqlModel'
|
|
12
|
+
import {useDisplay} from 'vuetify'
|
|
13
|
+
|
|
14
|
+
defineOptions({
|
|
15
|
+
inheritAttrs: false,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$props']> {
|
|
19
|
+
title: string // Toolbar title for this model-backed iterator/table block.
|
|
20
|
+
noDataText?: string // Empty-state message shown when model query has no results.
|
|
21
|
+
dialogFullscreen?: boolean // Opens create/edit dialog fullscreen by default.
|
|
22
|
+
initialData?: Record<string, any> // Default values merged with `modelBy` before opening create dialog.
|
|
23
|
+
toolbarColor?: string // Toolbar/action color theme token.
|
|
24
|
+
importable?: boolean // Enables import action for creating many model rows at once.
|
|
25
|
+
exportable?: boolean // Enables export action for current model result set.
|
|
26
|
+
insertable?: boolean // Enables create action for new model rows.
|
|
27
|
+
searchable?: boolean // Shows search input and updates GraphQL model search state.
|
|
28
|
+
search?: string // External search value synced into internal search ref via watcher.
|
|
29
|
+
|
|
30
|
+
viewSwitch?: boolean // Displays control for switching between card iterator and table presentations.
|
|
31
|
+
viewSwitchMultiple?: boolean // Allows multiple selected view chips when switch UI supports it.
|
|
32
|
+
|
|
33
|
+
cols?: string | number | boolean // Base columns used by iterator card grid.
|
|
34
|
+
xxl?: string | number | boolean // Card grid columns at `xxl` breakpoint.
|
|
35
|
+
xl?: string | number | boolean // Card grid columns at `xl` breakpoint.
|
|
36
|
+
lg?: string | number | boolean // Card grid columns at `lg` breakpoint.
|
|
37
|
+
md?: string | number | boolean // Card grid columns at `md` breakpoint.
|
|
38
|
+
sm?: string | number | boolean // Card grid columns at `sm` breakpoint.
|
|
39
|
+
itemsPerPage?: string | number // Items per page for iterator/table pagination (`all` supported).
|
|
40
|
+
|
|
41
|
+
preferTable?: string | number | boolean // Auto view strategy: always table (`true`) or table after N items (number).
|
|
42
|
+
preferTableXxl?: string | number | boolean // `xxl` override for `preferTable`.
|
|
43
|
+
preferTableXl?: string | number | boolean // `xl` override for `preferTable`.
|
|
44
|
+
preferTableLg?: string | number | boolean // `lg` override for `preferTable`.
|
|
45
|
+
preferTableMd?: string | number | boolean // `md` override for `preferTable`.
|
|
46
|
+
preferTableSm?: string | number | boolean // `sm` override for `preferTable`.
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Public props accepted by ModelIterator.
|
|
51
|
+
* Document each prop field with intent, defaults, and side effects for clear generated docs.
|
|
52
|
+
*/
|
|
53
|
+
const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
|
|
54
|
+
noDataText: 'ไม่พบข้อมูล',
|
|
55
|
+
dialogFullscreen: false,
|
|
56
|
+
toolbarColor: 'primary',
|
|
57
|
+
importable: false,
|
|
58
|
+
exportable: false,
|
|
59
|
+
insertable: true,
|
|
60
|
+
searchable: true,
|
|
61
|
+
modelKey: 'id',
|
|
62
|
+
modelBy: undefined,
|
|
63
|
+
fields: () => [],
|
|
64
|
+
|
|
65
|
+
viewSwitch: false,
|
|
66
|
+
viewSwitchMultiple:false,
|
|
67
|
+
|
|
68
|
+
cols: 12,
|
|
69
|
+
xxl: false,
|
|
70
|
+
xl: false,
|
|
71
|
+
lg: 2,
|
|
72
|
+
md: 4,
|
|
73
|
+
sm: 6,
|
|
74
|
+
itemsPerPage: 12,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const attrs = useAttrs()
|
|
78
|
+
const plainAttrs = computed(() => {
|
|
79
|
+
const returnAttrs = omit(attrs, ['modelValue', 'onUpdate:modelValue'])
|
|
80
|
+
if (props.headers) returnAttrs['headers'] = props.headers
|
|
81
|
+
return returnAttrs
|
|
82
|
+
})
|
|
83
|
+
const slots = useSlots()
|
|
84
|
+
const tableSlots = computed(() => {
|
|
85
|
+
return omit(slots, ['item'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const display = useDisplay()
|
|
89
|
+
const isProgrammaticSet = ref(false)
|
|
90
|
+
const userOverrodeView = ref(false)
|
|
91
|
+
|
|
92
|
+
const viewType = ref<string[] | string>('iterator')
|
|
93
|
+
|
|
94
|
+
function setViewTypeSafely(val: 'iterator' | 'table') {
|
|
95
|
+
isProgrammaticSet.value = true
|
|
96
|
+
if (Array.isArray(viewType.value)) {
|
|
97
|
+
viewType.value = [val]
|
|
98
|
+
} else {
|
|
99
|
+
viewType.value = val
|
|
100
|
+
}
|
|
101
|
+
nextTick(() => { isProgrammaticSet.value = false })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
watch(viewType, () => {
|
|
105
|
+
if (!isProgrammaticSet.value) userOverrodeView.value = true
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
function parsePrefer(val: unknown): boolean | number {
|
|
109
|
+
if (val === true) return true
|
|
110
|
+
const n = Number(val)
|
|
111
|
+
return Number.isFinite(n) && n > 0 ? n : false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const computedPreferTable = computed<boolean | number>(() => {
|
|
115
|
+
const bp = display.name?.value
|
|
116
|
+
if (bp === 'xxl' && props.preferTableXxl !== undefined) return parsePrefer(props.preferTableXxl)
|
|
117
|
+
if (bp === 'xl' && props.preferTableXl !== undefined) return parsePrefer(props.preferTableXl)
|
|
118
|
+
if (bp === 'lg' && props.preferTableLg !== undefined) return parsePrefer(props.preferTableLg)
|
|
119
|
+
if (bp === 'md' && props.preferTableMd !== undefined) return parsePrefer(props.preferTableMd)
|
|
120
|
+
if (bp === 'sm' && props.preferTableSm !== undefined) return parsePrefer(props.preferTableSm)
|
|
121
|
+
return parsePrefer(props.preferTable)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const currentItem = ref<Record<string, any> | undefined>(undefined)
|
|
125
|
+
const isDialogOpen = ref<boolean>(false)
|
|
126
|
+
|
|
127
|
+
const { items, itemsLength,
|
|
128
|
+
search, setSearch, currentOptions,
|
|
129
|
+
canServerPageable, canServerSearch, canCreate, canUpdate, canDelete,
|
|
130
|
+
createItem, importItems, updateItem, deleteItem,
|
|
131
|
+
loadItems, reload,
|
|
132
|
+
isLoading } = useGraphqlModel(props)
|
|
133
|
+
|
|
134
|
+
function openDialog(item?: object) {
|
|
135
|
+
currentItem.value = item
|
|
136
|
+
nextTick(() => {
|
|
137
|
+
isDialogOpen.value = true
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const itemsPerPageInternal = ref<string | number>()
|
|
142
|
+
watch(() => props.itemsPerPage, (newValue) => {
|
|
143
|
+
if (newValue.toString().toLowerCase() == 'all') itemsPerPageInternal.value = '-1'
|
|
144
|
+
else if (newValue) itemsPerPageInternal.value = newValue
|
|
145
|
+
}, { immediate: true })
|
|
146
|
+
|
|
147
|
+
type SortItem = { key: string, order?: boolean | 'asc' | 'desc' }
|
|
148
|
+
const sortBy = ref<SortItem[]>()
|
|
149
|
+
|
|
150
|
+
const pageCount = computed(() => {
|
|
151
|
+
if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value == '-1' || Number(itemsPerPageInternal.value) <= 0) return 1
|
|
152
|
+
else return Math.ceil(itemsLength.value / Number(itemsPerPageInternal.value))
|
|
153
|
+
})
|
|
154
|
+
const currentPage = ref<number>(1)
|
|
155
|
+
|
|
156
|
+
watch([currentPage, itemsPerPageInternal, sortBy], () => {
|
|
157
|
+
if (canServerPageable.value) {
|
|
158
|
+
loadItems({
|
|
159
|
+
page: currentPage.value,
|
|
160
|
+
itemsPerPage: (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) ? '-1' : itemsPerPageInternal.value,
|
|
161
|
+
sortBy: sortBy.value,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}, { immediate: true })
|
|
165
|
+
|
|
166
|
+
watch(
|
|
167
|
+
[itemsLength, computedPreferTable, () => display.name?.value],
|
|
168
|
+
([len, prefer]) => {
|
|
169
|
+
if (userOverrodeView.value) return // respect explicit user choice forever (until remount)
|
|
170
|
+
|
|
171
|
+
let target: 'iterator' | 'table' = 'iterator'
|
|
172
|
+
if (prefer === true) {
|
|
173
|
+
target = 'table'
|
|
174
|
+
} else if (typeof prefer === 'number') {
|
|
175
|
+
if (Number(len) >= prefer) target = 'table'
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!viewType.value?.includes(target)) setViewTypeSafely(target)
|
|
179
|
+
},
|
|
180
|
+
{ immediate: true }
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const operation = ref({ openDialog, createItem, importItems, updateItem, deleteItem, reload, setSearch, canServerPageable, canServerSearch, canCreate, canUpdate, canDelete })
|
|
184
|
+
|
|
185
|
+
const computedInitialData = computed(() => {
|
|
186
|
+
return Object.assign({}, props.initialData, props.modelBy)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const computedSkeletonPerPage = computed(() => {
|
|
190
|
+
if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) return 1
|
|
191
|
+
else return Number(itemsPerPageInternal.value)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
watch(()=>props.search,()=>{
|
|
195
|
+
search.value = props.search
|
|
196
|
+
},{immediate:true})
|
|
197
|
+
|
|
198
|
+
defineExpose({ reload,operation })
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<template>
|
|
202
|
+
<v-card>
|
|
203
|
+
<v-data-iterator
|
|
204
|
+
v-bind="plainAttrs"
|
|
205
|
+
v-model:items-per-page="itemsPerPageInternal"
|
|
206
|
+
v-model:sort-by="sortBy"
|
|
207
|
+
:items="items"
|
|
208
|
+
:item-value="modelKey"
|
|
209
|
+
:search="search"
|
|
210
|
+
:loading="isLoading"
|
|
211
|
+
>
|
|
212
|
+
<template #default="defaultProps" v-if="viewType.includes('iterator')">
|
|
213
|
+
<slot
|
|
214
|
+
v-bind="defaultProps"
|
|
215
|
+
:operation="operation"
|
|
216
|
+
>
|
|
217
|
+
<v-container fluid>
|
|
218
|
+
<v-row>
|
|
219
|
+
<v-col
|
|
220
|
+
v-for="(item, index) in defaultProps.items"
|
|
221
|
+
:key="index"
|
|
222
|
+
:cols="cols"
|
|
223
|
+
:sm="sm"
|
|
224
|
+
:md="md"
|
|
225
|
+
:lg="lg"
|
|
226
|
+
:xl="xl"
|
|
227
|
+
:xxl="xxl"
|
|
228
|
+
>
|
|
229
|
+
<slot
|
|
230
|
+
name="item"
|
|
231
|
+
:item="item"
|
|
232
|
+
:operation="operation"
|
|
233
|
+
/>
|
|
234
|
+
</v-col>
|
|
235
|
+
</v-row>
|
|
236
|
+
</v-container>
|
|
237
|
+
</slot>
|
|
238
|
+
</template>
|
|
239
|
+
<template #loader="loaderProps" v-if="viewType.includes('iterator')">
|
|
240
|
+
<slot
|
|
241
|
+
name="loader"
|
|
242
|
+
v-bind="loaderProps"
|
|
243
|
+
>
|
|
244
|
+
<v-container fluid>
|
|
245
|
+
<v-row>
|
|
246
|
+
<v-col
|
|
247
|
+
v-for="key in computedSkeletonPerPage"
|
|
248
|
+
:key="key"
|
|
249
|
+
:cols="cols"
|
|
250
|
+
:sm="sm"
|
|
251
|
+
:md="md"
|
|
252
|
+
:lg="lg"
|
|
253
|
+
:xl="xl"
|
|
254
|
+
:xxl="xxl"
|
|
255
|
+
>
|
|
256
|
+
<slot name="loaderItem">
|
|
257
|
+
<v-skeleton-loader
|
|
258
|
+
type="paragraph"
|
|
259
|
+
/>
|
|
260
|
+
</slot>
|
|
261
|
+
</v-col>
|
|
262
|
+
</v-row>
|
|
263
|
+
</v-container>
|
|
264
|
+
</slot>
|
|
265
|
+
</template>
|
|
266
|
+
<template #header="headerProps">
|
|
267
|
+
<slot
|
|
268
|
+
name="header"
|
|
269
|
+
v-bind="headerProps"
|
|
270
|
+
:items="items"
|
|
271
|
+
:operation="operation"
|
|
272
|
+
>
|
|
273
|
+
<VToolbar :color="toolbarColor">
|
|
274
|
+
<v-row
|
|
275
|
+
justify="end"
|
|
276
|
+
class="ma-1"
|
|
277
|
+
dense
|
|
278
|
+
no-gutters
|
|
279
|
+
align="center"
|
|
280
|
+
>
|
|
281
|
+
<v-col cols="7">
|
|
282
|
+
<VToolbarTitle class="pl-3">
|
|
283
|
+
<slot
|
|
284
|
+
name="title"
|
|
285
|
+
:reload="reload"
|
|
286
|
+
>
|
|
287
|
+
{{ title }}
|
|
288
|
+
<v-icon
|
|
289
|
+
size="small"
|
|
290
|
+
@click="reload"
|
|
291
|
+
>
|
|
292
|
+
mdi mdi-refresh
|
|
293
|
+
</v-icon>
|
|
294
|
+
</slot>
|
|
295
|
+
</VToolbarTitle>
|
|
296
|
+
</v-col>
|
|
297
|
+
<v-col cols="5">
|
|
298
|
+
<slot name="search" :items="items" :operation="operation" v-if="props.searchable">
|
|
299
|
+
<VTextField
|
|
300
|
+
v-model="search"
|
|
301
|
+
class="justify-end w-100"
|
|
302
|
+
density="compact"
|
|
303
|
+
hide-details
|
|
304
|
+
placeholder="ค้นหา"
|
|
305
|
+
clearable
|
|
306
|
+
variant="solo"
|
|
307
|
+
/>
|
|
308
|
+
</slot>
|
|
309
|
+
</v-col>
|
|
310
|
+
</v-row>
|
|
311
|
+
|
|
312
|
+
<VToolbarItems>
|
|
313
|
+
<slot name="toolbarItems" :items="items" :operation="operation"/>
|
|
314
|
+
<ImportCSV
|
|
315
|
+
v-if="props.importable && canCreate && props.insertable"
|
|
316
|
+
icon="mdi mdi-file-upload"
|
|
317
|
+
variant="flat"
|
|
318
|
+
:color="toolbarColor"
|
|
319
|
+
@import="importItems"
|
|
320
|
+
/>
|
|
321
|
+
<ExportCSV
|
|
322
|
+
v-if="props.exportable && items.length"
|
|
323
|
+
icon="mdi mdi-file-download"
|
|
324
|
+
variant="flat"
|
|
325
|
+
:file-name="title"
|
|
326
|
+
:model-value="items"
|
|
327
|
+
:color="toolbarColor"
|
|
328
|
+
/>
|
|
329
|
+
<VBtn
|
|
330
|
+
v-if="canCreate && props.insertable"
|
|
331
|
+
:color="toolbarColor"
|
|
332
|
+
prepend-icon="mdi mdi-plus"
|
|
333
|
+
variant="flat"
|
|
334
|
+
@click="openDialog()"
|
|
335
|
+
>
|
|
336
|
+
add
|
|
337
|
+
</VBtn>
|
|
338
|
+
<v-row align="center" class="mr-2 ml-2" v-if="viewSwitch">
|
|
339
|
+
<v-btn-toggle v-model="viewType" :multiple="viewSwitchMultiple">
|
|
340
|
+
<v-btn icon="mdi mdi-view-grid-outline" value="iterator"></v-btn>
|
|
341
|
+
<v-btn icon="mdi mdi-table-large" value="table"></v-btn>
|
|
342
|
+
</v-btn-toggle>
|
|
343
|
+
</v-row>
|
|
344
|
+
</VToolbarItems>
|
|
345
|
+
</VToolbar>
|
|
346
|
+
</slot>
|
|
347
|
+
<template v-if="viewType.includes('table')">
|
|
348
|
+
<v-data-table-server
|
|
349
|
+
v-if="canServerPageable"
|
|
350
|
+
v-bind="plainAttrs"
|
|
351
|
+
color="primary"
|
|
352
|
+
:items="items"
|
|
353
|
+
:items-length="itemsLength"
|
|
354
|
+
:item-value="props.modelKey"
|
|
355
|
+
:search="search"
|
|
356
|
+
:loading="isLoading"
|
|
357
|
+
@update:options="loadItems"
|
|
358
|
+
>
|
|
359
|
+
<!-- @ts-ignore -->
|
|
360
|
+
<template
|
|
361
|
+
v-for="(_, name, index) in (tableSlots as {})"
|
|
362
|
+
:key="index"
|
|
363
|
+
#[name]="slotData"
|
|
364
|
+
>
|
|
365
|
+
<slot
|
|
366
|
+
:name="name"
|
|
367
|
+
v-bind="((slotData || {}) as object)"
|
|
368
|
+
:operation="operation"
|
|
369
|
+
/>
|
|
370
|
+
</template>
|
|
371
|
+
<template
|
|
372
|
+
v-if="!$slots['item.action']"
|
|
373
|
+
#item.action="{ item }"
|
|
374
|
+
>
|
|
375
|
+
<v-btn
|
|
376
|
+
v-if="canUpdate"
|
|
377
|
+
variant="flat"
|
|
378
|
+
density="compact"
|
|
379
|
+
icon="mdi mdi-note-edit"
|
|
380
|
+
@click="openDialog(item)"
|
|
381
|
+
/>
|
|
382
|
+
<v-btn
|
|
383
|
+
v-if="canDelete"
|
|
384
|
+
variant="flat"
|
|
385
|
+
density="compact"
|
|
386
|
+
icon="mdi mdi-delete"
|
|
387
|
+
@click="deleteItem(item)"
|
|
388
|
+
/>
|
|
389
|
+
</template>
|
|
390
|
+
</v-data-table-server>
|
|
391
|
+
<v-data-table
|
|
392
|
+
v-else
|
|
393
|
+
v-bind="plainAttrs"
|
|
394
|
+
color="primary"
|
|
395
|
+
:items="items"
|
|
396
|
+
:item-value="props.modelKey"
|
|
397
|
+
:search="search"
|
|
398
|
+
:loading="isLoading"
|
|
399
|
+
>
|
|
400
|
+
<!-- @ts-ignore -->
|
|
401
|
+
<template
|
|
402
|
+
v-for="(_, name, index) in (tableSlots as {})"
|
|
403
|
+
:key="index"
|
|
404
|
+
#[name]="slotData"
|
|
405
|
+
>
|
|
406
|
+
<slot
|
|
407
|
+
:name="name"
|
|
408
|
+
v-bind="((slotData || {}) as object)"
|
|
409
|
+
:operation="operation"
|
|
410
|
+
/>
|
|
411
|
+
</template>
|
|
412
|
+
<template
|
|
413
|
+
v-if="!$slots['item.action']"
|
|
414
|
+
#item.action="{ item }"
|
|
415
|
+
>
|
|
416
|
+
<v-btn
|
|
417
|
+
v-if="canUpdate"
|
|
418
|
+
variant="flat"
|
|
419
|
+
density="compact"
|
|
420
|
+
icon="mdi mdi-note-edit"
|
|
421
|
+
@click="openDialog(item)"
|
|
422
|
+
/>
|
|
423
|
+
<v-btn
|
|
424
|
+
v-if="canDelete"
|
|
425
|
+
variant="flat"
|
|
426
|
+
density="compact"
|
|
427
|
+
icon="mdi mdi-delete"
|
|
428
|
+
@click="deleteItem(item)"
|
|
429
|
+
/>
|
|
430
|
+
</template>
|
|
431
|
+
</v-data-table>
|
|
432
|
+
</template>
|
|
433
|
+
</template>
|
|
434
|
+
<template #footer="footerProps" v-if="viewType.includes('iterator')">
|
|
435
|
+
<v-container fluid>
|
|
436
|
+
<v-row
|
|
437
|
+
align-content="center"
|
|
438
|
+
justify="end"
|
|
439
|
+
dense
|
|
440
|
+
>
|
|
441
|
+
<v-spacer />
|
|
442
|
+
<v-select
|
|
443
|
+
v-model="itemsPerPageInternal"
|
|
444
|
+
density="compact"
|
|
445
|
+
variant="outlined"
|
|
446
|
+
:items="[6, 12, 18, 24, 30, { title: 'All', value: '-1' }]"
|
|
447
|
+
min-width="220"
|
|
448
|
+
max-width="220"
|
|
449
|
+
hide-details
|
|
450
|
+
>
|
|
451
|
+
<template #prepend>
|
|
452
|
+
Items per page:
|
|
453
|
+
</template>
|
|
454
|
+
</v-select>
|
|
455
|
+
<v-pagination
|
|
456
|
+
v-if="!canServerPageable"
|
|
457
|
+
v-model="currentPage"
|
|
458
|
+
density="compact"
|
|
459
|
+
:length="footerProps.pageCount"
|
|
460
|
+
total-visible="6"
|
|
461
|
+
show-first-last-page
|
|
462
|
+
@first="footerProps.setPage(1)"
|
|
463
|
+
@last="footerProps.setPage(footerProps.pageCount)"
|
|
464
|
+
@next="footerProps.nextPage"
|
|
465
|
+
@prev="footerProps.prevPage"
|
|
466
|
+
@update:model-value="footerProps.setPage"
|
|
467
|
+
/>
|
|
468
|
+
<v-pagination
|
|
469
|
+
v-else
|
|
470
|
+
v-model="currentPage"
|
|
471
|
+
density="compact"
|
|
472
|
+
:length="pageCount"
|
|
473
|
+
total-visible="6"
|
|
474
|
+
show-first-last-page
|
|
475
|
+
/>
|
|
476
|
+
</v-row>
|
|
477
|
+
</v-container>
|
|
478
|
+
</template>
|
|
479
|
+
</v-data-iterator>
|
|
480
|
+
<FormDialog
|
|
481
|
+
v-model="isDialogOpen"
|
|
482
|
+
:title="title"
|
|
483
|
+
:fullscreen="dialogFullscreen"
|
|
484
|
+
:initial-data="computedInitialData"
|
|
485
|
+
:form-data="currentItem"
|
|
486
|
+
@create="createItem"
|
|
487
|
+
@update="updateItem"
|
|
488
|
+
>
|
|
489
|
+
<template #default="slotData">
|
|
490
|
+
<slot
|
|
491
|
+
name="form"
|
|
492
|
+
v-bind="slotData"
|
|
493
|
+
/>
|
|
494
|
+
</template>
|
|
495
|
+
</FormDialog>
|
|
496
|
+
</v-card>
|
|
497
|
+
</template>
|