@omnitend/dashboard-for-laravel 0.19.0 → 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 +2 -2
- package/package.json +1 -1
- package/resources/css/theme.scss +13 -7
- package/resources/js/components/base/DAutocomplete.vue +22 -0
- package/resources/js/components/base/DFormSelect.vue +103 -5
- package/resources/js/components/extended/DXTable.vue +24 -5
package/package.json
CHANGED
package/resources/css/theme.scss
CHANGED
|
@@ -91,6 +91,19 @@ $input-focus-box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
|
|
91
91
|
// Gray-300) text inputs. Share the input border colour across all controls.
|
|
92
92
|
$form-check-input-border: 1px solid $input-border-color;
|
|
93
93
|
|
|
94
|
+
// Bigger check/radio boxes (Bootstrap's 1em default reads small next to the
|
|
95
|
+
// theme's inputs and is a fiddly touch target). Drive it through Bootstrap's own
|
|
96
|
+
// `$form-check-input-width` — NOT a raw `.form-check-input { width }` override —
|
|
97
|
+
// so the container's `padding-left` and the input's negative `margin-left` (both
|
|
98
|
+
// derived from `$form-check-padding-start`) track the larger box. Overriding just
|
|
99
|
+
// the CSS width left the padding at `1em + .5em`, which the ~20px box then filled
|
|
100
|
+
// almost entirely, collapsing the box↔label gap to ~1px (cramped/flush). The
|
|
101
|
+
// explicit padding-start keeps a real gap and avoids a rem+em unit mix in
|
|
102
|
+
// Bootstrap's default `$form-check-input-width + .5em`. Switches are unaffected
|
|
103
|
+
// (they use the independent `$form-switch-*` variables).
|
|
104
|
+
$form-check-input-width: 1.25rem;
|
|
105
|
+
$form-check-padding-start: $form-check-input-width + 0.5rem;
|
|
106
|
+
|
|
94
107
|
// ----------------------------------------------------------------------------
|
|
95
108
|
// Cards
|
|
96
109
|
// ----------------------------------------------------------------------------
|
|
@@ -193,13 +206,6 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
193
206
|
);
|
|
194
207
|
}
|
|
195
208
|
|
|
196
|
-
// Bigger check/radio boxes — Bootstrap's 1em default reads small next to the
|
|
197
|
-
// theme's inputs and is a fiddly touch target.
|
|
198
|
-
.form-check-input {
|
|
199
|
-
width: 1.25rem;
|
|
200
|
-
height: 1.25rem;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
209
|
// Horizontal ("label-left") forms: a compact settings-form rhythm, with the
|
|
204
210
|
// label + control top-aligned so a checkbox/switch sits on the label's first
|
|
205
211
|
// line rather than floating above it (the label column carries a top offset to
|
|
@@ -68,4 +68,26 @@ defineOptions({
|
|
|
68
68
|
--bs-btn-active-color: var(--bs-body-color);
|
|
69
69
|
flex: 0 0 auto;
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
The clear affordance renders as Bootstrap's `.btn-close` — a bold, filled ✕
|
|
74
|
+
that reads as a heavy black mark against a light field (and is noisy when
|
|
75
|
+
several autocompletes sit in a row, e.g. a table's column filters). Scale it
|
|
76
|
+
down via `font-size` (btn-close sizes both its box and its background SVG in
|
|
77
|
+
`em`, so this shrinks it proportionally without clipping) and rest it at a
|
|
78
|
+
lower opacity so it's a quiet clear affordance that only asserts on
|
|
79
|
+
hover/focus. Matches the softened trigger above.
|
|
80
|
+
*/
|
|
81
|
+
.d-autocomplete :deep(.b-autocomplete-clear.btn-close) {
|
|
82
|
+
font-size: 0.8em;
|
|
83
|
+
opacity: 0.4;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.d-autocomplete :deep(.b-autocomplete-clear.btn-close:hover) {
|
|
87
|
+
opacity: 0.7;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.d-autocomplete :deep(.b-autocomplete-clear.btn-close:focus) {
|
|
91
|
+
opacity: 0.9;
|
|
92
|
+
}
|
|
71
93
|
</style>
|
|
@@ -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) {
|