@omnitend/dashboard-for-laravel 0.22.0 → 0.23.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/extended/DXDashboard.vue.d.ts +12 -1
- package/dist/components/extended/DXDashboardNavbar.vue.d.ts +7 -2
- package/dist/components/extended/DXField.vue.d.ts +4 -4
- package/dist/components/extended/DXTable.vue.d.ts +37 -2
- package/dist/components/extended/DXUserAvatar.vue.d.ts +40 -0
- package/dist/dashboard-for-laravel.js +7992 -7883
- 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 -0
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +23 -0
- package/docs/public/api-reference.json +120 -6
- package/docs/public/docs-map.md +3 -2
- package/docs/public/llms.txt +3 -2
- package/package.json +1 -1
- package/resources/js/components/extended/DXDashboard.vue +42 -4
- package/resources/js/components/extended/DXDashboardNavbar.vue +5 -18
- package/resources/js/components/extended/DXField.vue +122 -26
- package/resources/js/components/extended/DXTable.vue +304 -133
- package/resources/js/components/extended/DXUserAvatar.vue +104 -0
- package/resources/js/index.ts +1 -0
- package/resources/js/types/index.ts +25 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -231,6 +231,29 @@ export interface FieldDefinition {
|
|
|
231
231
|
* applied as `disabled`.
|
|
232
232
|
*/
|
|
233
233
|
readonly?: MaybeFn<boolean>;
|
|
234
|
+
/**
|
|
235
|
+
* Display the value as static text instead of a control — no border, no
|
|
236
|
+
* input box (Bootstrap's `.form-control-plaintext`). For values a
|
|
237
|
+
* profile/settings page shows but never lets you edit, where a `readonly`
|
|
238
|
+
* input still reads as "you could edit this, but can't".
|
|
239
|
+
*
|
|
240
|
+
* Implies read-only, for every field type. Anything backed by a real input
|
|
241
|
+
* — the text family, textarea, currency, percentage, autocomplete — renders
|
|
242
|
+
* as static text; control types with no native readonly state
|
|
243
|
+
* (select/radio/checkbox/switch/file) are disabled instead, exactly as they
|
|
244
|
+
* are for `readonly`.
|
|
245
|
+
*
|
|
246
|
+
* Enforced over `inputProps`: unlike the field's other bindings, which
|
|
247
|
+
* `inputProps` may override, a plaintext field cannot be made editable
|
|
248
|
+
* again through it.
|
|
249
|
+
*/
|
|
250
|
+
plaintext?: MaybeFn<boolean>;
|
|
251
|
+
/**
|
|
252
|
+
* Only for `type: "password"`. Show the reveal (eye) toggle that switches
|
|
253
|
+
* the input between masked and plain text. Defaults to `true` — set
|
|
254
|
+
* `false` to render a bare password input.
|
|
255
|
+
*/
|
|
256
|
+
revealable?: boolean;
|
|
234
257
|
/**
|
|
235
258
|
* Conditionally show or hide this field. When omitted the field is
|
|
236
259
|
* always visible. Boolean or a function of the form model; evaluated
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-
|
|
2
|
+
"generated": "2026-07-11T08:44:15.505Z",
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "@omnitend/dashboard-for-laravel",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.22.0"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
8
8
|
"base": [
|
|
@@ -1468,6 +1468,22 @@
|
|
|
1468
1468
|
{
|
|
1469
1469
|
"name": "name",
|
|
1470
1470
|
"title": "binding"
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
"title": "binding",
|
|
1474
|
+
"type": {
|
|
1475
|
+
"name": "function"
|
|
1476
|
+
},
|
|
1477
|
+
"name": "toggleSidebar",
|
|
1478
|
+
"description": "Shows/hides the sidebar."
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
"title": "binding",
|
|
1482
|
+
"type": {
|
|
1483
|
+
"name": "boolean"
|
|
1484
|
+
},
|
|
1485
|
+
"name": "sidebarHidden",
|
|
1486
|
+
"description": "Whether the sidebar is currently hidden."
|
|
1471
1487
|
}
|
|
1472
1488
|
]
|
|
1473
1489
|
},
|
|
@@ -1562,7 +1578,7 @@
|
|
|
1562
1578
|
},
|
|
1563
1579
|
{
|
|
1564
1580
|
"name": "user-icon",
|
|
1565
|
-
"description": "Custom user avatar/icon in the dropdown trigger.",
|
|
1581
|
+
"description": "Custom user avatar/icon in the dropdown trigger. To decorate the default avatar (e.g. add a notification dot) rather than replace it, render `DXUserAvatar` — the same component used here, so the styling comes with it.",
|
|
1566
1582
|
"bindings": [
|
|
1567
1583
|
{
|
|
1568
1584
|
"title": "binding",
|
|
@@ -1571,6 +1587,14 @@
|
|
|
1571
1587
|
},
|
|
1572
1588
|
"name": "initial",
|
|
1573
1589
|
"description": "The first letter of the user's name."
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
"title": "binding",
|
|
1593
|
+
"type": {
|
|
1594
|
+
"name": "object"
|
|
1595
|
+
},
|
|
1596
|
+
"name": "user",
|
|
1597
|
+
"description": "The signed-in user."
|
|
1574
1598
|
}
|
|
1575
1599
|
]
|
|
1576
1600
|
},
|
|
@@ -2857,6 +2881,39 @@
|
|
|
2857
2881
|
"required": false,
|
|
2858
2882
|
"description": "API endpoint for creating new items (e.g., \"/api/products\") — enables \"New\" button"
|
|
2859
2883
|
},
|
|
2884
|
+
{
|
|
2885
|
+
"name": "showCreateButton",
|
|
2886
|
+
"type": "boolean",
|
|
2887
|
+
"required": false,
|
|
2888
|
+
"default": "true",
|
|
2889
|
+
"description": "Render the built-in \"New {item}\" button in the card header (default\n`true` whenever `createUrl` is set). Set `false` to drive the create\nmodal from your own trigger elsewhere — e.g. a button in the dashboard\nnavbar's actions slot calling the exposed `openCreate()`. With no\n`title` and no `header` slot, the header is then dropped entirely\nrather than left empty."
|
|
2890
|
+
},
|
|
2891
|
+
{
|
|
2892
|
+
"name": "footClone",
|
|
2893
|
+
"type": "boolean",
|
|
2894
|
+
"required": false,
|
|
2895
|
+
"default": "false",
|
|
2896
|
+
"description": "Render a `<tfoot>` mirroring the header row, so `#foot(<key>)` slots can\nhold per-column totals that line up under their data (#99). Without it\nthere is no footer to put them in. For a fully custom footer, use the\n`custom-foot` slot instead."
|
|
2897
|
+
},
|
|
2898
|
+
{
|
|
2899
|
+
"name": "showEmpty",
|
|
2900
|
+
"type": "boolean",
|
|
2901
|
+
"required": false,
|
|
2902
|
+
"default": "true",
|
|
2903
|
+
"description": "Show a message row when there are no rows (default `true`). An empty\ntable body is indistinguishable from a broken one, so the message is on\nby default. Override the wording with `emptyText`, or the whole row with\nthe `empty` slot."
|
|
2904
|
+
},
|
|
2905
|
+
{
|
|
2906
|
+
"name": "emptyText",
|
|
2907
|
+
"type": "string",
|
|
2908
|
+
"required": false,
|
|
2909
|
+
"description": "Message shown when there are no rows. Defaults to \"No {items} found\", or\n\"No {items} match your filters\" when a column filter is active."
|
|
2910
|
+
},
|
|
2911
|
+
{
|
|
2912
|
+
"name": "expandedItems",
|
|
2913
|
+
"type": "Array",
|
|
2914
|
+
"required": false,
|
|
2915
|
+
"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."
|
|
2916
|
+
},
|
|
2860
2917
|
{
|
|
2861
2918
|
"name": "clientSide",
|
|
2862
2919
|
"type": "boolean",
|
|
@@ -2925,6 +2982,11 @@
|
|
|
2925
2982
|
"description": "`v-model:sortBy` update, emitted with the normalized single-column sort array when sorting changes.",
|
|
2926
2983
|
"properties": []
|
|
2927
2984
|
},
|
|
2985
|
+
{
|
|
2986
|
+
"name": "update:expandedItems",
|
|
2987
|
+
"description": "`v-model:expandedItems` update, emitted with the rows currently expanded.",
|
|
2988
|
+
"properties": []
|
|
2989
|
+
},
|
|
2928
2990
|
{
|
|
2929
2991
|
"name": "update:filters",
|
|
2930
2992
|
"description": "`v-model:filters` update, emitted with the new filter map when a column filter changes.",
|
|
@@ -2949,7 +3011,7 @@
|
|
|
2949
3011
|
},
|
|
2950
3012
|
{
|
|
2951
3013
|
"name": "name",
|
|
2952
|
-
"description": "
|
|
3014
|
+
"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.",
|
|
2953
3015
|
"bindings": [
|
|
2954
3016
|
{
|
|
2955
3017
|
"name": "name",
|
|
@@ -3150,12 +3212,64 @@
|
|
|
3150
3212
|
}
|
|
3151
3213
|
],
|
|
3152
3214
|
"methods": []
|
|
3215
|
+
},
|
|
3216
|
+
{
|
|
3217
|
+
"name": "DXUserAvatar",
|
|
3218
|
+
"category": "extended",
|
|
3219
|
+
"filePath": "resources/js/components/extended/DXUserAvatar.vue",
|
|
3220
|
+
"description": "The circular user avatar shown in the dashboard navbar's user menu: a single initial on a solid disc, with an optional notification dot. It exists as its own component because `DXDashboardNavbar`'s `user-icon` slot is one consumers routinely *decorate* rather than replace — adding an unread badge, wrapping it in a link. The navbar's styles are scoped, so slot content (compiled in the consumer's scope) can't reuse them; without this component every override starts by re-implementing the avatar's CSS, which then drifts from the theme. Rendering it here means an override is one line and stays on theme.",
|
|
3221
|
+
"props": [
|
|
3222
|
+
{
|
|
3223
|
+
"name": "user",
|
|
3224
|
+
"type": "union",
|
|
3225
|
+
"required": false,
|
|
3226
|
+
"default": "null",
|
|
3227
|
+
"description": "The user whose initial is shown. The first letter of `name` is used."
|
|
3228
|
+
},
|
|
3229
|
+
{
|
|
3230
|
+
"name": "initial",
|
|
3231
|
+
"type": "string",
|
|
3232
|
+
"required": false,
|
|
3233
|
+
"default": "\"\"",
|
|
3234
|
+
"description": "Overrides the initial derived from `user.name` (e.g. two-letter initials)."
|
|
3235
|
+
},
|
|
3236
|
+
{
|
|
3237
|
+
"name": "badge",
|
|
3238
|
+
"type": "boolean",
|
|
3239
|
+
"required": false,
|
|
3240
|
+
"default": "false",
|
|
3241
|
+
"description": "Show a notification dot on the avatar (e.g. unread updates)."
|
|
3242
|
+
},
|
|
3243
|
+
{
|
|
3244
|
+
"name": "badgeVariant",
|
|
3245
|
+
"type": "ColorVariant",
|
|
3246
|
+
"required": false,
|
|
3247
|
+
"default": "\"danger\"",
|
|
3248
|
+
"description": "Colour of the notification dot."
|
|
3249
|
+
},
|
|
3250
|
+
{
|
|
3251
|
+
"name": "badgeLabel",
|
|
3252
|
+
"type": "string",
|
|
3253
|
+
"required": false,
|
|
3254
|
+
"default": "\"Unread notifications\"",
|
|
3255
|
+
"description": "Accessible text for the notification dot, announced to screen readers\n(the dot itself is a colour-only signal). Ignored unless `badge` is set."
|
|
3256
|
+
}
|
|
3257
|
+
],
|
|
3258
|
+
"events": [],
|
|
3259
|
+
"slots": [
|
|
3260
|
+
{
|
|
3261
|
+
"name": "default",
|
|
3262
|
+
"description": "Replaces the initial inside the disc (e.g. a photo or an icon).",
|
|
3263
|
+
"bindings": []
|
|
3264
|
+
}
|
|
3265
|
+
],
|
|
3266
|
+
"methods": []
|
|
3153
3267
|
}
|
|
3154
3268
|
]
|
|
3155
3269
|
},
|
|
3156
3270
|
"stats": {
|
|
3157
|
-
"totalComponents":
|
|
3271
|
+
"totalComponents": 74,
|
|
3158
3272
|
"baseComponents": 57,
|
|
3159
|
-
"extendedComponents":
|
|
3273
|
+
"extendedComponents": 17
|
|
3160
3274
|
}
|
|
3161
3275
|
}
|
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-11T08:44:15.559Z
|
|
5
5
|
|
|
6
6
|
This file provides a complete map of all available documentation for AI agents and developers.
|
|
7
7
|
|
|
@@ -82,6 +82,7 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
82
82
|
- [DXStatCard](/components/extended/DXStatCard): A KPI / stat tile: a big formatted value with a title, optional subtitle, an
|
|
83
83
|
- [DXSwitch](/components/extended/DXSwitch): A "filled box" toggle switch built on `DFormCheckbox`: a bordered box sized like
|
|
84
84
|
- [DXTable](/components/extended/DXTable): A comprehensive data table component with built-in pagination, sorting, filtering, and CRUD operatio
|
|
85
|
+
- [DXUserAvatar](/components/extended/DXUserAvatar): The circular user avatar shown in the dashboard navbar's user menu: a single
|
|
85
86
|
|
|
86
87
|
### Examples
|
|
87
88
|
|
|
@@ -100,4 +101,4 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
100
101
|
|
|
101
102
|
---
|
|
102
103
|
|
|
103
|
-
**Total Pages**:
|
|
104
|
+
**Total Pages**: 77
|
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 69 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -81,7 +81,7 @@ Lightweight type-safe wrappers around Bootstrap Vue Next components providing AP
|
|
|
81
81
|
- [DToaster](/components/base/dtoaster): Bootstrap Vue Next Toaster wrapper
|
|
82
82
|
- [DTooltip](/components/base/dtooltip): Bootstrap Vue Next Tooltip wrapper
|
|
83
83
|
|
|
84
|
-
## Extended Components (
|
|
84
|
+
## Extended Components (12 components)
|
|
85
85
|
|
|
86
86
|
Custom dashboard components with advanced functionality beyond Bootstrap Vue Next.
|
|
87
87
|
|
|
@@ -96,6 +96,7 @@ Custom dashboard components with advanced functionality beyond Bootstrap Vue Nex
|
|
|
96
96
|
- [DXStatCard](/components/extended/dxstatcard): Extended dashboard component
|
|
97
97
|
- [DXSwitch](/components/extended/dxswitch): Extended dashboard component
|
|
98
98
|
- [DXTable](/components/extended/dxtable): Data table with pagination, filtering, and sorting
|
|
99
|
+
- [DXUserAvatar](/components/extended/dxuseravatar): Extended dashboard component
|
|
99
100
|
|
|
100
101
|
## Composables
|
|
101
102
|
|
package/package.json
CHANGED
|
@@ -25,8 +25,17 @@
|
|
|
25
25
|
:key="strippedName"
|
|
26
26
|
#[strippedName]="slotProps"
|
|
27
27
|
>
|
|
28
|
-
<!--
|
|
29
|
-
|
|
28
|
+
<!--
|
|
29
|
+
@slot Forwards any `sidebar-*` slot to DXDashboardSidebar with the `sidebar-` prefix stripped (e.g. `sidebar-brand` becomes the sidebar's `brand` slot).
|
|
30
|
+
@binding {function} toggleSidebar Shows/hides the sidebar — so slot content (e.g. a close affordance in the brand row) can drive it.
|
|
31
|
+
@binding {boolean} sidebarHidden Whether the sidebar is currently hidden.
|
|
32
|
+
-->
|
|
33
|
+
<slot
|
|
34
|
+
:name="originalName"
|
|
35
|
+
v-bind="slotProps"
|
|
36
|
+
:toggleSidebar="toggleSidebar"
|
|
37
|
+
:sidebarHidden="hidden"
|
|
38
|
+
/>
|
|
30
39
|
</template>
|
|
31
40
|
</DXDashboardSidebar>
|
|
32
41
|
|
|
@@ -46,8 +55,17 @@
|
|
|
46
55
|
:key="strippedName"
|
|
47
56
|
#[strippedName]="slotProps"
|
|
48
57
|
>
|
|
49
|
-
<!--
|
|
50
|
-
|
|
58
|
+
<!--
|
|
59
|
+
@slot Forwards any `navbar-*` slot to DXDashboardNavbar with the `navbar-` prefix stripped (e.g. `navbar-actions` becomes the navbar's `actions` slot).
|
|
60
|
+
@binding {function} toggleSidebar Shows/hides the sidebar.
|
|
61
|
+
@binding {boolean} sidebarHidden Whether the sidebar is currently hidden.
|
|
62
|
+
-->
|
|
63
|
+
<slot
|
|
64
|
+
:name="originalName"
|
|
65
|
+
v-bind="slotProps"
|
|
66
|
+
:toggleSidebar="toggleSidebar"
|
|
67
|
+
:sidebarHidden="hidden"
|
|
68
|
+
/>
|
|
51
69
|
</template>
|
|
52
70
|
</DXDashboardNavbar>
|
|
53
71
|
|
|
@@ -85,6 +103,12 @@ import type { Navigation, NavbarActionsOnMobile, NavbarSearchAlign } from '../..
|
|
|
85
103
|
|
|
86
104
|
const slots = useSlots();
|
|
87
105
|
|
|
106
|
+
// Slot-binding precedence: the forwarded slots bind the child's own slot props
|
|
107
|
+
// FIRST and `toggleSidebar`/`sidebarHidden` after, so ours win a name clash.
|
|
108
|
+
// That's deliberate — the dashboard guarantees those two bindings on every
|
|
109
|
+
// forwarded slot, and a child that later grew a same-named prop must not be
|
|
110
|
+
// able to silently take the guarantee away. No child uses those names today.
|
|
111
|
+
|
|
88
112
|
interface Props {
|
|
89
113
|
/** Navigation structure for sidebar */
|
|
90
114
|
navigation: Navigation;
|
|
@@ -252,6 +276,20 @@ const toggleSidebar = () => {
|
|
|
252
276
|
console.error('Error saving sidebar state:', error);
|
|
253
277
|
}
|
|
254
278
|
};
|
|
279
|
+
|
|
280
|
+
defineExpose({
|
|
281
|
+
/**
|
|
282
|
+
* Show/hide the sidebar (same as clicking the navbar's hamburger). The
|
|
283
|
+
* dashboard owns the visibility state, so this is the supported way to drive
|
|
284
|
+
* it from outside — page content, a custom brand row, a keyboard shortcut.
|
|
285
|
+
* Also available as a slot binding on every forwarded `sidebar-*`/`navbar-*`
|
|
286
|
+
* slot, which is usually more convenient.
|
|
287
|
+
*/
|
|
288
|
+
toggleSidebar,
|
|
289
|
+
|
|
290
|
+
/** Whether the sidebar is currently hidden. */
|
|
291
|
+
sidebarHidden: computed(() => hidden.value),
|
|
292
|
+
});
|
|
255
293
|
</script>
|
|
256
294
|
|
|
257
295
|
<style scoped>
|
|
@@ -84,13 +84,12 @@
|
|
|
84
84
|
>
|
|
85
85
|
<template #button-content>
|
|
86
86
|
<!--
|
|
87
|
-
@slot Custom user avatar/icon in the dropdown trigger.
|
|
87
|
+
@slot Custom user avatar/icon in the dropdown trigger. To decorate the default avatar (e.g. add a notification dot) rather than replace it, render `DXUserAvatar` — the same component used here, so the styling comes with it.
|
|
88
88
|
@binding {string} initial The first letter of the user's name.
|
|
89
|
+
@binding {object} user The signed-in user.
|
|
89
90
|
-->
|
|
90
|
-
<slot name="user-icon" :initial="getUserInitial(user)">
|
|
91
|
-
<
|
|
92
|
-
{{ getUserInitial(user) }}
|
|
93
|
-
</div>
|
|
91
|
+
<slot name="user-icon" :initial="getUserInitial(user)" :user="user">
|
|
92
|
+
<DXUserAvatar :user="user" />
|
|
94
93
|
</slot>
|
|
95
94
|
</template>
|
|
96
95
|
|
|
@@ -112,6 +111,7 @@ import { computed } from "vue";
|
|
|
112
111
|
import DContainer from "../base/DContainer.vue";
|
|
113
112
|
import DButton from "../base/DButton.vue";
|
|
114
113
|
import DDropdown from "../base/DDropdown.vue";
|
|
114
|
+
import DXUserAvatar from "./DXUserAvatar.vue";
|
|
115
115
|
import type { NavbarActionsOnMobile, NavbarSearchAlign } from "../../types/navigation";
|
|
116
116
|
|
|
117
117
|
const props = withDefaults(
|
|
@@ -246,17 +246,4 @@ const getUserInitial = (user: { name: string } | null) => {
|
|
|
246
246
|
margin-left: 0;
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
-
|
|
250
|
-
.user-avatar {
|
|
251
|
-
width: 32px;
|
|
252
|
-
height: 32px;
|
|
253
|
-
border-radius: 50%;
|
|
254
|
-
background-color: var(--bs-dark);
|
|
255
|
-
color: var(--bs-white);
|
|
256
|
-
display: flex;
|
|
257
|
-
align-items: center;
|
|
258
|
-
justify-content: center;
|
|
259
|
-
font-weight: 600;
|
|
260
|
-
font-size: 14px;
|
|
261
|
-
}
|
|
262
249
|
</style>
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
<DFormCheckbox
|
|
62
62
|
v-else
|
|
63
63
|
v-model="fieldValue"
|
|
64
|
-
:disabled="isDisabled || isReadonly"
|
|
65
|
-
v-bind="
|
|
64
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
65
|
+
v-bind="controlPropsWithGuards"
|
|
66
66
|
>
|
|
67
67
|
<DXFieldLabel :label="resolvedLabel" class="visually-hidden" />
|
|
68
68
|
</DFormCheckbox>
|
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
<DFormCheckbox
|
|
98
98
|
v-else
|
|
99
99
|
v-model="fieldValue"
|
|
100
|
-
:disabled="isDisabled || isReadonly"
|
|
101
|
-
v-bind="
|
|
100
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
101
|
+
v-bind="controlPropsWithGuards"
|
|
102
102
|
>
|
|
103
103
|
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
104
104
|
</DFormCheckbox>
|
|
@@ -159,8 +159,8 @@
|
|
|
159
159
|
<DXSwitch
|
|
160
160
|
v-else
|
|
161
161
|
v-model="switchModel"
|
|
162
|
-
:disabled="isDisabled || isReadonly"
|
|
163
|
-
v-bind="
|
|
162
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
163
|
+
v-bind="controlPropsWithGuards"
|
|
164
164
|
>
|
|
165
165
|
<DXFieldLabel
|
|
166
166
|
:label="switchText"
|
|
@@ -201,8 +201,8 @@
|
|
|
201
201
|
<DXSwitch
|
|
202
202
|
v-else
|
|
203
203
|
v-model="switchModel"
|
|
204
|
-
:disabled="isDisabled || isReadonly"
|
|
205
|
-
v-bind="
|
|
204
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
205
|
+
v-bind="controlPropsWithGuards"
|
|
206
206
|
>
|
|
207
207
|
<DXFieldLabel :label="switchText" :info="resolvedInfo" />
|
|
208
208
|
</DXSwitch>
|
|
@@ -306,8 +306,8 @@
|
|
|
306
306
|
v-model="fieldValue"
|
|
307
307
|
:field="field"
|
|
308
308
|
:model="model"
|
|
309
|
-
:disabled="isDisabled || isReadonly"
|
|
310
|
-
v-bind="
|
|
309
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
310
|
+
v-bind="controlPropsWithGuards"
|
|
311
311
|
/>
|
|
312
312
|
|
|
313
313
|
<!-- Textarea. Error clearing happens in the v-model setter. -->
|
|
@@ -319,8 +319,9 @@
|
|
|
319
319
|
:rows="field.rows || 3"
|
|
320
320
|
:state="fieldState"
|
|
321
321
|
:disabled="isDisabled"
|
|
322
|
-
:readonly="isReadonly"
|
|
323
|
-
|
|
322
|
+
:readonly="isReadonly || isPlaintext"
|
|
323
|
+
:plaintext="isPlaintext"
|
|
324
|
+
v-bind="inputPropsWithGuards"
|
|
324
325
|
/>
|
|
325
326
|
|
|
326
327
|
<!-- Select (sync or async options) -->
|
|
@@ -330,8 +331,8 @@
|
|
|
330
331
|
:required="field.required"
|
|
331
332
|
:options="resolvedOptions"
|
|
332
333
|
:state="fieldState"
|
|
333
|
-
:disabled="isDisabled || isReadonly"
|
|
334
|
-
v-bind="
|
|
334
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
335
|
+
v-bind="controlPropsWithGuards"
|
|
335
336
|
/>
|
|
336
337
|
|
|
337
338
|
<!-- Radio group -->
|
|
@@ -341,8 +342,8 @@
|
|
|
341
342
|
:options="resolvedOptions"
|
|
342
343
|
:required="field.required"
|
|
343
344
|
:state="fieldState"
|
|
344
|
-
:disabled="isDisabled || isReadonly"
|
|
345
|
-
v-bind="
|
|
345
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
346
|
+
v-bind="controlPropsWithGuards"
|
|
346
347
|
/>
|
|
347
348
|
|
|
348
349
|
<!-- File / image upload -->
|
|
@@ -352,8 +353,8 @@
|
|
|
352
353
|
:accept="field.accept || (field.type === 'image' ? 'image/*' : undefined)"
|
|
353
354
|
:required="field.required"
|
|
354
355
|
:state="fieldState"
|
|
355
|
-
:disabled="isDisabled || isReadonly"
|
|
356
|
-
v-bind="
|
|
356
|
+
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
357
|
+
v-bind="controlPropsWithGuards"
|
|
357
358
|
@change="handleFileChange"
|
|
358
359
|
/>
|
|
359
360
|
<img
|
|
@@ -384,8 +385,9 @@
|
|
|
384
385
|
:max="field.max"
|
|
385
386
|
:state="fieldState"
|
|
386
387
|
:disabled="isDisabled"
|
|
387
|
-
:readonly="isReadonly"
|
|
388
|
-
|
|
388
|
+
:readonly="isReadonly || isPlaintext"
|
|
389
|
+
:plaintext="isPlaintext"
|
|
390
|
+
v-bind="inputPropsWithGuards"
|
|
389
391
|
@input="handleCurrencyInput"
|
|
390
392
|
@focus="handleCurrencyFocus"
|
|
391
393
|
@blur="handleCurrencyBlur"
|
|
@@ -401,8 +403,9 @@
|
|
|
401
403
|
:max="field.max"
|
|
402
404
|
:state="fieldState"
|
|
403
405
|
:disabled="isDisabled"
|
|
404
|
-
:readonly="isReadonly"
|
|
405
|
-
|
|
406
|
+
:readonly="isReadonly || isPlaintext"
|
|
407
|
+
:plaintext="isPlaintext"
|
|
408
|
+
v-bind="inputPropsWithGuards"
|
|
406
409
|
/>
|
|
407
410
|
|
|
408
411
|
<template v-if="field.type === 'percentage'" #append>
|
|
@@ -422,9 +425,10 @@
|
|
|
422
425
|
:placeholder="field.placeholder"
|
|
423
426
|
:state="fieldState"
|
|
424
427
|
:disabled="isDisabled"
|
|
425
|
-
:readonly="isReadonly"
|
|
428
|
+
:readonly="isReadonly || isPlaintext"
|
|
429
|
+
:plaintext="isPlaintext"
|
|
426
430
|
autocomplete="off"
|
|
427
|
-
v-bind="
|
|
431
|
+
v-bind="inputPropsWithGuards"
|
|
428
432
|
/>
|
|
429
433
|
<datalist :id="datalistId">
|
|
430
434
|
<option
|
|
@@ -437,6 +441,31 @@
|
|
|
437
441
|
</datalist>
|
|
438
442
|
</template>
|
|
439
443
|
|
|
444
|
+
<!-- Password with a reveal toggle (the default for `type: "password"`).
|
|
445
|
+
Typing a long password blind is the one place the eye really earns
|
|
446
|
+
its keep, so it's opt-out (`revealable: false`) rather than opt-in. -->
|
|
447
|
+
<DInputGroup v-else-if="isRevealablePassword">
|
|
448
|
+
<DFormInput
|
|
449
|
+
v-model="textFieldModel"
|
|
450
|
+
:required="field.required"
|
|
451
|
+
:placeholder="field.placeholder"
|
|
452
|
+
:state="fieldState"
|
|
453
|
+
:disabled="isDisabled"
|
|
454
|
+
:readonly="isReadonly"
|
|
455
|
+
v-bind="passwordInputProps"
|
|
456
|
+
/>
|
|
457
|
+
<template #append>
|
|
458
|
+
<DButton
|
|
459
|
+
variant="outline-secondary"
|
|
460
|
+
:icon="passwordRevealed ? 'eye-slash' : 'eye'"
|
|
461
|
+
:aria-label="passwordRevealed ? 'Hide password' : 'Show password'"
|
|
462
|
+
:aria-pressed="passwordRevealed"
|
|
463
|
+
:disabled="isDisabled"
|
|
464
|
+
@click="passwordRevealed = !passwordRevealed"
|
|
465
|
+
/>
|
|
466
|
+
</template>
|
|
467
|
+
</DInputGroup>
|
|
468
|
+
|
|
440
469
|
<!-- Text-based inputs (text/email/password/number/url/tel/date/time/datetime) -->
|
|
441
470
|
<DFormInput
|
|
442
471
|
v-else
|
|
@@ -449,8 +478,9 @@
|
|
|
449
478
|
:max="field.max"
|
|
450
479
|
:state="fieldState"
|
|
451
480
|
:disabled="isDisabled"
|
|
452
|
-
:readonly="isReadonly"
|
|
453
|
-
|
|
481
|
+
:readonly="isReadonly || isPlaintext"
|
|
482
|
+
:plaintext="isPlaintext"
|
|
483
|
+
v-bind="inputPropsWithGuards"
|
|
454
484
|
/>
|
|
455
485
|
|
|
456
486
|
<!-- Validation error -->
|
|
@@ -505,6 +535,7 @@ import DXSwitch from "./DXSwitch.vue";
|
|
|
505
535
|
import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
|
|
506
536
|
import DFormText from "../base/DFormText.vue";
|
|
507
537
|
import DInputGroup from "../base/DInputGroup.vue";
|
|
538
|
+
import DButton from "../base/DButton.vue";
|
|
508
539
|
import DXFieldLabel from "./DXFieldLabel.vue";
|
|
509
540
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
510
541
|
import type { FieldDefinition, FieldOption, FieldType, LabelCols } from "../../types";
|
|
@@ -880,6 +911,71 @@ const isDisabled = computed(() => {
|
|
|
880
911
|
|
|
881
912
|
const isReadonly = computed(() => resolveMaybe(props.field.readonly) ?? false);
|
|
882
913
|
|
|
914
|
+
// Display-only: the value as static text rather than a control. bvn's
|
|
915
|
+
// `plaintext` only swaps the class, so pair it with `readonly` at every call
|
|
916
|
+
// site — otherwise the "static text" is still editable.
|
|
917
|
+
const isPlaintext = computed(() => resolveMaybe(props.field.plaintext) ?? false);
|
|
918
|
+
|
|
919
|
+
const passwordRevealed = ref(false);
|
|
920
|
+
|
|
921
|
+
const isRevealablePassword = computed(
|
|
922
|
+
() =>
|
|
923
|
+
props.field.type === "password" &&
|
|
924
|
+
(props.field.revealable ?? true) &&
|
|
925
|
+
!isPlaintext.value,
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
// DXTable reuses ONE DXField instance per field key across modal opens (the
|
|
929
|
+
// v-for is keyed on `field.key`, which is stable across rows) and swaps in a
|
|
930
|
+
// fresh form per record. Without this, revealing row A's password leaves row
|
|
931
|
+
// B's — and the create form's — showing in clear text.
|
|
932
|
+
watch(
|
|
933
|
+
() => [props.form, props.field.key],
|
|
934
|
+
() => {
|
|
935
|
+
passwordRevealed.value = false;
|
|
936
|
+
},
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
/*
|
|
940
|
+
* `inputProps` is an escape hatch that deliberately wins over the field's own
|
|
941
|
+
* bindings — it is spread last. But it must not win over the guarantees the
|
|
942
|
+
* field makes, or those guarantees are only documentation: a `plaintext` field
|
|
943
|
+
* that `inputProps: { readonly: false }` re-opens for editing isn't read-only,
|
|
944
|
+
* and a password whose `type` `inputProps` sets to "text" is unmasked under a
|
|
945
|
+
* toggle still reading "Show password". So the guards are re-applied ON TOP of
|
|
946
|
+
* inputProps. Everything else in inputProps still passes through, and a field
|
|
947
|
+
* that isn't plaintext/readonly is untouched — `inputProps: { readonly: true }`
|
|
948
|
+
* still works as before.
|
|
949
|
+
*/
|
|
950
|
+
|
|
951
|
+
// For controls with a native readonly state (the text family + textarea).
|
|
952
|
+
const inputPropsWithGuards = computed(() => {
|
|
953
|
+
const merged: Record<string, any> = { ...props.field.inputProps };
|
|
954
|
+
if (isPlaintext.value) {
|
|
955
|
+
merged.plaintext = true;
|
|
956
|
+
merged.readonly = true;
|
|
957
|
+
} else if (isReadonly.value) {
|
|
958
|
+
merged.readonly = true;
|
|
959
|
+
}
|
|
960
|
+
return merged;
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// For controls with NO native readonly state (select/radio/checkbox/switch/
|
|
964
|
+
// file/component), where read-only is expressed as `disabled`.
|
|
965
|
+
const controlPropsWithGuards = computed(() => {
|
|
966
|
+
const merged: Record<string, any> = { ...props.field.inputProps };
|
|
967
|
+
if (isDisabled.value || isReadonly.value || isPlaintext.value) {
|
|
968
|
+
merged.disabled = true;
|
|
969
|
+
}
|
|
970
|
+
return merged;
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// The reveal toggle owns the password input's `type`.
|
|
974
|
+
const passwordInputProps = computed(() => ({
|
|
975
|
+
...inputPropsWithGuards.value,
|
|
976
|
+
type: passwordRevealed.value ? "text" : "password",
|
|
977
|
+
}));
|
|
978
|
+
|
|
883
979
|
const inputType = computed<string>(() => {
|
|
884
980
|
const type: FieldType = props.field.type;
|
|
885
981
|
if (type === "datetime") return "datetime-local";
|