qdadm 0.16.0 → 0.17.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/README.md +153 -1
- package/package.json +15 -2
- package/src/components/forms/FormField.vue +64 -6
- package/src/components/forms/FormPage.vue +276 -0
- package/src/components/index.js +11 -0
- package/src/components/layout/BaseLayout.vue +183 -0
- package/src/components/layout/DashboardLayout.vue +100 -0
- package/src/components/layout/FormLayout.vue +261 -0
- package/src/components/layout/ListLayout.vue +334 -0
- package/src/components/layout/Zone.vue +165 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +140 -0
- package/src/components/layout/defaults/DefaultFooter.vue +56 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +53 -0
- package/src/components/layout/defaults/DefaultHeader.vue +69 -0
- package/src/components/layout/defaults/DefaultMenu.vue +197 -0
- package/src/components/layout/defaults/DefaultPagination.vue +79 -0
- package/src/components/layout/defaults/DefaultTable.vue +130 -0
- package/src/components/layout/defaults/DefaultToaster.vue +16 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +96 -0
- package/src/components/layout/defaults/index.js +17 -0
- package/src/composables/index.js +6 -6
- package/src/composables/useForm.js +135 -0
- package/src/composables/useFormPageBuilder.js +1154 -0
- package/src/composables/useHooks.js +53 -0
- package/src/composables/useLayoutResolver.js +260 -0
- package/src/composables/useListPageBuilder.js +336 -52
- package/src/composables/useNavigation.js +38 -2
- package/src/composables/useSignals.js +49 -0
- package/src/composables/useZoneRegistry.js +162 -0
- package/src/core/bundles.js +406 -0
- package/src/core/decorator.js +322 -0
- package/src/core/extension.js +386 -0
- package/src/core/index.js +28 -0
- package/src/entity/EntityManager.js +314 -16
- package/src/entity/auth/AuthAdapter.js +125 -0
- package/src/entity/auth/PermissiveAdapter.js +64 -0
- package/src/entity/auth/index.js +11 -0
- package/src/entity/index.js +3 -0
- package/src/entity/storage/MockApiStorage.js +349 -0
- package/src/entity/storage/SdkStorage.js +478 -0
- package/src/entity/storage/index.js +2 -0
- package/src/hooks/HookRegistry.js +411 -0
- package/src/hooks/index.js +12 -0
- package/src/index.js +9 -0
- package/src/kernel/Kernel.js +136 -4
- package/src/kernel/SignalBus.js +180 -0
- package/src/kernel/index.js +7 -0
- package/src/module/moduleRegistry.js +124 -6
- package/src/orchestrator/Orchestrator.js +73 -1
- package/src/zones/ZoneRegistry.js +821 -0
- package/src/zones/index.js +16 -0
- package/src/zones/zones.js +189 -0
- package/src/composables/useEntityTitle.js +0 -121
- package/src/composables/useManager.js +0 -20
- package/src/composables/usePageBuilder.js +0 -334
- package/src/composables/useStatus.js +0 -146
- package/src/composables/useSubEditor.js +0 -165
- package/src/composables/useTabSync.js +0 -110
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* DefaultFormActions - Default component for the form actions zone
|
|
4
|
+
*
|
|
5
|
+
* Renders save/cancel buttons with loading states, using the form state
|
|
6
|
+
* provided by FormLayout via Vue injection.
|
|
7
|
+
*
|
|
8
|
+
* This component is rendered by Zone when no blocks are registered
|
|
9
|
+
* in the 'actions' zone and no slot override is provided.
|
|
10
|
+
*
|
|
11
|
+
* Uses FormActions component for consistent button rendering.
|
|
12
|
+
*
|
|
13
|
+
* Inject:
|
|
14
|
+
* - qdadmFormState: { loading, saving, dirty, isEdit }
|
|
15
|
+
* - qdadmFormEmit: { save, saveAndClose, cancel, delete }
|
|
16
|
+
*/
|
|
17
|
+
import { inject, computed } from 'vue'
|
|
18
|
+
import FormActions from '../../forms/FormActions.vue'
|
|
19
|
+
|
|
20
|
+
// Inject form state from FormLayout
|
|
21
|
+
const formState = inject('qdadmFormState', null)
|
|
22
|
+
const formEmit = inject('qdadmFormEmit', null)
|
|
23
|
+
|
|
24
|
+
// Fallback values if not injected (for standalone testing)
|
|
25
|
+
const isEdit = computed(() => formState?.value?.isEdit ?? false)
|
|
26
|
+
const saving = computed(() => formState?.value?.saving ?? false)
|
|
27
|
+
const dirty = computed(() => formState?.value?.dirty ?? true)
|
|
28
|
+
|
|
29
|
+
// Event handlers
|
|
30
|
+
function onSave() {
|
|
31
|
+
formEmit?.save()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function onSaveAndClose() {
|
|
35
|
+
formEmit?.saveAndClose()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function onCancel() {
|
|
39
|
+
formEmit?.cancel()
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<FormActions
|
|
45
|
+
:isEdit="isEdit"
|
|
46
|
+
:saving="saving"
|
|
47
|
+
:dirty="dirty"
|
|
48
|
+
:showSaveAndClose="true"
|
|
49
|
+
@save="onSave"
|
|
50
|
+
@saveAndClose="onSaveAndClose"
|
|
51
|
+
@cancel="onCancel"
|
|
52
|
+
/>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* DefaultHeader - Default header component for BaseLayout
|
|
4
|
+
*
|
|
5
|
+
* Displays app branding (logo or name) and version badge.
|
|
6
|
+
* Extracted from AppLayout for zone-based architecture.
|
|
7
|
+
*
|
|
8
|
+
* This is the default component rendered in the "header" zone
|
|
9
|
+
* when no blocks are registered.
|
|
10
|
+
*/
|
|
11
|
+
import { inject, computed } from 'vue'
|
|
12
|
+
|
|
13
|
+
const features = inject('qdadmFeatures', { poweredBy: true })
|
|
14
|
+
|
|
15
|
+
// App config from useApp (injected by createQdadm plugin)
|
|
16
|
+
const app = inject('qdadmApp', {
|
|
17
|
+
name: 'Admin',
|
|
18
|
+
shortName: 'adm',
|
|
19
|
+
version: null,
|
|
20
|
+
logo: null
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="default-header">
|
|
26
|
+
<div class="header-branding">
|
|
27
|
+
<img v-if="app.logo" :src="app.logo" :alt="app.name" class="header-logo" />
|
|
28
|
+
<h1 v-else class="header-title">{{ app.name }}</h1>
|
|
29
|
+
</div>
|
|
30
|
+
<span v-if="app.version" class="header-version">v{{ app.version }}</span>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.default-header {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
align-items: flex-start;
|
|
39
|
+
gap: 0.5rem;
|
|
40
|
+
padding: 1.5rem;
|
|
41
|
+
border-bottom: 1px solid var(--p-surface-700, #334155);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.header-branding {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 0.5rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.header-title {
|
|
51
|
+
font-size: 1.25rem;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
margin: 0;
|
|
54
|
+
color: var(--p-surface-0, white);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.header-logo {
|
|
58
|
+
max-height: 32px;
|
|
59
|
+
max-width: 160px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.header-version {
|
|
63
|
+
font-size: 0.625rem;
|
|
64
|
+
color: var(--p-surface-400, #94a3b8);
|
|
65
|
+
background: var(--p-surface-700, #334155);
|
|
66
|
+
padding: 0.125rem 0.375rem;
|
|
67
|
+
border-radius: 0.25rem;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* DefaultMenu - Default navigation menu for BaseLayout
|
|
4
|
+
*
|
|
5
|
+
* Displays sidebar navigation with collapsible sections.
|
|
6
|
+
* Navigation items are auto-built from moduleRegistry.
|
|
7
|
+
* Extracted from AppLayout for zone-based architecture.
|
|
8
|
+
*
|
|
9
|
+
* This is the default component rendered in the "menu" zone
|
|
10
|
+
* when no blocks are registered.
|
|
11
|
+
*/
|
|
12
|
+
import { ref, watch, onMounted, computed, inject } from 'vue'
|
|
13
|
+
import { RouterLink, useRouter, useRoute } from 'vue-router'
|
|
14
|
+
import { useNavigation } from '../../../composables/useNavigation'
|
|
15
|
+
|
|
16
|
+
const router = useRouter()
|
|
17
|
+
const route = useRoute()
|
|
18
|
+
const { navSections, isNavActive, sectionHasActiveItem, handleNavClick } = useNavigation()
|
|
19
|
+
|
|
20
|
+
// App config for storage key namespacing
|
|
21
|
+
const app = inject('qdadmApp', { shortName: 'qdadm' })
|
|
22
|
+
|
|
23
|
+
// LocalStorage key for collapsed sections state
|
|
24
|
+
const STORAGE_KEY = computed(() => `${app.shortName.toLowerCase()}_nav_collapsed`)
|
|
25
|
+
|
|
26
|
+
// Collapsed sections state (section title -> boolean)
|
|
27
|
+
const collapsedSections = ref({})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load collapsed state from localStorage
|
|
31
|
+
*/
|
|
32
|
+
function loadCollapsedState() {
|
|
33
|
+
try {
|
|
34
|
+
const stored = localStorage.getItem(STORAGE_KEY.value)
|
|
35
|
+
if (stored) {
|
|
36
|
+
collapsedSections.value = JSON.parse(stored)
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.warn('Failed to load nav state:', e)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Save collapsed state to localStorage
|
|
45
|
+
*/
|
|
46
|
+
function saveCollapsedState() {
|
|
47
|
+
try {
|
|
48
|
+
localStorage.setItem(STORAGE_KEY.value, JSON.stringify(collapsedSections.value))
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.warn('Failed to save nav state:', e)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Toggle section collapsed state
|
|
56
|
+
*/
|
|
57
|
+
function toggleSection(sectionTitle) {
|
|
58
|
+
collapsedSections.value[sectionTitle] = !collapsedSections.value[sectionTitle]
|
|
59
|
+
saveCollapsedState()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if section should be shown expanded
|
|
64
|
+
* - Never collapsed if it contains active item
|
|
65
|
+
* - Otherwise respect user preference
|
|
66
|
+
*/
|
|
67
|
+
function isSectionExpanded(section) {
|
|
68
|
+
if (sectionHasActiveItem(section)) {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
return !collapsedSections.value[section.title]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Load state on mount
|
|
75
|
+
onMounted(() => {
|
|
76
|
+
loadCollapsedState()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Auto-expand section when navigating to an item in it
|
|
80
|
+
watch(() => route?.path, () => {
|
|
81
|
+
if (!route) return
|
|
82
|
+
for (const section of navSections.value) {
|
|
83
|
+
if (sectionHasActiveItem(section) && collapsedSections.value[section.title]) {
|
|
84
|
+
// Auto-expand but don't save (user can collapse again if they want)
|
|
85
|
+
collapsedSections.value[section.title] = false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<nav class="default-menu">
|
|
93
|
+
<div v-for="section in navSections" :key="section.title" class="nav-section">
|
|
94
|
+
<div
|
|
95
|
+
class="nav-section-title"
|
|
96
|
+
:class="{ 'nav-section-active': sectionHasActiveItem(section) }"
|
|
97
|
+
@click="toggleSection(section.title)"
|
|
98
|
+
>
|
|
99
|
+
<span>{{ section.title }}</span>
|
|
100
|
+
<i
|
|
101
|
+
class="nav-section-chevron pi"
|
|
102
|
+
:class="isSectionExpanded(section) ? 'pi-chevron-down' : 'pi-chevron-right'"
|
|
103
|
+
></i>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="nav-section-items" :class="{ 'nav-section-collapsed': !isSectionExpanded(section) }">
|
|
106
|
+
<RouterLink
|
|
107
|
+
v-for="item in section.items"
|
|
108
|
+
:key="item.route"
|
|
109
|
+
:to="{ name: item.route }"
|
|
110
|
+
class="nav-item"
|
|
111
|
+
:class="{ 'nav-item-active': isNavActive(item) }"
|
|
112
|
+
@click="handleNavClick($event, item)"
|
|
113
|
+
>
|
|
114
|
+
<i :class="item.icon"></i>
|
|
115
|
+
<span>{{ item.label }}</span>
|
|
116
|
+
</RouterLink>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</nav>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<style scoped>
|
|
123
|
+
.default-menu {
|
|
124
|
+
flex: 1;
|
|
125
|
+
overflow-y: auto;
|
|
126
|
+
padding: 1rem 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.nav-section {
|
|
130
|
+
margin-bottom: 0.5rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.nav-section-title {
|
|
134
|
+
padding: 0.5rem 1.5rem;
|
|
135
|
+
font-size: 0.75rem;
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
text-transform: uppercase;
|
|
138
|
+
letter-spacing: 0.05em;
|
|
139
|
+
color: var(--p-surface-400, #94a3b8);
|
|
140
|
+
display: flex;
|
|
141
|
+
justify-content: space-between;
|
|
142
|
+
align-items: center;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
user-select: none;
|
|
145
|
+
transition: color 0.15s;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.nav-section-title:hover {
|
|
149
|
+
color: var(--p-surface-200, #e2e8f0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.nav-section-active {
|
|
153
|
+
color: var(--p-primary-400, #60a5fa);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.nav-section-chevron {
|
|
157
|
+
font-size: 0.625rem;
|
|
158
|
+
transition: transform 0.2s;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.nav-section-items {
|
|
162
|
+
max-height: 500px;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
transition: max-height 0.2s ease-out, opacity 0.15s;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.nav-section-collapsed {
|
|
168
|
+
max-height: 0;
|
|
169
|
+
opacity: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.nav-item {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 0.75rem;
|
|
176
|
+
padding: 0.625rem 1.5rem;
|
|
177
|
+
color: var(--p-surface-300, #cbd5e1);
|
|
178
|
+
text-decoration: none;
|
|
179
|
+
transition: all 0.15s;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.nav-item:hover {
|
|
183
|
+
background: var(--p-surface-700, #334155);
|
|
184
|
+
color: var(--p-surface-0, white);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.nav-item-active {
|
|
188
|
+
background: var(--p-primary-600, #2563eb);
|
|
189
|
+
color: var(--p-surface-0, white);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.nav-item i {
|
|
193
|
+
font-size: 1rem;
|
|
194
|
+
width: 1.25rem;
|
|
195
|
+
text-align: center;
|
|
196
|
+
}
|
|
197
|
+
</style>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* DefaultPagination - Default pagination component for ListLayout
|
|
4
|
+
*
|
|
5
|
+
* Renders a PrimeVue Paginator with standard configuration.
|
|
6
|
+
* Used as the default for the "pagination" zone in ListLayout.
|
|
7
|
+
*
|
|
8
|
+
* When `lazy` is false and pagination is handled by DataTable,
|
|
9
|
+
* this component is typically not shown (DataTable has its own paginator).
|
|
10
|
+
*
|
|
11
|
+
* This is useful for:
|
|
12
|
+
* - Separate pagination controls outside the table
|
|
13
|
+
* - Custom pagination layouts
|
|
14
|
+
* - Server-side (lazy) pagination with separate paginator
|
|
15
|
+
*/
|
|
16
|
+
import Paginator from 'primevue/paginator'
|
|
17
|
+
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
/**
|
|
20
|
+
* First record index (0-based)
|
|
21
|
+
*/
|
|
22
|
+
first: {
|
|
23
|
+
type: Number,
|
|
24
|
+
default: 0
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* Number of rows per page
|
|
28
|
+
*/
|
|
29
|
+
rows: {
|
|
30
|
+
type: Number,
|
|
31
|
+
default: 10
|
|
32
|
+
},
|
|
33
|
+
/**
|
|
34
|
+
* Total number of records
|
|
35
|
+
*/
|
|
36
|
+
totalRecords: {
|
|
37
|
+
type: Number,
|
|
38
|
+
default: 0
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* Options for rows per page dropdown
|
|
42
|
+
*/
|
|
43
|
+
rowsPerPageOptions: {
|
|
44
|
+
type: Array,
|
|
45
|
+
default: () => [10, 25, 50, 100]
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* Template for the paginator layout
|
|
49
|
+
*/
|
|
50
|
+
template: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown'
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits(['page'])
|
|
57
|
+
|
|
58
|
+
function onPage(event) {
|
|
59
|
+
emit('page', event)
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<Paginator
|
|
65
|
+
:first="first"
|
|
66
|
+
:rows="rows"
|
|
67
|
+
:totalRecords="totalRecords"
|
|
68
|
+
:rowsPerPageOptions="rowsPerPageOptions"
|
|
69
|
+
:template="template"
|
|
70
|
+
@page="onPage"
|
|
71
|
+
class="default-pagination"
|
|
72
|
+
/>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<style scoped>
|
|
76
|
+
.default-pagination {
|
|
77
|
+
margin-top: 1rem;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* DefaultTable - Default table component for ListLayout
|
|
4
|
+
*
|
|
5
|
+
* Renders a PrimeVue DataTable with standard configuration.
|
|
6
|
+
* Used as the default for the "table" zone in ListLayout.
|
|
7
|
+
*
|
|
8
|
+
* Accepts props from the list page builder for data binding.
|
|
9
|
+
* Supports slots for column definitions.
|
|
10
|
+
*
|
|
11
|
+
* This is a minimal wrapper that expects:
|
|
12
|
+
* - items: Array of data to display
|
|
13
|
+
* - loading: Boolean loading state
|
|
14
|
+
* - Columns via default slot
|
|
15
|
+
*/
|
|
16
|
+
import { computed, useSlots } from 'vue'
|
|
17
|
+
import DataTable from 'primevue/datatable'
|
|
18
|
+
import Column from 'primevue/column'
|
|
19
|
+
|
|
20
|
+
const props = defineProps({
|
|
21
|
+
/**
|
|
22
|
+
* Data items to display in the table
|
|
23
|
+
*/
|
|
24
|
+
items: {
|
|
25
|
+
type: Array,
|
|
26
|
+
default: () => []
|
|
27
|
+
},
|
|
28
|
+
/**
|
|
29
|
+
* Loading state
|
|
30
|
+
*/
|
|
31
|
+
loading: {
|
|
32
|
+
type: Boolean,
|
|
33
|
+
default: false
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Field to use as unique key for rows
|
|
37
|
+
*/
|
|
38
|
+
dataKey: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: 'id'
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Current selection (for selectable tables)
|
|
44
|
+
*/
|
|
45
|
+
selection: {
|
|
46
|
+
type: Array,
|
|
47
|
+
default: undefined
|
|
48
|
+
},
|
|
49
|
+
/**
|
|
50
|
+
* Enable row selection
|
|
51
|
+
*/
|
|
52
|
+
selectable: {
|
|
53
|
+
type: Boolean,
|
|
54
|
+
default: false
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
57
|
+
* Enable striped rows
|
|
58
|
+
*/
|
|
59
|
+
stripedRows: {
|
|
60
|
+
type: Boolean,
|
|
61
|
+
default: true
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Enable removable sort
|
|
65
|
+
*/
|
|
66
|
+
removableSort: {
|
|
67
|
+
type: Boolean,
|
|
68
|
+
default: true
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* Current sort field
|
|
72
|
+
*/
|
|
73
|
+
sortField: {
|
|
74
|
+
type: String,
|
|
75
|
+
default: null
|
|
76
|
+
},
|
|
77
|
+
/**
|
|
78
|
+
* Current sort order (1 = asc, -1 = desc)
|
|
79
|
+
*/
|
|
80
|
+
sortOrder: {
|
|
81
|
+
type: Number,
|
|
82
|
+
default: 1
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const emit = defineEmits(['update:selection', 'sort'])
|
|
87
|
+
|
|
88
|
+
const slots = useSlots()
|
|
89
|
+
const hasDefaultSlot = computed(() => !!slots.default)
|
|
90
|
+
|
|
91
|
+
function onSelectionChange(value) {
|
|
92
|
+
emit('update:selection', value)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function onSort(event) {
|
|
96
|
+
emit('sort', event)
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<template>
|
|
101
|
+
<DataTable
|
|
102
|
+
:value="items"
|
|
103
|
+
:loading="loading"
|
|
104
|
+
:dataKey="dataKey"
|
|
105
|
+
:selection="selection"
|
|
106
|
+
:sortField="sortField"
|
|
107
|
+
:sortOrder="sortOrder"
|
|
108
|
+
:stripedRows="stripedRows"
|
|
109
|
+
:removableSort="removableSort"
|
|
110
|
+
@update:selection="onSelectionChange"
|
|
111
|
+
@sort="onSort"
|
|
112
|
+
class="default-table"
|
|
113
|
+
>
|
|
114
|
+
<!-- Columns from slot -->
|
|
115
|
+
<slot />
|
|
116
|
+
|
|
117
|
+
<!-- Selection column if selectable -->
|
|
118
|
+
<Column
|
|
119
|
+
v-if="selectable && !hasDefaultSlot"
|
|
120
|
+
selectionMode="multiple"
|
|
121
|
+
headerStyle="width: 3rem"
|
|
122
|
+
/>
|
|
123
|
+
</DataTable>
|
|
124
|
+
</template>
|
|
125
|
+
|
|
126
|
+
<style scoped>
|
|
127
|
+
.default-table {
|
|
128
|
+
width: 100%;
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* DefaultToaster - Default toaster component for BaseLayout
|
|
4
|
+
*
|
|
5
|
+
* Renders the Toast component for notifications.
|
|
6
|
+
* Uses PrimeVue Toast with default positioning.
|
|
7
|
+
*
|
|
8
|
+
* This is the default component rendered in the "toaster" zone
|
|
9
|
+
* when no blocks are registered.
|
|
10
|
+
*/
|
|
11
|
+
import Toast from 'primevue/toast'
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<Toast />
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* DefaultUserInfo - Default user info component for sidebar
|
|
4
|
+
*
|
|
5
|
+
* Displays user avatar, name, role, and logout button.
|
|
6
|
+
* Extracted from AppLayout for zone-based architecture.
|
|
7
|
+
*
|
|
8
|
+
* This component is used when auth is enabled.
|
|
9
|
+
*/
|
|
10
|
+
import { computed, inject } from 'vue'
|
|
11
|
+
import { useRouter } from 'vue-router'
|
|
12
|
+
import { useAuth } from '../../../composables/useAuth'
|
|
13
|
+
import Button from 'primevue/button'
|
|
14
|
+
|
|
15
|
+
const router = useRouter()
|
|
16
|
+
const { isAuthenticated, user, logout, authEnabled } = useAuth()
|
|
17
|
+
|
|
18
|
+
const userInitials = computed(() => {
|
|
19
|
+
const username = user.value?.username
|
|
20
|
+
if (!username) return '?'
|
|
21
|
+
return username.substring(0, 2).toUpperCase()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const userDisplayName = computed(() => {
|
|
25
|
+
return user.value?.username || user.value?.name || 'User'
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const userSubtitle = computed(() => {
|
|
29
|
+
return user.value?.email || user.value?.role || ''
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function handleLogout() {
|
|
33
|
+
logout()
|
|
34
|
+
router.push({ name: 'login' })
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div v-if="authEnabled" class="default-user-info">
|
|
40
|
+
<div class="user-avatar">{{ userInitials }}</div>
|
|
41
|
+
<div class="user-details">
|
|
42
|
+
<div class="user-name">{{ userDisplayName }}</div>
|
|
43
|
+
<div class="user-role">{{ userSubtitle }}</div>
|
|
44
|
+
</div>
|
|
45
|
+
<Button
|
|
46
|
+
icon="pi pi-sign-out"
|
|
47
|
+
severity="secondary"
|
|
48
|
+
text
|
|
49
|
+
rounded
|
|
50
|
+
@click="handleLogout"
|
|
51
|
+
v-tooltip.top="'Logout'"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<style scoped>
|
|
57
|
+
.default-user-info {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 0.75rem;
|
|
61
|
+
padding: 1rem 1.5rem;
|
|
62
|
+
border-top: 1px solid var(--p-surface-700, #334155);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.user-avatar {
|
|
66
|
+
width: 36px;
|
|
67
|
+
height: 36px;
|
|
68
|
+
border-radius: 50%;
|
|
69
|
+
background: var(--p-primary-600, #2563eb);
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
font-size: 0.875rem;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
color: var(--p-surface-0, white);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.user-details {
|
|
79
|
+
flex: 1;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.user-name {
|
|
84
|
+
font-weight: 500;
|
|
85
|
+
font-size: 0.875rem;
|
|
86
|
+
color: var(--p-surface-0, white);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.user-role {
|
|
90
|
+
font-size: 0.75rem;
|
|
91
|
+
color: var(--p-surface-400, #94a3b8);
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
text-overflow: ellipsis;
|
|
94
|
+
white-space: nowrap;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default zone components for layouts
|
|
3
|
+
*
|
|
4
|
+
* These components are rendered when no blocks are registered
|
|
5
|
+
* in their respective zones.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// BaseLayout defaults
|
|
9
|
+
export { default as DefaultHeader } from './DefaultHeader.vue'
|
|
10
|
+
export { default as DefaultMenu } from './DefaultMenu.vue'
|
|
11
|
+
export { default as DefaultFooter } from './DefaultFooter.vue'
|
|
12
|
+
export { default as DefaultUserInfo } from './DefaultUserInfo.vue'
|
|
13
|
+
export { default as DefaultBreadcrumb } from './DefaultBreadcrumb.vue'
|
|
14
|
+
export { default as DefaultToaster } from './DefaultToaster.vue'
|
|
15
|
+
|
|
16
|
+
// FormLayout defaults
|
|
17
|
+
export { default as DefaultFormActions } from './DefaultFormActions.vue'
|
package/src/composables/index.js
CHANGED
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
export { useBareForm } from './useBareForm'
|
|
6
6
|
export { useBreadcrumb } from './useBreadcrumb'
|
|
7
7
|
export { useDirtyState } from './useDirtyState'
|
|
8
|
-
export { useEntityTitle } from './useEntityTitle'
|
|
9
8
|
export { useForm } from './useForm'
|
|
9
|
+
export { useFormPageBuilder } from './useFormPageBuilder'
|
|
10
10
|
export * from './useJsonSyntax'
|
|
11
11
|
export { useListPageBuilder, PAGE_SIZE_OPTIONS } from './useListPageBuilder'
|
|
12
|
-
export { usePageBuilder } from './usePageBuilder'
|
|
13
12
|
export { usePageTitle } from './usePageTitle'
|
|
14
|
-
export { useSubEditor } from './useSubEditor'
|
|
15
|
-
export { useTabSync } from './useTabSync'
|
|
16
13
|
export { useApp } from './useApp'
|
|
17
14
|
export { useAuth } from './useAuth'
|
|
18
15
|
export { useNavContext } from './useNavContext'
|
|
19
16
|
export { useNavigation } from './useNavigation'
|
|
20
|
-
export { useStatus } from './useStatus'
|
|
21
17
|
export { useUnsavedChangesGuard } from './useUnsavedChangesGuard'
|
|
22
|
-
export {
|
|
18
|
+
export { useGuardDialog } from './useGuardStore'
|
|
19
|
+
export { useSignals } from './useSignals'
|
|
20
|
+
export { useZoneRegistry } from './useZoneRegistry'
|
|
21
|
+
export { useHooks } from './useHooks'
|
|
22
|
+
export { useLayoutResolver, createLayoutComponents, layoutMeta, LAYOUT_TYPES } from './useLayoutResolver'
|