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 +1 -1
- package/src/components/layout/AppLayout.vue +1 -1
- package/src/components/lists/ListPage.vue +13 -2
- package/src/composables/useListPage.js +80 -3
- package/src/security/pages/RoleList.vue +17 -19
- package/src/styles/_buttons.scss +1 -1
- package/src/styles/_lists.scss +67 -0
- package/src/styles/_main.scss +0 -1
package/package.json
CHANGED
|
@@ -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
|
-
<
|
|
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="
|
|
64
|
-
<
|
|
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
|
-
|
|
68
|
-
|
|
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="
|
|
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}
|
|
87
|
-
class="
|
|
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
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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>
|
package/src/styles/_buttons.scss
CHANGED
package/src/styles/_lists.scss
CHANGED
|
@@ -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
|
+
}
|