qdadm 0.13.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/CHANGELOG.md +270 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/package.json +48 -0
- package/src/assets/logo.svg +6 -0
- package/src/components/BoolCell.vue +28 -0
- package/src/components/dialogs/BulkStatusDialog.vue +43 -0
- package/src/components/dialogs/MultiStepDialog.vue +321 -0
- package/src/components/dialogs/SimpleDialog.vue +108 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
- package/src/components/display/CardsGrid.vue +155 -0
- package/src/components/display/CopyableId.vue +92 -0
- package/src/components/display/EmptyState.vue +114 -0
- package/src/components/display/IntensityBar.vue +171 -0
- package/src/components/display/RichCardsGrid.vue +220 -0
- package/src/components/editors/JsonEditorFoldable.vue +467 -0
- package/src/components/editors/JsonStructuredField.vue +218 -0
- package/src/components/editors/JsonViewer.vue +91 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +245 -0
- package/src/components/editors/ScopeEditor.vue +341 -0
- package/src/components/editors/VanillaJsonEditor.vue +185 -0
- package/src/components/forms/FormActions.vue +104 -0
- package/src/components/forms/FormField.vue +64 -0
- package/src/components/forms/FormTab.vue +217 -0
- package/src/components/forms/FormTabs.vue +108 -0
- package/src/components/index.js +44 -0
- package/src/components/layout/AppLayout.vue +430 -0
- package/src/components/layout/Breadcrumb.vue +106 -0
- package/src/components/layout/PageHeader.vue +75 -0
- package/src/components/layout/PageLayout.vue +93 -0
- package/src/components/lists/ActionButtons.vue +41 -0
- package/src/components/lists/ActionColumn.vue +37 -0
- package/src/components/lists/FilterBar.vue +53 -0
- package/src/components/lists/ListPage.vue +319 -0
- package/src/composables/index.js +19 -0
- package/src/composables/useApp.js +43 -0
- package/src/composables/useAuth.js +49 -0
- package/src/composables/useBareForm.js +143 -0
- package/src/composables/useBreadcrumb.js +221 -0
- package/src/composables/useDirtyState.js +103 -0
- package/src/composables/useEntityTitle.js +121 -0
- package/src/composables/useForm.js +254 -0
- package/src/composables/useGuardStore.js +37 -0
- package/src/composables/useJsonSyntax.js +101 -0
- package/src/composables/useListPageBuilder.js +1176 -0
- package/src/composables/useNavigation.js +89 -0
- package/src/composables/usePageBuilder.js +334 -0
- package/src/composables/useStatus.js +146 -0
- package/src/composables/useSubEditor.js +165 -0
- package/src/composables/useTabSync.js +110 -0
- package/src/composables/useUnsavedChangesGuard.js +122 -0
- package/src/entity/EntityManager.js +540 -0
- package/src/entity/index.js +11 -0
- package/src/entity/storage/ApiStorage.js +146 -0
- package/src/entity/storage/LocalStorage.js +220 -0
- package/src/entity/storage/MemoryStorage.js +201 -0
- package/src/entity/storage/index.js +10 -0
- package/src/index.js +29 -0
- package/src/kernel/Kernel.js +234 -0
- package/src/kernel/index.js +7 -0
- package/src/module/index.js +16 -0
- package/src/module/moduleRegistry.js +222 -0
- package/src/orchestrator/Orchestrator.js +141 -0
- package/src/orchestrator/index.js +8 -0
- package/src/orchestrator/useOrchestrator.js +61 -0
- package/src/plugin.js +142 -0
- package/src/styles/_alerts.css +48 -0
- package/src/styles/_code.css +33 -0
- package/src/styles/_dialogs.css +17 -0
- package/src/styles/_markdown.css +82 -0
- package/src/styles/_show-pages.css +84 -0
- package/src/styles/index.css +16 -0
- package/src/styles/main.css +845 -0
- package/src/styles/theme/components.css +286 -0
- package/src/styles/theme/index.css +10 -0
- package/src/styles/theme/tokens.css +125 -0
- package/src/styles/theme/utilities.css +172 -0
- package/src/utils/debugInjector.js +261 -0
- package/src/utils/formatters.js +165 -0
- package/src/utils/index.js +35 -0
- package/src/utils/transformers.js +105 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* ActionButtons - Renders action buttons from useTableActions
|
|
4
|
+
*
|
|
5
|
+
* Props:
|
|
6
|
+
* - actions: Array of action objects from getActions(row)
|
|
7
|
+
*
|
|
8
|
+
* Each action object should have:
|
|
9
|
+
* - icon: PrimeVue icon class
|
|
10
|
+
* - tooltip: Tooltip text
|
|
11
|
+
* - severity: Button severity
|
|
12
|
+
* - handler: Click handler function
|
|
13
|
+
* - isDisabled: Boolean disabled state
|
|
14
|
+
*/
|
|
15
|
+
import Button from 'primevue/button'
|
|
16
|
+
|
|
17
|
+
defineProps({
|
|
18
|
+
actions: {
|
|
19
|
+
type: Array,
|
|
20
|
+
required: true
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<div class="table-actions">
|
|
27
|
+
<Button
|
|
28
|
+
v-for="action in actions"
|
|
29
|
+
:key="action.name"
|
|
30
|
+
:icon="action.icon"
|
|
31
|
+
:severity="action.severity"
|
|
32
|
+
text
|
|
33
|
+
rounded
|
|
34
|
+
:disabled="action.isDisabled"
|
|
35
|
+
@click="action.handler"
|
|
36
|
+
v-tooltip.top="action.tooltip"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<!-- Styles in main.css: .table-actions -->
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* ActionColumn - Reusable table action buttons (edit, delete)
|
|
4
|
+
*
|
|
5
|
+
* Slots:
|
|
6
|
+
* - default: Extra actions (after edit/delete)
|
|
7
|
+
*
|
|
8
|
+
* Emits:
|
|
9
|
+
* - edit: When edit button is clicked
|
|
10
|
+
* - delete: When delete button is clicked
|
|
11
|
+
*/
|
|
12
|
+
import Button from 'primevue/button'
|
|
13
|
+
|
|
14
|
+
defineEmits(['edit', 'delete'])
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="table-actions">
|
|
19
|
+
<Button
|
|
20
|
+
icon="pi pi-pencil"
|
|
21
|
+
severity="secondary"
|
|
22
|
+
text
|
|
23
|
+
rounded
|
|
24
|
+
@click="$emit('edit')"
|
|
25
|
+
v-tooltip.top="'Edit'"
|
|
26
|
+
/>
|
|
27
|
+
<Button
|
|
28
|
+
icon="pi pi-trash"
|
|
29
|
+
severity="danger"
|
|
30
|
+
text
|
|
31
|
+
rounded
|
|
32
|
+
@click="$emit('delete')"
|
|
33
|
+
v-tooltip.top="'Delete'"
|
|
34
|
+
/>
|
|
35
|
+
<slot ></slot>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* FilterBar - Reusable filter bar with search field
|
|
4
|
+
*
|
|
5
|
+
* Props:
|
|
6
|
+
* - search: Current search value (v-model compatible)
|
|
7
|
+
* - placeholder: Search input placeholder
|
|
8
|
+
*
|
|
9
|
+
* Slots:
|
|
10
|
+
* - default: Custom filters (left side, before search)
|
|
11
|
+
*
|
|
12
|
+
* Emits:
|
|
13
|
+
* - update:search: When search value changes
|
|
14
|
+
*/
|
|
15
|
+
import { computed } from 'vue'
|
|
16
|
+
import InputText from 'primevue/inputtext'
|
|
17
|
+
import InputIcon from 'primevue/inputicon'
|
|
18
|
+
import IconField from 'primevue/iconfield'
|
|
19
|
+
|
|
20
|
+
const props = defineProps({
|
|
21
|
+
modelValue: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: ''
|
|
24
|
+
},
|
|
25
|
+
placeholder: {
|
|
26
|
+
type: String,
|
|
27
|
+
default: 'Search...'
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits(['update:modelValue'])
|
|
32
|
+
|
|
33
|
+
const searchModel = computed({
|
|
34
|
+
get: () => props.modelValue,
|
|
35
|
+
set: (value) => emit('update:modelValue', value)
|
|
36
|
+
})
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="filter-bar">
|
|
41
|
+
<div class="filter-bar-left">
|
|
42
|
+
<IconField>
|
|
43
|
+
<InputIcon class="pi pi-search" />
|
|
44
|
+
<InputText
|
|
45
|
+
v-model="searchModel"
|
|
46
|
+
:placeholder="placeholder"
|
|
47
|
+
:class="{ 'filter-active': searchModel }"
|
|
48
|
+
/>
|
|
49
|
+
</IconField>
|
|
50
|
+
<slot ></slot>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* ListPage - Unified list page component
|
|
4
|
+
*
|
|
5
|
+
* Renders a complete CRUD list page with:
|
|
6
|
+
* - PageHeader with title and create button
|
|
7
|
+
* - CardsGrid for stats/custom cards
|
|
8
|
+
* - FilterBar with search and custom filters
|
|
9
|
+
* - DataTable with actions column
|
|
10
|
+
*
|
|
11
|
+
* Props come from useListPageBuilder composable
|
|
12
|
+
*
|
|
13
|
+
* Filter types:
|
|
14
|
+
* - 'select' (default): Standard dropdown
|
|
15
|
+
* - 'autocomplete': Searchable dropdown with type-ahead
|
|
16
|
+
*/
|
|
17
|
+
import { computed, ref, watch } from 'vue'
|
|
18
|
+
import PageHeader from '../layout/PageHeader.vue'
|
|
19
|
+
import CardsGrid from '../display/CardsGrid.vue'
|
|
20
|
+
import FilterBar from './FilterBar.vue'
|
|
21
|
+
import ActionButtons from './ActionButtons.vue'
|
|
22
|
+
import DataTable from 'primevue/datatable'
|
|
23
|
+
import Column from 'primevue/column'
|
|
24
|
+
import Button from 'primevue/button'
|
|
25
|
+
import Select from 'primevue/select'
|
|
26
|
+
import AutoComplete from 'primevue/autocomplete'
|
|
27
|
+
|
|
28
|
+
const props = defineProps({
|
|
29
|
+
// Header
|
|
30
|
+
title: { type: String, required: true },
|
|
31
|
+
subtitle: { type: String, default: null },
|
|
32
|
+
breadcrumb: { type: Array, default: null },
|
|
33
|
+
headerActions: { type: Array, default: () => [] },
|
|
34
|
+
|
|
35
|
+
// Cards
|
|
36
|
+
cards: { type: Array, default: () => [] },
|
|
37
|
+
cardsColumns: { type: [Number, String], default: 'auto' },
|
|
38
|
+
|
|
39
|
+
// Table data
|
|
40
|
+
items: { type: Array, required: true },
|
|
41
|
+
loading: { type: Boolean, default: false },
|
|
42
|
+
dataKey: { type: String, default: 'id' },
|
|
43
|
+
|
|
44
|
+
// Selection
|
|
45
|
+
selected: { type: Array, default: () => [] },
|
|
46
|
+
selectable: { type: Boolean, default: false },
|
|
47
|
+
|
|
48
|
+
// Pagination
|
|
49
|
+
paginator: { type: Boolean, default: true },
|
|
50
|
+
rows: { type: Number, default: 10 },
|
|
51
|
+
rowsPerPageOptions: { type: Array, default: () => [10, 50, 100] },
|
|
52
|
+
totalRecords: { type: Number, default: 0 },
|
|
53
|
+
lazy: { type: Boolean, default: false },
|
|
54
|
+
|
|
55
|
+
// Sorting
|
|
56
|
+
sortField: { type: String, default: null },
|
|
57
|
+
sortOrder: { type: Number, default: 1 },
|
|
58
|
+
|
|
59
|
+
// Search
|
|
60
|
+
searchQuery: { type: String, default: '' },
|
|
61
|
+
searchPlaceholder: { type: String, default: 'Search...' },
|
|
62
|
+
|
|
63
|
+
// Filters
|
|
64
|
+
filters: { type: Array, default: () => [] },
|
|
65
|
+
filterValues: { type: Object, default: () => ({}) },
|
|
66
|
+
|
|
67
|
+
// Row Actions
|
|
68
|
+
getActions: { type: Function, default: null },
|
|
69
|
+
actionsWidth: { type: String, default: '120px' }
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
function resolveLabel(label, _action) {
|
|
73
|
+
return typeof label === 'function' ? label({ selectionCount: props.selected?.length || 0 }) : label
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Local copy of filterValues for proper v-model binding
|
|
77
|
+
// PrimeVue Select needs v-model to fire change events reliably
|
|
78
|
+
const localFilterValues = ref({})
|
|
79
|
+
|
|
80
|
+
// Autocomplete suggestions per filter (keyed by filter name)
|
|
81
|
+
const autocompleteSuggestions = ref({})
|
|
82
|
+
|
|
83
|
+
// For autocomplete filters, we store the selected option object (not just value)
|
|
84
|
+
// to display the label properly. This maps filter.name -> selected option object
|
|
85
|
+
const autocompleteSelected = ref({})
|
|
86
|
+
|
|
87
|
+
// Sync from prop to local (when parent updates)
|
|
88
|
+
watch(() => props.filterValues, (newVal) => {
|
|
89
|
+
localFilterValues.value = { ...newVal }
|
|
90
|
+
// Sync autocomplete selections - find matching option by value
|
|
91
|
+
for (const filter of props.filters) {
|
|
92
|
+
if (filter.type === 'autocomplete' && newVal[filter.name] != null) {
|
|
93
|
+
const option = filter.options?.find(opt =>
|
|
94
|
+
(opt.value ?? opt) === newVal[filter.name]
|
|
95
|
+
)
|
|
96
|
+
if (option) {
|
|
97
|
+
autocompleteSelected.value[filter.name] = option
|
|
98
|
+
}
|
|
99
|
+
} else if (filter.type === 'autocomplete' && newVal[filter.name] == null) {
|
|
100
|
+
autocompleteSelected.value[filter.name] = null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, { immediate: true, deep: true })
|
|
104
|
+
|
|
105
|
+
// Check if any filter or search has a non-null value
|
|
106
|
+
const hasActiveFilters = computed(() => {
|
|
107
|
+
const hasFilters = Object.values(props.filterValues).some(v => v !== null && v !== undefined && v !== '')
|
|
108
|
+
const hasSearch = props.searchQuery && props.searchQuery.trim() !== ''
|
|
109
|
+
return hasFilters || hasSearch
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const emit = defineEmits([
|
|
113
|
+
'update:selected',
|
|
114
|
+
'update:searchQuery',
|
|
115
|
+
'update:filterValues',
|
|
116
|
+
'page',
|
|
117
|
+
'sort'
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
function clearAllFilters() {
|
|
121
|
+
// Build empty filter values object
|
|
122
|
+
const cleared = {}
|
|
123
|
+
for (const key of Object.keys(localFilterValues.value)) {
|
|
124
|
+
cleared[key] = null
|
|
125
|
+
}
|
|
126
|
+
// Update local ref first (for immediate UI feedback)
|
|
127
|
+
localFilterValues.value = cleared
|
|
128
|
+
// Clear autocomplete selections too
|
|
129
|
+
autocompleteSelected.value = {}
|
|
130
|
+
// Then emit to parent
|
|
131
|
+
emit('update:filterValues', cleared)
|
|
132
|
+
// Also clear search
|
|
133
|
+
emit('update:searchQuery', '')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function onSelectionChange(value) {
|
|
137
|
+
emit('update:selected', value)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function onSearchChange(value) {
|
|
141
|
+
emit('update:searchQuery', value)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function onFilterChange(_name) {
|
|
145
|
+
emit('update:filterValues', { ...localFilterValues.value })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Handle autocomplete search/filter
|
|
150
|
+
* @param {Object} event - PrimeVue autocomplete event with query property
|
|
151
|
+
* @param {Object} filter - Filter definition from props.filters
|
|
152
|
+
*/
|
|
153
|
+
function onAutocompleteSearch(event, filter) {
|
|
154
|
+
const query = (event.query || '').toLowerCase()
|
|
155
|
+
const labelField = filter.optionLabel || 'label'
|
|
156
|
+
|
|
157
|
+
if (!query) {
|
|
158
|
+
// Show all options when query is empty
|
|
159
|
+
autocompleteSuggestions.value[filter.name] = [...(filter.options || [])]
|
|
160
|
+
} else {
|
|
161
|
+
// Filter options by label
|
|
162
|
+
autocompleteSuggestions.value[filter.name] = (filter.options || []).filter(opt => {
|
|
163
|
+
const label = typeof opt === 'string' ? opt : (opt[labelField] || '')
|
|
164
|
+
return label.toLowerCase().includes(query)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle autocomplete selection
|
|
171
|
+
* @param {Object} event - PrimeVue event with value property (selected option)
|
|
172
|
+
* @param {Object} filter - Filter definition
|
|
173
|
+
*/
|
|
174
|
+
function onAutocompleteSelect(event, filter) {
|
|
175
|
+
const selected = event.value
|
|
176
|
+
const valueField = filter.optionValue || 'value'
|
|
177
|
+
|
|
178
|
+
// Store the selected option for display
|
|
179
|
+
autocompleteSelected.value[filter.name] = selected
|
|
180
|
+
|
|
181
|
+
// Extract the actual value to send to API
|
|
182
|
+
const value = selected != null
|
|
183
|
+
? (typeof selected === 'string' ? selected : selected[valueField])
|
|
184
|
+
: null
|
|
185
|
+
|
|
186
|
+
localFilterValues.value[filter.name] = value
|
|
187
|
+
emit('update:filterValues', { ...localFilterValues.value })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handle autocomplete clear (user clears the input)
|
|
192
|
+
*/
|
|
193
|
+
function onAutocompleteClear(filter) {
|
|
194
|
+
autocompleteSelected.value[filter.name] = null
|
|
195
|
+
localFilterValues.value[filter.name] = null
|
|
196
|
+
emit('update:filterValues', { ...localFilterValues.value })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function onPage(event) {
|
|
200
|
+
emit('page', event)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function onSort(event) {
|
|
204
|
+
emit('sort', event)
|
|
205
|
+
}
|
|
206
|
+
</script>
|
|
207
|
+
|
|
208
|
+
<template>
|
|
209
|
+
<div>
|
|
210
|
+
<PageHeader :title="title" :subtitle="subtitle" :breadcrumb="breadcrumb">
|
|
211
|
+
<template #actions>
|
|
212
|
+
<slot name="header-actions" ></slot>
|
|
213
|
+
<Button
|
|
214
|
+
v-for="action in headerActions"
|
|
215
|
+
:key="action.name"
|
|
216
|
+
:label="resolveLabel(action.label)"
|
|
217
|
+
:icon="action.icon"
|
|
218
|
+
:severity="action.severity"
|
|
219
|
+
:loading="action.isLoading"
|
|
220
|
+
@click="action.onClick"
|
|
221
|
+
/>
|
|
222
|
+
</template>
|
|
223
|
+
</PageHeader>
|
|
224
|
+
|
|
225
|
+
<!-- Cards Zone -->
|
|
226
|
+
<CardsGrid :cards="cards" :columns="cardsColumns">
|
|
227
|
+
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
|
|
228
|
+
<slot :name="slotName" v-bind="slotProps" ></slot>
|
|
229
|
+
</template>
|
|
230
|
+
</CardsGrid>
|
|
231
|
+
|
|
232
|
+
<!-- Before Table Slot -->
|
|
233
|
+
<slot name="beforeTable" ></slot>
|
|
234
|
+
|
|
235
|
+
<div class="card">
|
|
236
|
+
<!-- Filter Bar -->
|
|
237
|
+
<FilterBar
|
|
238
|
+
:modelValue="searchQuery"
|
|
239
|
+
@update:modelValue="onSearchChange"
|
|
240
|
+
:placeholder="searchPlaceholder"
|
|
241
|
+
>
|
|
242
|
+
<template v-for="filter in filters" :key="filter.name">
|
|
243
|
+
<!-- Autocomplete filter -->
|
|
244
|
+
<AutoComplete
|
|
245
|
+
v-if="filter.type === 'autocomplete'"
|
|
246
|
+
v-model="autocompleteSelected[filter.name]"
|
|
247
|
+
:suggestions="autocompleteSuggestions[filter.name] || []"
|
|
248
|
+
@complete="onAutocompleteSearch($event, filter)"
|
|
249
|
+
@item-select="onAutocompleteSelect($event, filter)"
|
|
250
|
+
@clear="onAutocompleteClear(filter)"
|
|
251
|
+
:optionLabel="filter.optionLabel || 'label'"
|
|
252
|
+
:placeholder="filter.placeholder"
|
|
253
|
+
:dropdown="true"
|
|
254
|
+
:minLength="0"
|
|
255
|
+
:style="{ minWidth: filter.width || '160px' }"
|
|
256
|
+
:class="{ 'filter-active': localFilterValues[filter.name] != null && localFilterValues[filter.name] !== '' }"
|
|
257
|
+
:inputClass="'filter-autocomplete-input'"
|
|
258
|
+
/>
|
|
259
|
+
<!-- Standard select filter -->
|
|
260
|
+
<Select
|
|
261
|
+
v-else
|
|
262
|
+
v-model="localFilterValues[filter.name]"
|
|
263
|
+
@update:modelValue="onFilterChange(filter.name)"
|
|
264
|
+
:options="filter.options"
|
|
265
|
+
:optionLabel="filter.optionLabel || 'label'"
|
|
266
|
+
:optionValue="filter.optionValue || 'value'"
|
|
267
|
+
:placeholder="filter.placeholder"
|
|
268
|
+
:style="{ minWidth: filter.width || '160px' }"
|
|
269
|
+
:class="{ 'filter-active': localFilterValues[filter.name] != null && localFilterValues[filter.name] !== '' }"
|
|
270
|
+
/>
|
|
271
|
+
</template>
|
|
272
|
+
<slot name="filters" ></slot>
|
|
273
|
+
<Button
|
|
274
|
+
v-if="hasActiveFilters"
|
|
275
|
+
icon="pi pi-filter-slash"
|
|
276
|
+
severity="secondary"
|
|
277
|
+
text
|
|
278
|
+
rounded
|
|
279
|
+
size="small"
|
|
280
|
+
@click="clearAllFilters"
|
|
281
|
+
v-tooltip.top="'Clear filters'"
|
|
282
|
+
/>
|
|
283
|
+
</FilterBar>
|
|
284
|
+
|
|
285
|
+
<!-- Data Table -->
|
|
286
|
+
<DataTable
|
|
287
|
+
:value="items"
|
|
288
|
+
:loading="loading"
|
|
289
|
+
:dataKey="dataKey"
|
|
290
|
+
:paginator="paginator"
|
|
291
|
+
:rows="rows"
|
|
292
|
+
:rowsPerPageOptions="rowsPerPageOptions"
|
|
293
|
+
:totalRecords="totalRecords"
|
|
294
|
+
:lazy="lazy"
|
|
295
|
+
:sortField="sortField"
|
|
296
|
+
:sortOrder="sortOrder"
|
|
297
|
+
:selection="selected"
|
|
298
|
+
@update:selection="onSelectionChange"
|
|
299
|
+
@page="onPage"
|
|
300
|
+
@sort="onSort"
|
|
301
|
+
stripedRows
|
|
302
|
+
removableSort
|
|
303
|
+
>
|
|
304
|
+
<!-- Columns from slot -->
|
|
305
|
+
<slot name="columns" ></slot>
|
|
306
|
+
|
|
307
|
+
<!-- Actions column -->
|
|
308
|
+
<Column v-if="getActions" header="Actions" :style="{ width: actionsWidth }">
|
|
309
|
+
<template #body="{ data }">
|
|
310
|
+
<ActionButtons :actions="getActions(data)" />
|
|
311
|
+
</template>
|
|
312
|
+
</Column>
|
|
313
|
+
|
|
314
|
+
<!-- Selection column -->
|
|
315
|
+
<Column v-if="selectable" selectionMode="multiple" headerStyle="width: 3rem" />
|
|
316
|
+
</DataTable>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qdadm - Composables exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { useBareForm } from './useBareForm'
|
|
6
|
+
export { useBreadcrumb } from './useBreadcrumb'
|
|
7
|
+
export { useDirtyState } from './useDirtyState'
|
|
8
|
+
export { useEntityTitle } from './useEntityTitle'
|
|
9
|
+
export { useForm } from './useForm'
|
|
10
|
+
export * from './useJsonSyntax'
|
|
11
|
+
export { useListPageBuilder, PAGE_SIZE_OPTIONS } from './useListPageBuilder'
|
|
12
|
+
export { usePageBuilder } from './usePageBuilder'
|
|
13
|
+
export { useSubEditor } from './useSubEditor'
|
|
14
|
+
export { useTabSync } from './useTabSync'
|
|
15
|
+
export { useApp } from './useApp'
|
|
16
|
+
export { useAuth } from './useAuth'
|
|
17
|
+
export { useNavigation } from './useNavigation'
|
|
18
|
+
export { useStatus } from './useStatus'
|
|
19
|
+
export { useUnsavedChangesGuard } from './useUnsavedChangesGuard'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useApp - Access app branding and configuration
|
|
3
|
+
*
|
|
4
|
+
* Provides access to app config set via createQdadm bootstrap.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { name, version, logo } = useApp()
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { inject, computed } from 'vue'
|
|
11
|
+
|
|
12
|
+
export function useApp() {
|
|
13
|
+
const appConfig = inject('qdadmApp')
|
|
14
|
+
|
|
15
|
+
if (!appConfig) {
|
|
16
|
+
console.warn('[qdadm] qdadmApp not provided. Using defaults.')
|
|
17
|
+
return {
|
|
18
|
+
name: 'Admin',
|
|
19
|
+
shortName: 'Admin',
|
|
20
|
+
logo: null,
|
|
21
|
+
logoSmall: null,
|
|
22
|
+
version: null,
|
|
23
|
+
theme: {}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Version can be string or callback
|
|
28
|
+
const version = computed(() => {
|
|
29
|
+
if (typeof appConfig.version === 'function') {
|
|
30
|
+
return appConfig.version()
|
|
31
|
+
}
|
|
32
|
+
return appConfig.version
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
name: appConfig.name || 'Admin',
|
|
37
|
+
shortName: appConfig.shortName || appConfig.name || 'Admin',
|
|
38
|
+
logo: appConfig.logo,
|
|
39
|
+
logoSmall: appConfig.logoSmall,
|
|
40
|
+
version,
|
|
41
|
+
theme: appConfig.theme || {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAuth - Access authentication state
|
|
3
|
+
*
|
|
4
|
+
* Provides access to authAdapter set via createQdadm bootstrap.
|
|
5
|
+
* Returns neutral values if auth is disabled.
|
|
6
|
+
*
|
|
7
|
+
* Note: Permission checking (canRead/canWrite) is handled by EntityManager,
|
|
8
|
+
* not by useAuth. This keeps auth simple and delegates permission logic
|
|
9
|
+
* to where it belongs (the entity layer).
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const { isAuthenticated, user, logout } = useAuth()
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { inject, computed, ref } from 'vue'
|
|
16
|
+
|
|
17
|
+
export function useAuth() {
|
|
18
|
+
const auth = inject('authAdapter')
|
|
19
|
+
const features = inject('qdadmFeatures')
|
|
20
|
+
|
|
21
|
+
// If auth disabled or not provided, return neutral values
|
|
22
|
+
if (!features?.auth || !auth) {
|
|
23
|
+
return {
|
|
24
|
+
login: () => Promise.resolve({ token: null, user: null }),
|
|
25
|
+
logout: () => {},
|
|
26
|
+
getCurrentUser: () => Promise.resolve(null),
|
|
27
|
+
isAuthenticated: computed(() => true), // Always "authenticated"
|
|
28
|
+
user: ref(null),
|
|
29
|
+
authEnabled: false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Reactive user state
|
|
34
|
+
const user = computed(() => {
|
|
35
|
+
if (typeof auth.getUser === 'function') {
|
|
36
|
+
return auth.getUser()
|
|
37
|
+
}
|
|
38
|
+
return auth.user || null
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
login: auth.login,
|
|
43
|
+
logout: auth.logout,
|
|
44
|
+
getCurrentUser: auth.getCurrentUser,
|
|
45
|
+
isAuthenticated: computed(() => auth.isAuthenticated()),
|
|
46
|
+
user,
|
|
47
|
+
authEnabled: true
|
|
48
|
+
}
|
|
49
|
+
}
|