@quazardous/qdadm 1.22.0 → 2.1.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.
@@ -11,6 +11,7 @@ import type { EntityManager } from '../entity/EntityManager'
11
11
  import type { EntityAuthAdapter } from '../entity/auth/EntityAuthAdapter'
12
12
  import type { RoleProvider } from '../security/RolesProvider'
13
13
  import type { I18nOptions } from '../i18n/types'
14
+ import type { ApiClientSource } from '../api/apiClient'
14
15
 
15
16
  /**
16
17
  * Auth adapter interface (app-level authentication)
@@ -183,6 +184,14 @@ export interface KernelOptions {
183
184
  debug?: boolean
184
185
  onAuthExpired?: (payload: unknown) => void
185
186
  i18n?: I18nOptions
187
+ /**
188
+ * Default API HTTP client (axios-compatible `HttpClient`, or a factory),
189
+ * provided to composables/widgets as `qdadmApiClient`. Relative-endpoint
190
+ * lookups (`useOptionsLookup` endpoint mode, `ScopeEditor` fallback) route
191
+ * through it so the app's base URL + auth headers apply without per-call
192
+ * wiring. Typically the same client your ApiStorages use.
193
+ */
194
+ apiClient?: ApiClientSource
186
195
  /**
187
196
  * Existing Vue app to install qdadm onto. When provided, the Kernel
188
197
  * skips its own `createApp(WrappedRoot)` and reuses the supplied
@@ -205,6 +205,12 @@ export function applyVueMethods(KernelClass: { prototype: any }): void {
205
205
  app.provide('qdadmActiveStack', this.activeStack)
206
206
  app.provide('qdadmStackHydrator', this.stackHydrator)
207
207
 
208
+ if (this.options.apiClient) {
209
+ // Default API client for composables/widgets (useOptionsLookup,
210
+ // ScopeEditor) — relative endpoints get base URL + auth (#1198).
211
+ app.provide('qdadmApiClient', this.options.apiClient)
212
+ }
213
+
208
214
  if (this.options.debug && typeof window !== 'undefined') {
209
215
  const self = this
210
216
  // Single namespaced surface for browser-console & agent introspection.
@@ -16,22 +16,27 @@
16
16
  * Format a date string to localized date+time
17
17
  * Default format: toLocaleString() (e.g., "20/12/2025, 14:30:00")
18
18
  *
19
+ * The locale defaults to the browser locale everywhere in qdadm — pass
20
+ * `locale` only for an explicit per-field override.
21
+ *
19
22
  * @param dateStr - ISO date string or Date object
20
23
  * @param options - Intl.DateTimeFormat options
24
+ * @param locale - Explicit locale override (default: browser locale)
21
25
  * @returns Formatted date string or '-' if empty
22
26
  */
23
27
  export function formatDate(
24
28
  dateStr: string | Date | null | undefined,
25
- options: Intl.DateTimeFormatOptions = {}
29
+ options: Intl.DateTimeFormatOptions = {},
30
+ locale?: string
26
31
  ): string {
27
32
  if (!dateStr) return '-'
28
33
  const date = new Date(dateStr)
29
34
  if (isNaN(date.getTime())) return '-'
30
35
 
31
36
  if (Object.keys(options).length === 0) {
32
- return date.toLocaleString()
37
+ return date.toLocaleString(locale)
33
38
  }
34
- return date.toLocaleString(undefined, options)
39
+ return date.toLocaleString(locale, options)
35
40
  }
36
41
 
37
42
  /**
@@ -61,15 +66,23 @@ export function formatDateTimeShort(dateStr: string | Date | null | undefined):
61
66
 
62
67
  /**
63
68
  * Format to date only (20/12/2025)
69
+ *
70
+ * @param options - Intl.DateTimeFormat overrides merged over the defaults
71
+ * @param locale - Explicit locale override (default: browser locale)
64
72
  */
