qdadm 1.1.6 → 1.2.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/{forms → edit}/FormTab.vue +8 -15
- package/src/components/{forms → edit}/FormTabs.vue +21 -15
- package/src/components/index.ts +10 -7
- package/src/components/item/FieldGroups.vue +388 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +1 -1
- package/src/components/show/ShowPage.vue +88 -7
- package/src/composables/index.ts +14 -0
- package/src/composables/useEntityItemFormPage.ts +68 -141
- package/src/composables/useEntityItemShowPage.ts +52 -114
- package/src/composables/useFieldManager.ts +595 -0
- /package/src/components/{forms → edit}/FormActions.vue +0 -0
- /package/src/components/{forms → edit}/FormField.vue +0 -0
- /package/src/components/{forms → edit}/FormInput.vue +0 -0
- /package/src/components/{forms → edit}/FormPage.vue +0 -0
package/package.json
CHANGED
|
@@ -2,23 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* FormTab - Normalized tab header component
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* @deprecated Use FieldGroups with layout="tabs" instead. FieldGroups now supports
|
|
6
|
+
* icon, badge, count, visible, and disabled options on groups.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - icon: PrimeIcon class (e.g., 'pi-cog')
|
|
11
|
-
* - count: Number to display as badge
|
|
12
|
-
* - badge: Custom badge text/value
|
|
13
|
-
* - badgeSeverity: Badge color ('secondary', 'info', 'success', 'warn', 'danger')
|
|
14
|
-
* - visible: Show/hide tab (default: true)
|
|
15
|
-
* - disabled: Disable tab interaction
|
|
8
|
+
* ```ts
|
|
9
|
+
* // Before (deprecated)
|
|
10
|
+
* <FormTab value="general" label="General" icon="pi-cog" :count="5" />
|
|
16
11
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* <FormTab value="errors" label="Errors" :count="errors.length" badge-severity="danger" />
|
|
21
|
-
* <FormTab value="advanced" label="Advanced" :visible="isEdit" />
|
|
12
|
+
* // After (recommended)
|
|
13
|
+
* form.group('general', ['field1'], { label: 'General', icon: 'cog', count: 5 })
|
|
14
|
+
* ```
|
|
22
15
|
*/
|
|
23
16
|
|
|
24
17
|
import { type PropType } from 'vue'
|
|
@@ -2,22 +2,28 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* FormTabs - Normalized tab container for forms
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* @deprecated Use FieldGroups with layout="tabs" instead. FieldGroups now supports
|
|
6
|
+
* icon, badge, count, visible, and disabled options on groups.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* <
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
8
|
+
* ```vue
|
|
9
|
+
* <!-- Before (deprecated) -->
|
|
10
|
+
* <FormTabs v-model="activeTab">
|
|
11
|
+
* <template #tabs>
|
|
12
|
+
* <FormTab value="general" label="General" icon="pi-cog" />
|
|
13
|
+
* </template>
|
|
14
|
+
* <template #panels>
|
|
15
|
+
* <TabPanel value="general">...</TabPanel>
|
|
16
|
+
* </template>
|
|
17
|
+
* </FormTabs>
|
|
18
|
+
*
|
|
19
|
+
* <!-- After (recommended) -->
|
|
20
|
+
* form.group('general', ['field1', 'field2'], { label: 'General', icon: 'cog' })
|
|
21
|
+
* <FieldGroups :groups="form.groups.value" layout="tabs">
|
|
22
|
+
* <template #field="{ field }">
|
|
23
|
+
* <FormField :field="field" />
|
|
24
|
+
* </template>
|
|
25
|
+
* </FieldGroups>
|
|
26
|
+
* ```
|
|
21
27
|
*/
|
|
22
28
|
|
|
23
29
|
import Tabs from 'primevue/tabs'
|
package/src/components/index.ts
CHANGED
|
@@ -19,13 +19,16 @@ export { default as DefaultFooter } from './layout/defaults/DefaultFooter.vue'
|
|
|
19
19
|
export { default as DefaultUserInfo } from './layout/defaults/DefaultUserInfo.vue'
|
|
20
20
|
export { default as DefaultBreadcrumb } from './layout/defaults/DefaultBreadcrumb.vue'
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
export { default as
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export { default as
|
|
27
|
-
export { default as
|
|
28
|
-
export { default as
|
|
22
|
+
// Item (shared between edit and show)
|
|
23
|
+
export { default as FieldGroups } from './item/FieldGroups.vue'
|
|
24
|
+
|
|
25
|
+
// Edit (form pages)
|
|
26
|
+
export { default as FormPage } from './edit/FormPage.vue'
|
|
27
|
+
export { default as FormField } from './edit/FormField.vue'
|
|
28
|
+
export { default as FormInput } from './edit/FormInput.vue'
|
|
29
|
+
export { default as FormActions } from './edit/FormActions.vue'
|
|
30
|
+
export { default as FormTabs } from './edit/FormTabs.vue' // Deprecated: use FieldGroups with layout="tabs"
|
|
31
|
+
export { default as FormTab } from './edit/FormTab.vue' // Deprecated: use FieldGroups with layout="tabs"
|
|
29
32
|
|
|
30
33
|
// Show (read-only detail pages)
|
|
31
34
|
export { default as ShowPage } from './show/ShowPage.vue'
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FieldGroups - Generic field group renderer with configurable layouts
|
|
4
|
+
*
|
|
5
|
+
* Shared component for both edit (form) and show pages.
|
|
6
|
+
* Uses slots for field rendering to support different field components.
|
|
7
|
+
*
|
|
8
|
+
* Supports multiple layout modes:
|
|
9
|
+
* - flat: Simple sections with headers
|
|
10
|
+
* - sections: Fieldset-style sections
|
|
11
|
+
* - cards: Each group in a card
|
|
12
|
+
* - tabs: TabView for top-level groups
|
|
13
|
+
* - accordion: Collapsible panels
|
|
14
|
+
*
|
|
15
|
+
* ```vue
|
|
16
|
+
* <FieldGroups :groups="groups" :data="data" layout="tabs">
|
|
17
|
+
* <template #field="{ field, value }">
|
|
18
|
+
* <FormField :field="field" v-model="data[field.name]" />
|
|
19
|
+
* </template>
|
|
20
|
+
* </FieldGroups>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { computed, type PropType } from 'vue'
|
|
24
|
+
import Tabs from 'primevue/tabs'
|
|
25
|
+
import TabList from 'primevue/tablist'
|
|
26
|
+
import Tab from 'primevue/tab'
|
|
27
|
+
import TabPanels from 'primevue/tabpanels'
|
|
28
|
+
import TabPanel from 'primevue/tabpanel'
|
|
29
|
+
import Accordion from 'primevue/accordion'
|
|
30
|
+
import AccordionPanel from 'primevue/accordionpanel'
|
|
31
|
+
import AccordionHeader from 'primevue/accordionheader'
|
|
32
|
+
import AccordionContent from 'primevue/accordioncontent'
|
|
33
|
+
import Fieldset from 'primevue/fieldset'
|
|
34
|
+
import Card from 'primevue/card'
|
|
35
|
+
import Tag from 'primevue/tag'
|
|
36
|
+
|
|
37
|
+
type BadgeSeverity = 'secondary' | 'info' | 'success' | 'warn' | 'danger' | 'contrast'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Base field config interface
|
|
41
|
+
*/
|
|
42
|
+
interface BaseFieldConfig {
|
|
43
|
+
name: string
|
|
44
|
+
type: string
|
|
45
|
+
label: string
|
|
46
|
+
[key: string]: unknown
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Field group structure
|
|
51
|
+
*/
|
|
52
|
+
interface FieldGroup<T extends BaseFieldConfig = BaseFieldConfig> {
|
|
53
|
+
name: string
|
|
54
|
+
label: string
|
|
55
|
+
fields: T[]
|
|
56
|
+
children: FieldGroup<T>[]
|
|
57
|
+
parent?: string
|
|
58
|
+
// Tab/accordion options
|
|
59
|
+
icon?: string
|
|
60
|
+
badge?: string | number
|
|
61
|
+
badgeSeverity?: BadgeSeverity
|
|
62
|
+
count?: number
|
|
63
|
+
visible?: boolean
|
|
64
|
+
disabled?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type LayoutMode = 'flat' | 'sections' | 'cards' | 'tabs' | 'accordion'
|
|
68
|
+
|
|
69
|
+
const props = defineProps({
|
|
70
|
+
groups: {
|
|
71
|
+
type: Array as PropType<FieldGroup[]>,
|
|
72
|
+
required: true,
|
|
73
|
+
},
|
|
74
|
+
data: {
|
|
75
|
+
type: Object as PropType<Record<string, unknown> | null>,
|
|
76
|
+
default: null,
|
|
77
|
+
},
|
|
78
|
+
layout: {
|
|
79
|
+
type: String as PropType<LayoutMode>,
|
|
80
|
+
default: 'flat',
|
|
81
|
+
},
|
|
82
|
+
// Nested layout (for children groups)
|
|
83
|
+
childLayout: {
|
|
84
|
+
type: String as PropType<LayoutMode>,
|
|
85
|
+
default: 'sections',
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Expose slots for field rendering
|
|
90
|
+
defineSlots<{
|
|
91
|
+
field: (props: { field: BaseFieldConfig; value: unknown; groupName: string }) => unknown
|
|
92
|
+
// Optional: custom group header
|
|
93
|
+
'group-header'?: (props: { group: FieldGroup; layout: LayoutMode }) => unknown
|
|
94
|
+
// Optional: custom group content wrapper
|
|
95
|
+
'group-content'?: (props: { group: FieldGroup; layout: LayoutMode }) => unknown
|
|
96
|
+
}>()
|
|
97
|
+
|
|
98
|
+
// Filter out default group for labeling purposes and hidden groups
|
|
99
|
+
const displayGroups = computed(() => {
|
|
100
|
+
return props.groups.filter((g) => {
|
|
101
|
+
// Filter by visibility (default to true)
|
|
102
|
+
if (g.visible === false) return false
|
|
103
|
+
// Filter out empty default groups
|
|
104
|
+
return g.name !== '_default' || g.fields.length > 0
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Get value from data
|
|
109
|
+
function getValue(fieldName: string): unknown {
|
|
110
|
+
return props.data?.[fieldName]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Normalize icon class
|
|
114
|
+
function getIconClass(icon: string | undefined): string | null {
|
|
115
|
+
if (!icon) return null
|
|
116
|
+
// Already has 'pi' prefix
|
|
117
|
+
if (icon.startsWith('pi')) {
|
|
118
|
+
return icon.includes(' ') ? icon : `pi ${icon}`
|
|
119
|
+
}
|
|
120
|
+
// Just icon name
|
|
121
|
+
return `pi pi-${icon}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Get badge value to display
|
|
125
|
+
function getBadgeValue(group: FieldGroup): string | number | null {
|
|
126
|
+
if (group.badge !== undefined && group.badge !== null) return group.badge
|
|
127
|
+
if (group.count !== undefined && group.count !== null && group.count > 0) return group.count
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<template>
|
|
133
|
+
<div class="field-groups" :class="`field-groups--${layout}`">
|
|
134
|
+
<!-- Flat layout: simple sections with optional headers -->
|
|
135
|
+
<template v-if="layout === 'flat'">
|
|
136
|
+
<div v-for="group in displayGroups" :key="group.name" class="field-group">
|
|
137
|
+
<slot name="group-header" :group="group" :layout="layout">
|
|
138
|
+
<h3 v-if="group.label && group.name !== '_default'" class="field-group-header">
|
|
139
|
+
{{ group.label }}
|
|
140
|
+
</h3>
|
|
141
|
+
</slot>
|
|
142
|
+
<div class="field-group-fields">
|
|
143
|
+
<template v-for="field in group.fields" :key="field.name">
|
|
144
|
+
<slot
|
|
145
|
+
name="field"
|
|
146
|
+
:field="field"
|
|
147
|
+
:value="getValue(field.name)"
|
|
148
|
+
:group-name="group.name"
|
|
149
|
+
/>
|
|
150
|
+
</template>
|
|
151
|
+
</div>
|
|
152
|
+
<!-- Recursive children -->
|
|
153
|
+
<FieldGroups
|
|
154
|
+
v-if="group.children.length > 0"
|
|
155
|
+
:groups="group.children"
|
|
156
|
+
:data="data"
|
|
157
|
+
:layout="childLayout"
|
|
158
|
+
>
|
|
159
|
+
<template #field="slotProps">
|
|
160
|
+
<slot name="field" v-bind="slotProps" />
|
|
161
|
+
</template>
|
|
162
|
+
</FieldGroups>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<!-- Sections layout: Fieldset style -->
|
|
167
|
+
<template v-else-if="layout === 'sections'">
|
|
168
|
+
<Fieldset
|
|
169
|
+
v-for="group in displayGroups"
|
|
170
|
+
:key="group.name"
|
|
171
|
+
:legend="group.label || undefined"
|
|
172
|
+
:toggleable="false"
|
|
173
|
+
>
|
|
174
|
+
<div class="field-group-fields">
|
|
175
|
+
<template v-for="field in group.fields" :key="field.name">
|
|
176
|
+
<slot
|
|
177
|
+
name="field"
|
|
178
|
+
:field="field"
|
|
179
|
+
:value="getValue(field.name)"
|
|
180
|
+
:group-name="group.name"
|
|
181
|
+
/>
|
|
182
|
+
</template>
|
|
183
|
+
</div>
|
|
184
|
+
<!-- Recursive children -->
|
|
185
|
+
<FieldGroups
|
|
186
|
+
v-if="group.children.length > 0"
|
|
187
|
+
:groups="group.children"
|
|
188
|
+
:data="data"
|
|
189
|
+
:layout="childLayout"
|
|
190
|
+
>
|
|
191
|
+
<template #field="slotProps">
|
|
192
|
+
<slot name="field" v-bind="slotProps" />
|
|
193
|
+
</template>
|
|
194
|
+
</FieldGroups>
|
|
195
|
+
</Fieldset>
|
|
196
|
+
</template>
|
|
197
|
+
|
|
198
|
+
<!-- Cards layout -->
|
|
199
|
+
<template v-else-if="layout === 'cards'">
|
|
200
|
+
<Card v-for="group in displayGroups" :key="group.name">
|
|
201
|
+
<template #title>{{ group.label }}</template>
|
|
202
|
+
<template #content>
|
|
203
|
+
<div class="field-group-fields">
|
|
204
|
+
<template v-for="field in group.fields" :key="field.name">
|
|
205
|
+
<slot
|
|
206
|
+
name="field"
|
|
207
|
+
:field="field"
|
|
208
|
+
:value="getValue(field.name)"
|
|
209
|
+
:group-name="group.name"
|
|
210
|
+
/>
|
|
211
|
+
</template>
|
|
212
|
+
</div>
|
|
213
|
+
<!-- Recursive children -->
|
|
214
|
+
<FieldGroups
|
|
215
|
+
v-if="group.children.length > 0"
|
|
216
|
+
:groups="group.children"
|
|
217
|
+
:data="data"
|
|
218
|
+
:layout="childLayout"
|
|
219
|
+
>
|
|
220
|
+
<template #field="slotProps">
|
|
221
|
+
<slot name="field" v-bind="slotProps" />
|
|
222
|
+
</template>
|
|
223
|
+
</FieldGroups>
|
|
224
|
+
</template>
|
|
225
|
+
</Card>
|
|
226
|
+
</template>
|
|
227
|
+
|
|
228
|
+
<!-- Tabs layout -->
|
|
229
|
+
<template v-else-if="layout === 'tabs'">
|
|
230
|
+
<Tabs :value="displayGroups[0]?.name || '0'">
|
|
231
|
+
<TabList>
|
|
232
|
+
<Tab
|
|
233
|
+
v-for="group in displayGroups"
|
|
234
|
+
:key="group.name"
|
|
235
|
+
:value="group.name"
|
|
236
|
+
:disabled="group.disabled"
|
|
237
|
+
>
|
|
238
|
+
<i v-if="group.icon" :class="getIconClass(group.icon)" class="tab-icon" />
|
|
239
|
+
<span class="tab-label">{{ group.label }}</span>
|
|
240
|
+
<Tag
|
|
241
|
+
v-if="getBadgeValue(group) !== null"
|
|
242
|
+
:value="String(getBadgeValue(group))"
|
|
243
|
+
:severity="group.badgeSeverity || 'secondary'"
|
|
244
|
+
class="tab-badge"
|
|
245
|
+
/>
|
|
246
|
+
</Tab>
|
|
247
|
+
</TabList>
|
|
248
|
+
<TabPanels>
|
|
249
|
+
<TabPanel v-for="group in displayGroups" :key="group.name" :value="group.name">
|
|
250
|
+
<div class="field-group-fields">
|
|
251
|
+
<template v-for="field in group.fields" :key="field.name">
|
|
252
|
+
<slot
|
|
253
|
+
name="field"
|
|
254
|
+
:field="field"
|
|
255
|
+
:value="getValue(field.name)"
|
|
256
|
+
:group-name="group.name"
|
|
257
|
+
/>
|
|
258
|
+
</template>
|
|
259
|
+
</div>
|
|
260
|
+
<!-- Recursive children -->
|
|
261
|
+
<FieldGroups
|
|
262
|
+
v-if="group.children.length > 0"
|
|
263
|
+
:groups="group.children"
|
|
264
|
+
:data="data"
|
|
265
|
+
:layout="childLayout"
|
|
266
|
+
>
|
|
267
|
+
<template #field="slotProps">
|
|
268
|
+
<slot name="field" v-bind="slotProps" />
|
|
269
|
+
</template>
|
|
270
|
+
</FieldGroups>
|
|
271
|
+
</TabPanel>
|
|
272
|
+
</TabPanels>
|
|
273
|
+
</Tabs>
|
|
274
|
+
</template>
|
|
275
|
+
|
|
276
|
+
<!-- Accordion layout -->
|
|
277
|
+
<template v-else-if="layout === 'accordion'">
|
|
278
|
+
<Accordion :value="[displayGroups[0]?.name || '0']" multiple>
|
|
279
|
+
<AccordionPanel
|
|
280
|
+
v-for="group in displayGroups"
|
|
281
|
+
:key="group.name"
|
|
282
|
+
:value="group.name"
|
|
283
|
+
:disabled="group.disabled"
|
|
284
|
+
>
|
|
285
|
+
<AccordionHeader>
|
|
286
|
+
<i v-if="group.icon" :class="getIconClass(group.icon)" class="accordion-icon" />
|
|
287
|
+
<span class="accordion-label">{{ group.label }}</span>
|
|
288
|
+
<Tag
|
|
289
|
+
v-if="getBadgeValue(group) !== null"
|
|
290
|
+
:value="String(getBadgeValue(group))"
|
|
291
|
+
:severity="group.badgeSeverity || 'secondary'"
|
|
292
|
+
class="accordion-badge"
|
|
293
|
+
/>
|
|
294
|
+
</AccordionHeader>
|
|
295
|
+
<AccordionContent>
|
|
296
|
+
<div class="field-group-fields">
|
|
297
|
+
<template v-for="field in group.fields" :key="field.name">
|
|
298
|
+
<slot
|
|
299
|
+
name="field"
|
|
300
|
+
:field="field"
|
|
301
|
+
:value="getValue(field.name)"
|
|
302
|
+
:group-name="group.name"
|
|
303
|
+
/>
|
|
304
|
+
</template>
|
|
305
|
+
</div>
|
|
306
|
+
<!-- Recursive children -->
|
|
307
|
+
<FieldGroups
|
|
308
|
+
v-if="group.children.length > 0"
|
|
309
|
+
:groups="group.children"
|
|
310
|
+
:data="data"
|
|
311
|
+
:layout="childLayout"
|
|
312
|
+
>
|
|
313
|
+
<template #field="slotProps">
|
|
314
|
+
<slot name="field" v-bind="slotProps" />
|
|
315
|
+
</template>
|
|
316
|
+
</FieldGroups>
|
|
317
|
+
</AccordionContent>
|
|
318
|
+
</AccordionPanel>
|
|
319
|
+
</Accordion>
|
|
320
|
+
</template>
|
|
321
|
+
</div>
|
|
322
|
+
</template>
|
|
323
|
+
|
|
324
|
+
<style scoped>
|
|
325
|
+
.field-groups {
|
|
326
|
+
display: flex;
|
|
327
|
+
flex-direction: column;
|
|
328
|
+
gap: 1rem;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.field-group {
|
|
332
|
+
display: flex;
|
|
333
|
+
flex-direction: column;
|
|
334
|
+
gap: 0.5rem;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.field-group-header {
|
|
338
|
+
font-size: 1rem;
|
|
339
|
+
font-weight: 600;
|
|
340
|
+
margin: 0 0 0.5rem 0;
|
|
341
|
+
padding-bottom: 0.5rem;
|
|
342
|
+
border-bottom: 1px solid var(--p-surface-200, #e2e8f0);
|
|
343
|
+
color: var(--p-text-color);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.field-group-fields {
|
|
347
|
+
display: flex;
|
|
348
|
+
flex-direction: column;
|
|
349
|
+
gap: 0.75rem;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* Cards layout spacing */
|
|
353
|
+
.field-groups--cards {
|
|
354
|
+
gap: 1rem;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* Tabs and accordion panels padding */
|
|
358
|
+
.field-groups--tabs :deep(.p-tabpanel),
|
|
359
|
+
.field-groups--accordion :deep(.p-accordioncontent-content) {
|
|
360
|
+
padding-top: 1rem;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Tab icon and badge styling */
|
|
364
|
+
.tab-icon {
|
|
365
|
+
margin-right: 0.5rem;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.tab-badge {
|
|
369
|
+
margin-left: 0.5rem;
|
|
370
|
+
font-size: 0.7rem;
|
|
371
|
+
padding: 0.15rem 0.4rem;
|
|
372
|
+
min-width: 1.25rem;
|
|
373
|
+
text-align: center;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Accordion icon and badge styling */
|
|
377
|
+
.accordion-icon {
|
|
378
|
+
margin-right: 0.5rem;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.accordion-badge {
|
|
382
|
+
margin-left: 0.5rem;
|
|
383
|
+
font-size: 0.7rem;
|
|
384
|
+
padding: 0.15rem 0.4rem;
|
|
385
|
+
min-width: 1.25rem;
|
|
386
|
+
text-align: center;
|
|
387
|
+
}
|
|
388
|
+
</style>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - qdadmFormEmit: { save, saveAndClose, cancel, delete }
|
|
16
16
|
*/
|
|
17
17
|
import { inject, computed, type Ref } from 'vue'
|
|
18
|
-
import FormActions from '../../
|
|
18
|
+
import FormActions from '../../edit/FormActions.vue'
|
|
19
19
|
|
|
20
20
|
interface FormState {
|
|
21
21
|
isEdit?: boolean
|
|
@@ -42,6 +42,8 @@ import PageHeader from '../layout/PageHeader.vue'
|
|
|
42
42
|
import Card from 'primevue/card'
|
|
43
43
|
import Button from 'primevue/button'
|
|
44
44
|
import Message from 'primevue/message'
|
|
45
|
+
import FieldGroups from '../item/FieldGroups.vue'
|
|
46
|
+
import ShowField from './ShowField.vue'
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* Page title parts for PageHeader
|
|
@@ -83,6 +85,22 @@ interface FetchError {
|
|
|
83
85
|
detail?: string
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Field group structure
|
|
90
|
+
*/
|
|
91
|
+
interface FieldGroup {
|
|
92
|
+
name: string
|
|
93
|
+
label: string
|
|
94
|
+
fields: ResolvedFieldConfig[]
|
|
95
|
+
children: FieldGroup[]
|
|
96
|
+
parent?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Layout mode for groups
|
|
101
|
+
*/
|
|
102
|
+
type LayoutMode = 'flat' | 'sections' | 'cards' | 'tabs' | 'accordion'
|
|
103
|
+
|
|
86
104
|
const props = defineProps({
|
|
87
105
|
// State
|
|
88
106
|
loading: { type: Boolean, default: false },
|
|
@@ -94,6 +112,9 @@ const props = defineProps({
|
|
|
94
112
|
// Fields (for auto-rendering - optional, can use #fields slot instead)
|
|
95
113
|
fields: { type: Array as PropType<ResolvedFieldConfig[]>, default: () => [] },
|
|
96
114
|
|
|
115
|
+
// Groups (hierarchical field organization)
|
|
116
|
+
groups: { type: Array as PropType<FieldGroup[]>, default: () => [] },
|
|
117
|
+
|
|
97
118
|
// Data (entity data for field values)
|
|
98
119
|
data: { type: Object as PropType<Record<string, unknown> | null>, default: null },
|
|
99
120
|
|
|
@@ -109,7 +130,13 @@ const props = defineProps({
|
|
|
109
130
|
horizontalFields: { type: Boolean, default: true },
|
|
110
131
|
labelWidth: { type: String, default: '140px' },
|
|
111
132
|
// Media zone options
|
|
112
|
-
mediaWidth: { type: String, default: '200px' }
|
|
133
|
+
mediaWidth: { type: String, default: '200px' },
|
|
134
|
+
|
|
135
|
+
// Group layout mode: flat, sections, cards, tabs, accordion
|
|
136
|
+
layout: { type: String as PropType<LayoutMode>, default: 'flat' },
|
|
137
|
+
|
|
138
|
+
// Child group layout mode (for nested groups)
|
|
139
|
+
childLayout: { type: String as PropType<LayoutMode>, default: 'sections' }
|
|
113
140
|
})
|
|
114
141
|
|
|
115
142
|
// Check if media slot is used
|
|
@@ -119,11 +146,19 @@ const slots = defineSlots<{
|
|
|
119
146
|
'header-actions'?: () => unknown
|
|
120
147
|
media?: () => unknown
|
|
121
148
|
fields?: () => unknown
|
|
149
|
+
groups?: () => unknown
|
|
122
150
|
footer?: () => unknown
|
|
123
151
|
error?: (props: { error: unknown }) => unknown
|
|
124
152
|
loading?: () => unknown
|
|
125
153
|
}>()
|
|
126
154
|
|
|
155
|
+
// Determine if we should use group rendering
|
|
156
|
+
const useGroupLayout = computed(() => {
|
|
157
|
+
// Use groups if: layout is not flat, or groups are explicitly defined (not just _default)
|
|
158
|
+
const hasRealGroups = props.groups.length > 0 && !(props.groups.length === 1 && props.groups[0]?.name === '_default')
|
|
159
|
+
return hasRealGroups && props.layout !== 'flat'
|
|
160
|
+
})
|
|
161
|
+
|
|
127
162
|
const emit = defineEmits<{
|
|
128
163
|
(e: 'edit'): void
|
|
129
164
|
(e: 'delete'): void
|
|
@@ -207,9 +242,32 @@ const fetchErrorMessage = computed<string | null>(() => {
|
|
|
207
242
|
<slot name="media" />
|
|
208
243
|
</div>
|
|
209
244
|
|
|
210
|
-
<!-- Fields zone -->
|
|
211
|
-
<div class="show-fields" :class="{ 'show-fields--horizontal': horizontalFields }">
|
|
212
|
-
|
|
245
|
+
<!-- Fields/Groups zone -->
|
|
246
|
+
<div class="show-fields" :class="{ 'show-fields--horizontal': horizontalFields && !useGroupLayout }">
|
|
247
|
+
<!-- Group layout mode -->
|
|
248
|
+
<template v-if="useGroupLayout">
|
|
249
|
+
<slot name="groups">
|
|
250
|
+
<FieldGroups
|
|
251
|
+
:groups="groups"
|
|
252
|
+
:data="data"
|
|
253
|
+
:layout="layout"
|
|
254
|
+
:child-layout="childLayout"
|
|
255
|
+
>
|
|
256
|
+
<template #field="{ field, value }">
|
|
257
|
+
<ShowField
|
|
258
|
+
:field="field"
|
|
259
|
+
:value="value as string | number | boolean | Date | Record<string, unknown> | unknown[]"
|
|
260
|
+
:horizontal="horizontalFields"
|
|
261
|
+
:label-width="labelWidth"
|
|
262
|
+
/>
|
|
263
|
+
</template>
|
|
264
|
+
</FieldGroups>
|
|
265
|
+
</slot>
|
|
266
|
+
</template>
|
|
267
|
+
<!-- Flat fields mode (default) -->
|
|
268
|
+
<template v-else>
|
|
269
|
+
<slot name="fields" />
|
|
270
|
+
</template>
|
|
213
271
|
</div>
|
|
214
272
|
</div>
|
|
215
273
|
|
|
@@ -245,9 +303,32 @@ const fetchErrorMessage = computed<string | null>(() => {
|
|
|
245
303
|
<slot name="media" />
|
|
246
304
|
</div>
|
|
247
305
|
|
|
248
|
-
<!-- Fields zone -->
|
|
249
|
-
<div class="show-fields" :class="{ 'show-fields--horizontal': horizontalFields }">
|
|
250
|
-
|
|
306
|
+
<!-- Fields/Groups zone -->
|
|
307
|
+
<div class="show-fields" :class="{ 'show-fields--horizontal': horizontalFields && !useGroupLayout }">
|
|
308
|
+
<!-- Group layout mode -->
|
|
309
|
+
<template v-if="useGroupLayout">
|
|
310
|
+
<slot name="groups">
|
|
311
|
+
<FieldGroups
|
|
312
|
+
:groups="groups"
|
|
313
|
+
:data="data"
|
|
314
|
+
:layout="layout"
|
|
315
|
+
:child-layout="childLayout"
|
|
316
|
+
>
|
|
317
|
+
<template #field="{ field, value }">
|
|
318
|
+
<ShowField
|
|
319
|
+
:field="field"
|
|
320
|
+
:value="value as string | number | boolean | Date | Record<string, unknown> | unknown[]"
|
|
321
|
+
:horizontal="horizontalFields"
|
|
322
|
+
:label-width="labelWidth"
|
|
323
|
+
/>
|
|
324
|
+
</template>
|
|
325
|
+
</FieldGroups>
|
|
326
|
+
</slot>
|
|
327
|
+
</template>
|
|
328
|
+
<!-- Flat fields mode (default) -->
|
|
329
|
+
<template v-else>
|
|
330
|
+
<slot name="fields" />
|
|
331
|
+
</template>
|
|
251
332
|
</div>
|
|
252
333
|
</div>
|
|
253
334
|
|
package/src/composables/index.ts
CHANGED
|
@@ -132,3 +132,17 @@ export {
|
|
|
132
132
|
type UserRecord,
|
|
133
133
|
} from './useUserImpersonator'
|
|
134
134
|
export { useCurrentEntity, type UseCurrentEntityReturn } from './useCurrentEntity'
|
|
135
|
+
export {
|
|
136
|
+
useFieldManager,
|
|
137
|
+
snakeCaseToTitle,
|
|
138
|
+
type BaseFieldDefinition,
|
|
139
|
+
type ResolvedFieldConfig as FieldManagerFieldConfig,
|
|
140
|
+
type FieldGroup,
|
|
141
|
+
type GroupDefinition,
|
|
142
|
+
type GroupOptions,
|
|
143
|
+
type GenerateFieldsOptions as FieldManagerGenerateOptions,
|
|
144
|
+
type AddFieldOptions as FieldManagerAddOptions,
|
|
145
|
+
type MoveFieldPosition,
|
|
146
|
+
type UseFieldManagerOptions,
|
|
147
|
+
type UseFieldManagerReturn,
|
|
148
|
+
} from './useFieldManager'
|