@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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/api/apiClient.ts +34 -0
- package/src/components/editors/ScopeEditor.vue +15 -4
- package/src/components/index.ts +0 -3
- package/src/components/layout/AppLayout.vue +2 -2
- package/src/components/layout/BaseLayout.vue +1 -1
- package/src/components/layout/PageHeader.vue +1 -1
- package/src/components/show/ShowDisplay.vue +6 -10
- package/src/composables/index.ts +0 -7
- package/src/composables/useListPage.ts +3 -8
- package/src/composables/useNavContext.ts +1 -1
- package/src/composables/useOptionsLookup.ts +128 -20
- package/src/composables/usePageTitle.ts +2 -2
- package/src/entity/EntityManager.crud.ts +6 -1
- package/src/entity/storage/ApiStorage.ts +0 -5
- package/src/entity/storage/LocalStorage.ts +0 -5
- package/src/entity/storage/MemoryStorage.ts +0 -5
- package/src/entity/storage/MockApiStorage.ts +0 -5
- package/src/entity/storage/SdkStorage.ts +0 -5
- package/src/generated/managers/testManager.ts +46 -0
- package/src/index.ts +6 -7
- package/src/kernel/Kernel.types.ts +9 -0
- package/src/kernel/Kernel.vue.ts +6 -0
- package/src/utils/formatters.ts +38 -7
- package/src/utils/index.ts +1 -0
- package/src/components/edit/FormTab.vue +0 -217
- package/src/components/edit/FormTabs.vue +0 -120
- package/src/components/lists/ActionColumn.vue +0 -40
- package/src/composables/useCurrentEntity.ts +0 -60
- package/src/composables/useForm.ts +0 -544
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ import { SecurityChecker, PermissionMatcher, PermissionRegistry } from '@quazard
|
|
|
31
31
|
import { PersistableRoleProvider, createLocalStorageRolesProvider } from '@quazardous/qdadm/security'
|
|
32
32
|
|
|
33
33
|
// Composables
|
|
34
|
-
import {
|
|
34
|
+
import { useBareForm, useListPage } from '@quazardous/qdadm/composables'
|
|
35
35
|
|
|
36
36
|
// Components
|
|
37
37
|
import { ListPage, PageLayout, AppLayout, FormField, FormActions } from '@quazardous/qdadm/components'
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel-level default API client (qdadm #1198)
|
|
3
|
+
*
|
|
4
|
+
* Apps already build an axios-compatible HttpClient (base URL + auth
|
|
5
|
+
* interceptors) for their ApiStorages. Registering it once on the kernel
|
|
6
|
+
* makes it available to composables/widgets that need ad-hoc API calls
|
|
7
|
+
* (useOptionsLookup endpoint mode, ScopeEditor), so relative endpoints
|
|
8
|
+
* get base URL + auth applied without per-call wiring:
|
|
9
|
+
*
|
|
10
|
+
* new Kernel({ apiClient: myAxios })
|
|
11
|
+
*/
|
|
12
|
+
import { inject } from 'vue'
|
|
13
|
+
import type { HttpClient } from '../entity/storage/ApiStorage'
|
|
14
|
+
|
|
15
|
+
/** Injection key under which the kernel provides the default API client. */
|
|
16
|
+
export const API_CLIENT_INJECTION_KEY = 'qdadmApiClient'
|
|
17
|
+
|
|
18
|
+
/** Accepted forms of the kernel `apiClient` option. */
|
|
19
|
+
export type ApiClientSource = HttpClient | (() => HttpClient)
|
|
20
|
+
|
|
21
|
+
/** Resolve an ApiClientSource (direct instance or factory) to a client. */
|
|
22
|
+
export function resolveApiClient(source: ApiClientSource | null | undefined): HttpClient | null {
|
|
23
|
+
if (!source) return null
|
|
24
|
+
return typeof source === 'function' ? source() : source
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Inject the kernel-registered default API client, or null when none is
|
|
29
|
+
* registered. Must be called from a setup context (like any inject).
|
|
30
|
+
*/
|
|
31
|
+
export function useApiClient(): HttpClient | null {
|
|
32
|
+
const source = inject<ApiClientSource | null>(API_CLIENT_INJECTION_KEY, null)
|
|
33
|
+
return resolveApiClient(source)
|
|
34
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, onMounted, watch, inject } from 'vue'
|
|
3
|
+
import type { HttpClient } from '../../entity/storage/ApiStorage'
|
|
3
4
|
import AutoComplete from 'primevue/autocomplete'
|
|
4
5
|
import Select from 'primevue/select'
|
|
5
6
|
import Button from 'primevue/button'
|
|
@@ -43,8 +44,10 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
43
44
|
defaultActions: null
|
|
44
45
|
})
|
|
45
46
|
|
|
46
|
-
// Get API adapter (optional)
|
|
47
|
+
// Get API adapter (optional); the kernel default client (Kernel({ apiClient }))
|
|
48
|
+
// serves as fallback so scope endpoints get base URL + auth without app wiring.
|
|
47
49
|
const api = inject<ApiAdapter | null>('apiAdapter', null)
|
|
50
|
+
const kernelApiClient = inject<HttpClient | (() => HttpClient) | null>('qdadmApiClient', null)
|
|
48
51
|
|
|
49
52
|
// Get global scope config (optional) - allows app-level configuration
|
|
50
53
|
const globalScopeConfig = inject<ScopeConfig>('scopeConfig', {})
|
|
@@ -178,8 +181,17 @@ function isRowComplete(row: ScopeRow): boolean {
|
|
|
178
181
|
async function loadScopeDefinition(): Promise<void> {
|
|
179
182
|
loading.value = true
|
|
180
183
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
let data: ScopeDefinition | null = null
|
|
185
|
+
if (api) {
|
|
186
|
+
data = await api.request('GET', config.value.endpoint)
|
|
187
|
+
} else if (kernelApiClient) {
|
|
188
|
+
// Fallback: kernel default API client (base URL + auth applied)
|
|
189
|
+
const client = typeof kernelApiClient === 'function' ? kernelApiClient() : kernelApiClient
|
|
190
|
+
data = (await client.get<ScopeDefinition>(config.value.endpoint)).data
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!data) {
|
|
194
|
+
// No API layer available, use defaults
|
|
183
195
|
scopeDefinition.value = {
|
|
184
196
|
resources: [...config.value.resources],
|
|
185
197
|
actions: [...config.value.actions],
|
|
@@ -187,7 +199,6 @@ async function loadScopeDefinition(): Promise<void> {
|
|
|
187
199
|
return
|
|
188
200
|
}
|
|
189
201
|
|
|
190
|
-
const data = await api.request('GET', config.value.endpoint)
|
|
191
202
|
scopeDefinition.value = {
|
|
192
203
|
resources: data.resources || [...config.value.resources],
|
|
193
204
|
actions: data.actions || [...config.value.actions],
|
package/src/components/index.ts
CHANGED
|
@@ -27,8 +27,6 @@ export { default as FormPage } from './edit/FormPage.vue'
|
|
|
27
27
|
export { default as FormField } from './edit/FormField.vue'
|
|
28
28
|
export { default as FormInput } from './edit/FormInput.vue'
|
|
29
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"
|
|
32
30
|
export { default as LookupField } from './edit/LookupField.vue'
|
|
33
31
|
export { default as LookupPickerDialog } from './edit/LookupPickerDialog.vue'
|
|
34
32
|
|
|
@@ -41,7 +39,6 @@ export { default as ParentCard } from './show/ParentCard.vue'
|
|
|
41
39
|
// Lists
|
|
42
40
|
export { default as ListPage } from './lists/ListPage.vue'
|
|
43
41
|
export { default as ActionButtons } from './lists/ActionButtons.vue'
|
|
44
|
-
export { default as ActionColumn } from './lists/ActionColumn.vue'
|
|
45
42
|
export { default as FilterBar } from './lists/FilterBar.vue'
|
|
46
43
|
|
|
47
44
|
// Editors (vanilla-jsoneditor free)
|
|
@@ -50,7 +50,7 @@ interface UserData {
|
|
|
50
50
|
const features = inject<FeaturesConfig>('qdadmFeatures', { poweredBy: true, breadcrumb: true })
|
|
51
51
|
const homeRoute = inject<string>('qdadmHomeRoute', 'home')
|
|
52
52
|
|
|
53
|
-
// Guard dialog from shared store (registered by useBareForm/
|
|
53
|
+
// Guard dialog from shared store (registered by useBareForm/useEntityItemFormPage when a form is active)
|
|
54
54
|
const guardDialog = useGuardDialog()
|
|
55
55
|
|
|
56
56
|
const router = useRouter()
|
|
@@ -238,7 +238,7 @@ watch(() => route.fullPath, () => {
|
|
|
238
238
|
})
|
|
239
239
|
|
|
240
240
|
// Navigation context (breadcrumb + navlinks from route config)
|
|
241
|
-
// Entity data comes from activeStack (populated by useEntityItemPage/
|
|
241
|
+
// Entity data comes from activeStack (populated by useEntityItemPage/useEntityItemFormPage)
|
|
242
242
|
const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks } = useNavContext()
|
|
243
243
|
|
|
244
244
|
// Allow child pages to override breadcrumb/navlinks via provide/inject
|
|
@@ -50,7 +50,7 @@ import DefaultBreadcrumb from './defaults/DefaultBreadcrumb.vue'
|
|
|
50
50
|
const slots = useSlots()
|
|
51
51
|
const hasMainSlot: boolean = !!slots.main
|
|
52
52
|
|
|
53
|
-
// Guard dialog from shared store (registered by useBareForm/
|
|
53
|
+
// Guard dialog from shared store (registered by useBareForm/useEntityItemFormPage when a form is active)
|
|
54
54
|
const guardDialog = useGuardDialog()
|
|
55
55
|
|
|
56
56
|
// Provide breadcrumb/navlinks override mechanism for child pages
|
|
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
47
47
|
badges: () => []
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
// Auto-injected title from
|
|
50
|
+
// Auto-injected title from the form composables (if available)
|
|
51
51
|
const injectedTitleParts = inject<Ref<TitleParts | null> | null>('qdadmPageTitleParts', null)
|
|
52
52
|
|
|
53
53
|
// Use props first, then injected, then fallback
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { computed, type PropType } from 'vue'
|
|
29
29
|
import { RouterLink, type RouteLocationRaw } from 'vue-router'
|
|
30
30
|
import Tag from 'primevue/tag'
|
|
31
|
+
import { formatDate, formatNumber, formatCurrency } from '../../utils/formatters'
|
|
31
32
|
|
|
32
33
|
type DisplayType =
|
|
33
34
|
| 'text'
|
|
@@ -101,13 +102,12 @@ const textValue = computed(() => {
|
|
|
101
102
|
return String(props.value)
|
|
102
103
|
})
|
|
103
104
|
|
|
104
|
-
// Format number value
|
|
105
|
+
// Format number value (browser locale by default — utils/formatters policy)
|
|
105
106
|
const numberValue = computed(() => {
|
|
106
107
|
if (isEmpty.value) return '-'
|
|
107
108
|
const num = Number(props.value)
|
|
108
109
|
if (isNaN(num)) return String(props.value)
|
|
109
|
-
|
|
110
|
-
return new Intl.NumberFormat(locale).format(num)
|
|
110
|
+
return formatNumber(num, {}, props.field.locale)
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
// Format currency value
|
|
@@ -115,9 +115,7 @@ const currencyValue = computed(() => {
|
|
|
115
115
|
if (isEmpty.value) return '-'
|
|
116
116
|
const num = Number(props.value)
|
|
117
117
|
if (isNaN(num)) return String(props.value)
|
|
118
|
-
|
|
119
|
-
const currency = props.field.currencyCode || 'USD'
|
|
120
|
-
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(num)
|
|
118
|
+
return formatCurrency(num, props.field.currencyCode || 'USD', props.field.locale)
|
|
121
119
|
})
|
|
122
120
|
|
|
123
121
|
// Format boolean value
|
|
@@ -143,9 +141,8 @@ const dateValue = computed(() => {
|
|
|
143
141
|
const v = props.value as unknown
|
|
144
142
|
const date = (typeof v === 'object' && v instanceof Date) ? v : new Date(String(v))
|
|
145
143
|
if (isNaN(date.getTime())) return String(props.value)
|
|
146
|
-
const locale = props.field.locale || 'en-US'
|
|
147
144
|
const format = props.field.dateFormat || 'short'
|
|
148
|
-
return
|
|
145
|
+
return formatDate(date, { dateStyle: format as 'short' | 'medium' | 'long' | 'full' }, props.field.locale)
|
|
149
146
|
})
|
|
150
147
|
|
|
151
148
|
// Format datetime value
|
|
@@ -154,8 +151,7 @@ const datetimeValue = computed(() => {
|
|
|
154
151
|
const v = props.value as unknown
|
|
155
152
|
const date = (typeof v === 'object' && v instanceof Date) ? v : new Date(String(v))
|
|
156
153
|
if (isNaN(date.getTime())) return String(props.value)
|
|
157
|
-
|
|
158
|
-
return new Intl.DateTimeFormat(locale, { dateStyle: 'short', timeStyle: 'short' }).format(date)
|
|
154
|
+
return formatDate(date, { dateStyle: 'short', timeStyle: 'short' }, props.field.locale)
|
|
159
155
|
})
|
|
160
156
|
|
|
161
157
|
// Format select value (lookup label from options)
|
package/src/composables/index.ts
CHANGED
|
@@ -15,12 +15,6 @@ export {
|
|
|
15
15
|
type SemanticBreadcrumbItem,
|
|
16
16
|
} from './useSemanticBreadcrumb'
|
|
17
17
|
export { useDirtyState, type UseDirtyStateReturn } from './useDirtyState'
|
|
18
|
-
export {
|
|
19
|
-
useForm,
|
|
20
|
-
type UseFormOptions,
|
|
21
|
-
type UseFormReturn,
|
|
22
|
-
type FormAlterConfig,
|
|
23
|
-
} from './useForm'
|
|
24
18
|
export {
|
|
25
19
|
useEntityItemFormPage,
|
|
26
20
|
useEntityItemFormPage as useFormPageBuilder,
|
|
@@ -132,7 +126,6 @@ export {
|
|
|
132
126
|
type UseUserImpersonatorReturn,
|
|
133
127
|
type UserRecord,
|
|
134
128
|
} from './useUserImpersonator'
|
|
135
|
-
export { useCurrentEntity, type UseCurrentEntityReturn } from './useCurrentEntity'
|
|
136
129
|
export {
|
|
137
130
|
useOptionsLookup,
|
|
138
131
|
type UseOptionsLookupConfig,
|
|
@@ -35,6 +35,7 @@ import { useEntityItemPage, type ParentConfig, type UseEntityItemPageReturn } fr
|
|
|
35
35
|
import { useActiveStack } from '../chain/useActiveStack.js'
|
|
36
36
|
import { FilterQuery } from '../query/FilterQuery'
|
|
37
37
|
import { useI18n } from '../i18n/useI18n'
|
|
38
|
+
import { formatDateOnly } from '../utils/formatters'
|
|
38
39
|
|
|
39
40
|
// Import types from dedicated types file
|
|
40
41
|
import type {
|
|
@@ -1307,18 +1308,12 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
|
|
|
1307
1308
|
})
|
|
1308
1309
|
|
|
1309
1310
|
// ============ UTILITIES ============
|
|
1311
|
+
// Browser-locale by default (unified policy — see utils/formatters).
|
|
1310
1312
|
function formatDate(
|
|
1311
1313
|
dateStr: string | null | undefined,
|
|
1312
1314
|
options: Intl.DateTimeFormatOptions = {}
|
|
1313
1315
|
): string {
|
|
1314
|
-
|
|
1315
|
-
const defaultOptions: Intl.DateTimeFormatOptions = {
|
|
1316
|
-
day: '2-digit',
|
|
1317
|
-
month: '2-digit',
|
|
1318
|
-
year: 'numeric',
|
|
1319
|
-
...options,
|
|
1320
|
-
}
|
|
1321
|
-
return new Date(dateStr).toLocaleDateString('fr-FR', defaultOptions)
|
|
1316
|
+
return formatDateOnly(dateStr, options)
|
|
1322
1317
|
}
|
|
1323
1318
|
|
|
1324
1319
|
// ============ STANDARD ACTIONS ============
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* useNavContext - Route-aware navigation context for breadcrumb and navlinks
|
|
3
3
|
*
|
|
4
4
|
* Uses semantic breadcrumb as the source of truth for navigation structure.
|
|
5
|
-
* Entity data comes from activeStack (set by useEntityItemPage/
|
|
5
|
+
* Entity data comes from activeStack (set by useEntityItemPage/useEntityItemFormPage).
|
|
6
6
|
*
|
|
7
7
|
* Semantic breadcrumb kinds:
|
|
8
8
|
* - entity-list: Entity collection (e.g., /books)
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { ref, onMounted, type Ref } from 'vue'
|
|
44
44
|
import { useOrchestrator } from '../orchestrator/useOrchestrator.js'
|
|
45
|
+
import { useApiClient } from '../api/apiClient'
|
|
45
46
|
|
|
46
47
|
export interface OptionsLookupItem {
|
|
47
48
|
label: string
|
|
@@ -66,11 +67,27 @@ export interface UseOptionsLookupConfig {
|
|
|
66
67
|
entity?: string
|
|
67
68
|
/** Extract distinct values from a specific field (entity mode only) */
|
|
68
69
|
field?: string
|
|
69
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Fetch options from an API endpoint.
|
|
72
|
+
*
|
|
73
|
+
* Routing (#1198): a **relative** endpoint goes through the kernel's
|
|
74
|
+
* default API client when one is registered (`Kernel({ apiClient })`) —
|
|
75
|
+
* base URL + auth applied, no per-call wiring. An **absolute** URL is
|
|
76
|
+
* fetched raw (escape hatch for third-party APIs, combine with `headers`).
|
|
77
|
+
* A relative endpoint with no registered client falls back to a raw fetch
|
|
78
|
+
* (legacy) with a console warning.
|
|
79
|
+
*/
|
|
70
80
|
endpoint?: string
|
|
81
|
+
/**
|
|
82
|
+
* Route the endpoint request through the named entity's storage
|
|
83
|
+
* (`storage.request('GET', endpoint)`) instead of the kernel client —
|
|
84
|
+
* explicit refinement for multi-API apps. Takes precedence over the
|
|
85
|
+
* kernel client.
|
|
86
|
+
*/
|
|
87
|
+
via?: string
|
|
71
88
|
/** When using endpoint: pick a sub-key from the response data */
|
|
72
89
|
pick?: string
|
|
73
|
-
/** Extra headers for
|
|
90
|
+
/** Extra headers for the raw-fetch paths (e.g. Authorization) */
|
|
74
91
|
headers?: Record<string, string> | (() => Record<string, string>)
|
|
75
92
|
/** Static options (no fetch needed) */
|
|
76
93
|
static?: unknown[]
|
|
@@ -164,10 +181,16 @@ export function useOptionsLookup(config: UseOptionsLookupConfig): UseOptionsLook
|
|
|
164
181
|
: defaultDecode
|
|
165
182
|
)
|
|
166
183
|
|
|
167
|
-
// Resolve orchestrator lazily (
|
|
168
|
-
|
|
184
|
+
// Resolve orchestrator lazily (entity mode, or via:'entityName' routing)
|
|
185
|
+
type LookupManager = {
|
|
186
|
+
list: (params?: Record<string, unknown>) => Promise<{ items: unknown[] }>
|
|
187
|
+
storage?: {
|
|
188
|
+
request?: (method: string, path: string, options?: Record<string, unknown>) => Promise<unknown>
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let getManager: ((name: string) => LookupManager) | null = null
|
|
169
192
|
|
|
170
|
-
if (config.entity) {
|
|
193
|
+
if (config.entity || config.via) {
|
|
171
194
|
try {
|
|
172
195
|
const orc = useOrchestrator()
|
|
173
196
|
// Generic-vs-non-generic: getManager is parameterised by an
|
|
@@ -180,6 +203,105 @@ export function useOptionsLookup(config: UseOptionsLookupConfig): UseOptionsLook
|
|
|
180
203
|
}
|
|
181
204
|
}
|
|
182
205
|
|
|
206
|
+
// Kernel default API client (Kernel({ apiClient })) — used for relative
|
|
207
|
+
// endpoints so base URL + auth apply without per-call wiring (#1198).
|
|
208
|
+
let apiClient: ReturnType<typeof useApiClient> = null
|
|
209
|
+
if (config.endpoint) {
|
|
210
|
+
try {
|
|
211
|
+
apiClient = useApiClient()
|
|
212
|
+
} catch {
|
|
213
|
+
// Outside a setup/injection context
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Bare-relative warning: once per composable instance, not per load
|
|
218
|
+
let warnedBareRelative = false
|
|
219
|
+
|
|
220
|
+
function warnLookup(msg: string): void {
|
|
221
|
+
console.warn(`[qdadm] useOptionsLookup(endpoint: '${config.endpoint}'): ${msg}`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Shared normalization for every endpoint path: unwrap data, pick, array-check. */
|
|
225
|
+
function normalizeEndpointPayload(json: unknown): unknown[] {
|
|
226
|
+
let data: unknown = (json as { data?: unknown })?.data ?? json
|
|
227
|
+
if (config.pick && typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
228
|
+
data = (data as Record<string, unknown>)[config.pick]
|
|
229
|
+
}
|
|
230
|
+
if (!Array.isArray(data)) {
|
|
231
|
+
warnLookup('response contained no usable array (after data/pick unwrap) — check the endpoint payload shape')
|
|
232
|
+
return []
|
|
233
|
+
}
|
|
234
|
+
return data
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Endpoint routing (#1198): via storage > relative→kernel client > raw fetch. */
|
|
238
|
+
async function loadFromEndpoint(): Promise<unknown[]> {
|
|
239
|
+
const endpoint = config.endpoint!
|
|
240
|
+
const isAbsolute = /^https?:\/\//i.test(endpoint)
|
|
241
|
+
|
|
242
|
+
// 1. via:'entityName' → that entity's storage (explicit refinement)
|
|
243
|
+
if (config.via && getManager) {
|
|
244
|
+
const storage = getManager(config.via)?.storage
|
|
245
|
+
if (storage?.request) {
|
|
246
|
+
try {
|
|
247
|
+
const json = await storage.request('GET', endpoint)
|
|
248
|
+
return normalizeEndpointPayload(json)
|
|
249
|
+
} catch (err) {
|
|
250
|
+
warnLookup(`request via '${config.via}' storage failed: ${err instanceof Error ? err.message : err}`)
|
|
251
|
+
throw err
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
warnLookup(`via: '${config.via}' has no storage.request() — falling back to the default routing`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 2. relative endpoint → kernel default API client (base URL + auth)
|
|
258
|
+
if (!isAbsolute && apiClient) {
|
|
259
|
+
try {
|
|
260
|
+
const { data } = await apiClient.get(endpoint)
|
|
261
|
+
return normalizeEndpointPayload(data)
|
|
262
|
+
} catch (err) {
|
|
263
|
+
warnLookup(`request through the kernel apiClient failed: ${err instanceof Error ? err.message : err}`)
|
|
264
|
+
throw err
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 3. raw fetch — absolute URL (escape hatch) or legacy relative call
|
|
269
|
+
if (!isAbsolute && !warnedBareRelative) {
|
|
270
|
+
warnedBareRelative = true
|
|
271
|
+
warnLookup(
|
|
272
|
+
'relative endpoint fetched bare — no API base URL or auth header applied. ' +
|
|
273
|
+
'Register Kernel({ apiClient }) (recommended), use via: \'entityName\', or pass an absolute URL + headers.'
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
const extraHeaders = typeof config.headers === 'function' ? config.headers() : config.headers
|
|
277
|
+
const response = await fetch(endpoint, {
|
|
278
|
+
credentials: 'include',
|
|
279
|
+
headers: { 'Accept': 'application/json', ...extraHeaders },
|
|
280
|
+
})
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
const authHint = response.status === 401 || response.status === 403
|
|
283
|
+
? ' — missing auth? register Kernel({ apiClient }) or pass headers'
|
|
284
|
+
: ''
|
|
285
|
+
warnLookup(`HTTP ${response.status}${authHint}`)
|
|
286
|
+
throw new Error(`HTTP ${response.status}`)
|
|
287
|
+
}
|
|
288
|
+
const contentType = response.headers.get('content-type') || ''
|
|
289
|
+
if (!contentType.includes('json')) {
|
|
290
|
+
warnLookup(
|
|
291
|
+
`response is not JSON (content-type: '${contentType || 'unknown'}') — the relative URL likely resolved ` +
|
|
292
|
+
'to the app origin (HTML page). Register Kernel({ apiClient }) or use an absolute URL.'
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
let json: unknown
|
|
296
|
+
try {
|
|
297
|
+
json = await response.json()
|
|
298
|
+
} catch (err) {
|
|
299
|
+
warnLookup(`response body is not parseable JSON: ${err instanceof Error ? err.message : err}`)
|
|
300
|
+
throw err
|
|
301
|
+
}
|
|
302
|
+
return normalizeEndpointPayload(json)
|
|
303
|
+
}
|
|
304
|
+
|
|
183
305
|
// Encoded strings cache for mapped mode (rebuilt on processItems)
|
|
184
306
|
let encodedStrings: string[] = []
|
|
185
307
|
|
|
@@ -255,21 +377,7 @@ export function useOptionsLookup(config: UseOptionsLookupConfig): UseOptionsLook
|
|
|
255
377
|
items = rawItems
|
|
256
378
|
}
|
|
257
379
|
} else if (config.endpoint) {
|
|
258
|
-
|
|
259
|
-
const response = await fetch(config.endpoint, {
|
|
260
|
-
credentials: 'include',
|
|
261
|
-
headers: { 'Accept': 'application/json', ...extraHeaders },
|
|
262
|
-
})
|
|
263
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
264
|
-
const json = await response.json()
|
|
265
|
-
|
|
266
|
-
let data = json.data ?? json
|
|
267
|
-
|
|
268
|
-
if (config.pick && typeof data === 'object' && !Array.isArray(data)) {
|
|
269
|
-
data = (data as Record<string, unknown>)[config.pick]
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
items = Array.isArray(data) ? data : []
|
|
380
|
+
items = await loadFromEndpoint()
|
|
273
381
|
}
|
|
274
382
|
|
|
275
383
|
processItems(items)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* usePageTitle - Provide custom page title for PageHeader
|
|
3
3
|
*
|
|
4
4
|
* Use this composable in custom pages to set the title displayed in PageHeader.
|
|
5
|
-
* For standard CRUD pages,
|
|
5
|
+
* For standard CRUD pages, useEntityItemFormPage handles this automatically.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* ```ts
|
|
@@ -77,7 +77,7 @@ export function usePageTitle(
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// Provide to PageHeader via same key as
|
|
80
|
+
// Provide to PageHeader via same key as the form composables
|
|
81
81
|
provide('qdadmPageTitleParts', titleParts)
|
|
82
82
|
|
|
83
83
|
return { titleParts, setTitle }
|
|
@@ -137,7 +137,7 @@ export function applyCrudMethods(EntityManagerClass: { prototype: any }): void {
|
|
|
137
137
|
const apiResponse = (await storage.request('GET', endpoint, {
|
|
138
138
|
params: requestParams,
|
|
139
139
|
context,
|
|
140
|
-
})) as { data?: unknown }
|
|
140
|
+
})) as { data?: unknown; total?: number; pagination?: { total?: number } }
|
|
141
141
|
// Normalize response
|
|
142
142
|
const data = apiResponse.data ?? apiResponse
|
|
143
143
|
const dataObj = data as {
|
|
@@ -150,9 +150,14 @@ export function applyCrudMethods(EntityManagerClass: { prototype: any }): void {
|
|
|
150
150
|
items: Array.isArray(data)
|
|
151
151
|
? (data as any[])
|
|
152
152
|
: dataObj.items || dataObj.data || [],
|
|
153
|
+
// Wrapper shapes like { data: [...], pagination: { total } } or
|
|
154
|
+
// { data: [...], total } carry the total on apiResponse, not on the
|
|
155
|
+
// unwrapped array — read both levels before falling back to page length.
|
|
153
156
|
total:
|
|
154
157
|
dataObj.total ??
|
|
155
158
|
dataObj.pagination?.total ??
|
|
159
|
+
apiResponse.total ??
|
|
160
|
+
apiResponse.pagination?.total ??
|
|
156
161
|
(Array.isArray(data) ? (data as any[]).length : 0),
|
|
157
162
|
}
|
|
158
163
|
} else {
|
|
@@ -57,11 +57,6 @@ export class ApiStorage<T extends EntityRecord = EntityRecord> extends IStorage<
|
|
|
57
57
|
supportsCaching: true,
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** @deprecated Use static ApiStorage.capabilities.supportsCaching instead */
|
|
61
|
-
get supportsCaching(): boolean {
|
|
62
|
-
return ApiStorage.capabilities.supportsCaching
|
|
63
|
-
}
|
|
64
|
-
|
|
65
60
|
readonly endpoint: string
|
|
66
61
|
readonly responseItemsKey: string
|
|
67
62
|
readonly responseTotalKey: string
|
|
@@ -27,11 +27,6 @@ export class LocalStorage<T extends EntityRecord = EntityRecord> extends IStorag
|
|
|
27
27
|
supportsCaching: false,
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** @deprecated Use static LocalStorage.capabilities.supportsCaching instead */
|
|
31
|
-
get supportsCaching(): boolean {
|
|
32
|
-
return LocalStorage.capabilities.supportsCaching
|
|
33
|
-
}
|
|
34
|
-
|
|
35
30
|
readonly key: string
|
|
36
31
|
readonly idField: string
|
|
37
32
|
readonly generateId: () => string
|
|
@@ -38,11 +38,6 @@ export class MemoryStorage<T extends EntityRecord = EntityRecord> extends IStora
|
|
|
38
38
|
supportsCaching: false,
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** @deprecated Use static MemoryStorage.capabilities.supportsCaching instead */
|
|
42
|
-
get supportsCaching(): boolean {
|
|
43
|
-
return MemoryStorage.capabilities.supportsCaching
|
|
44
|
-
}
|
|
45
|
-
|
|
46
41
|
readonly idField: string
|
|
47
42
|
readonly generateId: () => string
|
|
48
43
|
protected _data: Map<string, T>
|
|
@@ -35,11 +35,6 @@ export class MockApiStorage<T extends EntityRecord = EntityRecord> extends IStor
|
|
|
35
35
|
supportsCaching: false,
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** @deprecated Use static MockApiStorage.capabilities.supportsCaching instead */
|
|
39
|
-
get supportsCaching(): boolean {
|
|
40
|
-
return MockApiStorage.capabilities.supportsCaching
|
|
41
|
-
}
|
|
42
|
-
|
|
43
38
|
/**
|
|
44
39
|
* Instance capabilities getter.
|
|
45
40
|
* Merges static capabilities with instance-specific ones like requiresAuth.
|
|
@@ -91,11 +91,6 @@ export class SdkStorage<T extends EntityRecord = EntityRecord> extends IStorage<
|
|
|
91
91
|
supportsCaching: true,
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/** @deprecated Use SdkStorage.capabilities.supportsCaching instead */
|
|
95
|
-
get supportsCaching(): boolean {
|
|
96
|
-
return SdkStorage.capabilities.supportsCaching
|
|
97
|
-
}
|
|
98
|
-
|
|
99
94
|
readonly clientSidePagination: boolean
|
|
100
95
|
|
|
101
96
|
protected _sdk: SdkInstance | null
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestManager - Auto-generated EntityManager
|
|
3
|
+
*
|
|
4
|
+
* Generated by qdadm/gen/generateManagers
|
|
5
|
+
* DO NOT EDIT MANUALLY - Changes will be overwritten
|
|
6
|
+
*
|
|
7
|
+
* Entity: test
|
|
8
|
+
* Endpoint: /test
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { EntityManager } from '@quazardous/qdadm'
|
|
12
|
+
import type { EntityRecord } from '@quazardous/qdadm'
|
|
13
|
+
import { ApiStorage } from '@quazardous/qdadm'
|
|
14
|
+
|
|
15
|
+
export interface TestEntity extends EntityRecord {
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Schema definition for test
|
|
20
|
+
*/
|
|
21
|
+
export const testSchema = {
|
|
22
|
+
name: "test",
|
|
23
|
+
endpoint: "/test",
|
|
24
|
+
fields: {}
|
|
25
|
+
} as const
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Storage options for test
|
|
29
|
+
*/
|
|
30
|
+
const storageOptions = {
|
|
31
|
+
endpoint: "/test"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* TestManager instance
|
|
36
|
+
*
|
|
37
|
+
* Provides CRUD operations for test entity.
|
|
38
|
+
*/
|
|
39
|
+
export const testManager = new EntityManager<TestEntity>({
|
|
40
|
+
...{
|
|
41
|
+
name: "test",
|
|
42
|
+
idField: "id",
|
|
43
|
+
fields: {}
|
|
44
|
+
},
|
|
45
|
+
storage: new ApiStorage<TestEntity>(storageOptions)
|
|
46
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -166,12 +166,6 @@ export {
|
|
|
166
166
|
type SemanticBreadcrumbItem,
|
|
167
167
|
} from './composables/useSemanticBreadcrumb'
|
|
168
168
|
export { useDirtyState, type UseDirtyStateReturn } from './composables/useDirtyState'
|
|
169
|
-
export {
|
|
170
|
-
useForm,
|
|
171
|
-
type UseFormOptions,
|
|
172
|
-
type UseFormReturn,
|
|
173
|
-
type FormAlterConfig,
|
|
174
|
-
} from './composables/useForm'
|
|
175
169
|
export {
|
|
176
170
|
useEntityItemFormPage,
|
|
177
171
|
useEntityItemFormPage as useFormPageBuilder,
|
|
@@ -292,7 +286,6 @@ export {
|
|
|
292
286
|
type UseUserImpersonatorReturn,
|
|
293
287
|
type UserRecord,
|
|
294
288
|
} from './composables/useUserImpersonator'
|
|
295
|
-
export { useCurrentEntity, type UseCurrentEntityReturn } from './composables/useCurrentEntity'
|
|
296
289
|
export {
|
|
297
290
|
useOptionsLookup,
|
|
298
291
|
type UseOptionsLookupConfig,
|
|
@@ -300,6 +293,12 @@ export {
|
|
|
300
293
|
type OptionsLookupItem,
|
|
301
294
|
type OptionsDisplayMode,
|
|
302
295
|
} from './composables/useOptionsLookup'
|
|
296
|
+
export {
|
|
297
|
+
useApiClient,
|
|
298
|
+
resolveApiClient,
|
|
299
|
+
API_CLIENT_INJECTION_KEY,
|
|
300
|
+
type ApiClientSource,
|
|
301
|
+
} from './api/apiClient'
|
|
303
302
|
export { useChildPage, type UseChildPageReturn } from './composables/useChildPage'
|
|
304
303
|
export { useSidebarState, SIDEBAR_COLLAPSED_KEY } from './composables/useSidebarState'
|
|
305
304
|
|