@omnitend/dashboard-for-laravel 0.19.1 → 0.19.2
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/components/base/DFormSelect.vue.d.ts +25 -10
- package/dist/dashboard-for-laravel.js +4027 -3993
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/resources/js/components/base/DFormSelect.vue +103 -5
- package/resources/js/components/extended/DXTable.vue +24 -5
package/package.json
CHANGED
|
@@ -1,14 +1,112 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @component
|
|
3
|
+
* Type-safe wrapper around Bootstrap Vue Next BFormSelect.
|
|
4
|
+
*
|
|
5
|
+
* Adds one behaviour BVN lacks: a `select` option whose value is `null`
|
|
6
|
+
* round-trips correctly. BVN renders each option's value as a plain HTML
|
|
7
|
+
* `value` attribute, so when that value is `null` Vue omits the attribute and
|
|
8
|
+
* the browser falls back to using the option's *text* as its DOM value — a
|
|
9
|
+
* model bound to `null` then matches no option and the select renders blank.
|
|
10
|
+
*
|
|
11
|
+
* We map a `null` option value (and a `null` model) to a private string
|
|
12
|
+
* sentinel so BVN renders a real value attribute, while keeping the consumer's
|
|
13
|
+
* model and emitted value as `null`. Legacy Bootstrap-Vue (BS4) handled this;
|
|
14
|
+
* BVN does not. (Radios likely need the same treatment — see #81.)
|
|
15
|
+
*/
|
|
16
|
+
<script setup lang="ts" generic="T = unknown">
|
|
2
17
|
import { BFormSelect } from "bootstrap-vue-next";
|
|
3
|
-
import { useSlots } from "vue";
|
|
18
|
+
import { computed, useSlots } from "vue";
|
|
19
|
+
|
|
20
|
+
// Mirrors the shapes BVN's BFormSelect accepts per option (primitive, object, or
|
|
21
|
+
// group). Only object options can carry a `null` value.
|
|
22
|
+
export type OptionItem = string | number | boolean | Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
export interface Props<TValue = unknown> {
|
|
25
|
+
modelValue?: TValue | TValue[] | null;
|
|
26
|
+
options?: readonly OptionItem[];
|
|
27
|
+
/** Key on object options that holds the value (BVN default: "value"). */
|
|
28
|
+
valueField?: string;
|
|
29
|
+
/** Key on object options that holds nested options / groups (BVN default: "options"). */
|
|
30
|
+
optionsField?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A stable private value that stands in for a `null` option/model. Chosen to be
|
|
34
|
+
// vanishingly unlikely to collide with a real option value.
|
|
35
|
+
const NULL_SENTINEL = "__dfl_null_option_sentinel__";
|
|
36
|
+
|
|
37
|
+
const props = withDefaults(defineProps<Props<T>>(), {
|
|
38
|
+
valueField: "value",
|
|
39
|
+
optionsField: "options",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits<{
|
|
43
|
+
"update:modelValue": [value: T | T[] | null];
|
|
44
|
+
}>();
|
|
4
45
|
|
|
5
46
|
const slots = useSlots();
|
|
47
|
+
|
|
48
|
+
const encodeValue = (value: unknown): unknown =>
|
|
49
|
+
value === null ? NULL_SENTINEL : value;
|
|
50
|
+
|
|
51
|
+
const decodeValue = (value: unknown): unknown =>
|
|
52
|
+
value === NULL_SENTINEL ? null : value;
|
|
53
|
+
|
|
54
|
+
const isPlainOptionObject = (option: unknown): option is Record<string, unknown> =>
|
|
55
|
+
typeof option === "object" && option !== null && !Array.isArray(option);
|
|
56
|
+
|
|
57
|
+
// Replace any option whose resolved value is `null` with the sentinel. Recurses
|
|
58
|
+
// into option groups. Primitive options (string/number/boolean) can't be null.
|
|
59
|
+
const encodeOptions = (options: readonly OptionItem[]): OptionItem[] =>
|
|
60
|
+
options.map((option) => {
|
|
61
|
+
if (!isPlainOptionObject(option)) {
|
|
62
|
+
return option;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const nested = option[props.optionsField];
|
|
66
|
+
if (Array.isArray(nested)) {
|
|
67
|
+
return { ...option, [props.optionsField]: encodeOptions(nested) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (option[props.valueField] === null) {
|
|
71
|
+
return { ...option, [props.valueField]: NULL_SENTINEL };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return option;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const encodedOptions = computed<OptionItem[] | undefined>(() =>
|
|
78
|
+
props.options === undefined ? undefined : encodeOptions(props.options),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const encodedModel = computed<unknown>(() => {
|
|
82
|
+
const model = props.modelValue;
|
|
83
|
+
if (Array.isArray(model)) {
|
|
84
|
+
return model.map(encodeValue);
|
|
85
|
+
}
|
|
86
|
+
return encodeValue(model);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const onUpdateModelValue = (value: unknown): void => {
|
|
90
|
+
const decoded = Array.isArray(value) ? value.map(decodeValue) : decodeValue(value);
|
|
91
|
+
emit("update:modelValue", decoded as T | T[] | null);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
defineOptions({
|
|
95
|
+
inheritAttrs: false,
|
|
96
|
+
});
|
|
6
97
|
</script>
|
|
7
98
|
|
|
8
99
|
<template>
|
|
9
|
-
<BFormSelect
|
|
10
|
-
|
|
11
|
-
|
|
100
|
+
<BFormSelect
|
|
101
|
+
v-bind="$attrs"
|
|
102
|
+
:model-value="encodedModel"
|
|
103
|
+
:options="encodedOptions"
|
|
104
|
+
:value-field="valueField"
|
|
105
|
+
:options-field="optionsField"
|
|
106
|
+
@update:model-value="onUpdateModelValue"
|
|
107
|
+
>
|
|
108
|
+
<template v-for="(_, name) in slots" :key="name" #[name]="slotProps">
|
|
109
|
+
<slot :name="name" v-bind="slotProps" />
|
|
12
110
|
</template>
|
|
13
111
|
</BFormSelect>
|
|
14
112
|
</template>
|
|
@@ -1126,14 +1126,27 @@ watch(() => props.filters, (newFilters, oldFilters) => {
|
|
|
1126
1126
|
}
|
|
1127
1127
|
}, { deep: true });
|
|
1128
1128
|
|
|
1129
|
-
// Watch apiUrl changes to reset filter cache
|
|
1129
|
+
// Watch apiUrl changes to reset filter cache and refetch. The api-url isn't part
|
|
1130
|
+
// of BTable's provider context, so an api-url-only change doesn't re-invoke the
|
|
1131
|
+
// provider on its own — without this the table keeps showing the previous url's
|
|
1132
|
+
// rows until some other trigger (sort, per-page, create/edit/delete) fires.
|
|
1130
1133
|
watch(() => props.apiUrl, (newUrl, oldUrl) => {
|
|
1131
1134
|
if (newUrl !== oldUrl && isProviderMode.value) {
|
|
1132
|
-
// Clear cached filter values and pagination when API endpoint changes
|
|
1135
|
+
// Clear cached filter values and pagination when API endpoint changes.
|
|
1136
|
+
// The next provider call will request fresh filter values.
|
|
1133
1137
|
apiFilterValues.value = {};
|
|
1134
1138
|
apiPaginationMeta.value = null;
|
|
1135
1139
|
apiError.value = null;
|
|
1136
|
-
//
|
|
1140
|
+
// A url change is a fresh dataset — go back to page 1. If we were past
|
|
1141
|
+
// page 1, resetting the page re-invokes the provider on its own (so the
|
|
1142
|
+
// out-of-range page can't linger); if we were already on page 1 the prop
|
|
1143
|
+
// doesn't change, so refresh() forces the refetch. Exactly one fetch
|
|
1144
|
+
// either way.
|
|
1145
|
+
const wasOnFirstPage = apiCurrentPage.value === 1;
|
|
1146
|
+
apiCurrentPage.value = 1;
|
|
1147
|
+
if (wasOnFirstPage) {
|
|
1148
|
+
refresh();
|
|
1149
|
+
}
|
|
1137
1150
|
}
|
|
1138
1151
|
});
|
|
1139
1152
|
|
|
@@ -1382,8 +1395,14 @@ const handleBusyChange = (busy: boolean) => {
|
|
|
1382
1395
|
// Debounce timer for filter changes
|
|
1383
1396
|
let filterDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1384
1397
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1398
|
+
// DFormSelect's model can be null/array (it mirrors BVN's BFormSelect), but the
|
|
1399
|
+
// per-page selector only ever emits a numeric-string scalar; narrow defensively.
|
|
1400
|
+
const handlePerPageChange = (newPerPage: number | string | null | (number | string)[]) => {
|
|
1401
|
+
const rawPerPage = Array.isArray(newPerPage) ? newPerPage[0] : newPerPage;
|
|
1402
|
+
if (rawPerPage === null || rawPerPage === undefined) {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const perPageNum = typeof rawPerPage === 'string' ? parseInt(rawPerPage, 10) : rawPerPage;
|
|
1387
1406
|
|
|
1388
1407
|
// Update internal state if not using external perPage
|
|
1389
1408
|
if (props.perPage === undefined) {
|