@omnitend/dashboard-for-laravel 0.36.0 → 0.38.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/components/base/DButton.vue.d.ts +7 -2
- package/dist/components/extended/DXSwitch.vue.d.ts +7 -0
- package/dist/components/extended/DXTable.vue.d.ts +19 -1
- package/dist/dashboard-for-laravel.js +4527 -4510
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +6 -6
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/style.css +2 -2
- package/docs/public/api-reference.json +16 -4
- package/docs/public/docs-map.md +1 -1
- package/package.json +1 -1
- package/resources/css/theme.scss +107 -2
- package/resources/js/components/base/DButton.vue +8 -3
- package/resources/js/components/extended/DXSwitch.vue +19 -1
- package/resources/js/components/extended/DXTable.vue +78 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-
|
|
2
|
+
"generated": "2026-07-23T18:28:14.921Z",
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "@omnitend/dashboard-for-laravel",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.38.0"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
8
8
|
"base": [
|
|
@@ -211,8 +211,8 @@
|
|
|
211
211
|
"name": "variant",
|
|
212
212
|
"type": "union",
|
|
213
213
|
"required": false,
|
|
214
|
-
"default": "\"
|
|
215
|
-
"description": "The visual style variant of the button"
|
|
214
|
+
"default": "\"secondary\"",
|
|
215
|
+
"description": "The visual style variant of the button.\n\nDefaults to `'secondary'` (soft, non-emphatic). Under the soft-first\nhouse rule, `primary` is THE single emphatic action per page/modal, so a\nvariant-less button must NOT be emphatic by default — declare\n`variant=\"primary\"` explicitly where you mean the one loud action."
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
"name": "size",
|
|
@@ -3092,6 +3092,12 @@
|
|
|
3092
3092
|
"required": false,
|
|
3093
3093
|
"default": "\"success\"",
|
|
3094
3094
|
"description": "On-state colour. `\"success\"` (default) reads the house green-on / red-off\nstyle — an active/enabled/good switch (\"Product is current\", \"Stock is\ntracked\", \"Visible\"). `\"neutral\"` is for semantically-mixed switches\n(\"contains alcohol\", an allergen toggle, \"hidden on web shop\") that\nshouldn't imply good/bad — it uses the brand primary on / grey off (#158)."
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
"name": "size",
|
|
3098
|
+
"type": "\"sm\"",
|
|
3099
|
+
"required": false,
|
|
3100
|
+
"description": "`\"sm\"` renders a compact box (~31px vs the default ~35px input height) for\ndense rows that repeat the switch per item (a per-line visibility toggle in\na modal). Omit for the standard size, which matches the `.form-control`\nheight so a switch lines up with inputs beside it."
|
|
3095
3101
|
}
|
|
3096
3102
|
],
|
|
3097
3103
|
"events": [
|
|
@@ -3473,6 +3479,12 @@
|
|
|
3473
3479
|
"required": false,
|
|
3474
3480
|
"default": "false",
|
|
3475
3481
|
"description": "Apply `table-layout: fixed` to the underlying table. Off by default, so\nexisting tables keep their content-sized columns. When on, column widths\nstop depending on cell content, so they no longer reshuffle when a filter\nnarrows the rows — the table stays put as you filter (#156). Combine with\nper-field `width`/`minWidth` to control the proportions; columns with no\ndeclared width share the remaining space equally."
|
|
3482
|
+
},
|
|
3483
|
+
{
|
|
3484
|
+
"name": "primaryKey",
|
|
3485
|
+
"type": "string",
|
|
3486
|
+
"required": false,
|
|
3487
|
+
"description": "Field on each row whose value uniquely identifies it, forwarded to the\ninner bvn table's `primaryKey` (all three data modes). Without it bvn keys\nclient-side rows by their INDEX, which mis-associates per-row STATEFUL cell\ncomponents when a row is inserted/removed mid-interaction — a debounced\nedit in one row can end up saved against the wrong record after a\nconcurrent delete of a row above. Set it (e.g. `primary-key=\"id\"`) so bvn\nkeys rows by that field's value and each row keeps its identity across\ndata mutations. Non-breaking: omit it and rows key by index exactly as\nbefore."
|
|
3476
3488
|
}
|
|
3477
3489
|
],
|
|
3478
3490
|
"events": [
|
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-23T18:28:14.969Z
|
|
5
5
|
|
|
6
6
|
This file provides a complete map of all available documentation for AI agents and developers.
|
|
7
7
|
|
package/package.json
CHANGED
package/resources/css/theme.scss
CHANGED
|
@@ -98,6 +98,18 @@ $btn-padding-x-lg: 1.5rem; // 24px
|
|
|
98
98
|
|
|
99
99
|
$input-padding-y: 0.375rem; // compact fields (Bootstrap's default; 0.625rem read oversized)
|
|
100
100
|
$input-padding-x: 1rem;
|
|
101
|
+
|
|
102
|
+
// Match the sm/lg INPUT vertical padding to the sm/lg BUTTON padding above
|
|
103
|
+
// (PO18). Bootstrap sizes every child of an `.input-group-sm`/`-lg` — buttons
|
|
104
|
+
// INCLUDED — with the INPUT's sized padding, so leaving these at Bootstrap's
|
|
105
|
+
// defaults (0.25rem / 0.5rem) made a sized input-group sit ~8px / ~4px shorter
|
|
106
|
+
// than a `.btn-sm`/`.btn-lg` beside it. Aligning them makes sized controls share
|
|
107
|
+
// one height by construction. NB: the BASE `$input-padding-y` (0.375rem) stays
|
|
108
|
+
// deliberately more compact than `$btn-padding-y` (0.625rem) — see the comment
|
|
109
|
+
// above ("0.625rem read oversized"); only the SIZED variants are equalised,
|
|
110
|
+
// because those are the ones whose padding Bootstrap leaks onto grouped buttons.
|
|
111
|
+
$input-padding-y-sm: 0.5rem; // == $btn-padding-y-sm (was Bootstrap's 0.25rem)
|
|
112
|
+
$input-padding-y-lg: 0.75rem; // == $btn-padding-y-lg (was Bootstrap's 0.5rem)
|
|
101
113
|
$input-border-radius: $border-radius;
|
|
102
114
|
$input-border-color: #d1d5db; // Gray-300
|
|
103
115
|
$input-focus-border-color: $primary;
|
|
@@ -652,8 +664,52 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
652
664
|
}
|
|
653
665
|
|
|
654
666
|
// DXTable header titles: muted by default (#157) so the table's CONTENT is
|
|
655
|
-
// the loud layer.
|
|
656
|
-
|
|
667
|
+
// the loud layer. Lightened again for S5b — #157's `--bs-secondary-color`
|
|
668
|
+
// (rgba(33,37,41,.75), ~5.9:1 on white) still read near-black next to legacy
|
|
669
|
+
// omnitend's light-slate headers. `#7c8293` (legacy's `$text-secondary`) is
|
|
670
|
+
// the signed-off value (~3.6:1 on white — fine for the semibold/large header
|
|
671
|
+
// text it colours, below the 4.5:1 body-text bar). Still on the same token so
|
|
672
|
+
// a consumer can re-louden (e.g. `var(--bs-body-color)`).
|
|
673
|
+
--dx-table-header-color: #7c8293;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ----------------------------------------------------------------------------
|
|
677
|
+
// Tables — header alignment + filter-row affordance
|
|
678
|
+
// ----------------------------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
// B17: keep header labels vertically centred regardless of whether a column
|
|
681
|
+
// carries sort icons. Bootstrap sets `.table > thead { vertical-align: bottom }`,
|
|
682
|
+
// which floors a plain (non-sortable) column's label while a sortable
|
|
683
|
+
// neighbour's taller two-arrow sort stack holds ITS label higher — one header
|
|
684
|
+
// row, mixed baselines. Centring the whole thead makes vertical alignment
|
|
685
|
+
// independent of the sort indicator's presence. Scoped to `.table > thead` so a
|
|
686
|
+
// consumer who genuinely wants bottom-aligned headers can still opt back in on a
|
|
687
|
+
// specific table.
|
|
688
|
+
.table > thead {
|
|
689
|
+
vertical-align: middle;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// PL1: a magnifier glyph on DXTable's text/number filter inputs, so the filter
|
|
693
|
+
// row reads as "this searches" at a glance (legacy lt-table carried one).
|
|
694
|
+
//
|
|
695
|
+
// It MUST be a background-image, not a `::before`/`::after` — an `<input>` is a
|
|
696
|
+
// replaced element and cannot host pseudo-elements. The glyph is an inline
|
|
697
|
+
// bootstrap-icons "search" SVG data-URI, tinted `#7c8293` (the same muted slate
|
|
698
|
+
// as the header token above), sized and inset with `padding-left` so text
|
|
699
|
+
// clears it.
|
|
700
|
+
//
|
|
701
|
+
// Direct-child selector (`> th > input.form-control`) is deliberate: DXTable's
|
|
702
|
+
// text/number/date filters render a bare `.form-control` straight inside the
|
|
703
|
+
// `<th>`, whereas the `select` filter's DAutocomplete nests its input inside
|
|
704
|
+
// `.d-autocomplete .input-group .b-autocomplete-input-wrapper` — so it is not a
|
|
705
|
+
// direct child and correctly gets NO glyph (it has its own chevron affordance).
|
|
706
|
+
// `:not([type="date"])` further excludes the date filter (its own picker icon).
|
|
707
|
+
.filter-row > th > input.form-control:not([type="date"]) {
|
|
708
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%237c8293' viewBox='0 0 16 16'%3e%3cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3e%3c/svg%3e");
|
|
709
|
+
background-repeat: no-repeat;
|
|
710
|
+
background-position: left 0.65rem center;
|
|
711
|
+
background-size: 0.85em;
|
|
712
|
+
padding-left: 1.9rem;
|
|
657
713
|
}
|
|
658
714
|
|
|
659
715
|
// primary is near-black navy, so Bootstrap's darken-on-hover is imperceptible —
|
|
@@ -679,6 +735,35 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
679
735
|
}
|
|
680
736
|
}
|
|
681
737
|
|
|
738
|
+
// ----------------------------------------------------------------------------
|
|
739
|
+
// Dropdowns — chevron caret (B14 / PO5)
|
|
740
|
+
// ----------------------------------------------------------------------------
|
|
741
|
+
// Bootstrap's `.dropdown-toggle::after` is a CSS border-triangle that sits high
|
|
742
|
+
// against Poppins' line box and reads crude next to the rest of the theme.
|
|
743
|
+
// Replace it with the bootstrap-icons chevron glyph, optically centred with a
|
|
744
|
+
// little breathing room from the label. The font is already available: the
|
|
745
|
+
// theme `@import`s `bootstrap-icons/font/bootstrap-icons` (declaring the
|
|
746
|
+
// `@font-face`), and #77's post-build extraction ships that woff2 with the
|
|
747
|
+
// stylesheet — so this needs no new dependency and no data-URI. Values signed
|
|
748
|
+
// off after one padding iteration in the greendragon interim; the glyph metrics
|
|
749
|
+
// are tunable via the variables below.
|
|
750
|
+
$dx-dropdown-caret-size: 0.75em; // glyph size relative to the label
|
|
751
|
+
$dx-dropdown-caret-gap: 0.4em; // space between label and chevron
|
|
752
|
+
$dx-dropdown-caret-shift: 0.05em; // vertical nudge for optical centring
|
|
753
|
+
.dropdown-toggle::after {
|
|
754
|
+
border: none; // drop Bootstrap's border-triangle
|
|
755
|
+
font-family: "bootstrap-icons";
|
|
756
|
+
content: "\F282"; // bi-chevron-down
|
|
757
|
+
font-size: $dx-dropdown-caret-size;
|
|
758
|
+
line-height: 1;
|
|
759
|
+
vertical-align: $dx-dropdown-caret-shift;
|
|
760
|
+
margin-left: $dx-dropdown-caret-gap;
|
|
761
|
+
}
|
|
762
|
+
.dropup .dropdown-toggle::after {
|
|
763
|
+
border: none;
|
|
764
|
+
content: "\F286"; // bi-chevron-up
|
|
765
|
+
}
|
|
766
|
+
|
|
682
767
|
// ----------------------------------------------------------------------------
|
|
683
768
|
// Switches — green when ON, red when OFF, by default (#158)
|
|
684
769
|
// ----------------------------------------------------------------------------
|
|
@@ -719,6 +804,21 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
|
|
|
719
804
|
--dx-switch-pill: var(--bs-gray-400, #ced4da); // neutral pill in both states
|
|
720
805
|
}
|
|
721
806
|
|
|
807
|
+
// Optically centre the check/radio/switch box against its label's FIRST line.
|
|
808
|
+
// Bootstrap's `.25em` top margin is mathematically right for a 1em box in a
|
|
809
|
+
// 1.5em line box, but Poppins seats its glyphs low in the line box, so the box
|
|
810
|
+
// reads visibly HIGH beside the text (same optical-metrics issue as the B14
|
|
811
|
+
// dropdown caret). `0.325em` lands the box centre on Poppins' visual centre —
|
|
812
|
+
// judged against a rendered filmstrip (0.25/0.30/0.325/0.35em) with Poppins
|
|
813
|
+
// loaded, not from the maths. A top MARGIN (not flex-centring) is deliberate:
|
|
814
|
+
// it aligns to the first line, so a multi-line label keeps the box on line one
|
|
815
|
+
// rather than floating to the block's vertical centre. DXSwitch is unaffected —
|
|
816
|
+
// it flex-centres and zeroes this margin on its inner input at higher
|
|
817
|
+
// specificity (`.dx-switch :deep(.form-check-input) { margin: 0 }`).
|
|
818
|
+
.form-check-input {
|
|
819
|
+
margin-top: 0.325em;
|
|
820
|
+
}
|
|
821
|
+
|
|
722
822
|
.form-switch .form-check-input {
|
|
723
823
|
// OFF: light-red track, red thumb (left).
|
|
724
824
|
background-color: var(--dx-switch-off-bg);
|
|
@@ -851,6 +951,11 @@ $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #
|
|
|
851
951
|
.b-autocomplete-content {
|
|
852
952
|
min-width: max-content; // grow to the longest option (floors at the trigger width)
|
|
853
953
|
max-width: min(90vw, 32rem); // but don't run away
|
|
954
|
+
// S3b.1: bvn caps this popup at 300px, showing ~7 of a ~30-option list behind
|
|
955
|
+
// an inner scrollbar — it "doesn't drop all the way down the page like a
|
|
956
|
+
// native select". Let it fall most of the way down the viewport (but never
|
|
957
|
+
// taller than 42rem) so the list reads like a real dropdown.
|
|
958
|
+
max-height: min(70vh, 42rem);
|
|
854
959
|
}
|
|
855
960
|
|
|
856
961
|
// ----------------------------------------------------------------------------
|
|
@@ -37,8 +37,13 @@ type LinkVariant =
|
|
|
37
37
|
|
|
38
38
|
interface Props {
|
|
39
39
|
/**
|
|
40
|
-
* The visual style variant of the button
|
|
41
|
-
*
|
|
40
|
+
* The visual style variant of the button.
|
|
41
|
+
*
|
|
42
|
+
* Defaults to `'secondary'` (soft, non-emphatic). Under the soft-first
|
|
43
|
+
* house rule, `primary` is THE single emphatic action per page/modal, so a
|
|
44
|
+
* variant-less button must NOT be emphatic by default — declare
|
|
45
|
+
* `variant="primary"` explicitly where you mean the one loud action.
|
|
46
|
+
* @default 'secondary'
|
|
42
47
|
*/
|
|
43
48
|
variant?: ButtonVariant | LinkVariant | null;
|
|
44
49
|
|
|
@@ -105,7 +110,7 @@ interface Props {
|
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
const props = withDefaults(defineProps<Props>(), {
|
|
108
|
-
variant: "
|
|
113
|
+
variant: "secondary",
|
|
109
114
|
disabled: false,
|
|
110
115
|
loading: false,
|
|
111
116
|
block: false,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
`label` prop for simple text, or the default slot for richer label content.
|
|
22
22
|
-->
|
|
23
23
|
<template>
|
|
24
|
-
<div class="dx-switch" :class="[`dx-switch--${onVariant}`, { 'dx-switch--on': isOn }]">
|
|
24
|
+
<div class="dx-switch" :class="[`dx-switch--${onVariant}`, { 'dx-switch--on': isOn, 'dx-switch--sm': size === 'sm' }]">
|
|
25
25
|
<!-- For the neutral variant, tag the inner `.form-switch` with
|
|
26
26
|
`switch-neutral` so the global theme restores grey-off / primary-on for
|
|
27
27
|
the toggle itself (the box styling below handles the wrapper). -->
|
|
@@ -63,6 +63,13 @@ interface Props {
|
|
|
63
63
|
* shouldn't imply good/bad — it uses the brand primary on / grey off (#158).
|
|
64
64
|
*/
|
|
65
65
|
onVariant?: "success" | "neutral";
|
|
66
|
+
/**
|
|
67
|
+
* `"sm"` renders a compact box (~31px vs the default ~35px input height) for
|
|
68
|
+
* dense rows that repeat the switch per item (a per-line visibility toggle in
|
|
69
|
+
* a modal). Omit for the standard size, which matches the `.form-control`
|
|
70
|
+
* height so a switch lines up with inputs beside it.
|
|
71
|
+
*/
|
|
72
|
+
size?: "sm";
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -131,6 +138,17 @@ defineOptions({
|
|
|
131
138
|
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
/* Compact size (#… size="sm"): a shorter box for dense rows that repeat the
|
|
142
|
+
switch per item. `min-height: auto` drops the input-height floor so the box
|
|
143
|
+
shrinks to its padded content (~31px vs ~35px); the smaller font pulls the
|
|
144
|
+
label in to match. The fixed-rem toggle itself is unchanged — only the box
|
|
145
|
+
chrome tightens. */
|
|
146
|
+
.dx-switch--sm :deep(.form-check) {
|
|
147
|
+
font-size: 0.875rem;
|
|
148
|
+
min-height: auto;
|
|
149
|
+
padding: 0.25rem 0.625rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
134
152
|
/* Label on the left, toggle on the right (DOM order is input then label). */
|
|
135
153
|
.dx-switch :deep(.form-check-label) {
|
|
136
154
|
order: 1;
|
|
@@ -131,6 +131,23 @@
|
|
|
131
131
|
@update:model-value="handleSelectFilterChange(field, $event)"
|
|
132
132
|
/>
|
|
133
133
|
|
|
134
|
+
<!-- Native Select Filter (S3b): a plain <select> for consumers who
|
|
135
|
+
want OS-native behaviour (full-height menu, native keyboard/scroll)
|
|
136
|
+
instead of the DAutocomplete typeahead. Shares the select filter's
|
|
137
|
+
option list (getFieldFilterOptions) and change handler
|
|
138
|
+
(handleSelectFilterChange) EXACTLY — so it behaves identically to
|
|
139
|
+
filter:"select" in every way but the control. Single-select only:
|
|
140
|
+
`filterMultiple` is ignored here. The model maps "no filter" to the
|
|
141
|
+
FILTER_ALL_VALUE sentinel (see nativeSelectFilterValue) so the "All …"
|
|
142
|
+
option shows selected in a native control. -->
|
|
143
|
+
<DFormSelect
|
|
144
|
+
v-else-if="field.filter === 'select-native'"
|
|
145
|
+
:model-value="nativeSelectFilterValue(field)"
|
|
146
|
+
:options="getFieldFilterOptions(field) as unknown as Record<string, unknown>[]"
|
|
147
|
+
size="sm"
|
|
148
|
+
@update:model-value="handleSelectFilterChange(field, $event)"
|
|
149
|
+
/>
|
|
150
|
+
|
|
134
151
|
<!-- Number Filter -->
|
|
135
152
|
<DFormInput
|
|
136
153
|
v-else-if="field.filter === 'number'"
|
|
@@ -310,11 +327,17 @@ import DXTableShell from "./DXTableShell.vue";
|
|
|
310
327
|
import DSpinner from "../base/DSpinner.vue";
|
|
311
328
|
import DTable from "../base/DTable.vue";
|
|
312
329
|
import DFormInput from "../base/DFormInput.vue";
|
|
330
|
+
import DFormSelect from "../base/DFormSelect.vue";
|
|
313
331
|
import DAutocomplete from "../base/DAutocomplete.vue";
|
|
314
332
|
import DButton from "../base/DButton.vue";
|
|
315
333
|
import DXTablePagination from "./DXTablePagination.vue";
|
|
316
334
|
import DXTableEditorModal from "./DXTableEditorModal.vue";
|
|
317
|
-
|
|
335
|
+
// 'select' — DAutocomplete typeahead (browse-on-focus + type-to-narrow).
|
|
336
|
+
// 'select-native' — a plain native <select> (DFormSelect). Same options, value
|
|
337
|
+
// semantics and client-side matching as 'select'; differs ONLY in the control,
|
|
338
|
+
// for consumers who want OS-native menu behaviour (full-height menu, native
|
|
339
|
+
// keyboard/scroll). Single-select only — `filterMultiple` is ignored for it.
|
|
340
|
+
export type FilterType = 'text' | 'select' | 'select-native' | 'number' | 'date' | false;
|
|
318
341
|
|
|
319
342
|
export interface FilterOption {
|
|
320
343
|
value: string;
|
|
@@ -384,6 +407,12 @@ export interface TableField {
|
|
|
384
407
|
filterAllText?: string;
|
|
385
408
|
sortable?: boolean;
|
|
386
409
|
hint?: string;
|
|
410
|
+
/**
|
|
411
|
+
* The inline filter control for this column, or `false`/omit for none. See
|
|
412
|
+
* `FilterType`. `'select'` (typeahead) and `'select-native'` (plain
|
|
413
|
+
* `<select>`) share the same options and value semantics — pick the native
|
|
414
|
+
* one when OS-native menu behaviour matters more than type-to-narrow.
|
|
415
|
+
*/
|
|
387
416
|
filter?: FilterType;
|
|
388
417
|
filterOptions?: FilterOption[];
|
|
389
418
|
filterPlaceholder?: string;
|
|
@@ -769,6 +798,19 @@ export interface Props<TItem = any> {
|
|
|
769
798
|
* declared width share the remaining space equally.
|
|
770
799
|
*/
|
|
771
800
|
fixedLayout?: boolean;
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Field on each row whose value uniquely identifies it, forwarded to the
|
|
804
|
+
* inner bvn table's `primaryKey` (all three data modes). Without it bvn keys
|
|
805
|
+
* client-side rows by their INDEX, which mis-associates per-row STATEFUL cell
|
|
806
|
+
* components when a row is inserted/removed mid-interaction — a debounced
|
|
807
|
+
* edit in one row can end up saved against the wrong record after a
|
|
808
|
+
* concurrent delete of a row above. Set it (e.g. `primary-key="id"`) so bvn
|
|
809
|
+
* keys rows by that field's value and each row keeps its identity across
|
|
810
|
+
* data mutations. Non-breaking: omit it and rows key by index exactly as
|
|
811
|
+
* before.
|
|
812
|
+
*/
|
|
813
|
+
primaryKey?: string;
|
|
772
814
|
}
|
|
773
815
|
|
|
774
816
|
const props = withDefaults(defineProps<Props<T>>(), {
|
|
@@ -1102,7 +1144,9 @@ const clientSideFilteredItems = computed<T[]>(() => {
|
|
|
1102
1144
|
// Case-insensitive contains search
|
|
1103
1145
|
return String(itemValue).toLowerCase().includes(filterValue);
|
|
1104
1146
|
|
|
1147
|
+
// Both select controls match identically (exact string match).
|
|
1105
1148
|
case 'select':
|
|
1149
|
+
case 'select-native':
|
|
1106
1150
|
// Exact match
|
|
1107
1151
|
return String(itemValue) === candidate;
|
|
1108
1152
|
|
|
@@ -1272,6 +1316,15 @@ const fieldsHaveWidths = computed(() => props.fields.some(hasDeclaredWidth));
|
|
|
1272
1316
|
// key: a display column can filter on a different server param via `filterKey`.
|
|
1273
1317
|
const filterKeyFor = (field: TableField): string => field.filterKey ?? field.key;
|
|
1274
1318
|
|
|
1319
|
+
// Both select-style filters — 'select' (DAutocomplete typeahead) and
|
|
1320
|
+
// 'select-native' (plain <select>) — share the SAME option list, value
|
|
1321
|
+
// semantics and client-side matching; they differ only in the rendered control.
|
|
1322
|
+
// This predicate keeps the two in lockstep everywhere the option-derivation and
|
|
1323
|
+
// server-value logic runs, so a `select-native` column gets identical options to
|
|
1324
|
+
// what `select` would offer.
|
|
1325
|
+
const isSelectFilter = (field: TableField): boolean =>
|
|
1326
|
+
field.filter === 'select' || field.filter === 'select-native';
|
|
1327
|
+
|
|
1275
1328
|
// The value that means "has no value" for a select filter (#106).
|
|
1276
1329
|
const filterNullValueFor = (field: TableField): string => field.filterNullValue ?? 'null';
|
|
1277
1330
|
|
|
@@ -1286,6 +1339,17 @@ const handleSelectFilterChange = (field: TableField, value: unknown) => {
|
|
|
1286
1339
|
handleFilterChange(filterKeyFor(field), selected === FILTER_ALL_VALUE ? '' : selected);
|
|
1287
1340
|
};
|
|
1288
1341
|
|
|
1342
|
+
// The model value for a `select-native` control. Unlike DAutocomplete (which
|
|
1343
|
+
// falls back to '' and shows its placeholder), a native <select> selects its
|
|
1344
|
+
// FIRST option when the bound value matches none — which would desync the
|
|
1345
|
+
// control from the model. So the "no filter" state must map to FILTER_ALL_VALUE,
|
|
1346
|
+
// the value carried by the "All …" option, so that option shows as selected.
|
|
1347
|
+
// This is the read-side mirror of the write-side translation
|
|
1348
|
+
// `handleSelectFilterChange` already does (FILTER_ALL_VALUE → '') — the sentinel
|
|
1349
|
+
// is handled in one conceptual place, not reinvented.
|
|
1350
|
+
const nativeSelectFilterValue = (field: TableField): string =>
|
|
1351
|
+
(effectiveFilters.value[filterKeyFor(field)] as string) || FILTER_ALL_VALUE;
|
|
1352
|
+
|
|
1289
1353
|
// Multi-value select filter (#51): the autocomplete's array model maps
|
|
1290
1354
|
// straight onto the filters entry; an emptied selection removes the entry.
|
|
1291
1355
|
const multiFilterValueFor = (field: TableField): string[] => {
|
|
@@ -1361,7 +1425,7 @@ const derivedFilterOptions = computed<Record<string, FilterOption[]>>(() => {
|
|
|
1361
1425
|
const optionsByFieldKey: Record<string, FilterOption[]> = {};
|
|
1362
1426
|
|
|
1363
1427
|
for (const field of props.fields) {
|
|
1364
|
-
if (field
|
|
1428
|
+
if (!isSelectFilter(field)) continue;
|
|
1365
1429
|
if (field.deriveFilterOptions === false) continue;
|
|
1366
1430
|
// Explicit options win outright (see `getFieldFilterOptions`), so don't
|
|
1367
1431
|
// spend a pass over every row producing a list nothing will read.
|
|
@@ -1434,8 +1498,10 @@ const getFieldFilterOptions = (field: TableField): FilterOption[] => {
|
|
|
1434
1498
|
//
|
|
1435
1499
|
// A `filterMultiple` column omits it (#51): "no filter" inside a
|
|
1436
1500
|
// multi-select reads as a selectable value, and clearing is already
|
|
1437
|
-
// discoverable there — remove the chips, or the ✕.
|
|
1438
|
-
|
|
1501
|
+
// discoverable there — remove the chips, or the ✕. `select-native` is the
|
|
1502
|
+
// exception: it is ALWAYS single-select (it ignores `filterMultiple`), so it
|
|
1503
|
+
// still needs the "All" reset option even when `filterMultiple` is set.
|
|
1504
|
+
if (!field.filterMultiple || field.filter === 'select-native') {
|
|
1439
1505
|
options.push({ value: FILTER_ALL_VALUE, text: filterAllLabelFor(field) });
|
|
1440
1506
|
}
|
|
1441
1507
|
|
|
@@ -1750,7 +1816,7 @@ const shouldShowPerPageSelector = computed(() => {
|
|
|
1750
1816
|
// mutually exclusive, so a client-side derived column can never appear here.
|
|
1751
1817
|
const fieldsNeedingFilterValues = computed(() => {
|
|
1752
1818
|
return props.fields
|
|
1753
|
-
.filter(field => field
|
|
1819
|
+
.filter(field => isSelectFilter(field) && (!field.filterOptions || field.filterOptions.length === 0))
|
|
1754
1820
|
.map(field => filterKeyFor(field));
|
|
1755
1821
|
});
|
|
1756
1822
|
|
|
@@ -2243,6 +2309,13 @@ const tablePassthroughProps = computed(() => ({
|
|
|
2243
2309
|
? `No ${pluralItemName.value} match your filters`
|
|
2244
2310
|
: `No ${pluralItemName.value} found`),
|
|
2245
2311
|
expandedItems: props.expandedItems,
|
|
2312
|
+
// Row identity (#B16). Forwarded here rather than in `tableModeBindings`
|
|
2313
|
+
// because it is mode-INDEPENDENT — it must key rows by value in provider,
|
|
2314
|
+
// client-side AND inertia mode alike. `undefined` when unset, so bvn falls
|
|
2315
|
+
// back to index keys (its default) and nothing changes for tables that don't
|
|
2316
|
+
// opt in. Reaches BTable through DTable's `v-bind="$attrs"` (its prop is
|
|
2317
|
+
// `primaryKey`); DTable needs no change.
|
|
2318
|
+
primaryKey: props.primaryKey,
|
|
2246
2319
|
}));
|
|
2247
2320
|
// Computed: Singular and plural item names
|
|
2248
2321
|
const singularItemName = computed(() => props.itemName);
|