@omnitend/dashboard-for-laravel 0.25.0 → 0.26.0
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/assets/bootstrap-icons-6c757103.woff2 +0 -0
- package/dist/components/extended/DXTable.vue.d.ts +20 -0
- package/dist/dashboard-for-laravel.js +7843 -7813
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +8 -8
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/style.css +1 -1
- package/docs/public/api-reference.json +38 -25
- package/docs/public/docs-map.md +1 -1
- package/docs/public/llms.txt +2 -3
- package/package.json +2 -2
- package/resources/css/theme.scss +10 -4
- package/resources/js/components/extended/DXForm.vue +1 -1
- package/resources/js/components/extended/DXTable.vue +227 -32
- package/resources/js/index.ts +14 -1
- package/dist/components/base/DTab.vue.d.ts +0 -12
- package/resources/js/components/base/DTab.vue +0 -14
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-
|
|
2
|
+
"generated": "2026-07-13T10:49:19.086Z",
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "@omnitend/dashboard-for-laravel",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.26.0"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
8
8
|
"base": [
|
|
@@ -1121,27 +1121,6 @@
|
|
|
1121
1121
|
"slots": [],
|
|
1122
1122
|
"methods": []
|
|
1123
1123
|
},
|
|
1124
|
-
{
|
|
1125
|
-
"name": "DTab",
|
|
1126
|
-
"category": "base",
|
|
1127
|
-
"filePath": "resources/js/components/base/DTab.vue",
|
|
1128
|
-
"description": "",
|
|
1129
|
-
"props": [],
|
|
1130
|
-
"events": [],
|
|
1131
|
-
"slots": [
|
|
1132
|
-
{
|
|
1133
|
-
"name": "name",
|
|
1134
|
-
"description": "",
|
|
1135
|
-
"bindings": [
|
|
1136
|
-
{
|
|
1137
|
-
"name": "name",
|
|
1138
|
-
"title": "binding"
|
|
1139
|
-
}
|
|
1140
|
-
]
|
|
1141
|
-
}
|
|
1142
|
-
],
|
|
1143
|
-
"methods": []
|
|
1144
|
-
},
|
|
1145
1124
|
{
|
|
1146
1125
|
"name": "DTable",
|
|
1147
1126
|
"category": "base",
|
|
@@ -2928,6 +2907,18 @@
|
|
|
2928
2907
|
"required": false,
|
|
2929
2908
|
"description": "Rows currently expanded, as a `v-model`. Pair with the `row-expansion`\nslot to render detail content under a row (#112) instead of forcing every\nper-row detail into a modal."
|
|
2930
2909
|
},
|
|
2910
|
+
{
|
|
2911
|
+
"name": "rowClass",
|
|
2912
|
+
"type": "union",
|
|
2913
|
+
"required": false,
|
|
2914
|
+
"description": "Class(es) applied to each row's `<tr>` — a string, or a function of the\nrow. Use for conditional row styling (a variant, a muted/disabled look)\nwithout reaching into the table's internal DOM from global CSS."
|
|
2915
|
+
},
|
|
2916
|
+
{
|
|
2917
|
+
"name": "rowClickable",
|
|
2918
|
+
"type": "TSFunctionType",
|
|
2919
|
+
"required": false,
|
|
2920
|
+
"description": "Whether a given row is actionable. Only relevant when rows are clickable\n(an `editFields` modal or a `row-clicked` listener). A row this returns\n`false` for gets no pointer cursor, no hover highlight, and does not fire\n`row-clicked` or open the edit modal — so a row that isn't clickable\ndoesn't *look* clickable, and a click on it can't quietly do something."
|
|
2921
|
+
},
|
|
2931
2922
|
{
|
|
2932
2923
|
"name": "clientSide",
|
|
2933
2924
|
"type": "boolean",
|
|
@@ -3023,6 +3014,28 @@
|
|
|
3023
3014
|
"description": "Card header content; overrides the default title heading and the \"New {item}\" button.",
|
|
3024
3015
|
"bindings": []
|
|
3025
3016
|
},
|
|
3017
|
+
{
|
|
3018
|
+
"name": "thead-top",
|
|
3019
|
+
"description": "A row rendered ABOVE the column headers — a grouped-column banner (a `<th colspan>` spanning several columns), or a pinned totals row. Renders above DXTable's own filter row. Give it `<tr>`s.",
|
|
3020
|
+
"bindings": [
|
|
3021
|
+
{
|
|
3022
|
+
"title": "binding",
|
|
3023
|
+
"type": {
|
|
3024
|
+
"name": "object"
|
|
3025
|
+
},
|
|
3026
|
+
"name": "columns",
|
|
3027
|
+
"description": "The number of columns in the table."
|
|
3028
|
+
},
|
|
3029
|
+
{
|
|
3030
|
+
"title": "binding",
|
|
3031
|
+
"type": {
|
|
3032
|
+
"name": "object"
|
|
3033
|
+
},
|
|
3034
|
+
"name": "fields",
|
|
3035
|
+
"description": "The table's fields."
|
|
3036
|
+
}
|
|
3037
|
+
]
|
|
3038
|
+
},
|
|
3026
3039
|
{
|
|
3027
3040
|
"name": "name",
|
|
3028
3041
|
"description": "Any slot the underlying table supports, forwarded with its scope: `cell(<fieldKey>)` for a cell, `foot(<fieldKey>)` for a footer cell (needs `footClone`), `empty` for the no-rows message, `row-expansion` for expandable row detail, plus `custom-foot`, `top-row`, `bottom-row`, `thead-sub` and the `table-*` slots.",
|
|
@@ -3282,8 +3295,8 @@
|
|
|
3282
3295
|
]
|
|
3283
3296
|
},
|
|
3284
3297
|
"stats": {
|
|
3285
|
-
"totalComponents":
|
|
3286
|
-
"baseComponents":
|
|
3298
|
+
"totalComponents": 73,
|
|
3299
|
+
"baseComponents": 56,
|
|
3287
3300
|
"extendedComponents": 17
|
|
3288
3301
|
}
|
|
3289
3302
|
}
|
package/docs/public/docs-map.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Documentation Map
|
|
2
2
|
|
|
3
3
|
> Auto-generated hierarchical overview of all documentation
|
|
4
|
-
> Last updated: 2026-07-
|
|
4
|
+
> Last updated: 2026-07-13T10:49:19.148Z
|
|
5
5
|
|
|
6
6
|
This file provides a complete map of all available documentation for AI agents and developers.
|
|
7
7
|
|
package/docs/public/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Vue 3 dashboard components for Laravel with Bootstrap Vue Next
|
|
4
4
|
>
|
|
5
|
-
> A dual-package library (NPM + Composer) providing
|
|
5
|
+
> A dual-package library (NPM + Composer) providing 68 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
- [Theming](/guide/theming): Customize appearance with CSS variables
|
|
20
20
|
- [TypeScript](/guide/typescript): TypeScript types and best practices
|
|
21
21
|
|
|
22
|
-
## Base Components (
|
|
22
|
+
## Base Components (56 components)
|
|
23
23
|
|
|
24
24
|
Lightweight type-safe wrappers around Bootstrap Vue Next components providing API stability and consistent theming.
|
|
25
25
|
|
|
@@ -74,7 +74,6 @@ Lightweight type-safe wrappers around Bootstrap Vue Next components providing AP
|
|
|
74
74
|
- [DProgress](/components/base/dprogress): Bootstrap Vue Next Progress wrapper
|
|
75
75
|
- [DRow](/components/base/drow): Bootstrap Vue Next Row wrapper
|
|
76
76
|
- [DSpinner](/components/base/dspinner): Bootstrap Vue Next Spinner wrapper
|
|
77
|
-
- [DTab](/components/base/dtab): Bootstrap Vue Next Tab wrapper
|
|
78
77
|
- [DTable](/components/base/dtable): Bootstrap Vue Next Table wrapper
|
|
79
78
|
- [DTabs](/components/base/dtabs): Bootstrap Vue Next Tabs wrapper
|
|
80
79
|
- [DToast](/components/base/dtoast): Bootstrap Vue Next Toast wrapper
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnitend/dashboard-for-laravel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/dashboard-for-laravel.umd.cjs",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
31
|
"dev": "vite build --watch",
|
|
32
|
-
"build": "vite build && vue-tsc --declaration --emitDeclarationOnly --outDir dist",
|
|
32
|
+
"build": "vite build && node scripts/extract-icon-font.mjs && vue-tsc --declaration --emitDeclarationOnly --outDir dist",
|
|
33
33
|
"typecheck": "vue-tsc --noEmit",
|
|
34
34
|
"pretest": "vite build",
|
|
35
35
|
"test": "vitest",
|
package/resources/css/theme.scss
CHANGED
|
@@ -210,12 +210,18 @@ $dashboard-navbar-content-height: calc(
|
|
|
210
210
|
// bi-*">`) renders without the consumer wiring up their own icon set.
|
|
211
211
|
//
|
|
212
212
|
// Imported as SCSS (not the prebuilt CSS) so we can override the font `src` to
|
|
213
|
-
// woff2 ONLY — dropping bootstrap-icons' legacy `woff` fallback
|
|
214
|
-
//
|
|
215
|
-
// doubled the embedded font bytes in `dist/style.css` for no benefit (every
|
|
216
|
-
// browser we target reads woff2). The override must come BEFORE the import so
|
|
213
|
+
// woff2 ONLY — dropping bootstrap-icons' legacy `woff` fallback, which no
|
|
214
|
+
// browser we target needs. The override must come BEFORE the import so
|
|
217
215
|
// bootstrap-icons' own `!default` src doesn't re-add the woff. The bare
|
|
218
216
|
// `bootstrap-icons/...` specifier resolves through Vite's module resolver.
|
|
217
|
+
//
|
|
218
|
+
// The font itself does NOT ship inside `dist/style.css`. Vite always inlines
|
|
219
|
+
// CSS-referenced assets as base64 data URIs in library mode, which is
|
|
220
|
+
// pathological for a woff2 (already Brotli-compressed; base64 inflates it a
|
|
221
|
+
// third; gzip recovers none of it) — inlined, the font alone was 137 KB of the
|
|
222
|
+
// stylesheet's 191 KB gzip. `scripts/extract-icon-font.mjs` writes it back out
|
|
223
|
+
// as a real file post-build, so the browser fetches it only when a `.bi-*`
|
|
224
|
+
// glyph actually renders. See #77.
|
|
219
225
|
$bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2")
|
|
220
226
|
format("woff2");
|
|
221
227
|
@import "bootstrap-icons/font/bootstrap-icons";
|
|
@@ -172,7 +172,7 @@ import DAlert from "../base/DAlert.vue";
|
|
|
172
172
|
import DButton from "../base/DButton.vue";
|
|
173
173
|
import DCard from "../base/DCard.vue";
|
|
174
174
|
import DTabs from "../base/DTabs.vue";
|
|
175
|
-
import DTab from "
|
|
175
|
+
import { BTab as DTab } from "bootstrap-vue-next"; // raw BTab: BTabs scans slot vnodes for it (#119)
|
|
176
176
|
import DXFormField from "./DXFormField.vue";
|
|
177
177
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
178
178
|
import type { DefineFormReturn } from "../../composables/defineForm";
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
<DTable
|
|
45
45
|
v-else-if="isProviderMode"
|
|
46
46
|
ref="tableRef"
|
|
47
|
+
:key="tableSlotSignature($slots)"
|
|
47
48
|
:provider="effectiveProvider"
|
|
48
49
|
:fields="fields"
|
|
49
50
|
:sort-by="effectiveSortBy"
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
:hover="hover"
|
|
56
57
|
:responsive="responsive"
|
|
57
58
|
:busy="busy"
|
|
59
|
+
:tbody-tr-class="composeRowClass"
|
|
58
60
|
v-bind="tablePassthroughProps"
|
|
59
61
|
@update:sort-by="handleSortChange"
|
|
60
62
|
@update:expanded-items="emit('update:expandedItems', $event)"
|
|
@@ -62,9 +64,19 @@
|
|
|
62
64
|
@update:busy="handleBusyChange"
|
|
63
65
|
@row-clicked="handleRowClick"
|
|
64
66
|
>
|
|
65
|
-
<!--
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
<!-- DXTable owns `thead-top` (its filter row lives there), so a
|
|
68
|
+
consumer's own thead-top content is COMPOSED above it rather
|
|
69
|
+
than being dropped: a grouped column-header banner or a pinned
|
|
70
|
+
totals row sits above the headers where it belongs (#120). -->
|
|
71
|
+
<template v-if="hasFilters || $slots['thead-top']" #thead-top="theadScope">
|
|
72
|
+
<!--
|
|
73
|
+
@slot A row rendered ABOVE the column headers — a grouped-column banner (a `<th colspan>` spanning several columns), or a pinned totals row. Renders above DXTable's own filter row. Give it `<tr>`s.
|
|
74
|
+
@binding {object} columns The number of columns in the table.
|
|
75
|
+
@binding {object} fields The table's fields.
|
|
76
|
+
-->
|
|
77
|
+
<slot name="thead-top" v-bind="theadScope" />
|
|
78
|
+
|
|
79
|
+
<tr v-if="hasFilters" class="filter-row">
|
|
68
80
|
<th v-for="field in fields" :key="`filter-${field.key}`" class="p-2">
|
|
69
81
|
<!-- Text Filter -->
|
|
70
82
|
<DFormInput
|
|
@@ -130,7 +142,7 @@
|
|
|
130
142
|
table-busy/caption/colgroup, thead-sub, top-row, bottom-row.
|
|
131
143
|
See `isTableSlot` for the two DXTable renders itself. -->
|
|
132
144
|
<template
|
|
133
|
-
v-for="name in
|
|
145
|
+
v-for="name in forwardableSlotNames($slots)"
|
|
134
146
|
:key="name"
|
|
135
147
|
#[name]="slotProps"
|
|
136
148
|
>
|
|
@@ -144,6 +156,7 @@
|
|
|
144
156
|
<!-- Client-Side Mode: Local filtering, sorting, pagination -->
|
|
145
157
|
<DTable
|
|
146
158
|
v-else-if="isClientSideMode"
|
|
159
|
+
:key="tableSlotSignature($slots)"
|
|
147
160
|
:items="clientSidePaginatedItems"
|
|
148
161
|
:fields="fields"
|
|
149
162
|
:sort-by="effectiveSortBy"
|
|
@@ -153,14 +166,25 @@
|
|
|
153
166
|
:striped="striped"
|
|
154
167
|
:hover="hover"
|
|
155
168
|
:responsive="responsive"
|
|
169
|
+
:tbody-tr-class="composeRowClass"
|
|
156
170
|
v-bind="tablePassthroughProps"
|
|
157
171
|
@update:sort-by="handleSortChange"
|
|
158
172
|
@update:expanded-items="emit('update:expandedItems', $event)"
|
|
159
173
|
@row-clicked="handleRowClick"
|
|
160
174
|
>
|
|
161
|
-
<!--
|
|
162
|
-
|
|
163
|
-
|
|
175
|
+
<!-- DXTable owns `thead-top` (its filter row lives there), so a
|
|
176
|
+
consumer's own thead-top content is COMPOSED above it rather
|
|
177
|
+
than being dropped: a grouped column-header banner or a pinned
|
|
178
|
+
totals row sits above the headers where it belongs (#120). -->
|
|
179
|
+
<template v-if="hasFilters || $slots['thead-top']" #thead-top="theadScope">
|
|
180
|
+
<!--
|
|
181
|
+
@slot A row rendered ABOVE the column headers — a grouped-column banner (a `<th colspan>` spanning several columns), or a pinned totals row. Renders above DXTable's own filter row. Give it `<tr>`s.
|
|
182
|
+
@binding {object} columns The number of columns in the table.
|
|
183
|
+
@binding {object} fields The table's fields.
|
|
184
|
+
-->
|
|
185
|
+
<slot name="thead-top" v-bind="theadScope" />
|
|
186
|
+
|
|
187
|
+
<tr v-if="hasFilters" class="filter-row">
|
|
164
188
|
<th v-for="field in fields" :key="`filter-${field.key}`" class="p-2">
|
|
165
189
|
<!-- Text Filter -->
|
|
166
190
|
<DFormInput
|
|
@@ -226,7 +250,7 @@
|
|
|
226
250
|
table-busy/caption/colgroup, thead-sub, top-row, bottom-row.
|
|
227
251
|
See `isTableSlot` for the two DXTable renders itself. -->
|
|
228
252
|
<template
|
|
229
|
-
v-for="name in
|
|
253
|
+
v-for="name in forwardableSlotNames($slots)"
|
|
230
254
|
:key="name"
|
|
231
255
|
#[name]="slotProps"
|
|
232
256
|
>
|
|
@@ -240,6 +264,7 @@
|
|
|
240
264
|
<!-- Inertia Mode: Use items prop -->
|
|
241
265
|
<DTable
|
|
242
266
|
v-else-if="isInertiaMode"
|
|
267
|
+
:key="tableSlotSignature($slots)"
|
|
243
268
|
:items="items"
|
|
244
269
|
:fields="fields"
|
|
245
270
|
:sort-by="effectiveSortBy"
|
|
@@ -250,14 +275,25 @@
|
|
|
250
275
|
:hover="hover"
|
|
251
276
|
:responsive="responsive"
|
|
252
277
|
:busy="effectiveBusy"
|
|
278
|
+
:tbody-tr-class="composeRowClass"
|
|
253
279
|
v-bind="tablePassthroughProps"
|
|
254
280
|
@update:sort-by="handleSortChange"
|
|
255
281
|
@update:expanded-items="emit('update:expandedItems', $event)"
|
|
256
282
|
@row-clicked="handleRowClick"
|
|
257
283
|
>
|
|
258
|
-
<!--
|
|
259
|
-
|
|
260
|
-
|
|
284
|
+
<!-- DXTable owns `thead-top` (its filter row lives there), so a
|
|
285
|
+
consumer's own thead-top content is COMPOSED above it rather
|
|
286
|
+
than being dropped: a grouped column-header banner or a pinned
|
|
287
|
+
totals row sits above the headers where it belongs (#120). -->
|
|
288
|
+
<template v-if="hasFilters || $slots['thead-top']" #thead-top="theadScope">
|
|
289
|
+
<!--
|
|
290
|
+
@slot A row rendered ABOVE the column headers — a grouped-column banner (a `<th colspan>` spanning several columns), or a pinned totals row. Renders above DXTable's own filter row. Give it `<tr>`s.
|
|
291
|
+
@binding {object} columns The number of columns in the table.
|
|
292
|
+
@binding {object} fields The table's fields.
|
|
293
|
+
-->
|
|
294
|
+
<slot name="thead-top" v-bind="theadScope" />
|
|
295
|
+
|
|
296
|
+
<tr v-if="hasFilters" class="filter-row">
|
|
261
297
|
<th v-for="field in fields" :key="`filter-${field.key}`" class="p-2">
|
|
262
298
|
<!-- Text Filter -->
|
|
263
299
|
<DFormInput
|
|
@@ -323,7 +359,7 @@
|
|
|
323
359
|
table-busy/caption/colgroup, thead-sub, top-row, bottom-row.
|
|
324
360
|
See `isTableSlot` for the two DXTable renders itself. -->
|
|
325
361
|
<template
|
|
326
|
-
v-for="name in
|
|
362
|
+
v-for="name in forwardableSlotNames($slots)"
|
|
327
363
|
:key="name"
|
|
328
364
|
#[name]="slotProps"
|
|
329
365
|
>
|
|
@@ -917,6 +953,22 @@ export interface Props<TItem = any> {
|
|
|
917
953
|
*/
|
|
918
954
|
expandedItems?: TItem[];
|
|
919
955
|
|
|
956
|
+
/**
|
|
957
|
+
* Class(es) applied to each row's `<tr>` — a string, or a function of the
|
|
958
|
+
* row. Use for conditional row styling (a variant, a muted/disabled look)
|
|
959
|
+
* without reaching into the table's internal DOM from global CSS.
|
|
960
|
+
*/
|
|
961
|
+
rowClass?: string | ((item: TItem, index: number) => string | string[] | Record<string, boolean>);
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Whether a given row is actionable. Only relevant when rows are clickable
|
|
965
|
+
* (an `editFields` modal or a `row-clicked` listener). A row this returns
|
|
966
|
+
* `false` for gets no pointer cursor, no hover highlight, and does not fire
|
|
967
|
+
* `row-clicked` or open the edit modal — so a row that isn't clickable
|
|
968
|
+
* doesn't *look* clickable, and a click on it can't quietly do something.
|
|
969
|
+
*/
|
|
970
|
+
rowClickable?: (item: TItem, index: number) => boolean;
|
|
971
|
+
|
|
920
972
|
/** Enable client-side filtering, sorting, and pagination on items array */
|
|
921
973
|
clientSide?: boolean;
|
|
922
974
|
}
|
|
@@ -1045,10 +1097,30 @@ const rowsAreInteractive = computed(
|
|
|
1045
1097
|
() => (props.editFields?.length ?? 0) > 0 || hasListener('rowClicked'),
|
|
1046
1098
|
);
|
|
1047
1099
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
)
|
|
1100
|
+
/**
|
|
1101
|
+
* A row is actionable when the table is interactive AND the consumer's
|
|
1102
|
+
* `rowClickable` predicate (if any) says so — a row-clicked table can have rows
|
|
1103
|
+
* that aren't navigable (#115).
|
|
1104
|
+
*/
|
|
1105
|
+
const isRowActionable = (item: T, index: number): boolean =>
|
|
1106
|
+
rowsAreInteractive.value && (props.rowClickable?.(item, index) ?? true);
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* The `<tr>` class, composed from the consumer's `rowClass` and our own
|
|
1110
|
+
* actionable marker. The pointer/hover affordance hangs off the marker class
|
|
1111
|
+
* rather than a blanket `tbody tr` rule, so a non-actionable row simply doesn't
|
|
1112
|
+
* get it — which is what let consumers drop their global
|
|
1113
|
+
* `tbody tr:has(.marker)` CSS hack.
|
|
1114
|
+
*/
|
|
1115
|
+
const composeRowClass = (item: T, index: number) => {
|
|
1116
|
+
const consumerClass =
|
|
1117
|
+
typeof props.rowClass === 'function' ? props.rowClass(item, index) : props.rowClass;
|
|
1118
|
+
|
|
1119
|
+
const classes: any[] = [];
|
|
1120
|
+
if (consumerClass) classes.push(consumerClass);
|
|
1121
|
+
if (isRowActionable(item, index)) classes.push('dx-row-actionable');
|
|
1122
|
+
return classes;
|
|
1123
|
+
};
|
|
1052
1124
|
|
|
1053
1125
|
// Internal sortBy state, seeded from the prop when it's an initial value.
|
|
1054
1126
|
const internalSortBy = ref<BTableSortBy[]>(props.sortBy ? [...props.sortBy] : []);
|
|
@@ -1414,12 +1486,37 @@ const effectivePerPage = computed(() => {
|
|
|
1414
1486
|
// Client-Side Mode: Pagination (requires effectivePerPage)
|
|
1415
1487
|
// ============================================
|
|
1416
1488
|
|
|
1489
|
+
/*
|
|
1490
|
+
* The page the client-side view actually renders (#118).
|
|
1491
|
+
*
|
|
1492
|
+
* `clientSideCurrentPage` is only reset on a filter or per-page change, so when
|
|
1493
|
+
* the `items` prop SHRINKS — a consumer refetching a narrower dataset — it can
|
|
1494
|
+
* point past the end of the new set. The pagination metadata below already
|
|
1495
|
+
* clamped for display, but the slice used the raw value, so the pager said
|
|
1496
|
+
* "page 2 of 2" while the table sliced page 6 and rendered zero rows. Both read
|
|
1497
|
+
* from this now, so the view and its pager can't disagree.
|
|
1498
|
+
*/
|
|
1499
|
+
const clientSideLastPage = computed(() =>
|
|
1500
|
+
Math.max(1, Math.ceil(clientSideFilteredItems.value.length / effectivePerPage.value)),
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
const clientSidePage = computed(() =>
|
|
1504
|
+
Math.min(Math.max(1, clientSideCurrentPage.value), clientSideLastPage.value),
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
// Write the clamp back, so the NEXT interaction starts from a real page rather
|
|
1508
|
+
// than a stale one. Clamping to the last page (not resetting to 1) keeps the
|
|
1509
|
+
// user near where they were.
|
|
1510
|
+
watch(clientSideLastPage, (lastPage) => {
|
|
1511
|
+
if (clientSideCurrentPage.value > lastPage) clientSideCurrentPage.value = lastPage;
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1417
1514
|
// Client-side paginated items (final output)
|
|
1418
1515
|
const clientSidePaginatedItems = computed<T[]>(() => {
|
|
1419
1516
|
if (!isClientSideMode.value) return [];
|
|
1420
1517
|
|
|
1421
1518
|
const perPage = effectivePerPage.value;
|
|
1422
|
-
const start = (
|
|
1519
|
+
const start = (clientSidePage.value - 1) * perPage;
|
|
1423
1520
|
const end = start + perPage;
|
|
1424
1521
|
|
|
1425
1522
|
return clientSideSortedItems.value.slice(start, end);
|
|
@@ -1430,11 +1527,10 @@ const clientSidePagination = computed<PaginationData>(() => {
|
|
|
1430
1527
|
const total = clientSideFilteredItems.value.length;
|
|
1431
1528
|
const totalUnfiltered = props.items?.length || 0;
|
|
1432
1529
|
const perPage = effectivePerPage.value;
|
|
1433
|
-
const
|
|
1434
|
-
const lastPage = Math.max(1, Math.ceil(total / perPage));
|
|
1530
|
+
const lastPage = clientSideLastPage.value;
|
|
1435
1531
|
|
|
1436
|
-
//
|
|
1437
|
-
const validPage =
|
|
1532
|
+
// Same clamped page the slice uses — see `clientSidePage` (#118).
|
|
1533
|
+
const validPage = clientSidePage.value;
|
|
1438
1534
|
|
|
1439
1535
|
const from = total > 0 ? (validPage - 1) * perPage + 1 : 0;
|
|
1440
1536
|
const to = Math.min(validPage * perPage, total);
|
|
@@ -1833,6 +1929,43 @@ try {
|
|
|
1833
1929
|
// Forward only the keyed edit slots the consumer actually provided, so
|
|
1834
1930
|
// DXForm doesn't mistake an always-present (but empty) wrapper for
|
|
1835
1931
|
// a real custom-value override.
|
|
1932
|
+
/*
|
|
1933
|
+
* Seeding an EDIT form from a row (#117).
|
|
1934
|
+
*
|
|
1935
|
+
* `row[key] ?? field.default` is wrong: it can't tell "the row has no such key"
|
|
1936
|
+
* from "the row's value IS null", so an explicitly-null column gets overwritten
|
|
1937
|
+
* by the field's default — a value the user never saw, on a field they may not
|
|
1938
|
+
* even be able to see. Presence, not nullishness, decides.
|
|
1939
|
+
*/
|
|
1940
|
+
const seedValueFor = (field: any, row: T): any => {
|
|
1941
|
+
if (Object.prototype.hasOwnProperty.call(row as any, field.key)) {
|
|
1942
|
+
return (row as any)[field.key];
|
|
1943
|
+
}
|
|
1944
|
+
return field.default ?? '';
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
/*
|
|
1948
|
+
* Whether a field is currently VISIBLE, by the same rule DXForm renders by
|
|
1949
|
+
* (`when` + the legacy `show`), against the same model (context + live data).
|
|
1950
|
+
* Kept in step with DXForm.isFieldVisible.
|
|
1951
|
+
*/
|
|
1952
|
+
const editPredicateModel = computed(() => ({
|
|
1953
|
+
...((selectedItem.value as any) ?? {}),
|
|
1954
|
+
...(editForm.value?.data ?? {}),
|
|
1955
|
+
}));
|
|
1956
|
+
|
|
1957
|
+
const isEditFieldVisible = (field: any): boolean => {
|
|
1958
|
+
const when = field.when;
|
|
1959
|
+
const whenOk =
|
|
1960
|
+
when === undefined
|
|
1961
|
+
? true
|
|
1962
|
+
: typeof when === 'function'
|
|
1963
|
+
? when(editPredicateModel.value)
|
|
1964
|
+
: when;
|
|
1965
|
+
const showOk = field.show ? field.show() : true;
|
|
1966
|
+
return whenOk && showOk;
|
|
1967
|
+
};
|
|
1968
|
+
|
|
1836
1969
|
// Fields whose value actually belongs in the payload. A presentational field
|
|
1837
1970
|
// (submit: false) renders but holds no data — see FieldDefinition.submit.
|
|
1838
1971
|
const submittableEditFields = computed(() =>
|
|
@@ -1843,7 +1976,21 @@ const submittableEditFields = computed(() =>
|
|
|
1843
1976
|
// so a `submit: false` control — or a `span` slot calling `update` — can write
|
|
1844
1977
|
// its key back into the form data after seeding. Strip them on the way out.
|
|
1845
1978
|
const nonSubmittedFieldKeys = computed(
|
|
1846
|
-
() =>
|
|
1979
|
+
() =>
|
|
1980
|
+
new Set(
|
|
1981
|
+
(props.editFields ?? [])
|
|
1982
|
+
.filter(
|
|
1983
|
+
(field) =>
|
|
1984
|
+
// Presentational: lays the form out, holds no data (#110).
|
|
1985
|
+
field.submit === false ||
|
|
1986
|
+
// Hidden by `when` at submit time (#117). Submitting a field
|
|
1987
|
+
// the user cannot see is a silent write — and with `default`
|
|
1988
|
+
// set it writes a value they never chose. Omitting the key
|
|
1989
|
+
// leaves the stored value alone.
|
|
1990
|
+
!isEditFieldVisible(field),
|
|
1991
|
+
)
|
|
1992
|
+
.map((field) => field.key),
|
|
1993
|
+
),
|
|
1847
1994
|
);
|
|
1848
1995
|
|
|
1849
1996
|
const stripNonSubmittedFields = (data: Record<string, any>): Record<string, any> => {
|
|
@@ -1885,9 +2032,50 @@ const TABLE_SLOT_NAMES = new Set([
|
|
|
1885
2032
|
const isTableSlot = (name: string) =>
|
|
1886
2033
|
TABLE_SLOT_NAMES.has(name) || TABLE_SLOT_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
1887
2034
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2035
|
+
/*
|
|
2036
|
+
* The inner table is KEYED on the set of forwarded slot names (#114).
|
|
2037
|
+
*
|
|
2038
|
+
* bootstrap-vue-next's BTable captures its slot set at mount: a `cell(...)` slot
|
|
2039
|
+
* that comes into existence LATER — a consumer whose columns are data-driven and
|
|
2040
|
+
* only known once a fetch resolves — never reaches the cells, and the column
|
|
2041
|
+
* renders raw values as though no cell slot existed. Verified against raw BTable,
|
|
2042
|
+
* so it isn't something our forwarding chain drops.
|
|
2043
|
+
*
|
|
2044
|
+
* Nothing in the forwarding can fix that, so the inner table remounts when the
|
|
2045
|
+
* slot SET changes. Consumers were already remounting to work around it, but at
|
|
2046
|
+
* the DXTable level, which throws away per-page, filters, sort and page — all of
|
|
2047
|
+
* which live out here and now survive. The signature is names-only, so a slot's
|
|
2048
|
+
* CONTENT changing (the common case) doesn't remount anything.
|
|
2049
|
+
*
|
|
2050
|
+
* In provider mode a remount refetches; that only happens when the column set
|
|
2051
|
+
* itself changes, which is already a refetch in practice.
|
|
2052
|
+
*/
|
|
2053
|
+
/*
|
|
2054
|
+
* A FUNCTION, not a computed: `useSlots()` returns an object Vue mutates in
|
|
2055
|
+
* place with nothing to track, so a computed over its keys is captured on the
|
|
2056
|
+
* first render and never updates — which is what made late `cell(...)` slots
|
|
2057
|
+
* invisible (#114). Called from the template, it re-evaluates every render.
|
|
2058
|
+
*
|
|
2059
|
+
* It also has to EXCLUDE the slots DXTable renders itself: declaring `#thead-top`
|
|
2060
|
+
* here (even rendering nothing into it) would override DXTable's own thead-top
|
|
2061
|
+
* template, silently dropping the filter row and the consumer's banner (#120).
|
|
2062
|
+
*/
|
|
2063
|
+
const forwardableSlotNames = (slots: Record<string, unknown>): string[] =>
|
|
2064
|
+
Object.keys(slots).filter((name) => isTableSlot(name));
|
|
2065
|
+
|
|
2066
|
+
const tableSlotSignature = (slots: Record<string, unknown>): string =>
|
|
2067
|
+
Object.keys(slots).filter(isTableSlot).sort().join('|');
|
|
2068
|
+
|
|
2069
|
+
/*
|
|
2070
|
+
* The template iterates `$slots` DIRECTLY rather than a computed over
|
|
2071
|
+
* `useSlots()`. It has to: `useSlots()` returns an object that Vue MUTATES in
|
|
2072
|
+
* place, with no reactive dependency to track — so a computed over its keys is
|
|
2073
|
+
* captured on first render and never recomputes. A consumer whose `cell(...)`
|
|
2074
|
+
* slots come into existence after a fetch resolves (data-driven columns) got
|
|
2075
|
+
* columns with raw values and no cell templates, and had to remount the whole
|
|
2076
|
+
* table to fix it (#114). Reading `$slots` inside the render function re-reads
|
|
2077
|
+
* the current slot set on every update.
|
|
2078
|
+
*/
|
|
1891
2079
|
|
|
1892
2080
|
// Table-level features of the inner BTable, exposed verbatim and bound to all
|
|
1893
2081
|
// three data modes from one place.
|
|
@@ -1956,6 +2144,11 @@ const getFieldSortState = (fieldKey: string) => {
|
|
|
1956
2144
|
|
|
1957
2145
|
// Handle row click for editing
|
|
1958
2146
|
const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
2147
|
+
// A row the consumer marked non-actionable doesn't look clickable, so it
|
|
2148
|
+
// must not BE clickable either — otherwise a click that looks dead quietly
|
|
2149
|
+
// navigates or opens the modal (#115).
|
|
2150
|
+
if (props.rowClickable && !props.rowClickable(item, index)) return;
|
|
2151
|
+
|
|
1959
2152
|
// Always emit rowClicked for custom handling
|
|
1960
2153
|
emit('rowClicked', item, index, event);
|
|
1961
2154
|
|
|
@@ -1975,13 +2168,13 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1975
2168
|
// Presentational fields (submit: false) lay the form out; they hold
|
|
1976
2169
|
// no data and must not be POSTed just because they were declared.
|
|
1977
2170
|
submittableEditFields.value.forEach(field => {
|
|
1978
|
-
formData[field.key] = (item
|
|
2171
|
+
formData[field.key] = seedValueFor(field, item);
|
|
1979
2172
|
});
|
|
1980
2173
|
editForm.value = useForm(formData);
|
|
1981
2174
|
} else {
|
|
1982
2175
|
// Update existing form
|
|
1983
2176
|
submittableEditFields.value.forEach(field => {
|
|
1984
|
-
editForm.value.data[field.key] = (item
|
|
2177
|
+
editForm.value.data[field.key] = seedValueFor(field, item);
|
|
1985
2178
|
});
|
|
1986
2179
|
editForm.value.clearErrors();
|
|
1987
2180
|
}
|
|
@@ -2284,13 +2477,15 @@ defineExpose({
|
|
|
2284
2477
|
</script>
|
|
2285
2478
|
|
|
2286
2479
|
<style scoped>
|
|
2287
|
-
/* Rows that do something on click look like it
|
|
2288
|
-
|
|
2289
|
-
|
|
2480
|
+
/* Rows that do something on click look like it. Hung off a marker class rather
|
|
2481
|
+
than a blanket `tbody tr` rule, so a row the consumer's `rowClickable` says is
|
|
2482
|
+
not actionable simply doesn't get the affordance (#107, #115). */
|
|
2483
|
+
:deep(tbody tr.dx-row-actionable) {
|
|
2484
|
+
cursor: pointer;
|
|
2290
2485
|
}
|
|
2291
2486
|
|
|
2292
|
-
:deep(tbody tr:hover) {
|
|
2293
|
-
background-color:
|
|
2487
|
+
:deep(tbody tr.dx-row-actionable:hover) {
|
|
2488
|
+
background-color: var(--bs-table-hover-bg);
|
|
2294
2489
|
}
|
|
2295
2490
|
|
|
2296
2491
|
/* Improve pagination button sizing to match form controls */
|
package/resources/js/index.ts
CHANGED
|
@@ -85,7 +85,20 @@ export { default as DProgress } from "./components/base/DProgress.vue";
|
|
|
85
85
|
export { default as DRow } from "./components/base/DRow.vue";
|
|
86
86
|
export { default as DSpinner } from "./components/base/DSpinner.vue";
|
|
87
87
|
export { default as DTable } from "./components/base/DTable.vue";
|
|
88
|
-
|
|
88
|
+
/*
|
|
89
|
+
* DTab is a RAW RE-EXPORT of BTab, not a wrapper — same exception as
|
|
90
|
+
* DCarouselSlide above, and for the same reason.
|
|
91
|
+
*
|
|
92
|
+
* BTabs decides which tab to activate on mount by scanning its slot vnodes for
|
|
93
|
+
* the BTab component type (its initial-selection branch is gated on
|
|
94
|
+
* `tabElementsArray.length > 0`). A wrapper component in between hides that, so
|
|
95
|
+
* BTabs found no tabs to select and NO pane got `active`/`show`: the nav
|
|
96
|
+
* rendered and every body was invisible until the user clicked (#119).
|
|
97
|
+
*
|
|
98
|
+
* Bisected — BTabs+BTab and DTabs+BTab both activate the first tab; BTabs+DTab
|
|
99
|
+
* does not. So it is the child wrapper, and only the child, that has to go.
|
|
100
|
+
*/
|
|
101
|
+
export { BTab as DTab } from "bootstrap-vue-next";
|
|
89
102
|
export { default as DTabs } from "./components/base/DTabs.vue";
|
|
90
103
|
export { default as DToast } from "./components/base/DToast.vue";
|
|
91
104
|
export { default as DToaster } from "./components/base/DToaster.vue";
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
declare var __VLS_7: string | number, __VLS_8: any;
|
|
2
|
-
type __VLS_Slots = {} & {
|
|
3
|
-
[K in NonNullable<typeof __VLS_7>]?: (props: typeof __VLS_8) => any;
|
|
4
|
-
};
|
|
5
|
-
declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
|
-
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
7
|
-
export default _default;
|
|
8
|
-
type __VLS_WithSlots<T, S> = T & {
|
|
9
|
-
new (): {
|
|
10
|
-
$slots: S;
|
|
11
|
-
};
|
|
12
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { BTab } from "bootstrap-vue-next";
|
|
3
|
-
import { useSlots } from "vue";
|
|
4
|
-
|
|
5
|
-
const slots = useSlots();
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
|
-
<template>
|
|
9
|
-
<BTab v-bind="$attrs">
|
|
10
|
-
<template v-for="(_, name) in slots" :key="name" #[name]="slotProps">
|
|
11
|
-
<slot :name="name" v-bind="slotProps" />
|
|
12
|
-
</template>
|
|
13
|
-
</BTab>
|
|
14
|
-
</template>
|