@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 +1 -1
- package/dist/runtime/components/form/Iterator.vue +59 -0
- package/dist/runtime/components/label/DateAgo.vue +8 -5
- package/dist/runtime/components/master/Autocomplete.vue +52 -5
- package/dist/runtime/components/model/Autocomplete.vue +203 -69
- package/dist/runtime/components/model/Table.vue +17 -10
- package/dist/runtime/components/model/iterator.vue +59 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -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
|
|
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 =
|
|
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}
|
|
85
|
+
return `0 ${label}${suffix}`;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
|
|
86
|
-
return `${parts.join(' ')}
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
4
|
-
import {computed, ref,
|
|
5
|
-
import {watchDebounced} from '@vueuse/core'
|
|
6
|
-
import {useFuzzy} from '../../composables/utils/fuzzy'
|
|
7
|
-
import {useGraphQlOperation} from
|
|
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?:
|
|
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
|
-
|
|
43
|
-
let
|
|
44
|
-
if (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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
140
|
+
}
|
|
141
|
+
items.value = output
|
|
142
|
+
}
|
|
83
143
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
203
|
+
watchDebounced(searchData,()=>{
|
|
204
|
+
if (props.serverSearch) loadItems()
|
|
205
|
+
else applyFuzzy()
|
|
206
|
+
},{ debounce: props.serverSearch ? props.serverSearchDebounce : 300, maxWait: 1500 })
|
|
99
207
|
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
135
|
-
|
|
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="
|
|
139
|
-
#
|
|
270
|
+
v-if="!$slots.selection && showCode"
|
|
271
|
+
#selection="{ item }"
|
|
140
272
|
>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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(() => {
|