qdadm 0.58.0 → 0.59.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdadm",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -603,12 +603,12 @@ const showBreadcrumb = computed(() => {
603
603
  display: flex;
604
604
  flex-direction: column;
605
605
  transition: margin-left var(--fad-transition-slow);
606
+ overflow-y: auto;
606
607
  }
607
608
 
608
609
  .page-content {
609
610
  flex: 1;
610
611
  padding: 1rem;
611
- overflow-y: auto;
612
612
  }
613
613
 
614
614
  /* Dark mode support */
@@ -65,7 +65,10 @@ const props = defineProps({
65
65
 
66
66
  // Row Actions
67
67
  getActions: { type: Function, default: null },
68
- actionsWidth: { type: String, default: '120px' }
68
+ actionsWidth: { type: String, default: '120px' },
69
+
70
+ // Mobile row tap
71
+ hasRowTapAction: { type: Boolean, default: false }
69
72
  })
70
73
 
71
74
  function resolveLabel(label, _action) {
@@ -113,7 +116,8 @@ const emit = defineEmits([
113
116
  'update:searchQuery',
114
117
  'update:filterValues',
115
118
  'page',
116
- 'sort'
119
+ 'sort',
120
+ 'row-click'
117
121
  ])
118
122
 
119
123
  function clearAllFilters() {
@@ -202,6 +206,11 @@ function onPage(event) {
202
206
  function onSort(event) {
203
207
  emit('sort', event)
204
208
  }
209
+
210
+ function onRowClick(event) {
211
+ // event.data = row data, event.originalEvent = MouseEvent
212
+ emit('row-click', event.data, event.originalEvent)
213
+ }
205
214
  </script>
206
215
 
207
216
  <template>
@@ -301,6 +310,8 @@ function onSort(event) {
301
310
  @update:selection="onSelectionChange"
302
311
  @page="onPage"
303
312
  @sort="onSort"
313
+ @row-click="onRowClick"
314
+ :class="{ 'datatable-row-clickable': hasRowTapAction }"
304
315
  stripedRows
305
316
  removableSort
306
317
  >
@@ -1,4 +1,4 @@
1
- import { ref, computed, watch, onMounted, inject, provide } from 'vue'
1
+ import { ref, computed, watch, onMounted, onUnmounted, inject, provide } from 'vue'
2
2
  import { useRouter, useRoute } from 'vue-router'
3
3
  import { useConfirm } from 'primevue/useconfirm'
4
4
  import { useHooks } from './useHooks.js'
@@ -140,6 +140,9 @@ export function useListPage(config = {}) {
140
140
  const route = useRoute()
141
141
  const confirm = useConfirm()
142
142
 
143
+ // Mobile detection (viewport width < 768px)
144
+ const isMobile = ref(window.innerWidth < 768)
145
+
143
146
  // Get EntityManager via orchestrator
144
147
  const orchestrator = inject('qdadmOrchestrator')
145
148
 
@@ -1274,6 +1277,58 @@ export function useListPage(config = {}) {
1274
1277
  router.push({ name: `${routePrefix}-show`, params: { [manager.idField]: item[resolvedDataKey] } })
1275
1278
  }
1276
1279
 
1280
+ // ============ MOBILE ROW TAP ============
1281
+ /**
1282
+ * Get primary action for row tap (based on declared routes, not permissions)
1283
+ * Priority: edit > show
1284
+ * @returns {'edit'|'show'|null}
1285
+ */
1286
+ function getPrimaryRowAction() {
1287
+ const editRouteName = `${routePrefix}-edit`
1288
+ const showRouteName = `${routePrefix}-show`
1289
+
1290
+ if (router.hasRoute(editRouteName)) {
1291
+ return 'edit'
1292
+ }
1293
+ if (router.hasRoute(showRouteName)) {
1294
+ return 'show'
1295
+ }
1296
+ return null
1297
+ }
1298
+
1299
+ /**
1300
+ * Handle row click for mobile navigation
1301
+ * Only triggers on mobile, ignores clicks on interactive elements
1302
+ * @param {object} item - Row data
1303
+ * @param {Event} originalEvent - Original mouse/touch event
1304
+ */
1305
+ function onRowClick(item, originalEvent) {
1306
+ // Ignore if not mobile
1307
+ if (!isMobile.value) return
1308
+
1309
+ // Ignore if in selection mode
1310
+ if (hasBulkActions.value && selected.value.length > 0) return
1311
+
1312
+ // Ignore if click was on interactive element
1313
+ const target = originalEvent?.target
1314
+ if (target?.closest('button, a, .p-button, .table-actions, input, .p-checkbox')) {
1315
+ return
1316
+ }
1317
+
1318
+ // Execute primary action
1319
+ const action = getPrimaryRowAction()
1320
+ if (action === 'edit') {
1321
+ goToEdit(item)
1322
+ } else if (action === 'show') {
1323
+ goToShow(item)
1324
+ }
1325
+ }
1326
+
1327
+ /**
1328
+ * Whether row tap navigation is available (mobile + has edit/show route)
1329
+ */
1330
+ const hasRowTapAction = computed(() => isMobile.value && getPrimaryRowAction() !== null)
1331
+
1277
1332
  // ============ DELETE ============
1278
1333
  async function deleteItem(item, labelField = 'name') {
1279
1334
  try {
@@ -1561,7 +1616,15 @@ export function useListPage(config = {}) {
1561
1616
  // Initialize from registry immediately (sync)
1562
1617
  initFromRegistry()
1563
1618
 
1619
+ // Mobile detection resize handler
1620
+ function handleResize() {
1621
+ isMobile.value = window.innerWidth < 768
1622
+ }
1623
+
1564
1624
  onMounted(async () => {
1625
+ // Listen for viewport changes
1626
+ window.addEventListener('resize', handleResize)
1627
+
1565
1628
  // Restore filters from URL/session after registry init
1566
1629
  restoreFilters()
1567
1630
 
@@ -1581,6 +1644,10 @@ export function useListPage(config = {}) {
1581
1644
  }
1582
1645
  })
1583
1646
 
1647
+ onUnmounted(() => {
1648
+ window.removeEventListener('resize', handleResize)
1649
+ })
1650
+
1584
1651
  // ============ UTILITIES ============
1585
1652
  function formatDate(dateStr, options = {}) {
1586
1653
  if (!dateStr) return '-'
@@ -1699,7 +1766,10 @@ export function useListPage(config = {}) {
1699
1766
  filterValues: filterValues.value,
1700
1767
 
1701
1768
  // Row actions
1702
- getActions
1769
+ getActions,
1770
+
1771
+ // Mobile row tap
1772
+ hasRowTapAction: hasRowTapAction.value
1703
1773
  }))
1704
1774
 
1705
1775
  /**
@@ -1711,7 +1781,8 @@ export function useListPage(config = {}) {
1711
1781
  'update:searchQuery': (value) => { searchQuery.value = value },
1712
1782
  'update:filterValues': updateFilters,
1713
1783
  'page': onPage,
1714
- 'sort': onSort
1784
+ 'sort': onSort,
1785
+ 'row-click': onRowClick
1715
1786
  }
1716
1787
 
1717
1788
  return {
@@ -1807,6 +1878,12 @@ export function useListPage(config = {}) {
1807
1878
  goToEdit,
1808
1879
  goToShow,
1809
1880
 
1881
+ // Mobile row tap
1882
+ isMobile,
1883
+ hasRowTapAction,
1884
+ getPrimaryRowAction,
1885
+ onRowClick,
1886
+
1810
1887
  // Delete
1811
1888
  deleteItem,
1812
1889
  confirmDelete,
@@ -5,9 +5,8 @@
5
5
 
6
6
  import { useListPage, ListPage, useOrchestrator } from '../../index.js'
7
7
  import Column from 'primevue/column'
8
- import Tag from 'primevue/tag'
9
- import Chip from 'primevue/chip'
10
8
  import Message from 'primevue/message'
9
+ import Chip from 'primevue/chip'
11
10
 
12
11
  // ============ LIST BUILDER ============
13
12
  const list = useListPage({ entity: 'roles' })
@@ -50,7 +49,7 @@ const canPersist = manager?.canPersist ?? false
50
49
  <Column field="name" header="Role" sortable style="width: 25%">
51
50
  <template #body="{ data }">
52
51
  <div>
53
- <code class="role-name">{{ data.name }}</code>
52
+ <span class="p-role-label">{{ data.name }}</span>
54
53
  <div v-if="data.label && data.label !== data.name" class="text-sm text-color-secondary mt-1">
55
54
  {{ data.label }}
56
55
  </div>
@@ -60,13 +59,12 @@ const canPersist = manager?.canPersist ?? false
60
59
 
61
60
  <Column header="Inherits" style="width: 20%">
62
61
  <template #body="{ data }">
63
- <div v-if="data.inherits?.length" class="flex flex-wrap gap-1">
64
- <Tag
62
+ <div v-if="data.inherits?.length" class="p-role-labels">
63
+ <span
65
64
  v-for="parent in data.inherits"
66
65
  :key="parent"
67
- :value="parent"
68
- severity="secondary"
69
- />
66
+ class="p-role-label"
67
+ >{{ parent }}</span>
70
68
  </div>
71
69
  <span v-else class="text-color-secondary">-</span>
72
70
  </template>
@@ -74,17 +72,16 @@ const canPersist = manager?.canPersist ?? false
74
72
 
75
73
  <Column header="Permissions" style="width: 40%">
76
74
  <template #body="{ data }">
77
- <div v-if="data.permissions?.length" class="flex flex-wrap gap-1">
75
+ <div v-if="data.permissions?.length" class="chips-container">
78
76
  <Chip
79
77
  v-for="perm in data.permissions.slice(0, 5)"
80
78
  :key="perm"
81
79
  :label="perm"
82
- class="text-xs"
83
80
  />
84
81
  <Chip
85
82
  v-if="data.permissions.length > 5"
86
- :label="`+${data.permissions.length - 5} more`"
87
- class="text-xs"
83
+ :label="`+${data.permissions.length - 5}`"
84
+ class="chip-more"
88
85
  />
89
86
  </div>
90
87
  <span v-else class="text-color-secondary">No permissions</span>
@@ -95,12 +92,13 @@ const canPersist = manager?.canPersist ?? false
95
92
  </template>
96
93
 
97
94
  <style scoped>
98
- .role-name {
99
- padding: 0.2rem 0.4rem;
100
- background: var(--p-surface-100);
101
- border-radius: 4px;
102
- font-size: 0.875rem;
103
- color: var(--p-primary-color);
104
- font-weight: 500;
95
+ .chips-container {
96
+ display: flex;
97
+ flex-wrap: wrap;
98
+ gap: 0.25rem;
99
+ }
100
+
101
+ .chip-more {
102
+ border-style: dashed;
105
103
  }
106
104
  </style>
@@ -277,7 +277,7 @@
277
277
  background: transparent !important;
278
278
  border: 1px solid var(--p-primary-500) !important;
279
279
  color: var(--p-primary-500) !important;
280
- font-weight: $font-weight-medium;
280
+ font-weight: normal !important;
281
281
  white-space: nowrap;
282
282
  }
283
283
 
@@ -156,3 +156,70 @@
156
156
  font-size: $font-size-xs;
157
157
  }
158
158
  }
159
+
160
+ // =============================================================================
161
+ // Role & Permission Labels
162
+ // =============================================================================
163
+
164
+ .p-role-label,
165
+ .p-permission-label {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ background: transparent;
169
+ font-size: $font-size-xs;
170
+ font-weight: normal !important;
171
+ padding: 0.25rem 0.75rem;
172
+ border-radius: var(--p-tag-border-radius, 6px);
173
+ white-space: nowrap;
174
+
175
+ &--pill {
176
+ border-radius: 999px;
177
+ }
178
+
179
+ &--more {
180
+ border-style: dashed;
181
+ }
182
+ }
183
+
184
+ .p-role-label {
185
+ border: 1px solid var(--p-blue-500);
186
+ color: var(--p-blue-600);
187
+ }
188
+
189
+ .p-permission-label {
190
+ border: 1px solid var(--p-surface-400);
191
+ color: var(--p-surface-600);
192
+ }
193
+
194
+ .p-role-labels,
195
+ .p-permission-labels {
196
+ display: flex;
197
+ flex-wrap: wrap;
198
+ gap: $space-xs;
199
+ }
200
+
201
+ // =============================================================================
202
+ // Chip Override (thin border style)
203
+ // =============================================================================
204
+
205
+ .p-chip {
206
+ background: none !important;
207
+ border: 1px solid var(--p-surface-300) !important;
208
+ color: var(--p-surface-600) !important;
209
+ font-size: $font-size-xs !important;
210
+ padding: 0.2rem 0.6rem !important;
211
+ }
212
+
213
+ // =============================================================================
214
+ // Mobile Row Tap
215
+ // =============================================================================
216
+
217
+ .datatable-row-clickable {
218
+ .p-datatable-tbody > tr {
219
+ cursor: pointer;
220
+
221
+ &:active {
222
+ background: var(--p-primary-50) !important;
223
+ }
224
+ }
225
+ }
@@ -56,7 +56,6 @@ textarea::placeholder,
56
56
  .page-content {
57
57
  flex: 1;
58
58
  padding: $space-lg;
59
- overflow-y: auto;
60
59
  }
61
60
 
62
61
  // =============================================================================