@ramathibodi/nuxt-commons 4.0.7 → 4.0.9
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/module.mjs +7 -0
- package/dist/runtime/components/form/CheckboxGroup.d.vue.ts +4 -1
- package/dist/runtime/components/form/CheckboxGroup.vue +42 -23
- package/dist/runtime/components/form/CheckboxGroup.vue.d.ts +4 -1
- package/dist/runtime/components/form/Iterator.d.vue.ts +4 -0
- package/dist/runtime/components/form/Iterator.vue +8 -1
- package/dist/runtime/components/form/Iterator.vue.d.ts +4 -0
- package/dist/runtime/components/form/Pad.vue +11 -2
- package/dist/runtime/components/form/Table.d.vue.ts +4 -0
- package/dist/runtime/components/form/Table.vue +9 -0
- package/dist/runtime/components/form/Table.vue.d.ts +4 -0
- package/dist/runtime/components/form/images/Field.vue +13 -11
- package/dist/runtime/components/model/Table.d.vue.ts +4 -0
- package/dist/runtime/components/model/Table.vue +10 -0
- package/dist/runtime/components/model/Table.vue.d.ts +4 -0
- package/dist/runtime/components/model/iterator.d.vue.ts +4 -0
- package/dist/runtime/components/model/iterator.vue +7 -0
- package/dist/runtime/components/model/iterator.vue.d.ts +4 -0
- package/dist/runtime/composables/api.d.ts +35 -4
- package/dist/runtime/composables/api.js +22 -6
- package/dist/runtime/composables/document/template.js +6 -4
- package/dist/runtime/composables/document/templateMigrate.d.ts +43 -0
- package/dist/runtime/composables/document/templateMigrate.js +118 -0
- package/dist/runtime/composables/localStorageModel.js +10 -8
- package/dist/runtime/composables/lookupListMaster.js +6 -6
- package/dist/runtime/composables/perPagePreference.d.ts +41 -0
- package/dist/runtime/composables/perPagePreference.js +55 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -48,6 +48,13 @@ const module$1 = defineNuxtModule({
|
|
|
48
48
|
...runtimeConfig,
|
|
49
49
|
..._options
|
|
50
50
|
};
|
|
51
|
+
const publicConfig = _nuxt.options.runtimeConfig.public;
|
|
52
|
+
if (publicConfig.IDEMPOTENCY_HEADER == null) {
|
|
53
|
+
publicConfig.IDEMPOTENCY_HEADER = "Idempotency-Key";
|
|
54
|
+
}
|
|
55
|
+
if (publicConfig.IDEMPOTENCY_ENABLED == null) {
|
|
56
|
+
publicConfig.IDEMPOTENCY_ENABLED = true;
|
|
57
|
+
}
|
|
51
58
|
_nuxt.options.vite.optimizeDeps ||= {};
|
|
52
59
|
_nuxt.options.vite.optimizeDeps.include ||= [];
|
|
53
60
|
_nuxt.options.vite.optimizeDeps.include.push("painterro");
|
|
@@ -12,7 +12,10 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VCheckbox['$props'
|
|
|
12
12
|
}
|
|
13
13
|
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<Props>, {
|
|
14
14
|
inline: boolean;
|
|
15
|
-
}>>, {
|
|
15
|
+
}>>, {
|
|
16
|
+
validate: () => boolean;
|
|
17
|
+
reset: () => void;
|
|
18
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
19
|
"update:modelValue": (...args: any[]) => void;
|
|
17
20
|
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<Props>, {
|
|
18
21
|
inline: boolean;
|
|
@@ -12,6 +12,18 @@ const props = defineProps({
|
|
|
12
12
|
const emit = defineEmits(["update:modelValue"]);
|
|
13
13
|
const values = ref([]);
|
|
14
14
|
const valuesOther = ref();
|
|
15
|
+
const touched = ref(false);
|
|
16
|
+
const onUserInput = () => {
|
|
17
|
+
touched.value = true;
|
|
18
|
+
};
|
|
19
|
+
const validate = () => {
|
|
20
|
+
touched.value = true;
|
|
21
|
+
return !computedRules.value;
|
|
22
|
+
};
|
|
23
|
+
const reset = () => {
|
|
24
|
+
touched.value = false;
|
|
25
|
+
};
|
|
26
|
+
defineExpose({ validate, reset });
|
|
15
27
|
const computedRules = computed(() => {
|
|
16
28
|
if (props.rules) {
|
|
17
29
|
let rules = props.rules.map((rule) => rule(values.value.length ? values.value : null));
|
|
@@ -19,6 +31,7 @@ const computedRules = computed(() => {
|
|
|
19
31
|
return join(rules, ",");
|
|
20
32
|
}
|
|
21
33
|
});
|
|
34
|
+
const showError = computed(() => touched.value && !!computedRules.value);
|
|
22
35
|
const computedOther = computed(() => {
|
|
23
36
|
const itemOther = filter(props.items, { value: "other" });
|
|
24
37
|
return head(itemOther);
|
|
@@ -46,27 +59,33 @@ watch(props.modelValue, () => {
|
|
|
46
59
|
</script>
|
|
47
60
|
|
|
48
61
|
<template>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
<label class="text-body-1 opacity-60">{{ props.label }}</label>
|
|
63
|
+
<div :class="`d-flex ${inline ? 'flex-row' : 'flex-column'}`">
|
|
64
|
+
<v-checkbox
|
|
65
|
+
v-for="item in computeItems"
|
|
66
|
+
v-model="values"
|
|
67
|
+
:value="item.value"
|
|
68
|
+
density="compact"
|
|
69
|
+
hide-details
|
|
70
|
+
:error="showError"
|
|
71
|
+
:="$attrs"
|
|
72
|
+
:label="item.label"
|
|
73
|
+
@update:model-value="onUserInput"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
<v-text-field
|
|
77
|
+
v-if="computedOther"
|
|
78
|
+
v-model="valuesOther"
|
|
79
|
+
hide-details
|
|
80
|
+
:error="showError"
|
|
81
|
+
:="$attrs"
|
|
82
|
+
:label="computedOther.label"
|
|
83
|
+
@update:model-value="onUserInput"
|
|
84
|
+
/>
|
|
85
|
+
<label
|
|
86
|
+
v-if="showError"
|
|
87
|
+
class="text-error text-subtitle-2 ml-1"
|
|
88
|
+
>
|
|
89
|
+
{{ computedRules }}
|
|
90
|
+
</label>
|
|
72
91
|
</template>
|
|
@@ -12,7 +12,10 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VCheckbox['$props'
|
|
|
12
12
|
}
|
|
13
13
|
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<Props>, {
|
|
14
14
|
inline: boolean;
|
|
15
|
-
}>>, {
|
|
15
|
+
}>>, {
|
|
16
|
+
validate: () => boolean;
|
|
17
|
+
reset: () => void;
|
|
18
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
19
|
"update:modelValue": (...args: any[]) => void;
|
|
17
20
|
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<Props>, {
|
|
18
21
|
inline: boolean;
|
|
@@ -29,6 +29,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
|
|
|
29
29
|
preferTableLg?: string | number | boolean;
|
|
30
30
|
preferTableMd?: string | number | boolean;
|
|
31
31
|
preferTableSm?: string | number | boolean;
|
|
32
|
+
perPageStorageKey?: string;
|
|
33
|
+
perPageStorageEnabled?: boolean;
|
|
32
34
|
}
|
|
33
35
|
declare function setSearch(keyword: string): void;
|
|
34
36
|
declare function createItem(item: Record<string, any>, callback?: FormDialogCallback): void;
|
|
@@ -193,6 +195,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
193
195
|
md: number;
|
|
194
196
|
sm: number;
|
|
195
197
|
itemsPerPage: number;
|
|
198
|
+
perPageStorageEnabled: boolean;
|
|
196
199
|
}>>, {
|
|
197
200
|
errorMessages: import("vue").ComputedRef<(string & string[]) | (readonly string[] & string[]) | undefined>;
|
|
198
201
|
isValid: import("vue").ComputedRef<boolean | null | undefined>;
|
|
@@ -248,6 +251,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
248
251
|
md: number;
|
|
249
252
|
sm: number;
|
|
250
253
|
itemsPerPage: number;
|
|
254
|
+
perPageStorageEnabled: boolean;
|
|
251
255
|
}>>> & Readonly<{
|
|
252
256
|
"onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
|
|
253
257
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -5,6 +5,7 @@ import { VDataIterator } from "vuetify/components/VDataIterator";
|
|
|
5
5
|
import { VDataTable } from "vuetify/components/VDataTable";
|
|
6
6
|
import { VInput } from "vuetify/components/VInput";
|
|
7
7
|
import { useDisplay } from "vuetify";
|
|
8
|
+
import { usePerPagePreference } from "../../composables/perPagePreference";
|
|
8
9
|
defineOptions({
|
|
9
10
|
inheritAttrs: false
|
|
10
11
|
});
|
|
@@ -35,7 +36,9 @@ const props = defineProps({
|
|
|
35
36
|
preferTableXl: { type: [String, Number, Boolean], required: false },
|
|
36
37
|
preferTableLg: { type: [String, Number, Boolean], required: false },
|
|
37
38
|
preferTableMd: { type: [String, Number, Boolean], required: false },
|
|
38
|
-
preferTableSm: { type: [String, Number, Boolean], required: false }
|
|
39
|
+
preferTableSm: { type: [String, Number, Boolean], required: false },
|
|
40
|
+
perPageStorageKey: { type: String, required: false },
|
|
41
|
+
perPageStorageEnabled: { type: Boolean, required: false, default: true }
|
|
39
42
|
});
|
|
40
43
|
const emit = defineEmits(["update:modelValue"]);
|
|
41
44
|
const attrs = useAttrs();
|
|
@@ -122,6 +125,10 @@ watch(() => props.itemsPerPage, (newValue) => {
|
|
|
122
125
|
if (newValue.toString().toLowerCase() == "all") itemsPerPageInternal.value = "-1";
|
|
123
126
|
else if (newValue) itemsPerPageInternal.value = newValue;
|
|
124
127
|
}, { immediate: true });
|
|
128
|
+
usePerPagePreference(itemsPerPageInternal, {
|
|
129
|
+
storageKey: props.perPageStorageKey,
|
|
130
|
+
enabled: props.perPageStorageEnabled
|
|
131
|
+
});
|
|
125
132
|
function createItem(item, callback) {
|
|
126
133
|
if (items.value.length > 0) item[props.modelKey] = Math.max(...items.value.map((i) => i[props.modelKey] || 0)) + 1;
|
|
127
134
|
else item[props.modelKey] = 1;
|
|
@@ -29,6 +29,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
|
|
|
29
29
|
preferTableLg?: string | number | boolean;
|
|
30
30
|
preferTableMd?: string | number | boolean;
|
|
31
31
|
preferTableSm?: string | number | boolean;
|
|
32
|
+
perPageStorageKey?: string;
|
|
33
|
+
perPageStorageEnabled?: boolean;
|
|
32
34
|
}
|
|
33
35
|
declare function setSearch(keyword: string): void;
|
|
34
36
|
declare function createItem(item: Record<string, any>, callback?: FormDialogCallback): void;
|
|
@@ -193,6 +195,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
193
195
|
md: number;
|
|
194
196
|
sm: number;
|
|
195
197
|
itemsPerPage: number;
|
|
198
|
+
perPageStorageEnabled: boolean;
|
|
196
199
|
}>>, {
|
|
197
200
|
errorMessages: import("vue").ComputedRef<(string & string[]) | (readonly string[] & string[]) | undefined>;
|
|
198
201
|
isValid: import("vue").ComputedRef<boolean | null | undefined>;
|
|
@@ -248,6 +251,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
248
251
|
md: number;
|
|
249
252
|
sm: number;
|
|
250
253
|
itemsPerPage: number;
|
|
254
|
+
perPageStorageEnabled: boolean;
|
|
251
255
|
}>>> & Readonly<{
|
|
252
256
|
"onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
|
|
253
257
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -22,7 +22,7 @@ const props = defineProps({
|
|
|
22
22
|
parentTemplates: { type: [String, Array], required: false, default: () => [] },
|
|
23
23
|
dirtyClass: { type: String, required: false, default: "form-data-dirty" },
|
|
24
24
|
dirtyOnCreate: { type: Boolean, required: false, default: false },
|
|
25
|
-
sanitizeDelay: { type: Number, required: false, default:
|
|
25
|
+
sanitizeDelay: { type: Number, required: false, default: 5e3 }
|
|
26
26
|
});
|
|
27
27
|
const emit = defineEmits(["update:modelValue"]);
|
|
28
28
|
const disabled = ref(props.disabled);
|
|
@@ -53,7 +53,16 @@ const formData = ref({});
|
|
|
53
53
|
function isBlankString(v) {
|
|
54
54
|
return isString(v) && v.trim().length === 0;
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
function scheduleIdle(fn) {
|
|
57
|
+
if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
|
|
58
|
+
window.requestIdleCallback(fn, { timeout: 2e3 });
|
|
59
|
+
} else {
|
|
60
|
+
setTimeout(fn, 0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const sanitizeBlankStrings = debounce((val, original) => {
|
|
64
|
+
scheduleIdle(() => sanitizeBlankStringsRaw(val, original));
|
|
65
|
+
}, props.sanitizeDelay);
|
|
57
66
|
function sanitizeBlankStringsRaw(val, original) {
|
|
58
67
|
if (!original && props.originalData) {
|
|
59
68
|
sanitizeBlankStrings(val, props.originalData);
|
|
@@ -25,6 +25,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
|
|
|
25
25
|
inputPadOnly?: boolean;
|
|
26
26
|
saveAndStay?: boolean;
|
|
27
27
|
stringFields?: Array<string>;
|
|
28
|
+
perPageStorageKey?: string;
|
|
29
|
+
perPageStorageEnabled?: boolean;
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* Public props accepted by FormTable.
|
|
@@ -136,6 +138,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
136
138
|
inputPadOnly: boolean;
|
|
137
139
|
saveAndStay: boolean;
|
|
138
140
|
stringFields: () => never[];
|
|
141
|
+
perPageStorageEnabled: boolean;
|
|
139
142
|
}>>, {
|
|
140
143
|
errorMessages: import("vue").ComputedRef<(string & string[]) | (readonly string[] & string[]) | undefined>;
|
|
141
144
|
isValid: import("vue").ComputedRef<boolean | null | undefined>;
|
|
@@ -188,6 +191,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
188
191
|
inputPadOnly: boolean;
|
|
189
192
|
saveAndStay: boolean;
|
|
190
193
|
stringFields: () => never[];
|
|
194
|
+
perPageStorageEnabled: boolean;
|
|
191
195
|
}>>> & Readonly<{
|
|
192
196
|
"onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
|
|
193
197
|
"onOpen:dialog"?: ((...args: any[]) => any) | undefined;
|
|
@@ -5,6 +5,7 @@ import { computed, nextTick, ref, useAttrs, watch, useTemplateRef } from "vue";
|
|
|
5
5
|
import { omit } from "lodash-es";
|
|
6
6
|
import { useDialog } from "../../composables/dialog";
|
|
7
7
|
import { useLocalStorageModel } from "../../composables/localStorageModel";
|
|
8
|
+
import { usePerPagePreference } from "../../composables/perPagePreference";
|
|
8
9
|
defineOptions({
|
|
9
10
|
inheritAttrs: false
|
|
10
11
|
});
|
|
@@ -28,6 +29,8 @@ const props = defineProps({
|
|
|
28
29
|
inputPadOnly: { type: Boolean, required: false, default: false },
|
|
29
30
|
saveAndStay: { type: Boolean, required: false, default: false },
|
|
30
31
|
stringFields: { type: Array, required: false, default: () => [] },
|
|
32
|
+
perPageStorageKey: { type: String, required: false },
|
|
33
|
+
perPageStorageEnabled: { type: Boolean, required: false, default: true },
|
|
31
34
|
persist: { type: Boolean, required: false },
|
|
32
35
|
persistKey: { type: String, required: false },
|
|
33
36
|
persistPrefix: { type: String, required: false },
|
|
@@ -47,7 +50,12 @@ const inputRef = useTemplateRef("inputRef");
|
|
|
47
50
|
const items = ref([]);
|
|
48
51
|
const search = ref();
|
|
49
52
|
const currentItem = ref(void 0);
|
|
53
|
+
const itemsPerPageInternal = ref();
|
|
50
54
|
useLocalStorageModel(items, props);
|
|
55
|
+
usePerPagePreference(itemsPerPageInternal, {
|
|
56
|
+
storageKey: props.perPageStorageKey,
|
|
57
|
+
enabled: props.perPageStorageEnabled
|
|
58
|
+
});
|
|
51
59
|
function setSearch(keyword) {
|
|
52
60
|
search.value = keyword;
|
|
53
61
|
}
|
|
@@ -234,6 +242,7 @@ defineExpose({
|
|
|
234
242
|
</slot>
|
|
235
243
|
<v-data-table
|
|
236
244
|
v-bind="plainAttrs"
|
|
245
|
+
v-model:items-per-page="itemsPerPageInternal"
|
|
237
246
|
color="primary"
|
|
238
247
|
:items="items"
|
|
239
248
|
:search="search"
|
|
@@ -25,6 +25,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
|
|
|
25
25
|
inputPadOnly?: boolean;
|
|
26
26
|
saveAndStay?: boolean;
|
|
27
27
|
stringFields?: Array<string>;
|
|
28
|
+
perPageStorageKey?: string;
|
|
29
|
+
perPageStorageEnabled?: boolean;
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* Public props accepted by FormTable.
|
|
@@ -136,6 +138,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
136
138
|
inputPadOnly: boolean;
|
|
137
139
|
saveAndStay: boolean;
|
|
138
140
|
stringFields: () => never[];
|
|
141
|
+
perPageStorageEnabled: boolean;
|
|
139
142
|
}>>, {
|
|
140
143
|
errorMessages: import("vue").ComputedRef<(string & string[]) | (readonly string[] & string[]) | undefined>;
|
|
141
144
|
isValid: import("vue").ComputedRef<boolean | null | undefined>;
|
|
@@ -188,6 +191,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
188
191
|
inputPadOnly: boolean;
|
|
189
192
|
saveAndStay: boolean;
|
|
190
193
|
stringFields: () => never[];
|
|
194
|
+
perPageStorageEnabled: boolean;
|
|
191
195
|
}>>> & Readonly<{
|
|
192
196
|
"onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
|
|
193
197
|
"onOpen:dialog"?: ((...args: any[]) => any) | undefined;
|
|
@@ -20,7 +20,7 @@ const images = ref([]);
|
|
|
20
20
|
const uploadImages = ref([]);
|
|
21
21
|
const dialog = ref(false);
|
|
22
22
|
const dialogUpdate = ref(false);
|
|
23
|
-
const
|
|
23
|
+
const dataUpdateIndex = ref(null);
|
|
24
24
|
const dialogImageFullScreen = ref(false);
|
|
25
25
|
const imageFullScreen = ref({ title: "", image: "" });
|
|
26
26
|
let internalSync = false;
|
|
@@ -59,14 +59,15 @@ const addImage = (img) => {
|
|
|
59
59
|
const remove = (index) => {
|
|
60
60
|
images.value.splice(index, 1);
|
|
61
61
|
};
|
|
62
|
-
const setDataUpdate = (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
imageTitle: img.imageTitle ?? "",
|
|
66
|
-
imageProps: { ...img.imageProps ?? {} }
|
|
67
|
-
};
|
|
62
|
+
const setDataUpdate = (index) => {
|
|
63
|
+
if (index < 0 || index >= images.value.length) return;
|
|
64
|
+
dataUpdateIndex.value = index;
|
|
68
65
|
dialogUpdate.value = true;
|
|
69
66
|
};
|
|
67
|
+
const closeUpdateDialog = () => {
|
|
68
|
+
dialogUpdate.value = false;
|
|
69
|
+
dataUpdateIndex.value = null;
|
|
70
|
+
};
|
|
70
71
|
const checkDuplicationName = (name) => images.value.some(({ imageTitle }) => isEqual(imageTitle, name));
|
|
71
72
|
const isImageDataUrl = (dataUrl) => /^data:image\//i.test(dataUrl);
|
|
72
73
|
const imageSrcFromImageData = (imageData) => {
|
|
@@ -209,7 +210,7 @@ defineExpose({
|
|
|
209
210
|
<v-btn icon @click="remove(index)" :disabled="isDisabled?.value" :readonly="isReadonly?.value">
|
|
210
211
|
<v-icon>mdi mdi-delete-outline</v-icon>
|
|
211
212
|
</v-btn>
|
|
212
|
-
<v-btn color="primary" icon @click="setDataUpdate(
|
|
213
|
+
<v-btn color="primary" icon @click="setDataUpdate(index)" v-if="!image.imageData?.id" :disabled="isDisabled?.value" :readonly="isReadonly?.value">
|
|
213
214
|
<v-icon>mdi mdi-image-edit-outline</v-icon>
|
|
214
215
|
</v-btn>
|
|
215
216
|
</VToolbarItems>
|
|
@@ -219,7 +220,7 @@ defineExpose({
|
|
|
219
220
|
:src="imageSrcFromImageData(image)"
|
|
220
221
|
height="250"
|
|
221
222
|
@click="() => {
|
|
222
|
-
props.readonly || image.imageData?.id || isReadonly?.value ? openImageFullScreen(image) : setDataUpdate(
|
|
223
|
+
props.readonly || image.imageData?.id || isReadonly?.value ? openImageFullScreen(image) : setDataUpdate(index);
|
|
223
224
|
}"
|
|
224
225
|
:disabled="isDisabled?.value"
|
|
225
226
|
/>
|
|
@@ -235,8 +236,9 @@ defineExpose({
|
|
|
235
236
|
<!-- Edit dialog -->
|
|
236
237
|
<VDialog v-model="dialogUpdate" fullscreen transition="dialog-bottom-transition">
|
|
237
238
|
<FormImagesPad
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
v-if="dialogUpdate && dataUpdateIndex !== null && images[dataUpdateIndex]"
|
|
240
|
+
v-model="images[dataUpdateIndex].imageData.base64String"
|
|
241
|
+
@closedDialog="closeUpdateDialog"
|
|
240
242
|
/>
|
|
241
243
|
</VDialog>
|
|
242
244
|
|
|
@@ -21,6 +21,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
|
|
|
21
21
|
onlyOwnerEdit?: boolean;
|
|
22
22
|
onlyOwnerOverridePermission?: string | string[];
|
|
23
23
|
api?: boolean;
|
|
24
|
+
perPageStorageKey?: string;
|
|
25
|
+
perPageStorageEnabled?: boolean;
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* Public props accepted by ModelTable.
|
|
@@ -164,6 +166,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
164
166
|
stringFields: () => never[];
|
|
165
167
|
onlyOwnerEdit: boolean;
|
|
166
168
|
api: boolean;
|
|
169
|
+
perPageStorageEnabled: boolean;
|
|
167
170
|
}>>, {
|
|
168
171
|
reload: () => void;
|
|
169
172
|
operation: import("vue").Ref<{
|
|
@@ -240,6 +243,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
240
243
|
stringFields: () => never[];
|
|
241
244
|
onlyOwnerEdit: boolean;
|
|
242
245
|
api: boolean;
|
|
246
|
+
perPageStorageEnabled: boolean;
|
|
243
247
|
}>>> & Readonly<{
|
|
244
248
|
onDelete?: ((...args: any[]) => any) | undefined;
|
|
245
249
|
onCreate?: ((...args: any[]) => any) | undefined;
|
|
@@ -4,6 +4,7 @@ import { VDataTable } from "vuetify/components/VDataTable";
|
|
|
4
4
|
import { clone } from "lodash-es";
|
|
5
5
|
import { useGraphqlModel } from "../../composables/graphqlModel";
|
|
6
6
|
import { useApiModel } from "../../composables/apiModel";
|
|
7
|
+
import { usePerPagePreference } from "../../composables/perPagePreference";
|
|
7
8
|
import { useDialog } from "../../composables/dialog";
|
|
8
9
|
import { useUserPermission } from "../../composables/userPermission";
|
|
9
10
|
import { useState } from "#imports";
|
|
@@ -30,6 +31,8 @@ const props = defineProps({
|
|
|
30
31
|
onlyOwnerEdit: { type: Boolean, required: false, default: false },
|
|
31
32
|
onlyOwnerOverridePermission: { type: [String, Array], required: false },
|
|
32
33
|
api: { type: Boolean, required: false, default: false },
|
|
34
|
+
perPageStorageKey: { type: String, required: false },
|
|
35
|
+
perPageStorageEnabled: { type: Boolean, required: false, default: true },
|
|
33
36
|
modelName: { type: String, required: true },
|
|
34
37
|
modelKey: { type: String, required: false, default: "id" },
|
|
35
38
|
modelBy: { type: Object, required: false, default: void 0 },
|
|
@@ -57,6 +60,11 @@ const currentUsername = computed(() => {
|
|
|
57
60
|
const currentItem = ref(void 0);
|
|
58
61
|
const isDialogOpen = ref(false);
|
|
59
62
|
const isDialogReadonly = ref(false);
|
|
63
|
+
const itemsPerPageInternal = ref();
|
|
64
|
+
usePerPagePreference(itemsPerPageInternal, {
|
|
65
|
+
storageKey: props.perPageStorageKey ?? props.modelName,
|
|
66
|
+
enabled: props.perPageStorageEnabled
|
|
67
|
+
});
|
|
60
68
|
const {
|
|
61
69
|
items,
|
|
62
70
|
itemsLength,
|
|
@@ -208,6 +216,7 @@ defineExpose({ reload, operation, items });
|
|
|
208
216
|
<v-data-table-server
|
|
209
217
|
v-if="canServerPageable"
|
|
210
218
|
v-bind="plainAttrs"
|
|
219
|
+
v-model:items-per-page="itemsPerPageInternal"
|
|
211
220
|
color="primary"
|
|
212
221
|
:items="items"
|
|
213
222
|
:items-length="itemsLength"
|
|
@@ -258,6 +267,7 @@ defineExpose({ reload, operation, items });
|
|
|
258
267
|
<v-data-table
|
|
259
268
|
v-else
|
|
260
269
|
v-bind="plainAttrs"
|
|
270
|
+
v-model:items-per-page="itemsPerPageInternal"
|
|
261
271
|
color="primary"
|
|
262
272
|
:items="items"
|
|
263
273
|
:item-value="props.modelKey"
|
|
@@ -21,6 +21,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
|
|
|
21
21
|
onlyOwnerEdit?: boolean;
|
|
22
22
|
onlyOwnerOverridePermission?: string | string[];
|
|
23
23
|
api?: boolean;
|
|
24
|
+
perPageStorageKey?: string;
|
|
25
|
+
perPageStorageEnabled?: boolean;
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* Public props accepted by ModelTable.
|
|
@@ -164,6 +166,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
164
166
|
stringFields: () => never[];
|
|
165
167
|
onlyOwnerEdit: boolean;
|
|
166
168
|
api: boolean;
|
|
169
|
+
perPageStorageEnabled: boolean;
|
|
167
170
|
}>>, {
|
|
168
171
|
reload: () => void;
|
|
169
172
|
operation: import("vue").Ref<{
|
|
@@ -240,6 +243,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
240
243
|
stringFields: () => never[];
|
|
241
244
|
onlyOwnerEdit: boolean;
|
|
242
245
|
api: boolean;
|
|
246
|
+
perPageStorageEnabled: boolean;
|
|
243
247
|
}>>> & Readonly<{
|
|
244
248
|
onDelete?: ((...args: any[]) => any) | undefined;
|
|
245
249
|
onCreate?: ((...args: any[]) => any) | undefined;
|
|
@@ -27,6 +27,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
|
|
|
27
27
|
preferTableMd?: string | number | boolean;
|
|
28
28
|
preferTableSm?: string | number | boolean;
|
|
29
29
|
api?: boolean;
|
|
30
|
+
perPageStorageKey?: string;
|
|
31
|
+
perPageStorageEnabled?: boolean;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Public props accepted by ModelIterator.
|
|
@@ -232,6 +234,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
232
234
|
sm: number;
|
|
233
235
|
itemsPerPage: number;
|
|
234
236
|
api: boolean;
|
|
237
|
+
perPageStorageEnabled: boolean;
|
|
235
238
|
}>>, {
|
|
236
239
|
reload: () => void;
|
|
237
240
|
operation: import("vue").Ref<{
|
|
@@ -295,6 +298,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
295
298
|
sm: number;
|
|
296
299
|
itemsPerPage: number;
|
|
297
300
|
api: boolean;
|
|
301
|
+
perPageStorageEnabled: boolean;
|
|
298
302
|
}>>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
299
303
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
300
304
|
declare const _default: typeof __VLS_export;
|
|
@@ -5,6 +5,7 @@ import { VDataTable } from "vuetify/components/VDataTable";
|
|
|
5
5
|
import { omit } from "lodash-es";
|
|
6
6
|
import { useGraphqlModel } from "../../composables/graphqlModel";
|
|
7
7
|
import { useApiModel } from "../../composables/apiModel";
|
|
8
|
+
import { usePerPagePreference } from "../../composables/perPagePreference";
|
|
8
9
|
import { useDisplay } from "vuetify";
|
|
9
10
|
defineOptions({
|
|
10
11
|
inheritAttrs: false
|
|
@@ -36,6 +37,8 @@ const props = defineProps({
|
|
|
36
37
|
preferTableMd: { type: [String, Number, Boolean], required: false },
|
|
37
38
|
preferTableSm: { type: [String, Number, Boolean], required: false },
|
|
38
39
|
api: { type: Boolean, required: false, default: false },
|
|
40
|
+
perPageStorageKey: { type: String, required: false },
|
|
41
|
+
perPageStorageEnabled: { type: Boolean, required: false, default: true },
|
|
39
42
|
modelName: { type: String, required: true },
|
|
40
43
|
modelKey: { type: String, required: false, default: "id" },
|
|
41
44
|
modelBy: { type: Object, required: false, default: void 0 },
|
|
@@ -122,6 +125,10 @@ watch(() => props.itemsPerPage, (newValue) => {
|
|
|
122
125
|
if (newValue.toString().toLowerCase() == "all") itemsPerPageInternal.value = "-1";
|
|
123
126
|
else if (newValue) itemsPerPageInternal.value = newValue;
|
|
124
127
|
}, { immediate: true });
|
|
128
|
+
usePerPagePreference(itemsPerPageInternal, {
|
|
129
|
+
storageKey: props.perPageStorageKey ?? props.modelName,
|
|
130
|
+
enabled: props.perPageStorageEnabled
|
|
131
|
+
});
|
|
125
132
|
const sortBy = ref();
|
|
126
133
|
const pageCount = computed(() => {
|
|
127
134
|
if (!itemsPerPageInternal.value || itemsPerPageInternal.value == "All" || itemsPerPageInternal.value == "-1" || Number(itemsPerPageInternal.value) <= 0) return 1;
|
|
@@ -27,6 +27,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
|
|
|
27
27
|
preferTableMd?: string | number | boolean;
|
|
28
28
|
preferTableSm?: string | number | boolean;
|
|
29
29
|
api?: boolean;
|
|
30
|
+
perPageStorageKey?: string;
|
|
31
|
+
perPageStorageEnabled?: boolean;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Public props accepted by ModelIterator.
|
|
@@ -232,6 +234,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
232
234
|
sm: number;
|
|
233
235
|
itemsPerPage: number;
|
|
234
236
|
api: boolean;
|
|
237
|
+
perPageStorageEnabled: boolean;
|
|
235
238
|
}>>, {
|
|
236
239
|
reload: () => void;
|
|
237
240
|
operation: import("vue").Ref<{
|
|
@@ -295,6 +298,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
295
298
|
sm: number;
|
|
296
299
|
itemsPerPage: number;
|
|
297
300
|
api: boolean;
|
|
301
|
+
perPageStorageEnabled: boolean;
|
|
298
302
|
}>>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
299
303
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
300
304
|
declare const _default: typeof __VLS_export;
|
|
@@ -21,6 +21,27 @@
|
|
|
21
21
|
* so users can see stale empty / outdated lists for the full TTL window even after
|
|
22
22
|
* creating data themselves.
|
|
23
23
|
*
|
|
24
|
+
* Idempotency: every authenticated POST (REST and GraphQL via useGraphQl()) automatically
|
|
25
|
+
* receives an `Idempotency-Key` HTTP header. The value is computed client-side as
|
|
26
|
+
*
|
|
27
|
+
* SHA-256(`${floor(Date.now()/1000)}|${username}|${stableStringify(body ?? {})}`)
|
|
28
|
+
*
|
|
29
|
+
* in lowercase hex. The recipe is intentionally identical to the backend's server-side
|
|
30
|
+
* fallback in rama-spring-starter's @IdempotentMutation aspect, so a header-present and
|
|
31
|
+
* a header-absent request from the same user with the same body within the same wall-clock
|
|
32
|
+
* second resolve to the same dedup signature.
|
|
33
|
+
*
|
|
34
|
+
* Per-call escape hatch: pass `{ idempotent: false }` in the options argument to skip
|
|
35
|
+
* injection for a single call (e.g. a long-running mutation the caller wants to retry
|
|
36
|
+
* intentionally with the same body). A caller-supplied `Idempotency-Key` in
|
|
37
|
+
* `options.headers` always wins and is never overwritten.
|
|
38
|
+
*
|
|
39
|
+
* Configuration (consumer's `nuxt.config` `runtimeConfig.public`, mirroring the
|
|
40
|
+
* unprefixed `WS_API` / `WS_GRAPHQL` precedent):
|
|
41
|
+
* - IDEMPOTENCY_HEADER (default 'Idempotency-Key') — must match the backend's
|
|
42
|
+
* `rama.idempotency.header-name` setting if the consumer overrides it there.
|
|
43
|
+
* - IDEMPOTENCY_ENABLED (default true) — global kill switch.
|
|
44
|
+
*
|
|
24
45
|
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
25
46
|
*/
|
|
26
47
|
import type { UseFetchOptions } from 'nuxt/app';
|
|
@@ -35,6 +56,16 @@ export type CacheOption = boolean | number | {
|
|
|
35
56
|
} | {
|
|
36
57
|
ttlMs: number;
|
|
37
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Per-call options accepted by useApi alongside Nuxt's UseFetchOptions.
|
|
61
|
+
*
|
|
62
|
+
* `idempotent: false` skips Idempotency-Key header injection for this single call.
|
|
63
|
+
* Default is `true`. See the useApi() docblock for the full idempotency contract.
|
|
64
|
+
*/
|
|
65
|
+
export interface ApiIdempotencyOption {
|
|
66
|
+
idempotent?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export type ApiFetchOptions = UseFetchOptions<unknown> & ApiIdempotencyOption;
|
|
38
69
|
export declare function _resetLegacyHeuristicWarning(): void;
|
|
39
70
|
/**
|
|
40
71
|
* Resolve a cache option to seconds. Pure function, exported for tests.
|
|
@@ -60,10 +91,10 @@ export declare function resolveCacheTtlSeconds(cache: CacheOption | undefined |
|
|
|
60
91
|
declare function invalidateCache(prefix?: string): number;
|
|
61
92
|
export declare function useApi(): {
|
|
62
93
|
urlBuilder: (url: string | string[]) => string;
|
|
63
|
-
get: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?:
|
|
64
|
-
getPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?:
|
|
65
|
-
post: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?:
|
|
66
|
-
postPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?:
|
|
94
|
+
get: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: ApiFetchOptions, cache?: CacheOption) => Promise<T>;
|
|
95
|
+
getPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: ApiFetchOptions, cache?: CacheOption) => Promise<T>;
|
|
96
|
+
post: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: ApiFetchOptions, cache?: CacheOption) => Promise<T>;
|
|
97
|
+
postPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: ApiFetchOptions, cache?: CacheOption) => Promise<T>;
|
|
67
98
|
hashKey: (data: any) => Promise<string>;
|
|
68
99
|
invalidate: typeof invalidateCache;
|
|
69
100
|
};
|
|
@@ -7,6 +7,7 @@ import { useRuntimeConfig } from "#imports";
|
|
|
7
7
|
import { useAuthentication } from "../bridges/authentication.js";
|
|
8
8
|
const DEFAULT_TTL_SECONDS_FOR_TRUE = 5 * 60;
|
|
9
9
|
const CACHE_PREFIX = "api-cache-";
|
|
10
|
+
const IDEMPOTENCY_HEADER_DEFAULT = "Idempotency-Key";
|
|
10
11
|
let _legacyHeuristicWarned = false;
|
|
11
12
|
function warnLegacyHeuristic(value, resolvedSeconds) {
|
|
12
13
|
if (_legacyHeuristicWarned) return;
|
|
@@ -77,7 +78,7 @@ export function useApi() {
|
|
|
77
78
|
if (returnUrl.startsWith("http://") || returnUrl.startsWith("https://")) return returnUrl;
|
|
78
79
|
return trimEnd(config?.public.WS_API, "/") + "/" + trimStart(returnUrl, "/");
|
|
79
80
|
}
|
|
80
|
-
function optionBuilder(method, body, params, options = {}) {
|
|
81
|
+
async function optionBuilder(method, body, params, options = {}) {
|
|
81
82
|
const headers = {
|
|
82
83
|
"Content-Type": "application/json",
|
|
83
84
|
Accept: "application/json"
|
|
@@ -85,9 +86,24 @@ export function useApi() {
|
|
|
85
86
|
const auth = useAuthentication();
|
|
86
87
|
const token = auth.keycloak?.token || auth.token;
|
|
87
88
|
if (token) {
|
|
88
|
-
;
|
|
89
89
|
headers["Authorization"] = `Bearer ${token}`;
|
|
90
90
|
}
|
|
91
|
+
const callerHeaders = {
|
|
92
|
+
...options.headers || {}
|
|
93
|
+
};
|
|
94
|
+
const enabledConfig = config?.public?.IDEMPOTENCY_ENABLED;
|
|
95
|
+
const idempotencyEnabled = enabledConfig !== false;
|
|
96
|
+
const headerName = config?.public?.IDEMPOTENCY_HEADER || IDEMPOTENCY_HEADER_DEFAULT;
|
|
97
|
+
const callerProvidedHeader = Object.keys(callerHeaders).some(
|
|
98
|
+
(k) => k.toLowerCase() === headerName.toLowerCase()
|
|
99
|
+
);
|
|
100
|
+
if (method === "POST" && idempotencyEnabled && options.idempotent !== false && !callerProvidedHeader) {
|
|
101
|
+
const username = (typeof auth.getUsername === "function" ? auth.getUsername() : auth.userProfile?.username) ?? "";
|
|
102
|
+
const second = Math.floor(Date.now() / 1e3).toString();
|
|
103
|
+
const bodyJson = stableStringify(body ?? {});
|
|
104
|
+
headers[headerName] = await sha256(`${second}|${username}|${bodyJson}`);
|
|
105
|
+
}
|
|
106
|
+
const { idempotent: _omitIdempotent, ...passThroughOptions } = options;
|
|
91
107
|
const baseOptions = {
|
|
92
108
|
method,
|
|
93
109
|
body,
|
|
@@ -96,9 +112,9 @@ export function useApi() {
|
|
|
96
112
|
};
|
|
97
113
|
const finalHeaders = {
|
|
98
114
|
...headers,
|
|
99
|
-
...
|
|
115
|
+
...callerHeaders
|
|
100
116
|
};
|
|
101
|
-
Object.assign(baseOptions,
|
|
117
|
+
Object.assign(baseOptions, passThroughOptions);
|
|
102
118
|
return {
|
|
103
119
|
...baseOptions,
|
|
104
120
|
headers: finalHeaders
|
|
@@ -132,7 +148,7 @@ export function useApi() {
|
|
|
132
148
|
const builtUrl = urlBuilder(url);
|
|
133
149
|
const ttl = resolveCacheTtlSeconds(cache);
|
|
134
150
|
if (ttl === 0) {
|
|
135
|
-
return ofetch(builtUrl, optionBuilder(method, body, params, options));
|
|
151
|
+
return ofetch(builtUrl, await optionBuilder(method, body, params, options));
|
|
136
152
|
}
|
|
137
153
|
const keyData = { url: builtUrl, method, body, params, headers: options?.headers };
|
|
138
154
|
const key = CACHE_PREFIX + await hashKey(keyData);
|
|
@@ -140,7 +156,7 @@ export function useApi() {
|
|
|
140
156
|
if (cached !== null) {
|
|
141
157
|
return cached;
|
|
142
158
|
}
|
|
143
|
-
const result = await ofetch(builtUrl, optionBuilder(method, body, params, options));
|
|
159
|
+
const result = await ofetch(builtUrl, await optionBuilder(method, body, params, options));
|
|
144
160
|
ls.set(key, result, { ttl });
|
|
145
161
|
return result;
|
|
146
162
|
}
|
|
@@ -2,6 +2,7 @@ import { processTemplateFormTable } from "./templateFormTable.js";
|
|
|
2
2
|
import { processTemplateFormTableData } from "./templateFormTableData.js";
|
|
3
3
|
import { processTemplateFormHidden } from "./templateFormHidden.js";
|
|
4
4
|
import { some, includes, cloneDeep } from "lodash-es";
|
|
5
|
+
import { migrateInputAttributes, migrateTemplateString } from "./templateMigrate.js";
|
|
5
6
|
export const validationRulesRegex = /^(require(?:\([^)]*\))?|requireIf\([^)]*\)|requireTrue(?:\([^)]*\))?|requireTrueIf\([^)]*\)|numeric(?:\([^)]*\))?|range\([^)]*\)|integer(?:\([^)]*\))?|unique\([^)]*\)|length(?:\([^)]*\))?|lengthGreater\([^)]*\)|lengthLess\([^)]*\)|telephone(?:\([^)]*\))?|email(?:\([^)]*\))?|regex\([^)]*\)|idcard(?:\([^)]*\))?|DateFuture(?:\([^)]*\))?|DatetimeFuture(?:\([^)]*\))?|DateHappen(?:\([^)]*\))?|DatetimeHappen(?:\([^)]*\))?|DateAfter\([^)]*\)|DateBefore\([^)]*\)|DateEqual\([^)]*\))(,(require(?:\([^)]*\))?|requireIf\([^)]*\)|requireTrue(?:\([^)]*\))?|requireTrueIf\([^)]*\)|numeric(?:\([^)]*\))?|range\([^)]*\)|integer(?:\([^)]*\))?|unique\([^)]*\)|length(?:\([^)]*\))?|lengthGreater\([^)]*\)|lengthLess\([^)]*\)|telephone(?:\([^)]*\))?|email(?:\([^)]*\))?|regex\([^)]*\)|idcard(?:\([^)]*\))?|DateFuture(?:\([^)]*\))?|DatetimeFuture(?:\([^)]*\))?|DateHappen(?:\([^)]*\))?|DatetimeHappen(?:\([^)]*\))?|DateAfter\([^)]*\)|DateBefore\([^)]*\)|DateEqual\([^)]*\)))*$/;
|
|
6
7
|
export function useDocumentTemplate(items, parentTemplates) {
|
|
7
8
|
if (!items) return "";
|
|
@@ -16,11 +17,12 @@ export function useDocumentTemplate(items, parentTemplates) {
|
|
|
16
17
|
return "";
|
|
17
18
|
}
|
|
18
19
|
const templateString = items.map((item) => columnWrapTemplateItemString(item, parentTemplates || [])).join("");
|
|
19
|
-
|
|
20
|
+
const wrapped = !parentTemplates || parentTemplates.length == 0 ? `<v-container fluid><v-row density='compact'>${templateString}</v-row></v-container>` : `<v-row density='compact'>${templateString}</v-row>`;
|
|
21
|
+
return migrateTemplateString(wrapped);
|
|
20
22
|
}
|
|
21
23
|
export function templateItemToString(inputItem, parentTemplates, dataVariable = "data") {
|
|
22
24
|
let item = cloneDeep(inputItem);
|
|
23
|
-
item.inputAttributes = item.inputAttributes?.trim() || "";
|
|
25
|
+
item.inputAttributes = migrateInputAttributes(item.inputAttributes?.trim() || "", item.inputType);
|
|
24
26
|
let optionString = "";
|
|
25
27
|
if (item.inputOptions) {
|
|
26
28
|
if (item.inputType === "MasterAutocomplete") {
|
|
@@ -45,8 +47,8 @@ export function templateItemToString(inputItem, parentTemplates, dataVariable =
|
|
|
45
47
|
if (item.conditionalDisplay) {
|
|
46
48
|
item.inputAttributes = `${item.inputAttributes?.trim() || ""} v-if="${item.conditionalDisplay}"`.trim();
|
|
47
49
|
}
|
|
48
|
-
if (item.inputType === "FormDateTime" && !item.inputAttributes?.includes("
|
|
49
|
-
item.inputAttributes = `${item.inputAttributes?.trim() || ""}
|
|
50
|
+
if (item.inputType === "FormDateTime" && !item.inputAttributes?.includes("density")) {
|
|
51
|
+
item.inputAttributes = `${item.inputAttributes?.trim() || ""} density="compact"`.trim();
|
|
50
52
|
}
|
|
51
53
|
let templateString;
|
|
52
54
|
const validationRules = item.validationRules ? buildValidationRules(item.validationRules) || "" : "";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render-time migration helper that rewrites Vuetify 3 attribute syntax to
|
|
3
|
+
* Vuetify 4 inside Document templates. Used by `useDocumentTemplate`.
|
|
4
|
+
*
|
|
5
|
+
* Rules without `appliesTo` are universally safe and run in both the per-item
|
|
6
|
+
* pre-pass and the post-pass on the assembled template string. Rules with
|
|
7
|
+
* `appliesTo` only run in the pre-pass and only when `item.inputType` (or the
|
|
8
|
+
* tag name) matches.
|
|
9
|
+
*
|
|
10
|
+
* See `docs/superpowers/specs/2026-04-26-document-template-vuetify4-migration-design.md`.
|
|
11
|
+
*/
|
|
12
|
+
export interface Vuetify4MigrationRule {
|
|
13
|
+
/** v3 bare boolean attribute name, e.g. 'dense'. */
|
|
14
|
+
from: string;
|
|
15
|
+
/** v4 replacement attribute incl. value, e.g. 'density="compact"'. */
|
|
16
|
+
to: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional whitelist of input types / tag names the rule applies to. If
|
|
19
|
+
* omitted, the rule is universally safe and also runs in the post-pass.
|
|
20
|
+
*/
|
|
21
|
+
appliesTo?: string[];
|
|
22
|
+
}
|
|
23
|
+
/** Default rule table. Mutable / extensible by callers. */
|
|
24
|
+
export declare const vuetify4MigrationRules: Vuetify4MigrationRule[];
|
|
25
|
+
/**
|
|
26
|
+
* Walk an attribute string (the part inside `<tag …>`), replacing each bare
|
|
27
|
+
* boolean attribute that matches a rule with the rule's replacement. Skips
|
|
28
|
+
* text inside quoted attribute values. Internal — exported for unit tests.
|
|
29
|
+
*/
|
|
30
|
+
export declare function migrateAttributeString(attrs: string, applicableRules: Vuetify4MigrationRule[]): string;
|
|
31
|
+
/**
|
|
32
|
+
* Per-item pre-pass. Filter the rule set to rules that apply to `inputType`
|
|
33
|
+
* (rules with no `appliesTo` always apply), then rewrite the attribute string.
|
|
34
|
+
*/
|
|
35
|
+
export declare function migrateInputAttributes(attrs: string, inputType: string, rules?: Vuetify4MigrationRule[]): string;
|
|
36
|
+
/**
|
|
37
|
+
* Post-pass on the assembled template string. Walks opening tags (skipping
|
|
38
|
+
* closing tags and comments), extracts each tag's attribute region with
|
|
39
|
+
* quote-awareness so `>` inside `"…"` does not prematurely end the tag, then
|
|
40
|
+
* rewrites the attributes using only rules that have no `appliesTo` — those
|
|
41
|
+
* are universally safe to apply across both Vuetify and custom components.
|
|
42
|
+
*/
|
|
43
|
+
export declare function migrateTemplateString(template: string, rules?: Vuetify4MigrationRule[]): string;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const VARIANT_APPLIES_TO = [
|
|
2
|
+
"VTextField",
|
|
3
|
+
"VSelect",
|
|
4
|
+
"VAutocomplete",
|
|
5
|
+
"VCombobox",
|
|
6
|
+
"VTextarea",
|
|
7
|
+
"VFileInput",
|
|
8
|
+
"VCard",
|
|
9
|
+
"FormDate",
|
|
10
|
+
"FormTime",
|
|
11
|
+
"FormDateTime",
|
|
12
|
+
"MasterAutocomplete",
|
|
13
|
+
"ModelAutocomplete",
|
|
14
|
+
"ModelSelect",
|
|
15
|
+
"ModelCombobox"
|
|
16
|
+
];
|
|
17
|
+
export const vuetify4MigrationRules = [
|
|
18
|
+
{ from: "dense", to: 'density="compact"' },
|
|
19
|
+
{ from: "outlined", to: 'variant="outlined"', appliesTo: VARIANT_APPLIES_TO },
|
|
20
|
+
{ from: "filled", to: 'variant="filled"', appliesTo: VARIANT_APPLIES_TO },
|
|
21
|
+
{ from: "solo", to: 'variant="solo"', appliesTo: VARIANT_APPLIES_TO },
|
|
22
|
+
{ from: "flat", to: 'variant="flat"', appliesTo: ["VCard"] }
|
|
23
|
+
];
|
|
24
|
+
export function migrateAttributeString(attrs, applicableRules) {
|
|
25
|
+
if (!attrs || applicableRules.length === 0) return attrs;
|
|
26
|
+
let result = "";
|
|
27
|
+
let lastFlushed = 0;
|
|
28
|
+
let i = 0;
|
|
29
|
+
let inQuote = null;
|
|
30
|
+
while (i < attrs.length) {
|
|
31
|
+
const ch = attrs[i];
|
|
32
|
+
if (inQuote) {
|
|
33
|
+
if (ch === inQuote) inQuote = null;
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (ch === '"' || ch === "'") {
|
|
38
|
+
inQuote = ch;
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (i === 0 || /\s/.test(attrs[i - 1])) {
|
|
43
|
+
let matched = false;
|
|
44
|
+
for (const rule of applicableRules) {
|
|
45
|
+
const word = rule.from;
|
|
46
|
+
if (attrs.slice(i, i + word.length) !== word) continue;
|
|
47
|
+
const after = i + word.length;
|
|
48
|
+
if (after !== attrs.length && !/\s/.test(attrs[after])) continue;
|
|
49
|
+
result += attrs.slice(lastFlushed, i);
|
|
50
|
+
result += rule.to;
|
|
51
|
+
lastFlushed = after;
|
|
52
|
+
i = after;
|
|
53
|
+
matched = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (matched) continue;
|
|
57
|
+
}
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
result += attrs.slice(lastFlushed);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
export function migrateInputAttributes(attrs, inputType, rules = vuetify4MigrationRules) {
|
|
64
|
+
if (!attrs) return "";
|
|
65
|
+
const applicable = rules.filter((r) => !r.appliesTo || r.appliesTo.includes(inputType));
|
|
66
|
+
return migrateAttributeString(attrs, applicable);
|
|
67
|
+
}
|
|
68
|
+
export function migrateTemplateString(template, rules = vuetify4MigrationRules) {
|
|
69
|
+
if (!template) return template;
|
|
70
|
+
const applicable = rules.filter((r) => !r.appliesTo);
|
|
71
|
+
if (applicable.length === 0) return template;
|
|
72
|
+
let result = "";
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < template.length) {
|
|
75
|
+
const ch = template[i];
|
|
76
|
+
if (ch !== "<") {
|
|
77
|
+
result += ch;
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (template[i + 1] === "/" || template[i + 1] === "!") {
|
|
82
|
+
result += ch;
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const nameMatch = /^[a-z][\w:-]*/i.exec(template.slice(i + 1));
|
|
87
|
+
if (!nameMatch) {
|
|
88
|
+
result += ch;
|
|
89
|
+
i++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const tagName = nameMatch[0];
|
|
93
|
+
const attrsStart = i + 1 + tagName.length;
|
|
94
|
+
let j = attrsStart;
|
|
95
|
+
let inQuote = null;
|
|
96
|
+
while (j < template.length) {
|
|
97
|
+
const c = template[j];
|
|
98
|
+
if (inQuote) {
|
|
99
|
+
if (c === inQuote) inQuote = null;
|
|
100
|
+
} else if (c === '"' || c === "'") {
|
|
101
|
+
inQuote = c;
|
|
102
|
+
} else if (c === ">") {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
j++;
|
|
106
|
+
}
|
|
107
|
+
if (j >= template.length) {
|
|
108
|
+
result += template.slice(i);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
const tagEnd = template[j - 1] === "/" ? j - 1 : j;
|
|
112
|
+
const attrs = template.slice(attrsStart, tagEnd);
|
|
113
|
+
const closing = template.slice(tagEnd, j + 1);
|
|
114
|
+
result += `<${tagName}${migrateAttributeString(attrs, applicable)}${closing}`;
|
|
115
|
+
i = j + 1;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
@@ -21,10 +21,11 @@ export function useLocalStorageModel(model, props, options) {
|
|
|
21
21
|
function read() {
|
|
22
22
|
if (!storageKey.value) return void 0;
|
|
23
23
|
try {
|
|
24
|
-
|
|
25
|
-
decrypt: !!props.persistEncrypt
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const opts = {
|
|
25
|
+
decrypt: !!props.persistEncrypt
|
|
26
|
+
};
|
|
27
|
+
if (props.persistSecret !== void 0) opts.secret = props.persistSecret;
|
|
28
|
+
return ls.get(storageKey.value, opts);
|
|
28
29
|
} catch {
|
|
29
30
|
return void 0;
|
|
30
31
|
}
|
|
@@ -32,12 +33,13 @@ export function useLocalStorageModel(model, props, options) {
|
|
|
32
33
|
function write(val) {
|
|
33
34
|
if (!storageKey.value) return;
|
|
34
35
|
try {
|
|
35
|
-
|
|
36
|
+
const opts = {
|
|
36
37
|
ttl: props.persistTtl,
|
|
37
38
|
// seconds
|
|
38
|
-
encrypt: !!props.persistEncrypt
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
encrypt: !!props.persistEncrypt
|
|
40
|
+
};
|
|
41
|
+
if (props.persistSecret !== void 0) opts.secret = props.persistSecret;
|
|
42
|
+
ls.set(storageKey.value, serializer(val), opts);
|
|
41
43
|
} catch {
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -31,15 +31,15 @@ export function useLookupListMaster(props) {
|
|
|
31
31
|
);
|
|
32
32
|
});
|
|
33
33
|
const formatItemTitle = (item) => {
|
|
34
|
+
const raw = item?.raw ?? item ?? {};
|
|
35
|
+
const resolvedTitle = item?.title || raw?.[itemTitleField.value] || raw?.itemValue || raw?.itemCode;
|
|
34
36
|
if (props.meilisearch) {
|
|
35
|
-
const
|
|
36
|
-
const code = raw?.itemCode;
|
|
37
|
-
const title =
|
|
37
|
+
const formatted = raw?._formatted ?? {};
|
|
38
|
+
const code = formatted?.itemCode ?? raw?.itemCode;
|
|
39
|
+
const title = formatted?.[itemTitleField.value] || formatted?.itemValue || formatted?.itemCode || resolvedTitle;
|
|
38
40
|
return (props.showCode ? (code ?? "") + "-" : "") + (title ?? "");
|
|
39
41
|
} else {
|
|
40
|
-
|
|
41
|
-
const title = item.title ?? item?.itemValue ?? item?.itemCode;
|
|
42
|
-
return (props.showCode ? (code ?? "") + "-" : "") + (title ?? "");
|
|
42
|
+
return (props.showCode ? (raw?.itemCode ?? "") + "-" : "") + (resolvedTitle ?? "");
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
const computedNoDataText = computed(() => {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePerPagePreference persists a paginated component's items-per-page selection
|
|
3
|
+
* in localStorage, namespaced by the currently logged-in user and the active
|
|
4
|
+
* route. Built on top of `useLocalStorageModel` so it inherits the existing
|
|
5
|
+
* encryption + TTL + debounced-write behavior backed by `localstorage-slim`.
|
|
6
|
+
*
|
|
7
|
+
* The full storage key is composed as:
|
|
8
|
+
*
|
|
9
|
+
* nuxt-commons:perPage:<username|anon>:<route.path>:<storageKey>
|
|
10
|
+
*
|
|
11
|
+
* If `storageKey` is empty/undefined, persistence is disabled and the function
|
|
12
|
+
* is a no-op (so callers can opt-in safely without breaking existing usage).
|
|
13
|
+
*/
|
|
14
|
+
import { type Ref } from 'vue';
|
|
15
|
+
export interface PerPagePreferenceOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Logical key identifying the table/iterator instance (e.g. `modelName`).
|
|
18
|
+
* Persistence is disabled when this is empty.
|
|
19
|
+
*/
|
|
20
|
+
storageKey?: string;
|
|
21
|
+
/** TTL in seconds — defaults to 90 days. */
|
|
22
|
+
ttlSeconds?: number;
|
|
23
|
+
/** Encrypt the stored value via localstorage-slim — defaults to `true`. */
|
|
24
|
+
encrypt?: boolean;
|
|
25
|
+
/** Optional encryption secret. When omitted, localstorage-slim derives one. */
|
|
26
|
+
secret?: string;
|
|
27
|
+
/** Debounce window for writes, in ms — defaults to 200. */
|
|
28
|
+
debounceMs?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Explicit opt-out switch — when `false`, the composable becomes a no-op
|
|
31
|
+
* even if `storageKey` is set.
|
|
32
|
+
*/
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function usePerPagePreference(perPage: Ref<string | number | undefined>, options?: PerPagePreferenceOptions): {
|
|
36
|
+
isHydrated: Ref<boolean, boolean>;
|
|
37
|
+
storageKey: import("vue").ComputedRef<string>;
|
|
38
|
+
remove: () => void;
|
|
39
|
+
read: () => unknown;
|
|
40
|
+
write: (val: string | number | undefined) => void;
|
|
41
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useState, useRoute } from "#imports";
|
|
3
|
+
import { useLocalStorageModel } from "./localStorageModel.js";
|
|
4
|
+
function safeRoutePath() {
|
|
5
|
+
try {
|
|
6
|
+
const route = useRoute();
|
|
7
|
+
return route?.path || "";
|
|
8
|
+
} catch {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const DEFAULT_TTL_SECONDS = 90 * 24 * 60 * 60;
|
|
13
|
+
export function usePerPagePreference(perPage, options = {}) {
|
|
14
|
+
const authState = useState("authentication", () => ({}));
|
|
15
|
+
const username = computed(() => {
|
|
16
|
+
const profile = authState.value?.userProfile;
|
|
17
|
+
return profile?.username?.trim() || "anon";
|
|
18
|
+
});
|
|
19
|
+
const routePath = computed(() => safeRoutePath());
|
|
20
|
+
const persistKey = computed(() => {
|
|
21
|
+
const key = options.storageKey?.trim();
|
|
22
|
+
if (!key || options.enabled === false) return "";
|
|
23
|
+
return `${username.value}:${routePath.value}:${key}`;
|
|
24
|
+
});
|
|
25
|
+
return useLocalStorageModel(
|
|
26
|
+
perPage,
|
|
27
|
+
{
|
|
28
|
+
persist: !!persistKey.value,
|
|
29
|
+
persistKey: persistKey.value,
|
|
30
|
+
persistPrefix: "nuxt-commons:perPage",
|
|
31
|
+
persistEncrypt: options.encrypt ?? true,
|
|
32
|
+
persistSecret: options.secret,
|
|
33
|
+
persistTtl: options.ttlSeconds ?? DEFAULT_TTL_SECONDS,
|
|
34
|
+
persistDebounce: options.debounceMs ?? 200,
|
|
35
|
+
persistClearOnEmpty: false,
|
|
36
|
+
// A stored preference should win over the component's prop default
|
|
37
|
+
// (which often runs `immediate: true` on mount and populates the ref
|
|
38
|
+
// before onMounted hydrates).
|
|
39
|
+
persistAlwaysHydrate: true
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
isEmpty: (v) => v === void 0 || v === null || v === "",
|
|
43
|
+
// useLocalStorageModel hydrates whenever stored !== undefined, but
|
|
44
|
+
// localstorage-slim returns `null` for missing keys. Without this filter
|
|
45
|
+
// a first-time render with no stored entry would clobber the prop default
|
|
46
|
+
// (e.g. `itemsPerPage: 12`) with `null`.
|
|
47
|
+
deserializer: (raw) => {
|
|
48
|
+
if (raw === null || raw === void 0) {
|
|
49
|
+
throw new Error("skip hydration: no stored preference");
|
|
50
|
+
}
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
}
|