qdadm 1.1.7 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdadm",
3
- "version": "1.1.7",
3
+ "version": "1.2.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -2,23 +2,16 @@
2
2
  /**
3
3
  * FormTab - Normalized tab header component
4
4
  *
5
- * Provides consistent tab headers with optional icon, count badge, and visibility control.
5
+ * @deprecated Use FieldGroups with layout="tabs" instead. FieldGroups now supports
6
+ * icon, badge, count, visible, and disabled options on groups.
6
7
  *
7
- * Props:
8
- * - value: Tab identifier (required)
9
- * - label: Display text (required)
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
- * Usage:
18
- * <FormTab value="general" label="General" icon="pi-cog" />
19
- * <FormTab value="items" label="Items" :count="5" />
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
- * Provides consistent styling and behavior for form tabs.
6
- * Works with FormTab component for individual tabs.
5
+ * @deprecated Use FieldGroups with layout="tabs" instead. FieldGroups now supports
6
+ * icon, badge, count, visible, and disabled options on groups.
7
7
  *
8
- * Usage:
9
- * <FormTabs v-model="activeTab" @update:modelValue="onTabChange">
10
- * <template #tabs>
11
- * <FormTab value="general" label="General" icon="pi-cog" />
12
- * <FormTab value="items" label="Items" icon="pi-list" :count="items.length" />
13
- * <FormTab value="advanced" label="Advanced" :visible="isEdit" />
14
- * </template>
15
- * <template #panels>
16
- * <TabPanel value="general">...</TabPanel>
17
- * <TabPanel value="items">...</TabPanel>
18
- * <TabPanel value="advanced">...</TabPanel>
19
- * </template>
20
- * </FormTabs>
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'
@@ -19,19 +19,21 @@ 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
- // Forms
23
- export { default as FormPage } from './forms/FormPage.vue'
24
- export { default as FormField } from './forms/FormField.vue'
25
- export { default as FormInput } from './forms/FormInput.vue'
26
- export { default as FormActions } from './forms/FormActions.vue'
27
- export { default as FormTabs } from './forms/FormTabs.vue'
28
- export { default as FormTab } from './forms/FormTab.vue'
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'
32
35
  export { default as ShowField } from './show/ShowField.vue'
33
36
  export { default as ShowDisplay } from './show/ShowDisplay.vue'
34
- export { default as ShowGroups } from './show/ShowGroups.vue'
35
37
 
36
38
  // Lists
37
39
  export { default as ListPage } from './lists/ListPage.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 '../../forms/FormActions.vue'
18
+ import FormActions from '../../edit/FormActions.vue'
19
19
 
20
20
  interface FormState {
21
21
  isEdit?: boolean
@@ -42,7 +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 ShowGroups from './ShowGroups.vue'
45
+ import FieldGroups from '../item/FieldGroups.vue'
46
+ import ShowField from './ShowField.vue'
46
47
 
47
48
  /**
48
49
  * Page title parts for PageHeader
@@ -246,14 +247,21 @@ const fetchErrorMessage = computed<string | null>(() => {
246
247
  <!-- Group layout mode -->
247
248
  <template v-if="useGroupLayout">
248
249
  <slot name="groups">
249
- <ShowGroups
250
+ <FieldGroups
250
251
  :groups="groups"
251
252
  :data="data"
252
253
  :layout="layout"
253
254
  :child-layout="childLayout"
254
- :horizontal="horizontalFields"
255
- :label-width="labelWidth"
256
- />
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>
257
265
  </slot>
258
266
  </template>
259
267
  <!-- Flat fields mode (default) -->
@@ -300,14 +308,21 @@ const fetchErrorMessage = computed<string | null>(() => {
300
308
  <!-- Group layout mode -->
301
309
  <template v-if="useGroupLayout">
302
310
  <slot name="groups">
303
- <ShowGroups
311
+ <FieldGroups
304
312
  :groups="groups"
305
313
  :data="data"
306
314
  :layout="layout"
307
315
  :child-layout="childLayout"
308
- :horizontal="horizontalFields"
309
- :label-width="labelWidth"
310
- />
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>
311
326
  </slot>
312
327
  </template>
313
328
  <!-- Flat fields mode (default) -->
@@ -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'