65
- export function formatDateOnly(dateStr: string | Date | null | undefined): string {
73
+ export function formatDateOnly(
74
+ dateStr: string | Date | null | undefined,
75
+ options: Intl.DateTimeFormatOptions = {},
76
+ locale?: string
77
+ ): string {
66
78
  if (!dateStr) return '-'
67
79
  const date = new Date(dateStr)
68
80
  if (isNaN(date.getTime())) return '-'
69
- return date.toLocaleDateString(undefined, {
81
+ return date.toLocaleDateString(locale, {
70
82
  year: 'numeric',
71
83
  month: '2-digit',
72
84
  day: '2-digit',
85
+ ...options,
73
86
  })
74
87
  }
75
88
 
@@ -142,13 +155,31 @@ export function formatDurationBetween(
142
155
 
143
156
  /**
144
157
  * Format a number with locale-specific separators
158
+ *
159
+ * @param locale - Explicit locale override (default: browser locale)
145
160
  */
146
161
  export function formatNumber(
147
162
  value: number | null | undefined,
148
- options: Intl.NumberFormatOptions = {}
163
+ options: Intl.NumberFormatOptions = {},
164
+ locale?: string
165
+ ): string {
166
+ if (value === null || value === undefined) return '-'
167
+ return value.toLocaleString(locale, options)
168
+ }
169
+
170
+ /**
171
+ * Format a currency amount (e.g., "$1,234.56")
172
+ *
173
+ * @param currencyCode - ISO 4217 code (default 'USD')
174
+ * @param locale - Explicit locale override (default: browser locale)
175
+ */
176
+ export function formatCurrency(
177
+ value: number | null | undefined,
178
+ currencyCode = 'USD',
179
+ locale?: string
149
180
  ): string {
150
181
  if (value === null || value === undefined) return '-'
151
- return value.toLocaleString(undefined, options)
182
+ return new Intl.NumberFormat(locale, { style: 'currency', currency: currencyCode }).format(value)
152
183
  }
153
184
 
154
185
  /**
@@ -13,6 +13,7 @@ export {
13
13
  formatDuration,
14
14
  formatDurationBetween,
15
15
  formatNumber,
16
+ formatCurrency,
16
17
  formatBytes,
17
18
  formatPercent,
18
19
  } from './formatters'
@@ -1,217 +0,0 @@
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>
@@ -1,120 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * FormTabs - Normalized tab container for forms
4
- *
5
- * @deprecated Use FieldGroups with layout="tabs" instead. FieldGroups now supports
6
- * icon, badge, count, visible, and disabled options on groups.
7
- *
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
- * ```
27
- */
28
-
29
- import Tabs from 'primevue/tabs'
30
- import TabList from 'primevue/tablist'
31
- import TabPanels from 'primevue/tabpanels'
32
-
33
- defineProps({
34
- /**
35
- * Active tab value (v-model)
36
- */
37
- modelValue: {
38
- type: String,
39
- required: true
40
- }
41
- })
42
-
43
- const emit = defineEmits<{
44
- 'update:modelValue': [value: string]
45
- }>()
46
-
47
- function onTabChange(value: string | number): void {
48
- emit('update:modelValue', String(value))
49
- }
50
- </script>
51
-
52
- <template>
53
- <div class="form-tabs">
54
- <Tabs :value="modelValue" @update:value="onTabChange">
55
- <TabList class="form-tabs__list">
56
- <slot name="tabs" ></slot>
57
- </TabList>
58
- <TabPanels class="form-tabs__panels">
59
- <slot name="panels" ></slot>
60
- </TabPanels>
61
- </Tabs>
62
- </div>
63
- </template>
64
-
65
- <style scoped>
66
- /*
67
- * Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
68
- * Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
69
- */
70
- .form-tabs {
71
- margin-top: 1rem;
72
- }
73
-
74
- .form-tabs :deep(.form-tabs__list) {
75
- border-bottom: 1px solid var(--p-surface-200);
76
- gap: 0;
77
- }
78
-
79
- .form-tabs :deep(.form-tabs__panels) {
80
- padding: 1.5rem 0;
81
- }
82
-
83
- /* Tab styling */
84
- .form-tabs :deep(.p-tab) {
85
- padding: 0.75rem 1.25rem;
86
- border-radius: 0;
87
- border-bottom: 2px solid transparent;
88
- margin-bottom: -1px;
89
- transition: all 0.2s ease;
90
- }
91
-
92
- .form-tabs :deep(.p-tab:hover) {
93
- background: var(--p-surface-50);
94
- }
95
-
96
- .form-tabs :deep(.p-tab[data-p-active="true"]) {
97
- border-bottom-color: var(--p-primary-500);
98
- color: var(--p-primary-500);
99
- }
100
-
101
- /* Tab icon alignment */
102
- .form-tabs :deep(.p-tab .pi) {
103
- margin-right: 0.5rem;
104
- }
105
-
106
- /* Tab badge/count styling */
107
- .form-tabs :deep(.tab-badge) {
108
- margin-left: 0.5rem;
109
- font-size: 0.7rem;
110
- padding: 0.15rem 0.4rem;
111
- min-width: 1.25rem;
112
- text-align: center;
113
- }
114
-
115
- /* Disabled tab */
116
- .form-tabs :deep(.p-tab[data-p-disabled="true"]) {
117
- opacity: 0.5;
118
- cursor: not-allowed;
119
- }
120
- </style>
@@ -1,40 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * ActionColumn - Reusable table action buttons (edit, delete)
4
- *
5
- * Slots:
6
- * - default: Extra actions (after edit/delete)
7
- *
8
- * Emits:
9
- * - edit: When edit button is clicked
10
- * - delete: When delete button is clicked
11
- */
12
- import Button from 'primevue/button'
13
-
14
- defineEmits<{
15
- edit: []
16
- delete: []
17
- }>()
18
- </script>
19
-
20
- <template>
21
- <div class="table-actions">
22
- <Button
23
- icon="pi pi-pencil"
24
- severity="secondary"
25
- text
26
- rounded
27
- @click="$emit('edit')"
28
- v-tooltip.top="'Edit'"
29
- />
30
- <Button
31
- icon="pi pi-trash"
32
- severity="danger"
33
- text
34
- rounded
35
- @click="$emit('delete')"
36
- v-tooltip.top="'Delete'"
37
- />
38
- <slot ></slot>
39
- </div>
40
- </template>
@@ -1,60 +0,0 @@
1
- /**
2
- * @deprecated Use useStackHydrator() instead
3
- *
4
- * Legacy composable for setting breadcrumb entity data.
5
- * This has been replaced by the activeStack + stackHydrator system.
6
- *
7
- * Migration:
8
- * ```ts
9
- * // Before (deprecated)
10
- * const { setBreadcrumbEntity } = useCurrentEntity()
11
- * setBreadcrumbEntity(data)
12
- *
13
- * // After
14
- * const hydrator = useStackHydrator()
15
- * hydrator.setCurrentData(data)
16
- * ```
17
- */
18
- import { useStackHydrator } from '../chain/useStackHydrator'
19
-
20
- /**
21
- * Return type for useCurrentEntity
22
- */
23
- export interface UseCurrentEntityReturn {
24
- /** @deprecated Use hydrator.setCurrentData() instead */
25
- setBreadcrumbEntity: (data: unknown, level?: number) => void
26
- /** @deprecated Use hydrator.setCurrentData() instead */
27
- setCurrentEntity: (data: unknown) => void
28
- }
29
-
30
- /**
31
- * @deprecated Use useStackHydrator() instead
32
- */
33
- export function useCurrentEntity(): UseCurrentEntityReturn {
34
- console.warn(
35
- '[qdadm] useCurrentEntity is deprecated. Use useStackHydrator() instead.'
36
- )
37
-
38
- const hydrator = useStackHydrator()
39
-
40
- /**
41
- * @deprecated Use hydrator.setCurrentData() instead
42
- */
43
- function setBreadcrumbEntity(data: unknown, level = 1): void {
44
- if (level === 1) {
45
- hydrator.setCurrentData(data)
46
- } else {
47
- // For parent levels, use setEntityData with entity name
48
- // But we don't have entity name here - just set current
49
- hydrator.setCurrentData(data)
50
- }
51
- }
52
-
53
- // Backwards compat alias
54
- const setCurrentEntity = (data: unknown): void => setBreadcrumbEntity(data, 1)
55
-
56
- return {
57
- setBreadcrumbEntity,
58
- setCurrentEntity,
59
- }
60
- }