@ramathibodi/nuxt-commons 0.1.53 → 0.1.55

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.53",
7
+ "version": "0.1.55",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -3,7 +3,7 @@ import {computed, ref, watch} from 'vue'
3
3
  import * as prettier from 'prettier'
4
4
  import prettierPluginHtml from 'prettier/plugins/html'
5
5
  import {useDocumentTemplate, validationRulesRegex,optionStringToChoiceObject} from '../../composables/document/template'
6
- import {cloneDeep} from "lodash-es";
6
+ import {cloneDeep , filter} from "lodash-es";
7
7
 
8
8
  interface Props {
9
9
  title?: string
@@ -342,14 +342,14 @@ const ruleOptions = (inputType: string) => (value: any) => {
342
342
  </v-container>
343
343
  </template>
344
344
 
345
- <template v-for="header of useFilter(optionData.headers,(item)=> item.template)" #[`item.${header.key}`]="{item}">
345
+ <template v-for="header of filter(optionData.headers,(item)=> item.template)" #[`item.${header.key}`]="{item}">
346
346
  <form-pad
347
347
  :template="header.template"
348
348
  v-model="item[header.key]">
349
349
  </form-pad>
350
350
 
351
351
  </template>
352
- <template v-for="header of useFilter(optionData.headers,(item)=> item.headerTemplate)" #[`header.${header.key}`]="{item}">
352
+ <template v-for="header of filter(optionData.headers,(item)=> item.headerTemplate)" #[`header.${header.key}`]="{item}">
353
353
  <form-pad
354
354
  :template="header.headerTemplate"
355
355
  >
@@ -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'
@@ -1,41 +1,92 @@
1
1
  <script setup lang="ts">
2
- import { DateTime, type ToRelativeOptions } from "luxon";
2
+ import { DateTime } from "luxon";
3
3
  import { computed } from "vue";
4
4
 
5
+ type Unit = 'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds';
6
+ type Locale = 'th' | 'en' | 'en-US' | 'th-TH';
7
+
5
8
  interface Props {
6
9
  modelValue: DateTime;
7
- locale?: 'TH' | 'EN';
10
+ locale?: Locale;
11
+ showSuffix?: boolean;
12
+ units?: Unit[];
8
13
  }
9
14
 
10
15
  const props = withDefaults(defineProps<Props>(), {
11
- locale: 'TH',
16
+ locale: 'th',
17
+ showSuffix: true
12
18
  });
13
19
 
14
- const timeAgo = computed(() => {
20
+ // Fallback map: map complex locale (e.g., en-US) to base (e.g., en)
21
+ const normalizeLocale = (locale: Locale): 'en' | 'th' => {
22
+ if (locale.startsWith('en')) return 'en';
23
+ if (locale.startsWith('th')) return 'th';
24
+ return 'th'; // default fallback
25
+ };
26
+
27
+ const localizedLabels: Record<'en' | 'th', Record<Unit, string>> = {
28
+ en: {
29
+ years: 'years',
30
+ months: 'months',
31
+ days: 'days',
32
+ hours: 'hours',
33
+ minutes: 'minutes',
34
+ seconds: 'seconds',
35
+ },
36
+ th: {
37
+ years: 'ปี',
38
+ months: 'เดือน',
39
+ days: 'วัน',
40
+ hours: 'ชั่วโมง',
41
+ minutes: 'นาที',
42
+ seconds: 'วินาที',
43
+ },
44
+ };
45
+
46
+ const localizedSuffix: Record<'en' | 'th', string> = {
47
+ en: 'ago',
48
+ th: 'ที่ผ่านมา',
49
+ };
50
+
51
+ const outputText = computed(() => {
15
52
  const now = DateTime.now();
16
- const diff = now.diff(props.modelValue, ['years', 'months', 'days', 'hours', 'minutes', 'seconds']).toObject();
17
-
18
- let unit: ToRelativeOptions['unit'] = 'seconds';
19
- if (diff.years && diff.years > 0) {
20
- unit = 'years';
21
- } else if (diff.months && diff.months > 0) {
22
- unit = 'months';
23
- } else if (diff.days && diff.days > 0) {
24
- unit = 'days';
25
- } else if (diff.hours && diff.hours > 0) {
26
- unit = 'hours';
27
- } else if (diff.minutes && diff.minutes > 0) {
28
- unit = 'minutes';
53
+ const baseLocale = normalizeLocale(props.locale);
54
+
55
+ const units: Unit[] = props.units?.length
56
+ ? props.units
57
+ : ['years', 'months', 'days', 'hours', 'minutes', 'seconds'];
58
+
59
+ const diff = now.diff(props.modelValue, units).toObject();
60
+
61
+ if (!props.units) {
62
+ const foundUnit = units.find(unit => (diff[unit] ?? 0) >= 1);
63
+ const value = Math.floor(diff[foundUnit!] ?? 0);
64
+ const label = localizedLabels[baseLocale][foundUnit!];
65
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
66
+ return `${value} ${label}${suffix}`;
67
+ }
68
+
69
+ const parts = units.map(unit => {
70
+ const value = Math.floor(diff[unit] ?? 0);
71
+ if (value > 0) {
72
+ const label = localizedLabels[baseLocale][unit];
73
+ return `${value} ${label}`;
74
+ }
75
+ return '';
76
+ }).filter(Boolean);
77
+
78
+ if (parts.length === 0) {
79
+ const lastUnit = units.at(-1)!;
80
+ const label = localizedLabels[baseLocale][lastUnit];
81
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
82
+ return `0 ${label} ${suffix}`;
29
83
  }
30
84
 
31
- return props.modelValue.setLocale(props.locale).toRelative({ unit });
85
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
86
+ return `${parts.join(' ')} ${suffix}`;
32
87
  });
33
88
  </script>
34
89
 
35
90
  <template>
36
- <span>{{ timeAgo }}</span>
91
+ <span>{{ outputText }}</span>
37
92
  </template>
38
-
39
- <style scoped>
40
-
41
- </style>
@@ -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(() => {
@@ -36,7 +36,7 @@ const modelItemValue = computedAsync<string>(async () => {
36
36
  }
37
37
  }
38
38
  return props.notFoundText
39
- }, props.placeholder )
39
+ }, props.placeholder ?? '' )
40
40
  </script>
41
41
 
42
42
  <template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",