@ramathibodi/nuxt-commons 0.1.54 → 0.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.1.54",
7
+ "version": "0.1.56",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -5,6 +5,7 @@ import type {FormDialogCallback} from '../../types/formDialog'
5
5
  import {VDataIterator} from "vuetify/components/VDataIterator";
6
6
  import {VDataTable} from "vuetify/components/VDataTable";
7
7
  import {VInput} from 'vuetify/components/VInput'
8
+ import {useDisplay} from 'vuetify'
8
9
 
9
10
  defineOptions({
10
11
  inheritAttrs: false,
@@ -35,6 +36,13 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
35
36
  md?: string | number | boolean
36
37
  sm?: string | number | boolean
37
38
  itemsPerPage?: string | number
39
+
40
+ preferTable?: string | number | boolean
41
+ preferTableXxl?: string | number | boolean
42
+ preferTableXl?: string | number | boolean
43
+ preferTableLg?: string | number | boolean
44
+ preferTableMd?: string | number | boolean
45
+ preferTableSm?: string | number | boolean
38
46
  }
39
47
 
40
48
  const props = withDefaults(defineProps<Props>(), {
@@ -75,8 +83,42 @@ const tableSlots = computed(() => {
75
83
  return omit(slots, ['item'])
76
84
  })
77
85
 
86
+ const display = useDisplay()
87
+ const isProgrammaticSet = ref(false)
88
+ const userOverrodeView = ref(false)
89
+
78
90
  const viewType = ref<string[] | string>('iterator')
79
91
 
92
+ function setViewTypeSafely(val: 'iterator' | 'table') {
93
+ isProgrammaticSet.value = true
94
+ if (Array.isArray(viewType.value)) {
95
+ viewType.value = [val]
96
+ } else {
97
+ viewType.value = val
98
+ }
99
+ nextTick(() => { isProgrammaticSet.value = false })
100
+ }
101
+
102
+ watch(viewType, () => {
103
+ if (!isProgrammaticSet.value) userOverrodeView.value = true
104
+ })
105
+
106
+ function parsePrefer(val: unknown): boolean | number {
107
+ if (val === true) return true
108
+ const n = Number(val)
109
+ return Number.isFinite(n) && n > 0 ? n : false
110
+ }
111
+
112
+ const computedPreferTable = computed<boolean | number>(() => {
113
+ const bp = display.name?.value
114
+ if (bp === 'xxl' && props.preferTableXxl !== undefined) return parsePrefer(props.preferTableXxl)
115
+ if (bp === 'xl' && props.preferTableXl !== undefined) return parsePrefer(props.preferTableXl)
116
+ if (bp === 'lg' && props.preferTableLg !== undefined) return parsePrefer(props.preferTableLg)
117
+ if (bp === 'md' && props.preferTableMd !== undefined) return parsePrefer(props.preferTableMd)
118
+ if (bp === 'sm' && props.preferTableSm !== undefined) return parsePrefer(props.preferTableSm)
119
+ return parsePrefer(props.preferTable)
120
+ })
121
+
80
122
  const items = ref<Record<string, any>[]>([])
81
123
  const search = ref<string>()
82
124
  const currentItem = ref<Record<string, any> | undefined>(undefined)
@@ -109,6 +151,23 @@ watch(items, (newValue) => {
109
151
  emit('update:modelValue', newValue)
110
152
  }, { deep: true })
111
153
 
154
+ watch(
155
+ [() => items.value?.length, computedPreferTable, () => display.name?.value],
156
+ ([len, prefer]) => {
157
+ if (userOverrodeView.value) return // respect explicit user choice forever (until remount)
158
+
159
+ let target: 'iterator' | 'table' = 'iterator'
160
+ if (prefer === true) {
161
+ target = 'table'
162
+ } else if (typeof prefer === 'number') {
163
+ if (Number(len) >= prefer) target = 'table'
164
+ }
165
+
166
+ if (!viewType.value?.includes(target)) setViewTypeSafely(target)
167
+ },
168
+ { immediate: true }
169
+ )
170
+
112
171
  const itemsPerPageInternal = ref<string | number>()
113
172
  watch(() => props.itemsPerPage, (newValue) => {
114
173
  if (newValue.toString().toLowerCase() == 'all') itemsPerPageInternal.value = '-1'
@@ -7,6 +7,7 @@ type Locale = 'th' | 'en' | 'en-US' | 'th-TH';
7
7
 
8
8
  interface Props {
9
9
  modelValue: DateTime;
10
+ endDate?: DateTime;
10
11
  locale?: Locale;
11
12
  showSuffix?: boolean;
12
13
  units?: Unit[];
@@ -49,15 +50,16 @@ const localizedSuffix: Record<'en' | 'th', string> = {
49
50
  };
50
51
 
51
52
  const outputText = computed(() => {
52
- const now = DateTime.now();
53
+ const base = props.endDate ?? DateTime.now(); // 👈 ใช้ baseDate ถ้ามี ไม่งั้น fallback เป็น now
53
54
  const baseLocale = normalizeLocale(props.locale);
54
55
 
55
56
  const units: Unit[] = props.units?.length
56
57
  ? props.units
57
58
  : ['years', 'months', 'days', 'hours', 'minutes', 'seconds'];
58
59
 
59
- const diff = now.diff(props.modelValue, units).toObject();
60
+ const diff = base.diff(props.modelValue, units).toObject();
60
61
 
62
+ // แสดงแบบ single unit
61
63
  if (!props.units) {
62
64
  const foundUnit = units.find(unit => (diff[unit] ?? 0) >= 1);
63
65
  const value = Math.floor(diff[foundUnit!] ?? 0);
@@ -66,6 +68,7 @@ const outputText = computed(() => {
66
68
  return `${value} ${label}${suffix}`;
67
69
  }
68
70
 
71
+ // แสดงหลาย unit
69
72
  const parts = units.map(unit => {
70
73
  const value = Math.floor(diff[unit] ?? 0);
71
74
  if (value > 0) {
@@ -79,14 +82,14 @@ const outputText = computed(() => {
79
82
  const lastUnit = units.at(-1)!;
80
83
  const label = localizedLabels[baseLocale][lastUnit];
81
84
  const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
82
- return `0 ${label} ${suffix}`;
85
+ return `0 ${label}${suffix}`;
83
86
  }
84
87
 
85
88
  const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
86
- return `${parts.join(' ')} ${suffix}`;
89
+ return `${parts.join(' ')}${suffix}`;
87
90
  });
88
91
  </script>
89
92
 
90
93
  <template>
91
94
  <span>{{ outputText }}</span>
92
- </template>
95
+ </template>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import {VAutocomplete} from 'vuetify/components/VAutocomplete'
3
- import {concat} from 'lodash-es'
3
+ import {union} from 'lodash-es'
4
4
  import {computed} from 'vue'
5
5
 
6
6
  interface Props extends /* @vue-ignore */ InstanceType<typeof VAutocomplete['$props']> {
@@ -16,6 +16,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VAutocomplete['$pr
16
16
  waitForFilter?: boolean
17
17
  waitForFilterText?: string
18
18
 
19
+ meilisearch?: boolean
20
+
19
21
  cache?: boolean | number
20
22
  }
21
23
 
@@ -27,6 +29,8 @@ const props = withDefaults(defineProps<Props>(), {
27
29
  noDataText: 'ไม่พบข้อมูล',
28
30
  waitForFilter: false,
29
31
 
32
+ meilisearch: false,
33
+
30
34
  cache: false,
31
35
  })
32
36
 
@@ -34,6 +38,7 @@ const computedModelName = computed(()=>{
34
38
  let operation = 'masterItemByGroupKey'
35
39
  if (props.filterText) operation = 'masterItemByGroupKeyAndFilterText'
36
40
  if (props.itemCodes && props.itemCodes.length>0) operation = 'masterItemByGroupKeyAndItemCodeIn'
41
+ if (props.meilisearch) operation = 'masterItemByGroupKeyAndFilterTextAndKeyword'
37
42
  if (props.waitForFilter && !props.filterText) operation = ""
38
43
  return operation
39
44
  })
@@ -49,14 +54,29 @@ const computedModelBy = computed(()=>{
49
54
  return modelBy
50
55
  })
51
56
 
57
+ const computedModelSelectedItemBy = computed(()=>{
58
+ return {groupKey: props.groupKey}
59
+ })
60
+
52
61
  const computedFields = computed(()=>{
53
- return concat(['itemCode', 'itemValue', 'itemValueAlternative'],props.fields)
62
+ let fields : Array<string | object> = []
63
+ if (props.filterText) fields.push('filterText')
64
+ if (props.meilisearch) fields.push('_formatted')
65
+ return union(['itemCode', 'itemValue', 'itemValueAlternative'],fields,props.fields || [])
54
66
  })
55
67
 
56
68
  const itemTitleField = computed(() => {
57
69
  return (props.lang == 'TH') ? 'itemValue' : 'itemValueAlternative'
58
70
  })
59
71
 
72
+ const formatItemTitle = (item: any)=>{
73
+ if (props.meilisearch) {
74
+ return (props.showCode ? item.raw?._formatted?.itemCode+'-' : '')+(item.raw?._formatted?.[itemTitleField.value] || item.raw?._formatted?.itemValue || item.raw?._formatted?.itemCode)
75
+ } else {
76
+ return (props.showCode ? item.raw.itemCode+'-' : '')+(item.title || item.raw.itemValue || item.raw.itemCode)
77
+ }
78
+ }
79
+
60
80
  const computedNoDataText = computed(() => {
61
81
  if (props.waitForFilter && !props.filterText) return props.waitForFilterText
62
82
  return props.noDataText
@@ -82,7 +102,12 @@ const computedSortBy = computed(()=>{
82
102
  item-value="itemCode"
83
103
  :no-data-text="computedNoDataText"
84
104
  :show-code="props.showCode"
85
- :cache="props.cache"
105
+ :cache="props.cache && !props.meilisearch"
106
+ :server-search="props.meilisearch && !!computedModelName"
107
+ server-search-key="keyword"
108
+ model-selected-item="masterItemByGroupKeyAndItemCodeIn"
109
+ :model-selected-item-by="computedModelSelectedItemBy"
110
+ model-selected-item-key="itemCodes"
86
111
  >
87
112
  <template
88
113
  v-for="(_, name, index) in ($slots as {})"
@@ -97,13 +122,35 @@ const computedSortBy = computed(()=>{
97
122
  </template>
98
123
 
99
124
  <template
100
- v-if="!$slots.item"
125
+ v-if="!$slots.item && !meilisearch"
101
126
  #item="{ props, item }"
102
127
  >
103
128
  <v-list-item
104
129
  v-bind="props"
105
- :title="item.title"
130
+ :title="formatItemTitle(item)"
106
131
  />
107
132
  </template>
133
+
134
+ <template
135
+ v-if="!$slots.item && meilisearch"
136
+ #item="{ props, item }"
137
+ >
138
+ <v-list-item v-bind="props">
139
+ <template #title v-if="item.raw?._formatted?.itemValue">
140
+ <span v-html="formatItemTitle(item)" class="meilisearch-item"></span>
141
+ </template>
142
+ </v-list-item>
143
+ </template>
144
+
145
+ <template
146
+ v-if="!$slots.selection && showCode"
147
+ #selection="{ item }"
148
+ >
149
+ {{ formatItemTitle(item) }}
150
+ </template>
108
151
  </model-autocomplete>
109
152
  </template>
153
+
154
+ <style>
155
+ .meilisearch-item em{font-weight:700}
156
+ </style>
@@ -1,10 +1,10 @@
1
1
  <script lang="ts" setup>
2
- import {VAutocomplete} from 'vuetify/components/VAutocomplete'
3
- import {concat, isEmpty, isArray, sortBy} from 'lodash-es'
4
- import {computed, ref,watchEffect} from 'vue'
5
- import {watchDebounced} from '@vueuse/core'
6
- import {useFuzzy} from '../../composables/utils/fuzzy'
7
- import {useGraphQlOperation} from "../../composables/graphqlOperation";
2
+ import { VAutocomplete } from 'vuetify/components/VAutocomplete'
3
+ import { union, isEmpty, isArray, sortBy, castArray } from 'lodash-es'
4
+ import { computed, ref, watch, defineModel } from 'vue'
5
+ import { watchDebounced } from '@vueuse/core'
6
+ import { useFuzzy } from '../../composables/utils/fuzzy'
7
+ import { useGraphQlOperation } from '../../composables/graphqlOperation'
8
8
 
9
9
  interface Props extends /* @vue-ignore */ InstanceType<typeof VAutocomplete['$props']> {
10
10
  fuzzy?: boolean
@@ -12,7 +12,7 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VAutocomplete['$pr
12
12
  showCode?: boolean
13
13
 
14
14
  modelName: string
15
- modelBy?: object
15
+ modelBy?: Record<string, any>
16
16
  itemTitle: string
17
17
  itemValue: string
18
18
  fields?: Array<string | object>
@@ -20,105 +20,232 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VAutocomplete['$pr
20
20
 
21
21
  serverSearch?: boolean
22
22
  serverSearchKey?: string
23
+ searchSearchSort?: boolean
24
+ serverSearchDebounce?: number
25
+
26
+ serverSearchText?: string
27
+ serverSearchLoadingText?: string
28
+
29
+ modelSelectedItem?: string
30
+ modelSelectedItemBy?: Record<string, any>
31
+ modelSelectedItemKey?: string
32
+
33
+ filterKeys?: string|string[]
34
+ noDataText?: string
35
+ placeholder?: string
36
+ multiple?: boolean
23
37
  }
24
38
 
25
39
  const props = withDefaults(defineProps<Props>(), {
26
40
  fuzzy: false,
27
41
  showCode: false,
28
-
29
42
  cache: false,
30
-
31
43
  serverSearch: false,
44
+ searchSearchSort: false,
45
+ serverSearchText: "Type to search…",
46
+ serverSearchLoadingText: "Searching…",
47
+ serverSearchDebounce: 500,
32
48
  })
33
49
 
50
+ const emit = defineEmits<{
51
+ (e: 'update:selectedObject', object: any|any[]): void
52
+ }>()
53
+
34
54
  const modelItems = ref<Array<any>>([])
35
55
  const items = ref<Array<any>>([])
36
-
37
56
  const searchData = ref<string>('')
38
57
 
39
58
  const isLoading = ref(false)
40
59
  const isErrorLoading = ref(false)
41
60
 
42
- watchEffect(()=>{
43
- let fields: any[] = [props.itemTitle,props.itemValue]
44
- if (props.fields) fields = concat(fields, props.fields)
61
+ const queryFields = computed(() => {
62
+ let fieldsArray: any[] = [props.itemTitle, props.itemValue]
63
+ if (props.fields) fieldsArray = union(fieldsArray, props.fields)
64
+ return fieldsArray
65
+ })
66
+
67
+ const computedFilterKeys = computed(() => {
68
+ if (props.filterKeys) return props.filterKeys
69
+ return ['title','raw.'+props.itemValue]
70
+ })
71
+
72
+ const computedPlaceholder = computed(() => {
73
+ if (!props.serverSearch) return props.placeholder
74
+ return isLoading.value ? props.serverSearchLoadingText : props.serverSearchText
75
+ })
76
+
77
+ const computedNoDataText = computed(() => {
78
+ if (!props.serverSearch) return props.noDataText
79
+ if (isLoading.value) return props.serverSearchLoadingText
80
+ if (!searchData.value) return props.serverSearchText
81
+ return props.noDataText
82
+ })
83
+
84
+ let requestId = 0
45
85
 
46
- const variables: Record<string, any> = Object.assign({},props.modelBy)
86
+ async function loadItems() {
87
+ if (!props.modelName) {
88
+ modelItems.value = []
89
+ items.value = []
90
+ return
91
+ }
92
+
93
+ const id = ++requestId
94
+ isLoading.value = true
95
+
96
+ const variables: Record<string, any> = { ...(props.modelBy || {}) }
47
97
  if (props.serverSearch && props.serverSearchKey) {
48
98
  variables[props.serverSearchKey] = searchData.value
49
99
  }
50
100
 
51
- if (props.modelName) {
52
- isLoading.value = true
53
-
54
- useGraphQlOperation('Query',props.modelName,fields,variables,props.cache).then((result: any) => {
55
- if (isArray(result)) {
56
- modelItems.value = result
57
- items.value = result
58
- isErrorLoading.value = false
59
- } else {
60
- isErrorLoading.value = true
61
- }
62
- }).catch((_error) => {
101
+ try {
102
+ const result: any = await useGraphQlOperation('Query', props.modelName, queryFields.value, variables, props.cache)
103
+
104
+ if (id !== requestId) return
105
+
106
+ if (isArray(result)) {
107
+ modelItems.value = result
108
+ items.value = result
109
+ isErrorLoading.value = false
110
+ } else {
111
+ isErrorLoading.value = true
63
112
  modelItems.value = []
64
113
  items.value = []
65
- isErrorLoading.value = true
66
- }).finally(() => {
67
- isLoading.value = false
68
- })
69
- } else {
114
+ }
115
+ } catch (_error) {
116
+ if (id !== requestId) return
117
+ isErrorLoading.value = true
70
118
  modelItems.value = []
71
119
  items.value = []
120
+ } finally {
121
+ if (id === requestId) isLoading.value = false
122
+ }
123
+ }
124
+
125
+ function applyFuzzy() {
126
+ if (!props.fuzzy) return
127
+
128
+ if (isEmpty(searchData.value)) {
129
+ items.value = modelItems.value
130
+ return
72
131
  }
73
- })
74
132
 
75
- async function fuzzySearch() {
76
- if (props.fuzzy) {
77
- if (isEmpty(searchData.value)) {
78
- items.value = modelItems.value
133
+ const results: any = useFuzzy(searchData.value, modelItems.value, queryFields.value)
134
+ const output: any[] = []
135
+ if (results.value?.length) {
136
+ for (let index = 0; index < results.value.length; index++) {
137
+ const result = results.value[index]
138
+ if (result?.item) output.push(result.item)
79
139
  }
80
- else {
81
- let fields: any[] = [props.itemTitle,props.itemValue]
82
- if (props.fields) fields = concat(fields, props.fields)
140
+ }
141
+ items.value = output
142
+ }
83
143
 
84
- const results: any = useFuzzy(searchData.value, modelItems.value, fields)
85
- items.value = []
86
- if (results.value.length) {
87
- for (let i = 0; results.value.length > i; i++) {
88
- if (results.value[i].item) items.value.push(results.value[i].item)
89
- }
90
- }
144
+ const selectedItems = defineModel<any>()
145
+ const selectedItemsObject = ref<any[]>([])
146
+ async function syncSelectedFromModelValue() {
147
+ const modelValueData: any = selectedItems.value
148
+ const values = castArray(modelValueData).filter(value => value !== undefined && value !== null && value !== '')
149
+
150
+ if (!values.length) {
151
+ emit("update:selectedObject",(props.multiple) ? [] : undefined)
152
+ selectedItemsObject.value = []
153
+ return
154
+ }
155
+
156
+ let alreadyInObject : any[] = selectedItemsObject.value?.filter(item => values.includes(item?.[props.itemValue])) || []
157
+
158
+ const haveSet = new Set<any>([...alreadyInObject.map(item => item?.[props.itemValue])])
159
+ const missingValues: any[] = values.filter(value => !haveSet.has(value))
160
+
161
+ const stillMissing: any[] = []
162
+ for (const value of missingValues) {
163
+ const localHit = modelItems.value.find(item => item?.[props.itemValue] === value)
164
+ if (localHit) {
165
+ alreadyInObject.push(localHit)
166
+ haveSet.add(value)
167
+ } else {
168
+ stillMissing.push(value)
91
169
  }
92
170
  }
171
+
172
+ if (stillMissing.length && props.modelSelectedItem) {
173
+ try {
174
+ const key = props.modelSelectedItemKey || 'id'
175
+ const variables: Record<string, any> = { ...(props.modelSelectedItemBy || {}) }
176
+ variables[key] = values
177
+
178
+ const result: any = await useGraphQlOperation('Query', props.modelSelectedItem, queryFields.value,variables,props.cache)
179
+ selectedItemsObject.value = castArray(result)
180
+ } catch (_error) {
181
+ void _error
182
+ }
183
+ } else {
184
+ selectedItemsObject.value = alreadyInObject
185
+ }
186
+
187
+ emit("update:selectedObject",(props.multiple) ? selectedItemsObject.value : selectedItemsObject.value[0])
93
188
  }
94
189
 
95
- watchDebounced(searchData, fuzzySearch, { debounce: 1000, maxWait: 5000 })
190
+ watch(
191
+ () => [
192
+ props.modelName,
193
+ props.serverSearch,
194
+ props.serverSearchKey,
195
+ props.cache,
196
+ props.modelBy,
197
+ queryFields.value,
198
+ ],
199
+ () => loadItems(),
200
+ { immediate: true, deep: true },
201
+ )
96
202
 
97
- const computedItems = computed(()=>{
98
- let sortByField = (!props.sortBy || typeof props.sortBy === "string") ? [props.sortBy || ((props.showCode) ? props.itemValue : props.itemTitle)] : props.sortBy
203
+ watchDebounced(searchData,()=>{
204
+ if (props.serverSearch) loadItems()
205
+ else applyFuzzy()
206
+ },{ debounce: props.serverSearch ? props.serverSearchDebounce : 300, maxWait: 1500 })
99
207
 
100
- const baseItems = props.fuzzy && !isEmpty(searchData.value)
208
+ watch(()=>modelItems.value, () => {
209
+ if (!props.serverSearch) applyFuzzy()
210
+ })
211
+
212
+ watch(()=>props.modelValue,()=>{
213
+ syncSelectedFromModelValue()
214
+ },{ immediate: true, deep: true })
215
+
216
+ const computedItems = computed(() => {
217
+ const sortByField = !props.sortBy || typeof props.sortBy === 'string'
218
+ ? [props.sortBy || (props.showCode ? props.itemValue : props.itemTitle)]
219
+ : props.sortBy
220
+
221
+ const baseItems = (props.fuzzy || (props.serverSearch && !props.searchSearchSort)) && !isEmpty(searchData.value)
101
222
  ? items.value
102
223
  : sortBy(items.value, sortByField)
103
224
 
104
- return baseItems.map(item => ({
105
- ...item,
106
- [props.itemTitle]: props.showCode
107
- ? `${item[props.itemValue]}-${item[props.itemTitle]}`
108
- : item[props.itemTitle]
109
- }))
225
+ for (const selectedItem of selectedItemsObject.value || []) {
226
+ if (!baseItems.find(existingItem => existingItem[props.itemValue] === selectedItem[props.itemValue])) {
227
+ baseItems.push(selectedItem)
228
+ }
229
+ }
230
+
231
+ return baseItems
110
232
  })
111
233
  </script>
112
234
 
113
235
  <template>
114
236
  <v-autocomplete
237
+ v-model="selectedItems"
115
238
  v-model:search="searchData"
116
239
  v-bind="$attrs"
117
240
  :items="computedItems"
118
- :no-filter="props.fuzzy"
241
+ :filter-keys="computedFilterKeys"
242
+ :no-filter="props.fuzzy || props.serverSearch"
119
243
  :item-title="props.itemTitle"
120
244
  :item-value="props.itemValue"
121
245
  :loading="isLoading"
246
+ :placeholder="computedPlaceholder"
247
+ :no-data-text="computedNoDataText"
248
+ :multiple="props.multiple"
122
249
  >
123
250
  <!-- @ts-ignore -->
124
251
  <template
@@ -126,21 +253,28 @@ const computedItems = computed(()=>{
126
253
  :key="index"
127
254
  #[name]="slotData"
128
255
  >
129
- <slot
130
- :name="name"
131
- v-bind="((slotData || {}) as object)"
132
- />
256
+ <slot :name="name" v-bind="((slotData || {}) as object)" />
133
257
  </template>
134
- <template v-if="!$slots.item" #item="{ props, item }">
135
- <v-list-item v-bind="props" :title="(item as Record<string, any>)[String(props.itemTitle)]" />
258
+
259
+ <template
260
+ v-if="!$slots.item"
261
+ #item="{ props, item }"
262
+ >
263
+ <v-list-item
264
+ v-bind="props"
265
+ :title="(showCode ? item.value+'-' : '')+item.title"
266
+ />
136
267
  </template>
268
+
137
269
  <template
138
- v-if="isErrorLoading"
139
- #append
270
+ v-if="!$slots.selection && showCode"
271
+ #selection="{ item }"
140
272
  >
141
- <v-icon color="error">
142
- mdi mdi-alert
143
- </v-icon>
273
+ {{item.value+'-'+item.title}}
274
+ </template>
275
+
276
+ <template v-if="isErrorLoading" #append>
277
+ <v-icon color="error">mdi mdi-alert</v-icon>
144
278
  </template>
145
279
  </v-autocomplete>
146
- </template>
280
+ </template>
@@ -269,18 +269,25 @@ defineExpose({ reload,operation })
269
269
  #item.action="{ item }"
270
270
  >
271
271
  <v-btn
272
- v-if="canUpdate"
273
- variant="flat"
274
- density="compact"
275
- icon="mdi mdi-note-edit"
276
- @click="openDialog(item)"
272
+ v-if="!canUpdate || !canEditRow(item)"
273
+ variant="flat"
274
+ density="compact"
275
+ icon="mdi mdi-note-search"
276
+ @click="openDialogReadonly(item)"
277
277
  />
278
278
  <v-btn
279
- v-if="canDelete"
280
- variant="flat"
281
- density="compact"
282
- icon="mdi mdi-delete"
283
- @click="confirmDeleteItem(item)"
279
+ v-if="canUpdate && canEditRow(item)"
280
+ variant="flat"
281
+ density="compact"
282
+ icon="mdi mdi-note-edit"
283
+ @click="openDialog(item)"
284
+ />
285
+ <v-btn
286
+ v-if="canDelete && canEditRow(item)"
287
+ variant="flat"
288
+ density="compact"
289
+ icon="mdi mdi-delete"
290
+ @click="confirmDeleteItem(item)"
284
291
  />
285
292
  </template>
286
293
  </v-data-table>
@@ -5,6 +5,7 @@ import {VDataTable} from "vuetify/components/VDataTable";
5
5
  import {omit} from 'lodash-es'
6
6
  import type {GraphqlModelProps} from '../../composables/graphqlModel'
7
7
  import {useGraphqlModel} from '../../composables/graphqlModel'
8
+ import {useDisplay} from 'vuetify'
8
9
 
9
10
  defineOptions({
10
11
  inheritAttrs: false,
@@ -32,6 +33,13 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
32
33
  md?: string | number | boolean
33
34
  sm?: string | number | boolean
34
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
35
43
  }
36
44
 
37
45
  const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
@@ -69,8 +77,42 @@ const tableSlots = computed(() => {
69
77
  return omit(slots, ['item'])
70
78
  })
71
79
 
80
+ const display = useDisplay()
81
+ const isProgrammaticSet = ref(false)
82
+ const userOverrodeView = ref(false)
83
+
72
84
  const viewType = ref<string[] | string>('iterator')
73
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
+
74
116
  const currentItem = ref<Record<string, any> | undefined>(undefined)
75
117
  const isDialogOpen = ref<boolean>(false)
76
118
 
@@ -113,6 +155,23 @@ watch([currentPage, itemsPerPageInternal, sortBy], () => {
113
155
  }
114
156
  }, { immediate: true })
115
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
+
116
175
  const operation = ref({ openDialog, createItem, importItems, updateItem, deleteItem, reload, setSearch, canServerPageable, canServerSearch, canCreate, canUpdate, canDelete })
117
176
 
118
177
  const computedInitialData = computed(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",