@omnitend/dashboard-for-laravel 0.41.0 → 0.41.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72,8 +72,7 @@
72
72
  supplied their own `table-colgroup` (forwarded generically) —
73
73
  so a table with no widths gets no colgroup and renders exactly
74
74
  as before. `<col>` widths are authoritative under
75
- `table-layout: fixed`, where the filter row (not the header)
76
- is the width-determining first row. -->
75
+ `table-layout: fixed`, regardless of any consumer header banners. -->
77
76
  <template
78
77
  v-if="fieldsHaveWidths && !$slots['table-colgroup']"
79
78
  #table-colgroup
@@ -85,99 +84,100 @@
85
84
  />
86
85
  </template>
87
86
 
88
- <!-- DXTable owns `thead-top` (its filter row lives there), so a
89
- consumer's own thead-top content is COMPOSED above it rather
90
- than being dropped: a grouped column-header banner or a pinned
91
- totals row sits above the headers where it belongs (#120). -->
92
- <template v-if="hasFilters || $slots['thead-top']" #thead-top="theadScope">
87
+ <template v-if="$slots['thead-top']" #thead-top="theadScope">
93
88
  <!--
94
- @slot A row rendered ABOVE the column headers — a grouped-column banner (a `<th colspan>` spanning several columns), or a pinned totals row. Renders above DXTable's own filter row. Give it `<tr>`s.
89
+ @slot Rows above the column headers, such as grouped-column banners or pinned totals. Give it `<tr>`s.
95
90
  @binding {object} columns The number of columns in the table.
96
91
  @binding {object} fields The table's fields.
97
92
  -->
98
93
  <slot name="thead-top" v-bind="theadScope" />
94
+ </template>
99
95
 
100
- <tr v-if="hasFilters" class="filter-row">
101
- <th v-for="field in fields" :key="`filter-${field.key}`" class="p-2">
102
- <!-- Text Filter -->
103
- <DFormInput
104
- v-if="field.filter === 'text'"
105
- :model-value="effectiveFilters[filterKeyFor(field)] || ''"
106
- :placeholder="field.filterPlaceholder || `Search ${field.label || field.key}...`"
107
- size="sm"
108
- @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
109
- />
110
-
111
- <!-- Multi-value Select Filter (#51): several values at once; the filters entry is an array -->
112
- <DAutocomplete
113
- v-else-if="field.filter === 'select' && field.filterMultiple"
114
- :model-value="multiFilterValueFor(field)"
115
- :options="getFieldFilterOptions(field)"
116
- :placeholder="field.filterPlaceholder || filterAllLabelFor(field)"
117
- size="sm"
118
- multiple
119
- open-on-focus
120
- @update:model-value="handleMultiSelectFilterChange(field, $event)"
121
- />
122
-
123
- <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
124
- <DAutocomplete
125
- v-else-if="field.filter === 'select'"
126
- :model-value="(effectiveFilters[filterKeyFor(field)] as string) || ''"
127
- :options="getFieldFilterOptions(field)"
128
- :placeholder="field.filterPlaceholder || filterAllLabelFor(field)"
129
- size="sm"
130
- open-on-focus
131
- @update:model-value="handleSelectFilterChange(field, $event)"
132
- />
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
-
151
- <!-- Number Filter -->
152
- <DFormInput
153
- v-else-if="field.filter === 'number'"
154
- :model-value="effectiveFilters[filterKeyFor(field)] || ''"
155
- :placeholder="field.filterPlaceholder || `Filter ${field.label || field.key}...`"
156
- type="number"
157
- size="sm"
158
- @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
159
- />
160
-
161
- <!-- Date Filter -->
162
- <DFormInput
163
- v-else-if="field.filter === 'date'"
164
- :model-value="effectiveFilters[filterKeyFor(field)] || ''"
165
- type="date"
166
- size="sm"
167
- @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
168
- />
96
+ <!-- The table supplies the below-header row and one cell per field. -->
97
+ <template v-if="hasFilters || $slots['thead-sub']" #thead-sub="{ field, ...filterScope }">
98
+ <div v-if="hasFilters" class="dx-table-filter-cell">
99
+ <!-- Text Filter -->
100
+ <DFormInput
101
+ v-if="field.filter === 'text'"
102
+ :model-value="effectiveFilters[filterKeyFor(field)] || ''"
103
+ :placeholder="field.filterPlaceholder || `Search ${field.label || field.key}...`"
104
+ size="sm"
105
+ @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
106
+ />
107
+
108
+ <!-- Multi-value Select Filter (#51): several values at once; the filters entry is an array -->
109
+ <DAutocomplete
110
+ v-else-if="field.filter === 'select' && field.filterMultiple"
111
+ :model-value="multiFilterValueFor(field)"
112
+ :options="getFieldFilterOptions(field)"
113
+ :placeholder="field.filterPlaceholder || filterAllLabelFor(field)"
114
+ size="sm"
115
+ multiple
116
+ open-on-focus
117
+ @update:model-value="handleMultiSelectFilterChange(field, $event)"
118
+ />
119
+
120
+ <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
121
+ <DAutocomplete
122
+ v-else-if="field.filter === 'select'"
123
+ :model-value="(effectiveFilters[filterKeyFor(field)] as string) || ''"
124
+ :options="getFieldFilterOptions(field)"
125
+ :placeholder="field.filterPlaceholder || filterAllLabelFor(field)"
126
+ size="sm"
127
+ open-on-focus
128
+ @update:model-value="handleSelectFilterChange(field, $event)"
129
+ />
130
+
131
+ <!-- Native Select Filter (S3b): a plain <select> for consumers who
132
+ want OS-native behaviour (full-height menu, native keyboard/scroll)
133
+ instead of the DAutocomplete typeahead. Shares the select filter's
134
+ option list (getFieldFilterOptions) and change handler
135
+ (handleSelectFilterChange) EXACTLY — so it behaves identically to
136
+ filter:"select" in every way but the control. Single-select only:
137
+ `filterMultiple` is ignored here. The model maps "no filter" to the
138
+ FILTER_ALL_VALUE sentinel (see nativeSelectFilterValue) so the "All …"
139
+ option shows selected in a native control. -->
140
+ <DFormSelect
141
+ v-else-if="field.filter === 'select-native'"
142
+ :model-value="nativeSelectFilterValue(field)"
143
+ :options="getFieldFilterOptions(field) as unknown as Record<string, unknown>[]"
144
+ size="sm"
145
+ @update:model-value="handleSelectFilterChange(field, $event)"
146
+ />
147
+
148
+ <!-- Number Filter -->
149
+ <DFormInput
150
+ v-else-if="field.filter === 'number'"
151
+ :model-value="effectiveFilters[filterKeyFor(field)] || ''"
152
+ :placeholder="field.filterPlaceholder || `Filter ${field.label || field.key}...`"
153
+ type="number"
154
+ size="sm"
155
+ @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
156
+ />
157
+
158
+ <!-- Date Filter -->
159
+ <DFormInput
160
+ v-else-if="field.filter === 'date'"
161
+ :model-value="effectiveFilters[filterKeyFor(field)] || ''"
162
+ type="date"
163
+ size="sm"
164
+ @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
165
+ />
169
166
 
170
- <!-- No filter for this column -->
171
- <div v-else></div>
172
- </th>
173
- </tr>
167
+ <!-- No filter for this column -->
168
+ <div v-else></div>
169
+ </div>
170
+ <slot name="thead-sub" v-bind="{ ...filterScope, field }" />
174
171
  </template>
175
172
 
176
173
  <!-- Custom headers for all fields -->
177
174
  <template v-for="field in fields" :key="`head-${field.key}`" #[`head(${field.key})`]="{ label }">
178
175
  <div class="d-flex align-items-center justify-content-between gap-2">
179
176
  <div class="flex-grow-1">
180
- <div class="fw-semibold">{{ headerLabel(field, label) }}</div>
177
+ <div class="fw-semibold">
178
+ <DXFieldLabel v-if="field.info" :label="headerLabel(field, label)" :info="field.info" />
179
+ <template v-else>{{ headerLabel(field, label) }}</template>
180
+ </div>
181
181
  <small v-if="field.hint" class="text-muted d-block" style="font-weight: normal;">{{ field.hint }}</small>
182
182
  </div>
183
183
 
@@ -329,6 +329,7 @@ import DTable from "../base/DTable.vue";
329
329
  import DFormInput from "../base/DFormInput.vue";
330
330
  import DFormSelect from "../base/DFormSelect.vue";
331
331
  import DAutocomplete from "../base/DAutocomplete.vue";
332
+ import DXFieldLabel from "./DXFieldLabel.vue";
332
333
  import DButton from "../base/DButton.vue";
333
334
  import DXTablePagination from "./DXTablePagination.vue";
334
335
  import DXTableEditorModal from "./DXTableEditorModal.vue";
@@ -407,6 +408,8 @@ export interface TableField {
407
408
  filterAllText?: string;
408
409
  sortable?: boolean;
409
410
  hint?: string;
411
+ /** Help revealed from a focusable info icon beside the column label. */
412
+ info?: string;
410
413
  /**
411
414
  * The inline filter control for this column, or `false`/omit for none. See
412
415
  * `FilterType`. `'select'` (typeahead) and `'select-native'` (plain
@@ -1318,9 +1321,8 @@ const dottedCellFields = (slots: Record<string, unknown>): TableField[] =>
1318
1321
  /*
1319
1322
  * Per-column widths (#156). A field's `width`/`minWidth` is applied through a
1320
1323
  * `<colgroup>` `<col>`, NOT the header `<th>`: with `table-layout: fixed` the
1321
- * column widths are taken from the FIRST row of cells, and DXTable's inline
1322
- * filter row (`thead-top`) is that first row — so a width on the header `<th>`
1323
- * would be ignored whenever filters are present. A `<col>` width is
1324
+ * column widths are taken from the FIRST row of cells, which may be a
1325
+ * consumer banner rather than the column labels. A `<col>` width is
1324
1326
  * authoritative regardless of row order, in both fixed and auto layout.
1325
1327
  */
1326
1328
  const sizeToCss = (value: string | number): string =>
@@ -1771,6 +1773,7 @@ watch(clientSideLastPage, (lastPage) => {
1771
1773
  // Client-side paginated items (final output)
1772
1774
  const clientSidePaginatedItems = computed<T[]>(() => {
1773
1775
  if (!isClientSideMode.value) return [];
1776
+ if (!props.showPagination) return clientSideSortedItems.value;
1774
1777
 
1775
1778
  const perPage = effectivePerPage.value;
1776
1779
  const start = (clientSidePage.value - 1) * perPage;
@@ -1787,19 +1790,19 @@ const clientSidePagination = computed<PaginationData>(() => {
1787
1790
  const lastPage = clientSideLastPage.value;
1788
1791
 
1789
1792
  // Same clamped page the slice uses — see `clientSidePage` (#118).
1790
- const validPage = clientSidePage.value;
1793
+ const validPage = props.showPagination ? clientSidePage.value : 1;
1791
1794
 
1792
- const from = total > 0 ? (validPage - 1) * perPage + 1 : 0;
1793
- const to = Math.min(validPage * perPage, total);
1795
+ const from = total > 0 ? (props.showPagination ? (validPage - 1) * perPage + 1 : 1) : 0;
1796
+ const to = props.showPagination ? Math.min(validPage * perPage, total) : total;
1794
1797
 
1795
1798
  return {
1796
1799
  current_page: validPage,
1797
- per_page: perPage,
1800
+ per_page: props.showPagination ? perPage : Math.max(1, total),
1798
1801
  total,
1799
1802
  total_unfiltered: totalUnfiltered !== total ? totalUnfiltered : undefined,
1800
1803
  from,
1801
1804
  to,
1802
- last_page: lastPage,
1805
+ last_page: props.showPagination ? lastPage : 1,
1803
1806
  };
1804
1807
  });
1805
1808
 
@@ -1826,7 +1829,7 @@ const handleClientSidePageChange = (page: number) => {
1826
1829
  // Computed: determine if per-page selector should be shown
1827
1830
  // Hide it when total items is less than the smallest page size option
1828
1831
  const shouldShowPerPageSelector = computed(() => {
1829
- if (!props.showPerPageSelector) return false;
1832
+ if (!props.showPagination || !props.showPerPageSelector) return false;
1830
1833
 
1831
1834
  const smallestOption = Math.min(...props.perPageOptions);
1832
1835
  const total = isClientSideMode.value
@@ -2262,7 +2265,7 @@ const refresh = () => {
2262
2265
  *
2263
2266
  * - `head(<key>)` — DXTable draws its own column headers (sort indicators,
2264
2267
  * field hints). Forwarding a consumer's would silently drop those.
2265
- * - `thead-top` — DXTable renders the inline filter row there.
2268
+ * - `thead-top` / `thead-sub` — composed with the below-header filter cells.
2266
2269
  *
2267
2270
  * `head-end(<key>)` is DXTable's OWN additive header slot (#99) and is likewise
2268
2271
  * not forwarded: the inner table has no such slot, and the prefixes below are
@@ -2312,11 +2315,10 @@ const isTableSlot = (name: string) =>
2312
2315
  * invisible (#114). Called from the template, it re-evaluates every render.
2313
2316
  *
2314
2317
  * It also has to EXCLUDE the slots DXTable renders itself: declaring `#thead-top`
2315
- * here (even rendering nothing into it) would override DXTable's own thead-top
2316
- * template, silently dropping the filter row and the consumer's banner (#120).
2318
+ * or `#thead-sub` here would override the composed header/filter slots.
2317
2319
  */
2318
2320
  const forwardableSlotNames = (slots: Record<string, unknown>): string[] =>
2319
- Object.keys(slots).filter((name) => isTableSlot(name));
2321
+ Object.keys(slots).filter((name) => name !== 'thead-sub' && isTableSlot(name));
2320
2322
 
2321
2323
  const tableSlotSignature = (slots: Record<string, unknown>): string => {
2322
2324
  // DXTable's OWN dotted-cell slots count too (#121). bvn captures its cell
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  @component
3
- Internal pagination + per-page footer for `DXTable`. Renders the pager, the
3
+ Pagination + per-page footer shared by `DXTable` and standalone tables. Renders the pager, the
4
4
  per-page selector, and the "X to Y out of Z items" info line (with the
5
5
  "Filtered from N" note when a column filter is active).
6
6
 
@@ -9,6 +9,8 @@ export { default as DXDashboard } from "./components/extended/DXDashboard.vue";
9
9
  export { default as DXForm } from "./components/extended/DXForm.vue";
10
10
  export { default as DXField } from "./components/extended/DXField.vue";
11
11
  export { default as DXFieldLabel } from "./components/extended/DXFieldLabel.vue";
12
+ export { default as DXModalActions } from "./components/extended/DXModalActions.vue";
13
+ export { default as DXLoading } from "./components/extended/DXLoading.vue";
12
14
  export { default as DXSwitch } from "./components/extended/DXSwitch.vue";
13
15
  export { default as DXCurrencyInput } from "./components/extended/DXCurrencyInput.vue";
14
16
  export { default as DXStatCard } from "./components/extended/DXStatCard.vue";
@@ -23,6 +25,7 @@ export { default as DXRepeater } from "./components/extended/DXRepeater.vue";
23
25
  */
24
26
  export { default as DXBasicForm } from "./components/extended/DXBasicForm.vue";
25
27
  export { default as DXTable } from "./components/extended/DXTable.vue";
28
+ export { default as DXTablePagination } from "./components/extended/DXTablePagination.vue";
26
29
  export { default as DXDashboardSidebar } from "./components/extended/DXDashboardSidebar.vue";
27
30
  export { default as DXDashboardNavbar } from "./components/extended/DXDashboardNavbar.vue";
28
31
  export { default as DXUserAvatar } from "./components/extended/DXUserAvatar.vue";