@quazardous/qdadm 1.19.3
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/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- package/src/zones/zones.ts +191 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FormInput - Auto-renders the appropriate input based on field config
|
|
4
|
+
*
|
|
5
|
+
* Takes a field config object and renders the matching PrimeVue component.
|
|
6
|
+
* Supports hint override but field.type takes precedence if specified.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <FormInput :field="f" v-model="form.data.value[f.name]" />
|
|
10
|
+
*/
|
|
11
|
+
import { computed, type PropType } from 'vue'
|
|
12
|
+
import InputText from 'primevue/inputtext'
|
|
13
|
+
import InputNumber from 'primevue/inputnumber'
|
|
14
|
+
import Textarea from 'primevue/textarea'
|
|
15
|
+
import Select from 'primevue/select'
|
|
16
|
+
import Checkbox from 'primevue/checkbox'
|
|
17
|
+
import Password from 'primevue/password'
|
|
18
|
+
import DatePicker from 'primevue/datepicker'
|
|
19
|
+
|
|
20
|
+
type InputType = 'text' | 'email' | 'password' | 'number' | 'textarea' | 'select' | 'boolean' | 'date' | 'datetime'
|
|
21
|
+
type ModelValue = string | number | boolean | Date | null
|
|
22
|
+
|
|
23
|
+
interface SelectOption {
|
|
24
|
+
label?: string
|
|
25
|
+
value?: string | number
|
|
26
|
+
[key: string]: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FieldConfig {
|
|
30
|
+
name: string
|
|
31
|
+
type?: InputType
|
|
32
|
+
placeholder?: string
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
readonly?: boolean
|
|
35
|
+
options?: SelectOption[]
|
|
36
|
+
optionLabel?: string
|
|
37
|
+
optionValue?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const props = defineProps({
|
|
41
|
+
field: { type: Object as PropType<FieldConfig>, required: true },
|
|
42
|
+
modelValue: { type: [String, Number, Boolean, Date, Object] as PropType<ModelValue>, default: null },
|
|
43
|
+
hint: { type: String as () => InputType | null, default: null } // Optional type hint override
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const emit = defineEmits<{
|
|
47
|
+
'update:modelValue': [value: ModelValue]
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
const value = computed<ModelValue>({
|
|
51
|
+
get: (): ModelValue => props.modelValue,
|
|
52
|
+
set: (v: ModelValue): void => emit('update:modelValue', v)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Type-specific computed values for PrimeVue components
|
|
56
|
+
const stringValue = computed<string | null>({
|
|
57
|
+
get: (): string | null => (props.modelValue as string | null) ?? null,
|
|
58
|
+
set: (v: string | null): void => emit('update:modelValue', v)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const numberValue = computed<number | null>({
|
|
62
|
+
get: (): number | null => (props.modelValue as number | null) ?? null,
|
|
63
|
+
set: (v: number | null): void => emit('update:modelValue', v)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const booleanValue = computed<boolean>({
|
|
67
|
+
get: (): boolean => (props.modelValue as boolean) ?? false,
|
|
68
|
+
set: (v: boolean): void => emit('update:modelValue', v)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Resolve component type: field.type > hint > 'text'
|
|
72
|
+
const inputType = computed<InputType>(() => props.field?.type || props.hint || 'text')
|
|
73
|
+
|
|
74
|
+
// Date value with string ↔ Date conversion for DatePicker
|
|
75
|
+
const dateValue = computed<Date | null>({
|
|
76
|
+
get: (): Date | null => {
|
|
77
|
+
const v = props.modelValue
|
|
78
|
+
if (!v) return null
|
|
79
|
+
if (v instanceof Date) return v
|
|
80
|
+
// Convert ISO string to Date
|
|
81
|
+
return new Date(v as string)
|
|
82
|
+
},
|
|
83
|
+
set: (v: Date | null): void => {
|
|
84
|
+
// Convert Date back to ISO string for storage
|
|
85
|
+
emit('update:modelValue', v ? v.toISOString() : null)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<InputText
|
|
92
|
+
v-if="inputType === 'text' || inputType === 'email'"
|
|
93
|
+
v-model="stringValue"
|
|
94
|
+
:placeholder="field.placeholder"
|
|
95
|
+
:disabled="field.disabled"
|
|
96
|
+
:readonly="field.readonly"
|
|
97
|
+
class="w-full"
|
|
98
|
+
/>
|
|
99
|
+
<Password
|
|
100
|
+
v-else-if="inputType === 'password'"
|
|
101
|
+
v-model="stringValue"
|
|
102
|
+
:placeholder="field.placeholder"
|
|
103
|
+
:disabled="field.disabled"
|
|
104
|
+
:feedback="false"
|
|
105
|
+
toggleMask
|
|
106
|
+
class="w-full"
|
|
107
|
+
/>
|
|
108
|
+
<InputNumber
|
|
109
|
+
v-else-if="inputType === 'number'"
|
|
110
|
+
v-model="numberValue"
|
|
111
|
+
:placeholder="field.placeholder"
|
|
112
|
+
:disabled="field.disabled"
|
|
113
|
+
:readonly="field.readonly"
|
|
114
|
+
:useGrouping="false"
|
|
115
|
+
class="w-full"
|
|
116
|
+
/>
|
|
117
|
+
<Textarea
|
|
118
|
+
v-else-if="inputType === 'textarea'"
|
|
119
|
+
v-model="stringValue"
|
|
120
|
+
:placeholder="field.placeholder"
|
|
121
|
+
:disabled="field.disabled"
|
|
122
|
+
:readonly="field.readonly"
|
|
123
|
+
rows="3"
|
|
124
|
+
class="w-full"
|
|
125
|
+
/>
|
|
126
|
+
<Select
|
|
127
|
+
v-else-if="inputType === 'select'"
|
|
128
|
+
v-model="value"
|
|
129
|
+
:options="field.options"
|
|
130
|
+
:optionLabel="field.optionLabel"
|
|
131
|
+
:optionValue="field.optionValue"
|
|
132
|
+
:placeholder="field.placeholder"
|
|
133
|
+
:disabled="field.disabled"
|
|
134
|
+
class="w-full"
|
|
135
|
+
/>
|
|
136
|
+
<Checkbox
|
|
137
|
+
v-else-if="inputType === 'boolean'"
|
|
138
|
+
v-model="booleanValue"
|
|
139
|
+
:disabled="field.disabled"
|
|
140
|
+
binary
|
|
141
|
+
/>
|
|
142
|
+
<DatePicker
|
|
143
|
+
v-else-if="inputType === 'date' || inputType === 'datetime'"
|
|
144
|
+
v-model="dateValue"
|
|
145
|
+
:placeholder="field.placeholder"
|
|
146
|
+
:disabled="field.disabled"
|
|
147
|
+
:showTime="inputType === 'datetime'"
|
|
148
|
+
class="w-full"
|
|
149
|
+
/>
|
|
150
|
+
<!-- Fallback to text -->
|
|
151
|
+
<InputText
|
|
152
|
+
v-else
|
|
153
|
+
v-model="stringValue"
|
|
154
|
+
:placeholder="field.placeholder"
|
|
155
|
+
:disabled="field.disabled"
|
|
156
|
+
class="w-full"
|
|
157
|
+
/>
|
|
158
|
+
</template>
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FormPage - Unified form page component
|
|
4
|
+
*
|
|
5
|
+
* Renders a complete CRUD form page with:
|
|
6
|
+
* - PageHeader with title and action buttons
|
|
7
|
+
* - Loading and error states
|
|
8
|
+
* - Form content via slots
|
|
9
|
+
* - FormActions footer
|
|
10
|
+
* - UnsavedChangesDialog integration
|
|
11
|
+
*
|
|
12
|
+
* Props come from useEntityItemFormPage composable:
|
|
13
|
+
*
|
|
14
|
+
* ```vue
|
|
15
|
+
* const form = useEntityItemFormPage({ entity: 'books' })
|
|
16
|
+
* form.generateFields()
|
|
17
|
+
* form.addSaveAction()
|
|
18
|
+
*
|
|
19
|
+
* <FormPage v-bind="form.props.value" v-on="form.events">
|
|
20
|
+
* <template #fields>
|
|
21
|
+
* <FormField v-model="form.data.title" name="title" />
|
|
22
|
+
* </template>
|
|
23
|
+
* </FormPage>
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Slots:
|
|
27
|
+
* - #nav: PageNav for breadcrumb customization
|
|
28
|
+
* - #toolbar: Custom toolbar actions (between header and form)
|
|
29
|
+
* - #header-actions: Custom header action buttons
|
|
30
|
+
* - #fields: Form fields content (required)
|
|
31
|
+
* - #footer: Custom footer (replaces FormActions if provided)
|
|
32
|
+
* - #error: Custom error display
|
|
33
|
+
* - #loading: Custom loading display
|
|
34
|
+
*/
|
|
35
|
+
import { computed, type PropType } from 'vue'
|
|
36
|
+
import PageHeader from '../layout/PageHeader.vue'
|
|
37
|
+
import FormActions from './FormActions.vue'
|
|
38
|
+
import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
|
|
39
|
+
import Card from 'primevue/card'
|
|
40
|
+
import Button from 'primevue/button'
|
|
41
|
+
import Message from 'primevue/message'
|
|
42
|
+
import type { ResolvedAction, ResolvedFieldConfig } from '../../composables/useEntityItemFormPage'
|
|
43
|
+
import type { GuardDialogState } from '../../composables/useUnsavedChangesGuard'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Page title parts for PageHeader
|
|
47
|
+
*/
|
|
48
|
+
interface PageTitleParts {
|
|
49
|
+
action: string
|
|
50
|
+
entityName: string | undefined
|
|
51
|
+
entityLabel: string | undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Error summary item
|
|
56
|
+
*/
|
|
57
|
+
interface ErrorSummaryItem {
|
|
58
|
+
field: string
|
|
59
|
+
label: string
|
|
60
|
+
message: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fetch error type
|
|
65
|
+
*/
|
|
66
|
+
interface FetchError {
|
|
67
|
+
message?: string
|
|
68
|
+
detail?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const props = defineProps({
|
|
72
|
+
// Mode
|
|
73
|
+
isEdit: { type: Boolean, default: false },
|
|
74
|
+
mode: { type: String as PropType<'create' | 'edit'>, default: 'create' },
|
|
75
|
+
|
|
76
|
+
// State
|
|
77
|
+
loading: { type: Boolean, default: false },
|
|
78
|
+
saving: { type: Boolean, default: false },
|
|
79
|
+
dirty: { type: Boolean, default: false },
|
|
80
|
+
|
|
81
|
+
// Title (use title OR titleParts)
|
|
82
|
+
title: { type: String as PropType<string | null>, default: null },
|
|
83
|
+
titleParts: { type: Object as PropType<PageTitleParts | null>, default: null },
|
|
84
|
+
|
|
85
|
+
// Fields (for auto-rendering - optional, can use #fields slot instead)
|
|
86
|
+
fields: { type: Array as PropType<ResolvedFieldConfig[]>, default: () => [] },
|
|
87
|
+
|
|
88
|
+
// Actions (from builder.actions)
|
|
89
|
+
actions: { type: Array as PropType<ResolvedAction[]>, default: () => [] },
|
|
90
|
+
|
|
91
|
+
// Validation state
|
|
92
|
+
errors: { type: Object as PropType<Record<string, string>>, default: () => ({}) },
|
|
93
|
+
hasErrors: { type: Boolean, default: false },
|
|
94
|
+
errorSummary: { type: Array as PropType<ErrorSummaryItem[] | null>, default: null },
|
|
95
|
+
submitted: { type: Boolean, default: false },
|
|
96
|
+
|
|
97
|
+
// Guard dialog (from useUnsavedChangesGuard)
|
|
98
|
+
guardDialog: { type: Object as PropType<GuardDialogState | null>, default: null },
|
|
99
|
+
|
|
100
|
+
// Error for fetch failures (separate from validation errors)
|
|
101
|
+
fetchError: { type: [String, Object] as PropType<string | FetchError | null>, default: null },
|
|
102
|
+
|
|
103
|
+
// UI options
|
|
104
|
+
showFormActions: { type: Boolean, default: true },
|
|
105
|
+
showSaveAndClose: { type: Boolean, default: true },
|
|
106
|
+
cardWrapper: { type: Boolean, default: true }
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const emit = defineEmits<{
|
|
110
|
+
(e: 'save'): void
|
|
111
|
+
(e: 'saveAndClose'): void
|
|
112
|
+
(e: 'cancel'): void
|
|
113
|
+
(e: 'delete'): void
|
|
114
|
+
}>()
|
|
115
|
+
|
|
116
|
+
// Header actions: all actions except save, delete, cancel (those go in footer)
|
|
117
|
+
const headerActions = computed<ResolvedAction[]>(() =>
|
|
118
|
+
props.actions.filter((a: ResolvedAction) => !['save', 'delete', 'cancel'].includes(a.name))
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
// Get error message from fetchError
|
|
122
|
+
const fetchErrorMessage = computed<string | null>(() => {
|
|
123
|
+
if (!props.fetchError) return null
|
|
124
|
+
if (typeof props.fetchError === 'string') return props.fetchError
|
|
125
|
+
return props.fetchError.message || props.fetchError.detail || 'Failed to load entity'
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Guard dialog handlers
|
|
129
|
+
function onGuardSaveAndLeave(): void {
|
|
130
|
+
if (props.guardDialog?.onSaveAndLeave) {
|
|
131
|
+
props.guardDialog.onSaveAndLeave()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function onGuardLeave(): void {
|
|
136
|
+
if (props.guardDialog?.onLeave) {
|
|
137
|
+
props.guardDialog.onLeave()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function onGuardStay(): void {
|
|
142
|
+
if (props.guardDialog?.onStay) {
|
|
143
|
+
props.guardDialog.onStay()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<template>
|
|
149
|
+
<div class="form-page">
|
|
150
|
+
<!-- Nav slot for PageNav (child routes) -->
|
|
151
|
+
<slot name="nav" />
|
|
152
|
+
|
|
153
|
+
<PageHeader :title="title" :title-parts="titleParts">
|
|
154
|
+
<template #actions>
|
|
155
|
+
<slot name="header-actions" />
|
|
156
|
+
<!-- Header actions from builder (excludes save/delete/cancel) -->
|
|
157
|
+
<Button
|
|
158
|
+
v-for="action in headerActions"
|
|
159
|
+
:key="action.name"
|
|
160
|
+
:label="action.label"
|
|
161
|
+
:icon="action.icon"
|
|
162
|
+
:severity="action.severity"
|
|
163
|
+
:loading="action.isLoading"
|
|
164
|
+
:disabled="action.isDisabled"
|
|
165
|
+
@click="action.onClick"
|
|
166
|
+
/>
|
|
167
|
+
</template>
|
|
168
|
+
</PageHeader>
|
|
169
|
+
|
|
170
|
+
<!-- Toolbar slot (between header and form) -->
|
|
171
|
+
<slot name="toolbar" />
|
|
172
|
+
|
|
173
|
+
<!-- Loading State -->
|
|
174
|
+
<template v-if="loading">
|
|
175
|
+
<slot name="loading">
|
|
176
|
+
<div class="loading-state">
|
|
177
|
+
<i class="pi pi-spin pi-spinner" style="font-size: 2rem"></i>
|
|
178
|
+
</div>
|
|
179
|
+
</slot>
|
|
180
|
+
</template>
|
|
181
|
+
|
|
182
|
+
<!-- Error State (fetch error) -->
|
|
183
|
+
<template v-else-if="fetchError">
|
|
184
|
+
<slot name="error" :error="fetchError">
|
|
185
|
+
<Message severity="error" :closable="false" class="form-error-message">
|
|
186
|
+
{{ fetchErrorMessage }}
|
|
187
|
+
</Message>
|
|
188
|
+
</slot>
|
|
189
|
+
</template>
|
|
190
|
+
|
|
191
|
+
<!-- Form Content -->
|
|
192
|
+
<template v-else>
|
|
193
|
+
<!-- Validation Error Summary -->
|
|
194
|
+
<Message
|
|
195
|
+
v-if="errorSummary && errorSummary.length > 0"
|
|
196
|
+
severity="warn"
|
|
197
|
+
:closable="false"
|
|
198
|
+
class="validation-summary"
|
|
199
|
+
>
|
|
200
|
+
<ul class="validation-errors">
|
|
201
|
+
<li v-for="error in errorSummary" :key="error.field">
|
|
202
|
+
<strong>{{ error.label }}:</strong> {{ error.message }}
|
|
203
|
+
</li>
|
|
204
|
+
</ul>
|
|
205
|
+
</Message>
|
|
206
|
+
|
|
207
|
+
<!-- Card wrapper or direct content -->
|
|
208
|
+
<Card v-if="cardWrapper">
|
|
209
|
+
<template #content>
|
|
210
|
+
<slot name="fields" />
|
|
211
|
+
|
|
212
|
+
<!-- Form Actions (in footer) -->
|
|
213
|
+
<template v-if="showFormActions">
|
|
214
|
+
<slot name="footer">
|
|
215
|
+
<FormActions
|
|
216
|
+
:isEdit="isEdit"
|
|
217
|
+
:saving="saving"
|
|
218
|
+
:dirty="dirty"
|
|
219
|
+
:showSaveAndClose="showSaveAndClose"
|
|
220
|
+
@save="emit('save')"
|
|
221
|
+
@saveAndClose="emit('saveAndClose')"
|
|
222
|
+
@cancel="emit('cancel')"
|
|
223
|
+
/>
|
|
224
|
+
</slot>
|
|
225
|
+
</template>
|
|
226
|
+
</template>
|
|
227
|
+
</Card>
|
|
228
|
+
|
|
229
|
+
<template v-else>
|
|
230
|
+
<slot name="fields" />
|
|
231
|
+
|
|
232
|
+
<!-- Form Actions (in footer) -->
|
|
233
|
+
<template v-if="showFormActions">
|
|
234
|
+
<slot name="footer">
|
|
235
|
+
<FormActions
|
|
236
|
+
:isEdit="isEdit"
|
|
237
|
+
:saving="saving"
|
|
238
|
+
:dirty="dirty"
|
|
239
|
+
:showSaveAndClose="showSaveAndClose"
|
|
240
|
+
@save="emit('save')"
|
|
241
|
+
@saveAndClose="emit('saveAndClose')"
|
|
242
|
+
@cancel="emit('cancel')"
|
|
243
|
+
/>
|
|
244
|
+
</slot>
|
|
245
|
+
</template>
|
|
246
|
+
</template>
|
|
247
|
+
</template>
|
|
248
|
+
|
|
249
|
+
<!-- Unsaved Changes Dialog -->
|
|
250
|
+
<UnsavedChangesDialog
|
|
251
|
+
v-if="guardDialog"
|
|
252
|
+
:visible="guardDialog.visible.value"
|
|
253
|
+
:saving="saving"
|
|
254
|
+
:hasOnSave="guardDialog.hasOnSave"
|
|
255
|
+
@saveAndLeave="onGuardSaveAndLeave"
|
|
256
|
+
@leave="onGuardLeave"
|
|
257
|
+
@stay="onGuardStay"
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
</template>
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FormTab - Normalized tab header component
|
|
4
|
+
*
|
|
5
|
+
* @deprecated Use FieldGroups with layout="tabs" instead. FieldGroups now supports
|
|
6
|
+
* icon, badge, count, visible, and disabled options on groups.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // Before (deprecated)
|
|
10
|
+
* <FormTab value="general" label="General" icon="pi-cog" :count="5" />
|
|
11
|
+
*
|
|
12
|
+
* // After (recommended)
|
|
13
|
+
* form.group('general', ['field1'], { label: 'General', icon: 'cog', count: 5 })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type PropType } from 'vue'
|
|
18
|
+
import Tab from 'primevue/tab'
|
|
19
|
+
import Tag from 'primevue/tag'
|
|
20
|
+
|
|
21
|
+
type BadgeSeverity = 'secondary' | 'info' | 'success' | 'warn' | 'danger' | 'contrast'
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
/**
|
|
25
|
+
* Tab identifier (used for v-model matching)
|
|
26
|
+
*/
|
|
27
|
+
value: {
|
|
28
|
+
type: String,
|
|
29
|
+
required: true
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Display label
|
|
34
|
+
*/
|
|
35
|
+
label: {
|
|
36
|
+
type: String,
|
|
37
|
+
required: true
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* PrimeIcon class (without 'pi' prefix or with full class)
|
|
42
|
+
* Examples: 'pi-cog', 'cog', 'pi pi-cog'
|
|
43
|
+
*/
|
|
44
|
+
icon: {
|
|
45
|
+
type: String as () => string | null,
|
|
46
|
+
default: null
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Count to display as badge (shows only if > 0)
|
|
51
|
+
*/
|
|
52
|
+
count: {
|
|
53
|
+
type: Number as () => number | null,
|
|
54
|
+
default: null
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Custom badge text (alternative to count)
|
|
59
|
+
*/
|
|
60
|
+
badge: {
|
|
61
|
+
type: [String, Number] as unknown as PropType<string | number | null>,
|
|
62
|
+
default: null
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Badge severity/color
|
|
67
|
+
*/
|
|
68
|
+
badgeSeverity: {
|
|
69
|
+
type: String as () => BadgeSeverity,
|
|
70
|
+
default: 'secondary',
|
|
71
|
+
validator: (v: string) => ['secondary', 'info', 'success', 'warn', 'danger', 'contrast'].includes(v)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Show/hide the tab
|
|
76
|
+
*/
|
|
77
|
+
visible: {
|
|
78
|
+
type: Boolean,
|
|
79
|
+
default: true
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Disable tab interaction
|
|
84
|
+
*/
|
|
85
|
+
disabled: {
|
|
86
|
+
type: Boolean,
|
|
87
|
+
default: false
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Normalize icon class
|
|
93
|
+
*/
|
|
94
|
+
function getIconClass(): string | null {
|
|
95
|
+
if (!props.icon) return null
|
|
96
|
+
// Already has 'pi' prefix
|
|
97
|
+
if (props.icon.startsWith('pi')) {
|
|
98
|
+
return props.icon.includes(' ') ? props.icon : `pi ${props.icon}`
|
|
99
|
+
}
|
|
100
|
+
// Just icon name
|
|
101
|
+
return `pi pi-${props.icon}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get count value to display
|
|
106
|
+
*/
|
|
107
|
+
function getCountValue(): number | null {
|
|
108
|
+
if (props.count !== null && props.count > 0) return props.count
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get badge value to display (separate from count)
|
|
114
|
+
*/
|
|
115
|
+
function getBadgeValue(): string | number | null {
|
|
116
|
+
if (props.badge !== null) return props.badge
|
|
117
|
+
// If no badge but count provided, show count as badge (legacy behavior)
|
|
118
|
+
if (props.count !== null && props.count > 0 && props.badge === null) return props.count
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if we should show count and badge separately
|
|
124
|
+
*/
|
|
125
|
+
function showBothCountAndBadge(): boolean {
|
|
126
|
+
return props.count !== null && props.count > 0 && props.badge !== null
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<template>
|
|
131
|
+
<Tab
|
|
132
|
+
v-if="visible"
|
|
133
|
+
:value="value"
|
|
134
|
+
:disabled="disabled"
|
|
135
|
+
>
|
|
136
|
+
<i v-if="icon" :class="getIconClass()" class="tab-icon" ></i>
|
|
137
|
+
<span class="tab-label">{{ label }}</span>
|
|
138
|
+
<!-- When both count and badge are provided, show count first then badge indicator -->
|
|
139
|
+
<template v-if="showBothCountAndBadge()">
|
|
140
|
+
<Tag
|
|
141
|
+
:value="String(getCountValue())"
|
|
142
|
+
severity="secondary"
|
|
143
|
+
class="tab-badge"
|
|
144
|
+
/>
|
|
145
|
+
<span class="tab-alert-dot" :class="`alert-${badgeSeverity}`"></span>
|
|
146
|
+
</template>
|
|
147
|
+
<!-- Otherwise show single badge (legacy behavior) -->
|
|
148
|
+
<Tag
|
|
149
|
+
v-else-if="getBadgeValue() !== null"
|
|
150
|
+
:value="String(getBadgeValue())"
|
|
151
|
+
:severity="badgeSeverity"
|
|
152
|
+
class="tab-badge"
|
|
153
|
+
/>
|
|
154
|
+
</Tab>
|
|
155
|
+
</template>
|
|
156
|
+
|
|
157
|
+
<style scoped>
|
|
158
|
+
/*
|
|
159
|
+
* Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
|
|
160
|
+
* Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
|
|
161
|
+
*/
|
|
162
|
+
.tab-icon {
|
|
163
|
+
margin-right: 0.5rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.tab-label {
|
|
167
|
+
/* Ensure consistent text rendering */
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.tab-badge {
|
|
171
|
+
margin-left: 0.5rem;
|
|
172
|
+
font-size: 0.7rem;
|
|
173
|
+
padding: 0.15rem 0.4rem;
|
|
174
|
+
min-width: 1.25rem;
|
|
175
|
+
text-align: center;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.tab-alert-dot {
|
|
179
|
+
display: inline-block;
|
|
180
|
+
width: 8px !important;
|
|
181
|
+
height: 8px !important;
|
|
182
|
+
min-width: 8px;
|
|
183
|
+
max-width: 8px;
|
|
184
|
+
border-radius: 50%;
|
|
185
|
+
margin-left: 0.35rem;
|
|
186
|
+
animation: pulse 1.5s infinite;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.alert-warn {
|
|
190
|
+
background-color: var(--p-orange-500);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.alert-danger {
|
|
194
|
+
background-color: var(--p-red-500);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.alert-info {
|
|
198
|
+
background-color: var(--p-blue-500);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.alert-success {
|
|
202
|
+
background-color: var(--p-green-500);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.alert-secondary {
|
|
206
|
+
background-color: var(--p-gray-500);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.alert-contrast {
|
|
210
|
+
background-color: var(--p-primary-500);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@keyframes pulse {
|
|
214
|
+
0%, 100% { opacity: 1; }
|
|
215
|
+
50% { opacity: 0.5; }
|
|
216
|
+
}
|
|
217
|
+
</style>
|