@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,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CopyableId - Read-only ID field with copy-to-clipboard functionality
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <CopyableId :value="entityId" label="ID" />
|
|
7
|
+
* <CopyableId :value="apiKey" label="API Key" />
|
|
8
|
+
*/
|
|
9
|
+
import { ref, inject } from 'vue'
|
|
10
|
+
import Button from 'primevue/button'
|
|
11
|
+
import type { Orchestrator } from '../../orchestrator/Orchestrator'
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
value: string
|
|
15
|
+
label?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
19
|
+
label: 'ID'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
|
|
23
|
+
const copied = ref(false)
|
|
24
|
+
|
|
25
|
+
async function copyToClipboard(): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
await navigator.clipboard.writeText(props.value)
|
|
28
|
+
copied.value = true
|
|
29
|
+
orchestrator?.toast.success('Copied', `${props.label} copied to clipboard`, 'CopyableId')
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
copied.value = false
|
|
32
|
+
}, 2000)
|
|
33
|
+
} catch {
|
|
34
|
+
orchestrator?.toast.error('Error', 'Failed to copy to clipboard', 'CopyableId')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="form-field">
|
|
41
|
+
<label class="form-field-label">{{ label }}</label>
|
|
42
|
+
<div class="copyable-id">
|
|
43
|
+
<Button
|
|
44
|
+
type="button"
|
|
45
|
+
:icon="copied ? 'pi pi-check' : 'pi pi-copy'"
|
|
46
|
+
:severity="copied ? 'success' : 'secondary'"
|
|
47
|
+
size="small"
|
|
48
|
+
text
|
|
49
|
+
rounded
|
|
50
|
+
@click="copyToClipboard"
|
|
51
|
+
:aria-label="`Copy ${label}`"
|
|
52
|
+
class="copyable-id-button"
|
|
53
|
+
/>
|
|
54
|
+
<code class="copyable-id-value">{{ value }}</code>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<style scoped>
|
|
60
|
+
/*
|
|
61
|
+
* Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
|
|
62
|
+
* Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
|
|
63
|
+
*/
|
|
64
|
+
.copyable-id {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 0.5rem;
|
|
68
|
+
background: var(--p-surface-100);
|
|
69
|
+
padding: 0.5rem 0.75rem;
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
max-width: 100%;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.copyable-id-value {
|
|
75
|
+
font-family: monospace;
|
|
76
|
+
font-size: 0.875rem;
|
|
77
|
+
color: var(--p-surface-700);
|
|
78
|
+
word-break: break-all;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.copyable-id-button {
|
|
82
|
+
flex-shrink: 0;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* EmptyState - Reusable empty state display
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <EmptyState
|
|
7
|
+
* icon="pi-users"
|
|
8
|
+
* title="No agents found"
|
|
9
|
+
* description="Assign agents from their profile page"
|
|
10
|
+
* />
|
|
11
|
+
*
|
|
12
|
+
* <!-- With slot for custom content -->
|
|
13
|
+
* <EmptyState icon="pi-inbox" title="No items">
|
|
14
|
+
* <Button label="Create Item" @click="create" />
|
|
15
|
+
* </EmptyState>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type SizeVariant = 'sm' | 'md' | 'lg'
|
|
19
|
+
|
|
20
|
+
defineProps({
|
|
21
|
+
// PrimeIcon name (without "pi-" prefix if you want, both work)
|
|
22
|
+
icon: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: 'inbox'
|
|
25
|
+
},
|
|
26
|
+
// Main message
|
|
27
|
+
title: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: 'No items found'
|
|
30
|
+
},
|
|
31
|
+
// Secondary description (optional)
|
|
32
|
+
description: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: ''
|
|
35
|
+
},
|
|
36
|
+
// Size variant
|
|
37
|
+
size: {
|
|
38
|
+
type: String as () => SizeVariant,
|
|
39
|
+
default: 'md', // sm, md, lg
|
|
40
|
+
validator: (v: string) => ['sm', 'md', 'lg'].includes(v)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Normalize icon name (accept both "pi-inbox" and "inbox")
|
|
45
|
+
function getIconClass(icon: string): string {
|
|
46
|
+
if (icon.startsWith('pi-')) {
|
|
47
|
+
return `pi ${icon}`
|
|
48
|
+
}
|
|
49
|
+
return `pi pi-${icon}`
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div class="empty-state" :class="`empty-state--${size}`">
|
|
55
|
+
<i :class="getIconClass(icon)" class="empty-state-icon" />
|
|
56
|
+
<p class="empty-state-title">{{ title }}</p>
|
|
57
|
+
<small v-if="description" class="empty-state-description">{{ description }}</small>
|
|
58
|
+
<div v-if="$slots.default" class="empty-state-actions">
|
|
59
|
+
<slot />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, inject } from 'vue'
|
|
3
|
+
|
|
4
|
+
type SizeVariant = 'sm' | 'md' | 'lg'
|
|
5
|
+
type Severity = 'success' | 'warning' | 'danger'
|
|
6
|
+
|
|
7
|
+
interface ApiAdapter {
|
|
8
|
+
request: (method: string, endpoint: string) => Promise<Record<string, unknown>>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface BarProps {
|
|
12
|
+
percentage: number
|
|
13
|
+
tier: number
|
|
14
|
+
color: string
|
|
15
|
+
severity: Severity
|
|
16
|
+
threshold: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = defineProps({
|
|
20
|
+
value: {
|
|
21
|
+
type: Number,
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
showValue: {
|
|
25
|
+
type: Boolean,
|
|
26
|
+
default: true
|
|
27
|
+
},
|
|
28
|
+
size: {
|
|
29
|
+
type: String as () => SizeVariant,
|
|
30
|
+
default: 'md', // sm, md, lg
|
|
31
|
+
validator: (v: string) => ['sm', 'md', 'lg'].includes(v)
|
|
32
|
+
},
|
|
33
|
+
// Threshold configuration
|
|
34
|
+
threshold: {
|
|
35
|
+
type: Number as () => number | null,
|
|
36
|
+
default: null // If provided, use static threshold instead of loading from API
|
|
37
|
+
},
|
|
38
|
+
configEndpoint: {
|
|
39
|
+
type: String as () => string | null,
|
|
40
|
+
default: null // Endpoint to load threshold config from API
|
|
41
|
+
},
|
|
42
|
+
configThresholdKey: {
|
|
43
|
+
type: String,
|
|
44
|
+
default: 'threshold' // Key in config response containing threshold value
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Get API adapter (optional - only needed if loading from endpoint)
|
|
49
|
+
const api = inject<ApiAdapter | null>('apiAdapter', null)
|
|
50
|
+
|
|
51
|
+
// Threshold value
|
|
52
|
+
const loadedThreshold = ref<number>(8) // default fallback
|
|
53
|
+
const configLoaded = ref<boolean>(false)
|
|
54
|
+
|
|
55
|
+
// Computed threshold - use prop if provided, otherwise loaded value
|
|
56
|
+
const currentThreshold = computed<number>(() => props.threshold ?? loadedThreshold.value)
|
|
57
|
+
|
|
58
|
+
// Singleton pattern - load config once for all instances
|
|
59
|
+
let configPromise: Promise<void> | null = null
|
|
60
|
+
|
|
61
|
+
async function loadConfig(): Promise<void> {
|
|
62
|
+
// Skip if using static threshold or no endpoint configured
|
|
63
|
+
if (props.threshold !== null || !props.configEndpoint) {
|
|
64
|
+
configLoaded.value = true
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Skip if no API adapter
|
|
69
|
+
if (!api) {
|
|
70
|
+
console.warn('[IntensityBar] apiAdapter not provided, using default threshold')
|
|
71
|
+
configLoaded.value = true
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (configPromise) return configPromise
|
|
76
|
+
|
|
77
|
+
configPromise = api.request('GET', props.configEndpoint)
|
|
78
|
+
.then((data: Record<string, unknown>) => {
|
|
79
|
+
if (data?.[props.configThresholdKey]) {
|
|
80
|
+
loadedThreshold.value = data[props.configThresholdKey] as number
|
|
81
|
+
}
|
|
82
|
+
configLoaded.value = true
|
|
83
|
+
})
|
|
84
|
+
.catch(() => {
|
|
85
|
+
console.warn('[IntensityBar] Could not load config, using defaults')
|
|
86
|
+
configLoaded.value = true
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return configPromise
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Calculate bar properties based on threshold tiers
|
|
93
|
+
const barProps = computed<BarProps>(() => {
|
|
94
|
+
const threshold = currentThreshold.value
|
|
95
|
+
const tier = Math.floor(props.value / threshold)
|
|
96
|
+
const withinTier = props.value % threshold
|
|
97
|
+
const percentage = (withinTier / threshold) * 100
|
|
98
|
+
|
|
99
|
+
// Color: green (tier 0), orange (tier 1), red (tier 2+)
|
|
100
|
+
let color = 'var(--p-green-500)'
|
|
101
|
+
let severity: Severity = 'success'
|
|
102
|
+
if (tier === 1) {
|
|
103
|
+
color = 'var(--p-orange-500)'
|
|
104
|
+
severity = 'warning'
|
|
105
|
+
} else if (tier >= 2) {
|
|
106
|
+
color = 'var(--p-red-500)'
|
|
107
|
+
severity = 'danger'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { percentage, tier, color, severity, threshold }
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Size classes
|
|
114
|
+
const sizeClass = computed<string>(() => `intensity-bar--${props.size}`)
|
|
115
|
+
|
|
116
|
+
onMounted(() => {
|
|
117
|
+
loadConfig()
|
|
118
|
+
})
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<template>
|
|
122
|
+
<div class="intensity-container" :class="sizeClass">
|
|
123
|
+
<div
|
|
124
|
+
class="intensity-bar"
|
|
125
|
+
:style="{ '--bar-color': barProps.color, '--bar-width': barProps.percentage + '%' }"
|
|
126
|
+
:title="`Tier ${barProps.tier} (threshold: ${barProps.threshold})`"
|
|
127
|
+
>
|
|
128
|
+
<div class="intensity-bar-fill"></div>
|
|
129
|
+
</div>
|
|
130
|
+
<span v-if="showValue" class="intensity-value" :style="{ color: barProps.color }">
|
|
131
|
+
{{ value.toFixed(2) }}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
135
|
+
|
|
136
|
+
<style scoped>
|
|
137
|
+
/*
|
|
138
|
+
* Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
|
|
139
|
+
* Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
|
|
140
|
+
*/
|
|
141
|
+
.intensity-container {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 8px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.intensity-bar {
|
|
148
|
+
background: var(--p-surface-200);
|
|
149
|
+
border-radius: 4px;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
/* Default size (md) */
|
|
152
|
+
width: 80px;
|
|
153
|
+
height: 8px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.intensity-bar--sm .intensity-bar {
|
|
157
|
+
width: 60px;
|
|
158
|
+
height: 6px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.intensity-bar--lg .intensity-bar {
|
|
162
|
+
width: 120px;
|
|
163
|
+
height: 10px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.intensity-bar-fill {
|
|
167
|
+
height: 100%;
|
|
168
|
+
width: var(--bar-width);
|
|
169
|
+
background: var(--bar-color);
|
|
170
|
+
border-radius: 4px;
|
|
171
|
+
transition: width 0.3s ease, background 0.3s ease;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.intensity-value {
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
min-width: 45px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.intensity-bar--sm .intensity-value {
|
|
180
|
+
font-size: 0.8em;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.intensity-bar--md .intensity-value {
|
|
184
|
+
font-size: 0.9em;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.intensity-bar--lg .intensity-value {
|
|
188
|
+
font-size: 1em;
|
|
189
|
+
}
|
|
190
|
+
</style>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* RichCardsGrid - Grid of rich stat cards with title, value, subtitle
|
|
4
|
+
*
|
|
5
|
+
* Props:
|
|
6
|
+
* - cards: Array of card objects
|
|
7
|
+
* - columns: Number of columns (2, 3, 4, or 'auto')
|
|
8
|
+
*
|
|
9
|
+
* Card object:
|
|
10
|
+
* {
|
|
11
|
+
* name: 'users', // unique key
|
|
12
|
+
* title: 'Users', // card title
|
|
13
|
+
* value: 42, // main value
|
|
14
|
+
* subtitle: '10 active', // optional subtitle
|
|
15
|
+
* icon: 'pi pi-users', // optional icon
|
|
16
|
+
* severity: 'success', // optional: success, danger, warning, info
|
|
17
|
+
* onClick: () => {}, // optional click handler
|
|
18
|
+
* to: '/dashboard/users', // optional router link
|
|
19
|
+
* custom: true // use slot instead
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
import { useRouter } from 'vue-router'
|
|
23
|
+
import type { PropType } from 'vue'
|
|
24
|
+
|
|
25
|
+
type CardSeverity = 'success' | 'danger' | 'warning' | 'info'
|
|
26
|
+
type ColumnsType = 'auto' | 2 | 3 | 4 | 5 | 6
|
|
27
|
+
|
|
28
|
+
export interface RichCard {
|
|
29
|
+
name: string
|
|
30
|
+
title?: string
|
|
31
|
+
value?: string | number
|
|
32
|
+
subtitle?: string
|
|
33
|
+
icon?: string
|
|
34
|
+
severity?: CardSeverity
|
|
35
|
+
onClick?: () => void
|
|
36
|
+
to?: string
|
|
37
|
+
custom?: boolean
|
|
38
|
+
class?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const router = useRouter()
|
|
42
|
+
|
|
43
|
+
defineProps({
|
|
44
|
+
cards: {
|
|
45
|
+
type: Array as PropType<RichCard[]>,
|
|
46
|
+
default: () => []
|
|
47
|
+
},
|
|
48
|
+
columns: {
|
|
49
|
+
type: [Number, String] as PropType<ColumnsType>,
|
|
50
|
+
default: 'auto',
|
|
51
|
+
validator: (v: ColumnsType) => ['auto', 2, 3, 4, 5, 6].includes(v)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
function handleClick(card: RichCard): void {
|
|
56
|
+
if (card.onClick) {
|
|
57
|
+
card.onClick()
|
|
58
|
+
} else if (card.to) {
|
|
59
|
+
router.push(card.to)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isClickable(card: RichCard): boolean {
|
|
64
|
+
return !!(card.onClick || card.to)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getSeverityClass(severity: CardSeverity | undefined): string {
|
|
68
|
+
if (!severity) return ''
|
|
69
|
+
return `rich-card--${severity}`
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<template>
|
|
74
|
+
<div v-if="cards.length > 0" class="rich-cards-grid" :class="`rich-cards-grid--cols-${columns}`">
|
|
75
|
+
<template v-for="card in cards" :key="card.name">
|
|
76
|
+
<!-- Custom card: render slot -->
|
|
77
|
+
<div v-if="card.custom" class="rich-card rich-card--custom" :class="card.class">
|
|
78
|
+
<slot :name="`card-${card.name}`" :card="card" ></slot>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Rich stat card -->
|
|
82
|
+
<div
|
|
83
|
+
v-else
|
|
84
|
+
class="rich-card"
|
|
85
|
+
:class="[
|
|
86
|
+
getSeverityClass(card.severity),
|
|
87
|
+
{ 'rich-card--clickable': isClickable(card) },
|
|
88
|
+
card.class
|
|
89
|
+
]"
|
|
90
|
+
@click="handleClick(card)"
|
|
91
|
+
>
|
|
92
|
+
<div class="rich-card-header">
|
|
93
|
+
<span class="rich-card-title">{{ card.title }}</span>
|
|
94
|
+
<i v-if="card.icon" :class="card.icon" class="rich-card-icon"></i>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="rich-card-value">{{ card.value }}</div>
|
|
97
|
+
<div v-if="card.subtitle" class="rich-card-subtitle">{{ card.subtitle }}</div>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FormActions - Reusable form action buttons
|
|
4
|
+
*
|
|
5
|
+
* Props:
|
|
6
|
+
* - isEdit: Boolean - Edit mode (changes labels)
|
|
7
|
+
* - saving: Boolean - Loading state
|
|
8
|
+
* - dirty: Boolean - Form has unsaved changes
|
|
9
|
+
*
|
|
10
|
+
* Emits:
|
|
11
|
+
* - save: Save and stay on form
|
|
12
|
+
* - saveAndClose: Save and navigate back
|
|
13
|
+
* - cancel: Cancel/close without saving
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import Button from 'primevue/button'
|
|
17
|
+
import { useI18n } from '../../i18n/useI18n'
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n()
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
isEdit?: boolean
|
|
23
|
+
saving?: boolean
|
|
24
|
+
dirty?: boolean
|
|
25
|
+
showSaveAndClose?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
withDefaults(defineProps<Props>(), {
|
|
29
|
+
isEdit: false,
|
|
30
|
+
saving: false,
|
|
31
|
+
dirty: true, // Default true for backwards compatibility
|
|
32
|
+
showSaveAndClose: true
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
'save': []
|
|
37
|
+
'saveAndClose': []
|
|
38
|
+
'cancel': []
|
|
39
|
+
}>()
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div class="form-actions">
|
|
44
|
+
<div class="form-actions-left">
|
|
45
|
+
<Button
|
|
46
|
+
type="button"
|
|
47
|
+
:label="isEdit ? t('core.actions.update') : t('core.actions.create')"
|
|
48
|
+
:loading="saving"
|
|
49
|
+
:disabled="!dirty || saving"
|
|
50
|
+
icon="pi pi-check"
|
|
51
|
+
@click="emit('save')"
|
|
52
|
+
v-tooltip.top="t('core.tooltips.saveAndContinue')"
|
|
53
|
+
/>
|
|
54
|
+
<Button
|
|
55
|
+
v-if="showSaveAndClose"
|
|
56
|
+
type="button"
|
|
57
|
+
:label="isEdit ? t('core.actions.updateAndClose') : t('core.actions.createAndClose')"
|
|
58
|
+
:loading="saving"
|
|
59
|
+
:disabled="!dirty || saving"
|
|
60
|
+
icon="pi pi-check-circle"
|
|
61
|
+
severity="success"
|
|
62
|
+
@click="emit('saveAndClose')"
|
|
63
|
+
v-tooltip.top="t('core.tooltips.saveAndReturn')"
|
|
64
|
+
/>
|
|
65
|
+
<span v-if="dirty" class="dirty-indicator" v-tooltip.top="t('core.tooltips.unsavedChanges')">
|
|
66
|
+
<i class="pi pi-circle-fill"></i>
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
<Button
|
|
70
|
+
type="button"
|
|
71
|
+
:label="t('core.actions.cancel')"
|
|
72
|
+
severity="secondary"
|
|
73
|
+
icon="pi pi-times"
|
|
74
|
+
@click="emit('cancel')"
|
|
75
|
+
:disabled="saving"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FormField - Wrapper for form fields with automatic dirty state and error display
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <FormField name="username" label="Username *">
|
|
7
|
+
* <InputText v-model="form.username" />
|
|
8
|
+
* </FormField>
|
|
9
|
+
*
|
|
10
|
+
* The parent form (useEntityItemFormPage) provides:
|
|
11
|
+
* - isFieldDirty: function to check if field is dirty
|
|
12
|
+
* - getFieldError: function to get field error message
|
|
13
|
+
* - handleFieldBlur: function to trigger validation on blur
|
|
14
|
+
* - formSubmitted: ref indicating if form was submitted
|
|
15
|
+
*/
|
|
16
|
+
import { inject, computed, type Ref, type CSSProperties } from 'vue'
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
name: string
|
|
20
|
+
label?: string
|
|
21
|
+
hint?: string
|
|
22
|
+
fullWidth?: boolean
|
|
23
|
+
/** Override error message (useful for custom validation) */
|
|
24
|
+
error?: string | null
|
|
25
|
+
/** Show error only after form submission */
|
|
26
|
+
showErrorOnSubmit?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
30
|
+
label: '',
|
|
31
|
+
hint: '',
|
|
32
|
+
fullWidth: false,
|
|
33
|
+
error: null,
|
|
34
|
+
showErrorOnSubmit: false
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Inject from parent form (provided by useEntityItemFormPage)
|
|
38
|
+
const isFieldDirty = inject<(name: string) => boolean>('isFieldDirty', () => false)
|
|
39
|
+
const getFieldError = inject<(name: string) => string | null>('getFieldError', () => null)
|
|
40
|
+
const handleFieldBlur = inject<(name: string) => void>('handleFieldBlur', () => {})
|
|
41
|
+
const formSubmitted = inject<Ref<boolean>>('formSubmitted', { value: false } as Ref<boolean>)
|
|
42
|
+
|
|
43
|
+
const isDirty = computed((): boolean => isFieldDirty(props.name))
|
|
44
|
+
|
|
45
|
+
// Get error from prop or from form validation
|
|
46
|
+
const fieldError = computed((): string | null => {
|
|
47
|
+
if (props.error) return props.error
|
|
48
|
+
return getFieldError(props.name)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Show error if: form submitted OR field was touched (validated on blur)
|
|
52
|
+
const showError = computed((): boolean => {
|
|
53
|
+
if (!fieldError.value) return false
|
|
54
|
+
if (props.showErrorOnSubmit) return formSubmitted.value
|
|
55
|
+
return true
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const fieldClasses = computed((): (string | Record<string, boolean>)[] => [
|
|
59
|
+
'form-field',
|
|
60
|
+
{
|
|
61
|
+
'field-dirty': isDirty.value,
|
|
62
|
+
'field-invalid': showError.value
|
|
63
|
+
}
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
const fieldStyle = computed((): CSSProperties =>
|
|
67
|
+
props.fullWidth ? { gridColumn: '1 / -1' } : {}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
function onBlur(): void {
|
|
71
|
+
handleFieldBlur(props.name)
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<div :class="fieldClasses" :style="fieldStyle">
|
|
77
|
+
<label v-if="label" :for="name">{{ label }}</label>
|
|
78
|
+
<slot :onBlur="onBlur"></slot>
|
|
79
|
+
<small v-if="showError" class="field-error">{{ fieldError }}</small>
|
|
80
|
+
<small v-else-if="hint" class="field-hint">{{ hint }}</small>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|