@ramathibodi/nuxt-commons 0.1.73 → 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.
Files changed (111) hide show
  1. package/README.md +115 -96
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +1 -0
  4. package/dist/runtime/components/Alert.vue +58 -54
  5. package/dist/runtime/components/BarcodeReader.vue +130 -122
  6. package/dist/runtime/components/ExportCSV.vue +110 -102
  7. package/dist/runtime/components/FileBtn.vue +79 -67
  8. package/dist/runtime/components/ImportCSV.vue +151 -139
  9. package/dist/runtime/components/MrzReader.vue +168 -0
  10. package/dist/runtime/components/SplitterPanel.vue +67 -59
  11. package/dist/runtime/components/TabsGroup.vue +39 -31
  12. package/dist/runtime/components/TextBarcode.vue +66 -54
  13. package/dist/runtime/components/device/IdCardButton.vue +95 -83
  14. package/dist/runtime/components/device/IdCardWebSocket.vue +207 -195
  15. package/dist/runtime/components/device/Scanner.vue +350 -338
  16. package/dist/runtime/components/dialog/Confirm.vue +112 -100
  17. package/dist/runtime/components/dialog/Host.vue +88 -84
  18. package/dist/runtime/components/dialog/Index.vue +84 -72
  19. package/dist/runtime/components/dialog/Loading.vue +51 -39
  20. package/dist/runtime/components/dialog/default/Confirm.vue +112 -100
  21. package/dist/runtime/components/dialog/default/Loading.vue +60 -48
  22. package/dist/runtime/components/dialog/default/Notify.vue +82 -70
  23. package/dist/runtime/components/dialog/default/Printing.vue +46 -34
  24. package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -132
  25. package/dist/runtime/components/document/Form.vue +50 -42
  26. package/dist/runtime/components/document/TemplateBuilder.vue +536 -524
  27. package/dist/runtime/components/form/ActionPad.vue +156 -144
  28. package/dist/runtime/components/form/Birthdate.vue +116 -104
  29. package/dist/runtime/components/form/CheckboxGroup.vue +99 -87
  30. package/dist/runtime/components/form/CodeEditor.vue +45 -37
  31. package/dist/runtime/components/form/Date.vue +270 -258
  32. package/dist/runtime/components/form/DateTime.vue +220 -208
  33. package/dist/runtime/components/form/Dialog.vue +178 -166
  34. package/dist/runtime/components/form/EditPad.vue +157 -145
  35. package/dist/runtime/components/form/File.vue +295 -283
  36. package/dist/runtime/components/form/Hidden.vue +44 -32
  37. package/dist/runtime/components/form/Iterator.vue +538 -526
  38. package/dist/runtime/components/form/Login.vue +143 -131
  39. package/dist/runtime/components/form/Pad.vue +399 -387
  40. package/dist/runtime/components/form/SignPad.vue +226 -218
  41. package/dist/runtime/components/form/System.vue +34 -26
  42. package/dist/runtime/components/form/Table.vue +391 -379
  43. package/dist/runtime/components/form/TableData.vue +236 -224
  44. package/dist/runtime/components/form/Time.vue +177 -165
  45. package/dist/runtime/components/form/images/Capture.vue +245 -237
  46. package/dist/runtime/components/form/images/Edit.vue +133 -121
  47. package/dist/runtime/components/form/images/Field.vue +331 -320
  48. package/dist/runtime/components/form/images/Pad.vue +54 -42
  49. package/dist/runtime/components/label/Date.vue +37 -29
  50. package/dist/runtime/components/label/DateAgo.vue +102 -94
  51. package/dist/runtime/components/label/DateCount.vue +152 -144
  52. package/dist/runtime/components/label/Field.vue +111 -103
  53. package/dist/runtime/components/label/FormatMoney.vue +37 -29
  54. package/dist/runtime/components/label/Mask.vue +46 -38
  55. package/dist/runtime/components/label/Object.vue +21 -13
  56. package/dist/runtime/components/master/Autocomplete.vue +89 -81
  57. package/dist/runtime/components/master/Combobox.vue +88 -80
  58. package/dist/runtime/components/master/RadioGroup.vue +90 -78
  59. package/dist/runtime/components/master/Select.vue +70 -62
  60. package/dist/runtime/components/master/label.vue +55 -47
  61. package/dist/runtime/components/model/Autocomplete.vue +91 -79
  62. package/dist/runtime/components/model/Combobox.vue +90 -78
  63. package/dist/runtime/components/model/Pad.vue +114 -102
  64. package/dist/runtime/components/model/Select.vue +78 -72
  65. package/dist/runtime/components/model/Table.vue +370 -358
  66. package/dist/runtime/components/model/iterator.vue +497 -489
  67. package/dist/runtime/components/model/label.vue +58 -50
  68. package/dist/runtime/components/pdf/Print.vue +75 -63
  69. package/dist/runtime/components/pdf/View.vue +146 -134
  70. package/dist/runtime/composables/alert.d.ts +4 -0
  71. package/dist/runtime/composables/api.d.ts +4 -0
  72. package/dist/runtime/composables/dialog.d.ts +1 -1
  73. package/dist/runtime/composables/document/templateFormHidden.d.ts +4 -0
  74. package/dist/runtime/composables/graphql.d.ts +1 -1
  75. package/dist/runtime/composables/graphqlModel.d.ts +9 -9
  76. package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
  77. package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
  78. package/dist/runtime/composables/localStorageModel.d.ts +4 -0
  79. package/dist/runtime/composables/lookupList.d.ts +4 -0
  80. package/dist/runtime/composables/menu.d.ts +4 -0
  81. package/dist/runtime/composables/useMrzReader.d.ts +48 -0
  82. package/dist/runtime/composables/useMrzReader.js +423 -0
  83. package/dist/runtime/composables/useTesseract.d.ts +16 -0
  84. package/dist/runtime/composables/useTesseract.js +45 -0
  85. package/dist/runtime/composables/userPermission.d.ts +1 -1
  86. package/dist/runtime/labs/Calendar.vue +99 -99
  87. package/dist/runtime/labs/form/EditMobile.vue +152 -152
  88. package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
  89. package/dist/runtime/plugins/clientConfig.d.ts +1 -1
  90. package/dist/runtime/plugins/default.d.ts +1 -1
  91. package/dist/runtime/plugins/dialogManager.d.ts +1 -1
  92. package/dist/runtime/plugins/permission.d.ts +1 -1
  93. package/dist/runtime/types/alert.d.ts +11 -11
  94. package/dist/runtime/types/clientConfig.d.ts +13 -13
  95. package/dist/runtime/types/dialogManager.d.ts +35 -35
  96. package/dist/runtime/types/formDialog.d.ts +5 -5
  97. package/dist/runtime/types/graphqlOperation.d.ts +23 -23
  98. package/dist/runtime/types/menu.d.ts +31 -31
  99. package/dist/runtime/types/modules.d.ts +7 -7
  100. package/dist/runtime/types/permission.d.ts +13 -13
  101. package/dist/runtime/utils/asset.d.ts +2 -0
  102. package/dist/runtime/utils/asset.js +49 -0
  103. package/package.json +131 -122
  104. package/scripts/enrich-vue-docs-from-ai.mjs +197 -0
  105. package/scripts/generate-ai-summary.mjs +321 -0
  106. package/scripts/generate-composables-md.mjs +129 -0
  107. package/scripts/postInstall.cjs +70 -70
  108. package/templates/.codegen/codegen.ts +32 -32
  109. package/templates/.codegen/plugin-schema-object.js +161 -161
  110. package/templates/public/tesseract/mrz.traineddata.gz +0 -0
  111. package/templates/public/tesseract/ocrb.traineddata.gz +0 -0
