@omnitend/dashboard-for-laravel 0.45.0 → 0.47.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/IBMPlexSans-Bold-Latin1-914f1400.woff2 +0 -0
- package/dist/assets/IBMPlexSans-Medium-Latin1-b5610af0.woff2 +0 -0
- package/dist/assets/IBMPlexSans-Regular-Latin1-b5ad7bd3.woff2 +0 -0
- package/dist/assets/IBMPlexSans-SemiBold-Latin1-fff0ab3a.woff2 +0 -0
- package/dist/components/base/DFormFile.vue.d.ts +17 -0
- package/dist/components/base/DFormValidFeedback.vue.d.ts +12 -0
- package/dist/components/extended/DXDashboard.vue.d.ts +95 -111
- package/dist/components/extended/DXDashboardNavbar.vue.d.ts +2 -36
- package/dist/components/extended/DXDashboardSidebar.vue.d.ts +60 -79
- package/dist/components/extended/DXField.vue.d.ts +16 -0
- package/dist/components/extended/DXTable.vue.d.ts +10 -1
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +13 -3
- package/dist/dashboard-for-laravel.js +11767 -11479
- 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/index.d.ts +4 -2
- package/dist/style.css +2 -2
- package/dist/types/index.d.ts +60 -0
- package/dist/types/navigation.d.ts +153 -3
- package/dist/utils/formErrorTargets.d.ts +6 -2
- package/dist/utils/formSchema.d.ts +16 -0
- package/dist/utils/statCardFit.d.ts +19 -0
- package/docs/public/api-reference.json +171 -9
- package/docs/public/docs-map.md +4 -2
- package/docs/public/llms.txt +4 -2
- package/package.json +3 -2
- package/resources/css/theme.scss +339 -4
- package/resources/fonts/ibm-plex-sans/IBMPlexSans-Bold-Latin1.woff2 +0 -0
- package/resources/fonts/ibm-plex-sans/IBMPlexSans-Medium-Latin1.woff2 +0 -0
- package/resources/fonts/ibm-plex-sans/IBMPlexSans-Regular-Latin1.woff2 +0 -0
- package/resources/fonts/ibm-plex-sans/IBMPlexSans-SemiBold-Latin1.woff2 +0 -0
- package/resources/fonts/ibm-plex-sans/OFL.txt +93 -0
- package/resources/fonts/ibm-plex-sans/SOURCE.txt +18 -0
- package/resources/js/components/base/DFormFile.vue +103 -0
- package/resources/js/components/base/DFormValidFeedback.vue +16 -0
- package/resources/js/components/extended/DXDashboard.vue +41 -18
- package/resources/js/components/extended/DXDashboardNavbar.vue +9 -1
- package/resources/js/components/extended/DXDashboardSidebar.vue +25 -3
- package/resources/js/components/extended/DXField.vue +86 -23
- package/resources/js/components/extended/DXFormField.vue +3 -1
- package/resources/js/components/extended/DXStatCard.vue +154 -11
- package/resources/js/components/extended/DXTable.vue +62 -6
- package/resources/js/components/extended/DXTableEditorModal.vue +35 -2
- package/resources/js/composables/useResourceEditor.ts +64 -26
- package/resources/js/index.ts +11 -0
- package/resources/js/types/index.ts +64 -0
- package/resources/js/types/navigation.ts +175 -3
- package/resources/js/utils/formErrorTargets.ts +28 -2
- package/resources/js/utils/formSchema.ts +27 -0
- package/resources/js/utils/statCardFit.ts +210 -0
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
v-else-if="isProviderMode || isClientSideMode || isInertiaMode"
|
|
48
48
|
ref="tableRef"
|
|
49
49
|
:key="activeMode + ':' + tableSlotSignature($slots)"
|
|
50
|
-
:fields="
|
|
50
|
+
:fields="renderedFields"
|
|
51
51
|
:sort-by="effectiveSortBy"
|
|
52
52
|
:multisort="false"
|
|
53
53
|
:no-sortable-icon="true"
|
|
@@ -245,8 +245,10 @@
|
|
|
245
245
|
PaginationData; `handleActivePageChange` routes the page
|
|
246
246
|
event to the right per-mode handler. Previously three
|
|
247
247
|
byte-identical copies, one per mode. -->
|
|
248
|
+
<!-- Not while the loader shows: the count would read "0 items"
|
|
249
|
+
before the rows have arrived. -->
|
|
248
250
|
<DXTablePagination
|
|
249
|
-
v-if="activePagination"
|
|
251
|
+
v-if="activePagination && !(effectiveBusy && !isProviderMode)"
|
|
250
252
|
:pagination="activePagination"
|
|
251
253
|
:per-page="effectivePerPage"
|
|
252
254
|
:per-page-options="perPageOptions"
|
|
@@ -264,7 +266,8 @@
|
|
|
264
266
|
</DRow>
|
|
265
267
|
|
|
266
268
|
<!-- Edit/create modal (only when editFields provided). The consumer's
|
|
267
|
-
edit-value / edit-span / tab-* slots are
|
|
269
|
+
edit-value / edit-span / edit-password-actions / tab-* slots are
|
|
270
|
+
forwarded through to it
|
|
268
271
|
generically; it maps them onto DXForm and provides the row context
|
|
269
272
|
+ a close handle (#129). -->
|
|
270
273
|
<DXTableEditorModal
|
|
@@ -297,6 +300,7 @@
|
|
|
297
300
|
>
|
|
298
301
|
<!--
|
|
299
302
|
@slot edit-value(<fieldKey>) / edit-span(<fieldKey>) /
|
|
303
|
+
edit-password-actions(<fieldKey>) /
|
|
300
304
|
tab-content|tab-before|tab-after(<tabKey>) — forwarded to the edit
|
|
301
305
|
modal (see DXTableEditorModal for their bindings).
|
|
302
306
|
-->
|
|
@@ -340,7 +344,8 @@ import DXTableEditorModal from "./DXTableEditorModal.vue";
|
|
|
340
344
|
export type FilterType = 'text' | 'select' | 'select-native' | 'number' | 'date' | false;
|
|
341
345
|
|
|
342
346
|
export interface FilterOption {
|
|
343
|
-
|
|
347
|
+
/** Numbers and booleans are accepted and compared as their string form. */
|
|
348
|
+
value: string | number | boolean | null;
|
|
344
349
|
text: string;
|
|
345
350
|
}
|
|
346
351
|
|
|
@@ -449,6 +454,15 @@ export interface TableField {
|
|
|
449
454
|
* A number is pixels; a string is used verbatim.
|
|
450
455
|
*/
|
|
451
456
|
minWidth?: string | number;
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Render this column's figures tabular (every digit the same width), so
|
|
460
|
+
* amounts line up down the column: adds the global `.dx-tabular` class to
|
|
461
|
+
* the column's body cells and its header cell. Composes with the field's
|
|
462
|
+
* own `tdClass`/`thClass` (string, array, object or `tdClass` function)
|
|
463
|
+
* rather than replacing them. Default `false`.
|
|
464
|
+
*/
|
|
465
|
+
tabular?: boolean;
|
|
452
466
|
[key: string]: any;
|
|
453
467
|
}
|
|
454
468
|
|
|
@@ -1379,6 +1393,35 @@ const columnStyleFor = (field: TableField): Record<string, string> | undefined =
|
|
|
1379
1393
|
// content-sized default byte-for-byte.
|
|
1380
1394
|
const fieldsHaveWidths = computed(() => props.fields.some(hasDeclaredWidth));
|
|
1381
1395
|
|
|
1396
|
+
/*
|
|
1397
|
+
* Tabular columns (#215). A `tabular: true` field gets `.dx-tabular` added
|
|
1398
|
+
* to its `tdClass` and `thClass`, appended to the consumer's own value
|
|
1399
|
+
* (wrapped in an array, or called through when `tdClass` is a function) so
|
|
1400
|
+
* their classes survive. Fields without the flag pass through as the same
|
|
1401
|
+
* objects, so a table using no tabular column renders exactly as before.
|
|
1402
|
+
*/
|
|
1403
|
+
const TABULAR_CLASS = 'dx-tabular';
|
|
1404
|
+
|
|
1405
|
+
const withTabularClass = (consumerClass: unknown): unknown[] =>
|
|
1406
|
+
consumerClass === undefined || consumerClass === null
|
|
1407
|
+
? [TABULAR_CLASS]
|
|
1408
|
+
: [consumerClass, TABULAR_CLASS];
|
|
1409
|
+
|
|
1410
|
+
const renderedFields = computed<TableField[]>(() =>
|
|
1411
|
+
props.fields.map((field) => {
|
|
1412
|
+
if (field.tabular !== true) return field;
|
|
1413
|
+
const consumerTdClass = field.tdClass;
|
|
1414
|
+
return {
|
|
1415
|
+
...field,
|
|
1416
|
+
tdClass:
|
|
1417
|
+
typeof consumerTdClass === 'function'
|
|
1418
|
+
? (...args: unknown[]) => withTabularClass(consumerTdClass(...args))
|
|
1419
|
+
: withTabularClass(consumerTdClass),
|
|
1420
|
+
thClass: withTabularClass(field.thClass),
|
|
1421
|
+
};
|
|
1422
|
+
}),
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1382
1425
|
// Computed: Get effective filter options for a field
|
|
1383
1426
|
// The key a column's filter is SENT under, which is not always the column's own
|
|
1384
1427
|
// key: a display column can filter on a different server param via `filterKey`.
|
|
@@ -1579,9 +1622,21 @@ const getFieldFilterOptions = (field: TableField): FilterOption[] => {
|
|
|
1579
1622
|
options.push({ value: filterNullValueFor(field), text: field.filterNullText });
|
|
1580
1623
|
}
|
|
1581
1624
|
|
|
1582
|
-
// If field has static filterOptions, use those
|
|
1625
|
+
// If field has static filterOptions, use those. Their values are
|
|
1626
|
+
// stringified: options built from records carry numbers (ids) and booleans
|
|
1627
|
+
// (flags), but the filter map, the URL and the row match all speak strings,
|
|
1628
|
+
// and DAutocomplete matches its model to an option by strict equality — so a
|
|
1629
|
+
// raw `1` never re-selected the stored `'1'`, and picking it put a number in
|
|
1630
|
+
// the map that the active-filter check could not `.trim()`.
|
|
1583
1631
|
if (field.filterOptions && field.filterOptions.length > 0) {
|
|
1584
|
-
return [
|
|
1632
|
+
return [
|
|
1633
|
+
...options,
|
|
1634
|
+
...field.filterOptions.map((option) =>
|
|
1635
|
+
option.value === null || option.value === undefined
|
|
1636
|
+
? option
|
|
1637
|
+
: { ...option, value: String(option.value) },
|
|
1638
|
+
),
|
|
1639
|
+
];
|
|
1585
1640
|
}
|
|
1586
1641
|
|
|
1587
1642
|
// Otherwise, check for server-provided values (keyed by the filter key)
|
|
@@ -2461,6 +2516,7 @@ const handleDelete = editor.remove;
|
|
|
2461
2516
|
const EDIT_MODAL_SLOT_PREFIXES = [
|
|
2462
2517
|
'edit-value(',
|
|
2463
2518
|
'edit-span(',
|
|
2519
|
+
'edit-password-actions(',
|
|
2464
2520
|
'tab-content(',
|
|
2465
2521
|
'tab-before(',
|
|
2466
2522
|
'tab-after(',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
3
|
Internal edit/create modal for `DXTable` (#129). Renders the `DModal` +
|
|
4
|
-
`DXForm` and maps DXTable's `edit-value`/`edit-span`/`
|
|
5
|
-
DXForm's `value`/`span`/`tab-*` slots, providing the row context and a `close`
|
|
4
|
+
`DXForm` and maps DXTable's `edit-value`/`edit-span`/`edit-password-actions`/
|
|
5
|
+
`tab-*` slots onto DXForm's `value`/`span`/`password-actions`/`tab-*` slots, providing the row context and a `close`
|
|
6
6
|
handle. Extracted so `DXTable` is a thin façade over the table render, the
|
|
7
7
|
pagination, the editor logic (`useResourceEditor`) and this modal — rather than
|
|
8
8
|
owning all four.
|
|
@@ -115,6 +115,36 @@
|
|
|
115
115
|
/>
|
|
116
116
|
</template>
|
|
117
117
|
|
|
118
|
+
<!-- Forward edit-password-actions(key) → password-actions(key):
|
|
119
|
+
content beside the password input, inside its input group,
|
|
120
|
+
with a handle on dfl's own reveal (eye) state. -->
|
|
121
|
+
<template
|
|
122
|
+
v-for="key in editPasswordActionsSlotKeys"
|
|
123
|
+
:key="`epa-${key}`"
|
|
124
|
+
#[`password-actions(${key})`]="sp"
|
|
125
|
+
>
|
|
126
|
+
<!--
|
|
127
|
+
@slot Extra controls beside password field `<key>`'s input in the edit/create modal, inside its input group after the reveal (eye) toggle — e.g. a "Generate password" button. Name it `edit-password-actions(<fieldKey>)`.
|
|
128
|
+
@binding {object} item The row being edited (null in create mode).
|
|
129
|
+
@binding {any} value The current field value.
|
|
130
|
+
@binding {Function} update Call with a new value to update the field.
|
|
131
|
+
@binding {boolean} revealed Whether the password is currently shown as plain text.
|
|
132
|
+
@binding {Function} setRevealed Call with true to show the password (as the eye does) or false to mask it.
|
|
133
|
+
@binding {boolean} disabled Whether the field is disabled or read-only.
|
|
134
|
+
@binding {object} field The field definition.
|
|
135
|
+
-->
|
|
136
|
+
<slot
|
|
137
|
+
:name="`edit-password-actions(${key})`"
|
|
138
|
+
:item="item"
|
|
139
|
+
:value="sp.value"
|
|
140
|
+
:update="sp.update"
|
|
141
|
+
:revealed="sp.revealed"
|
|
142
|
+
:set-revealed="sp.setRevealed"
|
|
143
|
+
:disabled="sp.disabled"
|
|
144
|
+
:field="sp.field"
|
|
145
|
+
/>
|
|
146
|
+
</template>
|
|
147
|
+
|
|
118
148
|
<!-- Forward tab-content / tab-before / tab-after slots -->
|
|
119
149
|
<template
|
|
120
150
|
v-for="key in tabContentSlotKeys"
|
|
@@ -318,6 +348,9 @@ const editValueSlotKeys = computed(() =>
|
|
|
318
348
|
const editSpanSlotKeys = computed(() =>
|
|
319
349
|
fieldKeys.value.filter((key) => !!slots[`edit-span(${key})`]),
|
|
320
350
|
);
|
|
351
|
+
const editPasswordActionsSlotKeys = computed(() =>
|
|
352
|
+
fieldKeys.value.filter((key) => !!slots[`edit-password-actions(${key})`]),
|
|
353
|
+
);
|
|
321
354
|
const tabContentSlotKeys = computed(() =>
|
|
322
355
|
tabKeys.value.filter((key) => !!slots[`tab-content(${key})`]),
|
|
323
356
|
);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
resolveFieldDefault,
|
|
8
8
|
isFieldVisible as isFieldVisibleFor,
|
|
9
9
|
isSubmittableField,
|
|
10
|
+
nonSubmittedFieldKeys as nonSubmittedFieldKeysFor,
|
|
10
11
|
} from "../utils/formSchema";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -207,24 +208,67 @@ export function useResourceEditor<T = any>(
|
|
|
207
208
|
// Enforced at SUBMIT, not just at seeding: the modal still renders every field,
|
|
208
209
|
// so a `submit: false` control — or a `span` slot calling `update` — can write
|
|
209
210
|
// its key back into the form data after seeding. Strip them on the way out.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// Hidden by `when` at submit time (#117). Submitting a field
|
|
219
|
-
// the user cannot see is a silent write — and with `default`
|
|
220
|
-
// set it writes a value they never chose. Omitting the key
|
|
221
|
-
// leaves the stored value alone.
|
|
222
|
-
!isEditFieldVisible(field),
|
|
223
|
-
)
|
|
224
|
-
.map((field) => field.key),
|
|
225
|
-
),
|
|
211
|
+
//
|
|
212
|
+
// Decided per KEY (#228): a key is dropped only when no field with that key
|
|
213
|
+
// is both submitting and visible at submit time. A presentational field
|
|
214
|
+
// (#110) holds no data, and a field hidden by `when` (#117) must not write a
|
|
215
|
+
// value the user cannot see — but a display-only twin sharing its key with
|
|
216
|
+
// the visible editable field must not strip the editable field's value.
|
|
217
|
+
const nonSubmittedFieldKeys = computed(() =>
|
|
218
|
+
nonSubmittedFieldKeysFor(props.editFields ?? [], isEditFieldVisible),
|
|
226
219
|
);
|
|
227
220
|
|
|
221
|
+
/*
|
|
222
|
+
* Seed the form's data, ONE value per key (#228). Presentational fields are
|
|
223
|
+
* never seeded. When several submitting fields share a key (alternative
|
|
224
|
+
* controls for one value, switched by `when`), the key is seeded from the
|
|
225
|
+
* first of them that is visible against the seeded model, else the first
|
|
226
|
+
* declared. This used to be last-declared-wins, so a hidden alternative
|
|
227
|
+
* declared after the visible field replaced its default.
|
|
228
|
+
*
|
|
229
|
+
* Two submitting fields with one key that are visible TOGETHER are almost
|
|
230
|
+
* certainly a mistake (two controls bound to one value): the first declared
|
|
231
|
+
* wins, and DXTable warns once per key.
|
|
232
|
+
*/
|
|
233
|
+
const warnedSharedVisibleKeys = new Set<string>();
|
|
234
|
+
const seedFormData = (
|
|
235
|
+
valueFor: (field: FieldDefinition) => any,
|
|
236
|
+
context: Record<string, any>,
|
|
237
|
+
): Record<string, any> => {
|
|
238
|
+
const fieldsByKey = new Map<string, FieldDefinition[]>();
|
|
239
|
+
submittableEditFields.value.forEach((field) => {
|
|
240
|
+
const sharing = fieldsByKey.get(field.key);
|
|
241
|
+
if (sharing) sharing.push(field);
|
|
242
|
+
else fieldsByKey.set(field.key, [field]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const formData: Record<string, any> = {};
|
|
246
|
+
fieldsByKey.forEach((fields, key) => {
|
|
247
|
+
formData[key] = valueFor(fields[0]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const seededModel = { ...context, ...formData };
|
|
251
|
+
fieldsByKey.forEach((fields, key) => {
|
|
252
|
+
if (fields.length < 2) return;
|
|
253
|
+
const visibleFields = fields.filter((field) =>
|
|
254
|
+
isFieldVisibleFor(field, seededModel),
|
|
255
|
+
);
|
|
256
|
+
if (visibleFields.length > 1 && !warnedSharedVisibleKeys.has(key)) {
|
|
257
|
+
warnedSharedVisibleKeys.add(key);
|
|
258
|
+
// eslint-disable-next-line no-console
|
|
259
|
+
console.warn(
|
|
260
|
+
`[DXTable] ${visibleFields.length} submitting edit fields share the key "${key}" and are visible together, ` +
|
|
261
|
+
'so they edit one value; the first declared seeds it. ' +
|
|
262
|
+
'Give alternatives mutually exclusive `when` predicates, or mark a display-only one `submit: false`.',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (visibleFields.length > 0 && visibleFields[0] !== fields[0]) {
|
|
266
|
+
formData[key] = valueFor(visibleFields[0]);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return formData;
|
|
270
|
+
};
|
|
271
|
+
|
|
228
272
|
const stripNonSubmittedFields = (data: Record<string, any>): Record<string, any> => {
|
|
229
273
|
if (nonSubmittedFieldKeys.value.size === 0) return data;
|
|
230
274
|
return Object.fromEntries(
|
|
@@ -265,11 +309,9 @@ export function useResourceEditor<T = any>(
|
|
|
265
309
|
// between successive row opens. Presentational fields (submit: false)
|
|
266
310
|
// lay the form out; they hold no data and must not be POSTed just
|
|
267
311
|
// because they were declared.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
startSession(formData);
|
|
312
|
+
startSession(
|
|
313
|
+
seedFormData((field) => seedValueFor(field, item), item as any),
|
|
314
|
+
);
|
|
273
315
|
|
|
274
316
|
// Open modal
|
|
275
317
|
editFormInstanceKey.value++;
|
|
@@ -332,11 +374,7 @@ export function useResourceEditor<T = any>(
|
|
|
332
374
|
editLoading.value = false;
|
|
333
375
|
|
|
334
376
|
// A fresh form seeded with the field defaults (see `startSession`).
|
|
335
|
-
|
|
336
|
-
submittableEditFields.value.forEach(field => {
|
|
337
|
-
formData[field.key] = resolveFieldDefault(field);
|
|
338
|
-
});
|
|
339
|
-
startSession(formData);
|
|
377
|
+
startSession(seedFormData(resolveFieldDefault, {}));
|
|
340
378
|
editFormInstanceKey.value++;
|
|
341
379
|
showEditModal.value = true;
|
|
342
380
|
};
|
package/resources/js/index.ts
CHANGED
|
@@ -62,9 +62,11 @@ export { default as DDropdownItem } from "./components/base/DDropdownItem.vue";
|
|
|
62
62
|
export { default as DForm } from "./components/base/DForm.vue";
|
|
63
63
|
export { default as DFormCheckbox } from "./components/base/DFormCheckbox.vue";
|
|
64
64
|
export { default as DFormCheckboxGroup } from "./components/base/DFormCheckboxGroup.vue";
|
|
65
|
+
export { default as DFormFile } from "./components/base/DFormFile.vue";
|
|
65
66
|
export { default as DFormGroup } from "./components/base/DFormGroup.vue";
|
|
66
67
|
export { default as DFormInput } from "./components/base/DFormInput.vue";
|
|
67
68
|
export { default as DFormInvalidFeedback } from "./components/base/DFormInvalidFeedback.vue";
|
|
69
|
+
export { default as DFormValidFeedback } from "./components/base/DFormValidFeedback.vue";
|
|
68
70
|
export { default as DFormOtp } from "./components/base/DFormOtp.vue";
|
|
69
71
|
export { default as DFormRadio } from "./components/base/DFormRadio.vue";
|
|
70
72
|
export { default as DFormRadioGroup } from "./components/base/DFormRadioGroup.vue";
|
|
@@ -146,6 +148,8 @@ export type {
|
|
|
146
148
|
FormTab,
|
|
147
149
|
MaybeFn,
|
|
148
150
|
OptionsLoader,
|
|
151
|
+
PasswordActionsSlotProps,
|
|
152
|
+
EditPasswordActionsSlotProps,
|
|
149
153
|
// Strict, discriminated field types (#131) — opt-in; canonical in a future
|
|
150
154
|
// major. `FieldDef` is the union; the rest are its per-type members.
|
|
151
155
|
FieldDef,
|
|
@@ -216,7 +220,14 @@ export type {
|
|
|
216
220
|
NavigationItem,
|
|
217
221
|
NavigationGroup,
|
|
218
222
|
Navigation,
|
|
223
|
+
NavigationItemOf,
|
|
219
224
|
NavbarSearchAlign,
|
|
220
225
|
NavbarActionsOnMobile,
|
|
221
226
|
SidebarVariant,
|
|
227
|
+
DashboardUser,
|
|
228
|
+
DashboardNavbarSlots,
|
|
229
|
+
DashboardSidebarSlots,
|
|
230
|
+
DashboardSlotExtras,
|
|
231
|
+
DashboardSlots,
|
|
232
|
+
PrefixedDashboardSlots,
|
|
222
233
|
} from "./types/navigation";
|
|
@@ -274,6 +274,20 @@ export interface FieldDefinition {
|
|
|
274
274
|
*/
|
|
275
275
|
default?: any;
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* For a field whose value is a list of rows (a `repeater`, or a span
|
|
279
|
+
* field that renders its own rows and owns `key.N.*` error keys): the
|
|
280
|
+
* row's own name, called with the row (`model[key][index]`, or `{}` when
|
|
281
|
+
* the model has no such row) and its 0-based index. When it returns a
|
|
282
|
+
* non-empty string, the error summary lists that row's errors under it
|
|
283
|
+
* alone ("Chaucer Ale (Pint): Enter a price.") instead of
|
|
284
|
+
* "<sub-field label> (line N)". Returning `undefined`, `null` or `""`
|
|
285
|
+
* (a row with no name yet), or throwing, keeps the line-number label.
|
|
286
|
+
* Only labels: which field owns the error, and what a click on the
|
|
287
|
+
* summary row focuses, are unchanged.
|
|
288
|
+
*/
|
|
289
|
+
rowLabel?: (row: Record<string, any>, index: number) => string | null | undefined;
|
|
290
|
+
|
|
277
291
|
/** Label for a repeater's "add row" button (default: "Add"). */
|
|
278
292
|
addLabel?: string;
|
|
279
293
|
|
|
@@ -364,6 +378,10 @@ export interface FieldDefinition {
|
|
|
364
378
|
* every `editFields` key, so those decorative fields are POSTed alongside
|
|
365
379
|
* the real ones (an empty string, an empty array) purely because they were
|
|
366
380
|
* declared.
|
|
381
|
+
*
|
|
382
|
+
* Applies per KEY (#228): DXTable drops a key only when no field with that
|
|
383
|
+
* key is both submitting and visible, so a `submit: false` display twin
|
|
384
|
+
* does not strip the value of a visible editable field sharing its key.
|
|
367
385
|
*/
|
|
368
386
|
submit?: boolean;
|
|
369
387
|
|
|
@@ -579,6 +597,7 @@ export interface ComponentFieldDef extends BaseFieldDef {
|
|
|
579
597
|
export interface RepeaterFieldDef extends BaseFieldDef {
|
|
580
598
|
type: "repeater";
|
|
581
599
|
fields?: FieldDef[];
|
|
600
|
+
rowLabel?: (row: Record<string, any>, index: number) => string | null | undefined;
|
|
582
601
|
addLabel?: string;
|
|
583
602
|
minItems?: number;
|
|
584
603
|
maxItems?: number;
|
|
@@ -652,6 +671,51 @@ export interface FormTab {
|
|
|
652
671
|
lazy?: boolean;
|
|
653
672
|
}
|
|
654
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Slot props of DXField's `password-actions` slot (DXForm:
|
|
676
|
+
* `password-actions(<key>)`): content placed beside a password input, inside
|
|
677
|
+
* its input group after the reveal (eye) toggle, such as a "Generate password"
|
|
678
|
+
* button. `setRevealed` drives the same state the eye toggles.
|
|
679
|
+
*
|
|
680
|
+
* ```vue
|
|
681
|
+
* <template #password-actions(password)="{ update, setRevealed, disabled }">
|
|
682
|
+
* <DButton variant="outline-secondary" :disabled="disabled"
|
|
683
|
+
* @click="update(generatePassword()); setRevealed(true)">Generate</DButton>
|
|
684
|
+
* </template>
|
|
685
|
+
* ```
|
|
686
|
+
*/
|
|
687
|
+
export interface PasswordActionsSlotProps {
|
|
688
|
+
/** The field definition being rendered. */
|
|
689
|
+
field: FieldDefinition;
|
|
690
|
+
/** The model passed to field predicates (defaults to the live form data). */
|
|
691
|
+
model: any;
|
|
692
|
+
/** The current field value. */
|
|
693
|
+
value: any;
|
|
694
|
+
/** Write a new value (also marks the field touched and clears its error). */
|
|
695
|
+
update: (value: any) => void;
|
|
696
|
+
/** Whether the password is currently shown as plain text. */
|
|
697
|
+
revealed: boolean;
|
|
698
|
+
/**
|
|
699
|
+
* Show (`true`) or mask (`false`) the password, exactly as the eye does.
|
|
700
|
+
* A no-op on a `revealable: false` field, which never unmasks.
|
|
701
|
+
*/
|
|
702
|
+
setRevealed: (revealed: boolean) => void;
|
|
703
|
+
/** Whether the field is disabled or read-only. */
|
|
704
|
+
disabled: boolean;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Slot props of DXTable's `edit-password-actions(<key>)` slot, the edit/create
|
|
709
|
+
* modal's form of {@link PasswordActionsSlotProps}. `item` is the row being
|
|
710
|
+
* edited, `null` in create mode. (`model` is not passed, as with the modal's
|
|
711
|
+
* other field slots.)
|
|
712
|
+
*/
|
|
713
|
+
export interface EditPasswordActionsSlotProps
|
|
714
|
+
extends Omit<PasswordActionsSlotProps, "model"> {
|
|
715
|
+
/** The row being edited; `null` in create mode. */
|
|
716
|
+
item: any;
|
|
717
|
+
}
|
|
718
|
+
|
|
655
719
|
/**
|
|
656
720
|
* The theme's extra colour variant, `pending`: waiting, and the next move is
|
|
657
721
|
* not yours ("awaiting payment", "sent to printer, not yet confirmed").
|
|
@@ -8,7 +8,13 @@ export interface NavigationItem {
|
|
|
8
8
|
visible?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* A group of sidebar links. `TItem` is the app's own item type, for menus whose
|
|
13
|
+
* items carry extra keys (a permission, a route name): DXDashboard and
|
|
14
|
+
* DXDashboardSidebar infer it from the `navigation` prop, so the `link` slot's
|
|
15
|
+
* `item` has those keys too. Defaults to `NavigationItem`.
|
|
16
|
+
*/
|
|
17
|
+
export interface NavigationGroup<TItem extends NavigationItem = NavigationItem> {
|
|
12
18
|
label?: string;
|
|
13
19
|
/**
|
|
14
20
|
* Stable identifier for the group's collapsible open/closed state. Defaults
|
|
@@ -17,7 +23,7 @@ export interface NavigationGroup {
|
|
|
17
23
|
* state attached to the right group.
|
|
18
24
|
*/
|
|
19
25
|
key?: string;
|
|
20
|
-
items:
|
|
26
|
+
items: TItem[];
|
|
21
27
|
/**
|
|
22
28
|
* Per-group override for the sidebar's `collapsibleGroups` behaviour.
|
|
23
29
|
* Set to `false` to keep a group permanently expanded (no toggle header)
|
|
@@ -29,7 +35,26 @@ export interface NavigationGroup {
|
|
|
29
35
|
visible?: boolean;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
/** The sidebar's navigation: its groups, in order. See `NavigationGroup` for `TItem`. */
|
|
39
|
+
export type Navigation<TItem extends NavigationItem = NavigationItem> = NavigationGroup<TItem>[];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The item type of a navigation, as the `link` slot's `item` receives it:
|
|
43
|
+
* `NavigationItemOf<Navigation<AppItem>>` is `AppItem`. An `any` navigation
|
|
44
|
+
* (untyped page props) gives `any`, and an empty `[]` gives `NavigationItem`.
|
|
45
|
+
*
|
|
46
|
+
* DXDashboard and DXDashboardSidebar are generic over the whole navigation
|
|
47
|
+
* (`TNavigation`) and derive the item from it, rather than generic over the
|
|
48
|
+
* item: TypeScript infers `any` for a type parameter that IS the prop's type,
|
|
49
|
+
* but not for one nested inside it (`{ items: TItem[] }[]`), where an `any`
|
|
50
|
+
* navigation would have fallen back to `NavigationItem` and rejected the app's
|
|
51
|
+
* extra keys.
|
|
52
|
+
*/
|
|
53
|
+
export type NavigationItemOf<TNavigation extends Navigation> = [
|
|
54
|
+
TNavigation[number]["items"][number],
|
|
55
|
+
] extends [never]
|
|
56
|
+
? NavigationItem
|
|
57
|
+
: TNavigation[number]["items"][number];
|
|
33
58
|
|
|
34
59
|
/**
|
|
35
60
|
* Horizontal alignment of the navbar search slot content within its region:
|
|
@@ -54,3 +79,150 @@ export type SidebarVariant = "light" | "dark";
|
|
|
54
79
|
* Shared by DXDashboard (which forwards it) and DXDashboardNavbar.
|
|
55
80
|
*/
|
|
56
81
|
export type NavbarActionsOnMobile = "wrap" | "hide";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The signed-in user DXDashboardNavbar (and DXDashboard, which forwards it)
|
|
85
|
+
* shows in the avatar dropdown. Extra keys are allowed and reach the
|
|
86
|
+
* user-menu slots.
|
|
87
|
+
*/
|
|
88
|
+
export interface DashboardUser {
|
|
89
|
+
name: string;
|
|
90
|
+
email: string;
|
|
91
|
+
[key: string]: any;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A slot that receives no props. */
|
|
95
|
+
type NoSlotProps = Record<never, never>;
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* The slot interfaces below are what `defineSlots` uses and what an editor
|
|
99
|
+
* shows on hover. The API docs (`docs:generate:ai`) still read each slot from
|
|
100
|
+
* the `@slot`/`@binding` comments in the component's template: vue-docgen-api
|
|
101
|
+
* only reads `defineSlots` members whose keys are bare identifiers, and it
|
|
102
|
+
* drops the binding types when it does, so every key here is QUOTED on
|
|
103
|
+
* purpose. Keep the text in step with the template comments.
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/** Slots of DXDashboardNavbar, with their slot props. All are optional. */
|
|
107
|
+
export interface DashboardNavbarSlots {
|
|
108
|
+
/** Custom hamburger/menu icon. Defaults to a three-line SVG. */
|
|
109
|
+
"menu-icon"?(props: NoSlotProps): any;
|
|
110
|
+
/**
|
|
111
|
+
* Search input or component. Fills the middle of the bar at every width,
|
|
112
|
+
* aligned per `searchAlign`. The region is a size container named
|
|
113
|
+
* `dx-navbar-search`; a `<kbd>` hint inside it is hidden when it is
|
|
114
|
+
* narrower than 16rem.
|
|
115
|
+
*/
|
|
116
|
+
"search"?(props: NoSlotProps): any;
|
|
117
|
+
/**
|
|
118
|
+
* Page-level primary actions (e.g. a Create button). Right-aligned next to
|
|
119
|
+
* the user menu from `md` up; below, wraps to its own full-width row or is
|
|
120
|
+
* hidden entirely per `actionsOnMobile`.
|
|
121
|
+
*/
|
|
122
|
+
"actions"?(props: {
|
|
123
|
+
/** The current page title, for context. */
|
|
124
|
+
pageTitle: string;
|
|
125
|
+
}): any;
|
|
126
|
+
/** Replaces the entire user menu (the default avatar dropdown). */
|
|
127
|
+
"user-menu"?(props: {
|
|
128
|
+
/** The signed-in user; `null` for a guest. */
|
|
129
|
+
user: DashboardUser | null;
|
|
130
|
+
}): any;
|
|
131
|
+
/**
|
|
132
|
+
* Custom user avatar/icon in the dropdown trigger. To decorate the default
|
|
133
|
+
* avatar (e.g. add a notification dot) rather than replace it, render
|
|
134
|
+
* `DXUserAvatar`. Only rendered for a signed-in user.
|
|
135
|
+
*/
|
|
136
|
+
"user-icon"?(props: {
|
|
137
|
+
/** The first letter of the user's name. */
|
|
138
|
+
initial: string;
|
|
139
|
+
/** The signed-in user. */
|
|
140
|
+
user: DashboardUser;
|
|
141
|
+
}): any;
|
|
142
|
+
/** Items for the default user dropdown menu. Only rendered for a signed-in user. */
|
|
143
|
+
"user-menu-items"?(props: {
|
|
144
|
+
/** The signed-in user. */
|
|
145
|
+
user: DashboardUser;
|
|
146
|
+
}): any;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Slots of DXDashboardSidebar, with their slot props. All are optional.
|
|
151
|
+
* `TItem` is the navigation's item type (see `NavigationGroup`).
|
|
152
|
+
*/
|
|
153
|
+
export interface DashboardSidebarSlots<TItem extends NavigationItem = NavigationItem> {
|
|
154
|
+
/**
|
|
155
|
+
* Brand/logo area in the sidebar header. Defaults to the title's initial
|
|
156
|
+
* plus the full title (hidden when collapsed).
|
|
157
|
+
*/
|
|
158
|
+
"brand"?(props: {
|
|
159
|
+
/** Whether the sidebar rail is collapsed to icons only. */
|
|
160
|
+
collapsed: boolean;
|
|
161
|
+
/** The sidebar title text. */
|
|
162
|
+
title: string;
|
|
163
|
+
}): any;
|
|
164
|
+
/**
|
|
165
|
+
* Renders a single navigation item link. Defaults to an anchor with
|
|
166
|
+
* optional icon, label, and badge.
|
|
167
|
+
*/
|
|
168
|
+
"link"?(props: {
|
|
169
|
+
/**
|
|
170
|
+
* The navigation item (label, url, icon, badge, badgeColor), typed as the
|
|
171
|
+
* app's own item type when its items carry extra keys.
|
|
172
|
+
*/
|
|
173
|
+
item: TItem;
|
|
174
|
+
/** Whether this item matches the current route. */
|
|
175
|
+
isActive: boolean;
|
|
176
|
+
/** Whether the sidebar rail is collapsed to icons only. */
|
|
177
|
+
collapsed: boolean;
|
|
178
|
+
/** Whether the item's group is currently expanded. */
|
|
179
|
+
isExpanded: boolean;
|
|
180
|
+
}): any;
|
|
181
|
+
/**
|
|
182
|
+
* Utility/secondary links pinned to the bottom of the sidebar (help,
|
|
183
|
+
* changelog, sign-out, …), below the nav groups.
|
|
184
|
+
*/
|
|
185
|
+
"footer"?(props: {
|
|
186
|
+
/** Whether the sidebar rail is collapsed to icons only. */
|
|
187
|
+
collapsed: boolean;
|
|
188
|
+
}): any;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The two bindings DXDashboard adds to every forwarded `sidebar-*` and
|
|
193
|
+
* `navbar-*` slot. They are bound after the child's own slot props, so they
|
|
194
|
+
* win a name clash.
|
|
195
|
+
*/
|
|
196
|
+
export interface DashboardSlotExtras {
|
|
197
|
+
/** Shows/hides the sidebar. */
|
|
198
|
+
toggleSidebar: () => void;
|
|
199
|
+
/** Whether the sidebar is currently hidden. */
|
|
200
|
+
sidebarHidden: boolean;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The props a (possibly optional) slot function receives. */
|
|
204
|
+
type SlotPropsOf<Slot> = NonNullable<Slot> extends (props: infer Props) => any ? Props : never;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A child component's slots renamed with `Prefix` and given the dashboard's
|
|
208
|
+
* extra bindings: `{ brand(props: P) }` becomes
|
|
209
|
+
* `{ "sidebar-brand"(props: P & DashboardSlotExtras) }`. Derived from the
|
|
210
|
+
* child's slot types, so a slot added to the child is typed here for free.
|
|
211
|
+
*/
|
|
212
|
+
export type PrefixedDashboardSlots<Slots, Prefix extends string> = {
|
|
213
|
+
[Name in keyof Slots as `${Prefix}${Name & string}`]?: (
|
|
214
|
+
props: Omit<SlotPropsOf<Slots[Name]>, keyof DashboardSlotExtras> & DashboardSlotExtras,
|
|
215
|
+
) => any;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Slots of DXDashboard: the page content (`default`), every
|
|
220
|
+
* DXDashboardSidebar slot as `sidebar-<name>` and every DXDashboardNavbar
|
|
221
|
+
* slot as `navbar-<name>`. An unprefixed name (`#search`) is not forwarded.
|
|
222
|
+
* `TItem` is the navigation's item type (see `NavigationGroup`).
|
|
223
|
+
*/
|
|
224
|
+
export type DashboardSlots<TItem extends NavigationItem = NavigationItem> = {
|
|
225
|
+
/** The main page content. */
|
|
226
|
+
default?(props: NoSlotProps): any;
|
|
227
|
+
} & PrefixedDashboardSlots<DashboardSidebarSlots<TItem>, "sidebar-"> &
|
|
228
|
+
PrefixedDashboardSlots<DashboardNavbarSlots, "navbar-">;
|