@@ -1,489 +1,497 @@
1
- <script lang="ts" setup>
2
- import {computed, nextTick, ref, useAttrs, useSlots, watch} from 'vue'
3
- import {VDataIterator} from 'vuetify/components/VDataIterator'
4
- import {VDataTable} from "vuetify/components/VDataTable";
5
- import {omit} from 'lodash-es'
6
- import type {GraphqlModelProps} from '../../composables/graphqlModel'
7
- import {useGraphqlModel} from '../../composables/graphqlModel'
8
- import {useDisplay} from 'vuetify'
9
-
10
- defineOptions({
11
- inheritAttrs: false,
12
- })
13
-
14
- interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$props']> {
15
- title: string
16
- noDataText?: string
17
- dialogFullscreen?: boolean
18
- initialData?: Record<string, any>
19
- toolbarColor?: string
20
- importable?: boolean
21
- exportable?: boolean
22
- insertable?: boolean
23
- searchable?: boolean
24
- search?: string
25
-
26
- viewSwitch?: boolean
27
- viewSwitchMultiple?: boolean
28
-
29
- cols?: string | number | boolean
30
- xxl?: string | number | boolean
31
- xl?: string | number | boolean
32
- lg?: string | number | boolean
33
- md?: string | number | boolean
34
- sm?: string | number | boolean
35
- itemsPerPage?: string | number
36
-
37
- preferTable?: string | number | boolean
38
- preferTableXxl?: string | number | boolean
39
- preferTableXl?: string | number | boolean
40
- preferTableLg?: string | number | boolean
41
- preferTableMd?: string | number | boolean
42
- preferTableSm?: string | number | boolean
43
- }
44
-
45
- const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
46
- noDataText: 'ไม่พบข้อมูล',
47
- dialogFullscreen: false,
48
- toolbarColor: 'primary',
49
- importable: false,
50
- exportable: false,
51
- insertable: true,
52
- searchable: true,
53
- modelKey: 'id',
54
- modelBy: undefined,
55
- fields: () => [],
56
-
57
- viewSwitch: false,
58
- viewSwitchMultiple:false,
59
-
60
- cols: 12,
61
- xxl: false,
62
- xl: false,
63
- lg: 2,
64
- md: 4,
65
- sm: 6,
66
- itemsPerPage: 12,
67
- })
68
-
69
- const attrs = useAttrs()
70
- const plainAttrs = computed(() => {
71
- const returnAttrs = omit(attrs, ['modelValue', 'onUpdate:modelValue'])
72
- if (props.headers) returnAttrs['headers'] = props.headers
73
- return returnAttrs
74
- })
75
- const slots = useSlots()
76
- const tableSlots = computed(() => {
77
- return omit(slots, ['item'])
78
- })
79
-
80
- const display = useDisplay()
81
- const isProgrammaticSet = ref(false)
82
- const userOverrodeView = ref(false)
83
-
84
- const viewType = ref<string[] | string>('iterator')
85
-
86
- function setViewTypeSafely(val: 'iterator' | 'table') {
87
- isProgrammaticSet.value = true
88
- if (Array.isArray(viewType.value)) {
89
- viewType.value = [val]
90
- } else {
91
- viewType.value = val
92
- }
93
- nextTick(() => { isProgrammaticSet.value = false })
94
- }
95
-
96
- watch(viewType, () => {
97
- if (!isProgrammaticSet.value) userOverrodeView.value = true
98
- })
99
-
100
- function parsePrefer(val: unknown): boolean | number {
101
- if (val === true) return true
102
- const n = Number(val)
103
- return Number.isFinite(n) && n > 0 ? n : false
104
- }
105
-
106
- const computedPreferTable = computed<boolean | number>(() => {
107
- const bp = display.name?.value
108
- if (bp === 'xxl' && props.preferTableXxl !== undefined) return parsePrefer(props.preferTableXxl)
109
- if (bp === 'xl' && props.preferTableXl !== undefined) return parsePrefer(props.preferTableXl)
110
- if (bp === 'lg' && props.preferTableLg !== undefined) return parsePrefer(props.preferTableLg)
111
- if (bp === 'md' && props.preferTableMd !== undefined) return parsePrefer(props.preferTableMd)
112
- if (bp === 'sm' && props.preferTableSm !== undefined) return parsePrefer(props.preferTableSm)
113
- return parsePrefer(props.preferTable)
114
- })
115
-
116
- const currentItem = ref<Record<string, any> | undefined>(undefined)
117
- const isDialogOpen = ref<boolean>(false)
118
-
119
- const { items, itemsLength,
120
- search, setSearch, currentOptions,
121
- canServerPageable, canServerSearch, canCreate, canUpdate, canDelete,
122
- createItem, importItems, updateItem, deleteItem,
123
- loadItems, reload,
124
- isLoading } = useGraphqlModel(props)
125
-
126
- function openDialog(item?: object) {
127
- currentItem.value = item
128
- nextTick(() => {
129
- isDialogOpen.value = true
130
- })
131
- }
132
-
133
- const itemsPerPageInternal = ref<string | number>()
134
- watch(() => props.itemsPerPage, (newValue) => {
135
- if (newValue.toString().toLowerCase() == 'all') itemsPerPageInternal.value = '-1'
136
- else if (newValue) itemsPerPageInternal.value = newValue
137
- }, { immediate: true })
138
-
139
- type SortItem = { key: string, order?: boolean | 'asc' | 'desc' }
140
- const sortBy = ref<SortItem[]>()
141
-
142
- const pageCount = computed(() => {
143
- if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value == '-1' || Number(itemsPerPageInternal.value) <= 0) return 1
144
- else return Math.ceil(itemsLength.value / Number(itemsPerPageInternal.value))
145
- })
146
- const currentPage = ref<number>(1)
147
-
148
- watch([currentPage, itemsPerPageInternal, sortBy], () => {
149
- if (canServerPageable.value) {
150
- loadItems({
151
- page: currentPage.value,
152
- itemsPerPage: (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) ? '-1' : itemsPerPageInternal.value,
153
- sortBy: sortBy.value,
154
- })
155
- }
156
- }, { immediate: true })
157
-
158
- watch(
159
- [itemsLength, computedPreferTable, () => display.name?.value],
160
- ([len, prefer]) => {
161
- if (userOverrodeView.value) return // respect explicit user choice forever (until remount)
162
-
163
- let target: 'iterator' | 'table' = 'iterator'
164
- if (prefer === true) {
165
- target = 'table'
166
- } else if (typeof prefer === 'number') {
167
- if (Number(len) >= prefer) target = 'table'
168
- }
169
-
170
- if (!viewType.value?.includes(target)) setViewTypeSafely(target)
171
- },
172
- { immediate: true }
173
- )
174
-
175
- const operation = ref({ openDialog, createItem, importItems, updateItem, deleteItem, reload, setSearch, canServerPageable, canServerSearch, canCreate, canUpdate, canDelete })
176
-
177
- const computedInitialData = computed(() => {
178
- return Object.assign({}, props.initialData, props.modelBy)
179
- })
180
-
181
- const computedSkeletonPerPage = computed(() => {
182
- if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) return 1
183
- else return Number(itemsPerPageInternal.value)
184
- })
185
-
186
- watch(()=>props.search,()=>{
187
- search.value = props.search
188
- },{immediate:true})
189
-
190
- defineExpose({ reload,operation })
191
- </script>
192
-
193
- <template>
194
- <v-card>
195
- <v-data-iterator
196
- v-bind="plainAttrs"
197
- v-model:items-per-page="itemsPerPageInternal"
198
- v-model:sort-by="sortBy"
199
- :items="items"
200
- :item-value="modelKey"
201
- :search="search"
202
- :loading="isLoading"
203
- >
204
- <template #default="defaultProps" v-if="viewType.includes('iterator')">
205
- <slot
206
- v-bind="defaultProps"
207
- :operation="operation"
208
- >
209
- <v-container fluid>
210
- <v-row>
211
- <v-col
212
- v-for="(item, index) in defaultProps.items"
213
- :key="index"
214
- :cols="cols"
215
- :sm="sm"
216
- :md="md"
217
- :lg="lg"
218
- :xl="xl"
219
- :xxl="xxl"
220
- >
221
- <slot
222
- name="item"
223
- :item="item"
224
- :operation="operation"
225
- />
226
- </v-col>
227
- </v-row>
228
- </v-container>
229
- </slot>
230
- </template>
231
- <template #loader="loaderProps" v-if="viewType.includes('iterator')">
232
- <slot
233
- name="loader"
234
- v-bind="loaderProps"
235
- >
236
- <v-container fluid>
237
- <v-row>
238
- <v-col
239
- v-for="key in computedSkeletonPerPage"
240
- :key="key"
241
- :cols="cols"
242
- :sm="sm"
243
- :md="md"
244
- :lg="lg"
245
- :xl="xl"
246
- :xxl="xxl"
247
- >
248
- <slot name="loaderItem">
249
- <v-skeleton-loader
250
- type="paragraph"
251
- />
252
- </slot>
253
- </v-col>
254
- </v-row>
255
- </v-container>
256
- </slot>
257
- </template>
258
- <template #header="headerProps">
259
- <slot
260
- name="header"
261
- v-bind="headerProps"
262
- :items="items"
263
- :operation="operation"
264
- >
265
- <VToolbar :color="toolbarColor">
266
- <v-row
267
- justify="end"
268
- class="ma-1"
269
- dense
270
- no-gutters
271
- align="center"
272
- >
273
- <v-col cols="7">
274
- <VToolbarTitle class="pl-3">
275
- <slot
276
- name="title"
277
- :reload="reload"
278
- >
279
- {{ title }}
280
- <v-icon
281
- size="small"
282
- @click="reload"
283
- >
284
- mdi mdi-refresh
285
- </v-icon>
286
- </slot>
287
- </VToolbarTitle>
288
- </v-col>
289
- <v-col cols="5">
290
- <slot name="search" :items="items" :operation="operation" v-if="props.searchable">
291
- <VTextField
292
- v-model="search"
293
- class="justify-end w-100"
294
- density="compact"
295
- hide-details
296
- placeholder="ค้นหา"
297
- clearable
298
- variant="solo"
299
- />
300
- </slot>
301
- </v-col>
302
- </v-row>
303
-
304
- <VToolbarItems>
305
- <slot name="toolbarItems" :items="items" :operation="operation"/>
306
- <ImportCSV
307
- v-if="props.importable && canCreate && props.insertable"
308
- icon="mdi mdi-file-upload"
309
- variant="flat"
310
- :color="toolbarColor"
311
- @import="importItems"
312
- />
313
- <ExportCSV
314
- v-if="props.exportable && items.length"
315
- icon="mdi mdi-file-download"
316
- variant="flat"
317
- :file-name="title"
318
- :model-value="items"
319
- :color="toolbarColor"
320
- />
321
- <VBtn
322
- v-if="canCreate && props.insertable"
323
- :color="toolbarColor"
324
- prepend-icon="mdi mdi-plus"
325
- variant="flat"
326
- @click="openDialog()"
327
- >
328
- add
329
- </VBtn>
330
- <v-row align="center" class="mr-2 ml-2" v-if="viewSwitch">
331
- <v-btn-toggle v-model="viewType" :multiple="viewSwitchMultiple">
332
- <v-btn icon="mdi mdi-view-grid-outline" value="iterator"></v-btn>
333
- <v-btn icon="mdi mdi-table-large" value="table"></v-btn>
334
- </v-btn-toggle>
335
- </v-row>
336
- </VToolbarItems>
337
- </VToolbar>
338
- </slot>
339
- <template v-if="viewType.includes('table')">
340
- <v-data-table-server
341
- v-if="canServerPageable"
342
- v-bind="plainAttrs"
343
- color="primary"
344
- :items="items"
345
- :items-length="itemsLength"
346
- :item-value="props.modelKey"
347
- :search="search"
348
- :loading="isLoading"
349
- @update:options="loadItems"
350
- >
351
- <!-- @ts-ignore -->
352
- <template
353
- v-for="(_, name, index) in (tableSlots as {})"
354
- :key="index"
355
- #[name]="slotData"
356
- >
357
- <slot
358
- :name="name"
359
- v-bind="((slotData || {}) as object)"
360
- :operation="operation"
361
- />
362
- </template>
363
- <template
364
- v-if="!$slots['item.action']"
365
- #item.action="{ item }"
366
- >
367
- <v-btn
368
- v-if="canUpdate"
369
- variant="flat"
370
- density="compact"
371
- icon="mdi mdi-note-edit"
372
- @click="openDialog(item)"
373
- />
374
- <v-btn
375
- v-if="canDelete"
376
- variant="flat"
377
- density="compact"
378
- icon="mdi mdi-delete"
379
- @click="deleteItem(item)"
380
- />
381
- </template>
382
- </v-data-table-server>
383
- <v-data-table
384
- v-else
385
- v-bind="plainAttrs"
386
- color="primary"
387
- :items="items"
388
- :item-value="props.modelKey"
389
- :search="search"
390
- :loading="isLoading"
391
- >
392
- <!-- @ts-ignore -->
393
- <template
394
- v-for="(_, name, index) in (tableSlots as {})"
395
- :key="index"
396
- #[name]="slotData"
397
- >
398
- <slot
399
- :name="name"
400
- v-bind="((slotData || {}) as object)"
401
- :operation="operation"
402
- />
403
- </template>
404
- <template
405
- v-if="!$slots['item.action']"
406
- #item.action="{ item }"
407
- >
408
- <v-btn
409
- v-if="canUpdate"
410
- variant="flat"
411
- density="compact"
412
- icon="mdi mdi-note-edit"
413
- @click="openDialog(item)"
414
- />
415
- <v-btn
416
- v-if="canDelete"
417
- variant="flat"
418
- density="compact"
419
- icon="mdi mdi-delete"
420
- @click="deleteItem(item)"
421
- />
422
- </template>
423
- </v-data-table>
424
- </template>
425
- </template>
426
- <template #footer="footerProps" v-if="viewType.includes('iterator')">
427
- <v-container fluid>
428
- <v-row
429
- align-content="center"
430
- justify="end"
431
- dense
432
- >
433
- <v-spacer />
434
- <v-select
435
- v-model="itemsPerPageInternal"
436
- density="compact"
437
- variant="outlined"
438
- :items="[6, 12, 18, 24, 30, { title: 'All', value: '-1' }]"
439
- min-width="220"
440
- max-width="220"
441
- hide-details
442
- >
443
- <template #prepend>
444
- Items per page:
445
- </template>
446
- </v-select>
447
- <v-pagination
448
- v-if="!canServerPageable"
449
- v-model="currentPage"
450
- density="compact"
451
- :length="footerProps.pageCount"
452
- total-visible="6"
453
- show-first-last-page
454
- @first="footerProps.setPage(1)"
455
- @last="footerProps.setPage(footerProps.pageCount)"
456
- @next="footerProps.nextPage"
457
- @prev="footerProps.prevPage"
458
- @update:model-value="footerProps.setPage"
459
- />
460
- <v-pagination
461
- v-else
462
- v-model="currentPage"
463
- density="compact"
464
- :length="pageCount"
465
- total-visible="6"
466
- show-first-last-page
467
- />
468
- </v-row>
469
- </v-container>
470
- </template>
471
- </v-data-iterator>
472
- <FormDialog
473
- v-model="isDialogOpen"
474
- :title="title"
475
- :fullscreen="dialogFullscreen"
476
- :initial-data="computedInitialData"
477
- :form-data="currentItem"
478
- @create="createItem"
479
- @update="updateItem"
480
- >
481
- <template #default="slotData">
482
- <slot
483
- name="form"
484
- v-bind="slotData"
485
- />
486
- </template>
487
- </FormDialog>
488
- </v-card>
489
- </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>